Skip to content

Commit 7c65556

Browse files
committed
feat(docs): 重做 Track Studio 官网首页并完善 SEO
1 parent dddc89a commit 7c65556

42 files changed

Lines changed: 4935 additions & 1747 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.prettierignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ tsconfig.*.json
77
resources
88
rust_package
99
*.md
10+
docs/public/assets/unified-waveforms/list-previews.json

‎docs/.vitepress/appBridge.ts‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// 让官网直接渲染应用 renderer 里的真实 Vue 组件所需的构建配置。
2+
// 应用由 electron-vite 构建,这里补齐它在 VitePress(普通 Vite)下缺的几样东西:
3+
// - 路径别名 @renderer / @shared / src
4+
// - electron-vite 专有的 `?asset` 导入(改写成 Vite 标准的 `?url`)
5+
// - 应用自定义的 `*.wasm?inline`、`<template src>` 外置模板
6+
// - renderer 代码里读取的 process.env.NODE_ENV 与 vue-i18n 编译期开关
7+
import { readFile } from 'node:fs/promises'
8+
import { fileURLToPath, pathToFileURL } from 'node:url'
9+
import type { Plugin, UserConfig } from 'vite'
10+
11+
const repoRoot = fileURLToPath(new URL('../../', import.meta.url)).replace(/\\/g, '/')
12+
const rendererSrc = `${repoRoot}src/renderer/src`
13+
const sharedSrc = `${repoRoot}src/shared`
14+
15+
const assetQueryPlugin = (): Plugin => ({
16+
name: 'frkb-docs-asset-query',
17+
enforce: 'pre',
18+
async resolveId(source, importer, options) {
19+
if (!source.endsWith('?asset')) return null
20+
const resolved = await this.resolve(source.slice(0, -'?asset'.length), importer, {
21+
...options,
22+
skipSelf: true
23+
})
24+
return resolved ? `${resolved.id}?url` : null
25+
}
26+
})
27+
28+
// HorizontalBrowseRawWaveformDetail.vue 用 <template src="./*.template.html"> 外置模板。
29+
// electron-vite 用的 @vitejs/plugin-vue 6 支持外置模板;VitePress 1.x 内置的 plugin-vue 5 会把它当成
30+
// 空模板,组件渲染成 <template></template>。这里在进 SFC 编译前把外置模板内联回去。
31+
// 应用模板文件可能含 <template> 外壳,也可能只有模板正文;SFC 编译器需要完整的模板块。
32+
const inlineExternalTemplatePlugin = (): Plugin => ({
33+
name: 'frkb-docs-inline-external-template',
34+
enforce: 'pre',
35+
async transform(code, id) {
36+
const filePath = id.split('?')[0]
37+
if (!filePath.endsWith('.vue') || !filePath.startsWith(rendererSrc)) return null
38+
const match = code.match(/<template\s+src="([^"]+)"\s*><\/template>/)
39+
if (!match) return null
40+
const templatePath = fileURLToPath(new URL(match[1], pathToFileURL(filePath)))
41+
this.addWatchFile(templatePath)
42+
const templateSource = (await readFile(templatePath, 'utf8')).trim()
43+
const templateBlock = /^<template(?:\s|>)/.test(templateSource)
44+
? templateSource
45+
: `<template>\n${templateSource}\n</template>`
46+
return code.replace(match[0], templateBlock)
47+
}
48+
})
49+
50+
// 与 electron.vite.config.ts 的 inlineWasmPlugin 完全相同:`*.wasm?inline` 导出 base64 data URL
51+
// (Mixtape 的变速引擎 rubberband-wasm 这样引入)
52+
const inlineWasmPlugin = (): Plugin => ({
53+
name: 'frkb-docs-inline-wasm',
54+
enforce: 'pre',
55+
async load(id) {
56+
const [filePath, query = ''] = id.split('?')
57+
if (!filePath.endsWith('.wasm') || !query.split('&').includes('inline')) return null
58+
const bytes = await readFile(filePath)
59+
const dataUrl = `data:application/wasm;base64,${bytes.toString('base64')}`
60+
return `export default ${JSON.stringify(dataUrl)}`
61+
}
62+
})
63+
64+
export const appBridgeViteConfig: UserConfig = {
65+
plugins: [assetQueryPlugin(), inlineExternalTemplatePlugin(), inlineWasmPlugin()],
66+
resolve: {
67+
alias: [
68+
{ find: /^@renderer\//, replacement: `${rendererSrc}/` },
69+
{ find: /^@shared\//, replacement: `${sharedSrc}/` },
70+
{ find: /^src\//, replacement: `${repoRoot}src/` }
71+
]
72+
},
73+
define: {
74+
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'),
75+
// vue-i18n 的 esm-bundler 构建读取这些编译期开关,Vite + plugin-vue 默认只注入 Vue 自己的
76+
__VUE_PROD_DEVTOOLS__: 'false',
77+
__VUE_I18N_FULL_INSTALL__: 'true',
78+
__VUE_I18N_LEGACY_API__: 'false',
79+
__INTLIFY_PROD_DEVTOOLS__: 'false',
80+
__INTLIFY_JIT_COMPILATION__: 'true',
81+
__INTLIFY_DROP_MESSAGE_COMPILER__: 'false'
82+
},
83+
ssr: {
84+
// SSR 阶段也要走同一套 define,不能把 vue-i18n 当外部依赖直接 require
85+
noExternal: ['vue-i18n', '@intlify/core-base', '@intlify/shared', '@intlify/message-compiler']
86+
},
87+
css: {
88+
preprocessorOptions: {
89+
scss: { api: 'modern-compiler', silenceDeprecations: ['legacy-js-api'] }
90+
}
91+
},
92+
server: {
93+
fs: { allow: [repoRoot] }
94+
}
95+
}

‎docs/.vitepress/config.mts‎

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,35 @@
11
import { defineConfig } from 'vitepress'
2-
3-
const jsonLd = JSON.stringify({
4-
'@context': 'https://schema.org',
5-
'@type': 'SoftwareApplication',
6-
name: 'Track Studio',
7-
alternateName: ['FRKB', 'FRKB Rapid Audio Organization Tool'],
8-
applicationCategory: 'MultimediaApplication',
9-
operatingSystem: 'Windows, macOS',
10-
url: 'https://coderdjing.github.io/Track-Studio/',
11-
downloadUrl: 'https://github.com/coderDJing/Track-Studio/releases/latest',
12-
description:
13-
'Track Studio (formerly FRKB) is a desktop audio workflow tool for DJs: real file organization, SET playlists, fingerprint dedup, waveform preview, Rekordbox libraries, and Mixtape.'
14-
})
2+
import { appBridgeViteConfig } from './appBridge'
3+
import { buildSeoHead } from './seo'
154

165
export default defineConfig({
176
title: 'Track Studio',
18-
description: 'Track Studio (formerly FRKB) — Rapid Audio Organization Tool',
7+
description: 'Track Studio — DJ 音乐整理与演出准备工作站',
198
base: '/Track-Studio/',
9+
// 官网只做暗色,与应用暗色主题保持一致
10+
appearance: 'force-dark',
11+
// 首页直接渲染应用的真实组件,需要应用的别名与导入约定
12+
vite: appBridgeViteConfig,
2013
lastUpdated: true,
2114
cleanUrls: true,
15+
sitemap: { hostname: 'https://coderdjing.github.io/Track-Studio/' },
16+
transformHead: ({ page, title, description }) => buildSeoHead(page, title, description),
2217

2318
locales: {
2419
root: {
2520
label: '简体中文',
2621
lang: 'zh-CN',
27-
title: 'Track Studio - 开源音频快速整理工具',
22+
title: 'Track Studio - DJ 音乐整理与演出准备工作站',
2823
description:
29-
'内容感知去重、波形试听、Mixtape 自动录制、Stem 分轨与 Rekordbox U 盘库整合在同一套音频工作流里。'
24+
'为 DJ 整理真实音频文件、按指纹去重、双轨波形试听、编排 SET,并用 Mixtape 录制与 Stem 分轨准备演出。支持 Windows 和 macOS。'
3025
},
3126
en: {
3227
label: 'English',
3328
lang: 'en-US',
3429
link: '/en/',
35-
title: 'Track Studio - Fast Audio Organization Tool',
30+
title: 'Track Studio - DJ Music Library & Set Preparation',
3631
description:
37-
'Content-aware dedup, waveform preview, Mixtape auto-recording, stem separation, and Rekordbox USB libraries in one desktop workflow.'
32+
'Organize real audio files, find duplicates by fingerprint, audition on two decks, prepare SET playlists, record Mixtapes, and separate stems. For Windows and macOS.'
3833
}
3934
},
4035

@@ -45,6 +40,7 @@ export default defineConfig({
4540

4641
head: [
4742
['link', { rel: 'icon', href: '/Track-Studio/assets/icon.webp' }],
43+
['meta', { name: 'theme-color', content: '#181818' }],
4844
['link', { rel: 'preconnect', href: 'https://fonts.googleapis.com' }],
4945
['link', { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' }],
5046
[
@@ -53,7 +49,6 @@ export default defineConfig({
5349
href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800;900&display=swap',
5450
rel: 'stylesheet'
5551
}
56-
],
57-
['script', { type: 'application/ld+json' }, jsonLd]
52+
]
5853
]
5954
})

‎docs/.vitepress/seo.ts‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { HeadConfig } from 'vitepress'
2+
3+
const siteUrl = 'https://coderdjing.github.io/Track-Studio/'
4+
5+
type SeoPage = {
6+
path: string
7+
alternatePath: string
8+
language: 'zh-CN' | 'en-US'
9+
isHome: boolean
10+
}
11+
12+
const pages: Record<string, SeoPage> = {
13+
'index.md': { path: '', alternatePath: 'en/', language: 'zh-CN', isHome: true },
14+
'en/index.md': { path: 'en/', alternatePath: '', language: 'en-US', isHome: true },
15+
'features.md': {
16+
path: 'features',
17+
alternatePath: 'en/features',
18+
language: 'zh-CN',
19+
isHome: false
20+
},
21+
'en/features.md': {
22+
path: 'en/features',
23+
alternatePath: 'features',
24+
language: 'en-US',
25+
isHome: false
26+
}
27+
}
28+
29+
const absoluteUrl = (path: string) => `${siteUrl}${path}`
30+
31+
export function buildSeoHead(pagePath: string, title: string, description: string): HeadConfig[] {
32+
const page = pages[pagePath.replaceAll('\\', '/').replace(/^\//, '')]
33+
if (!page) return []
34+
35+
const url = absoluteUrl(page.path)
36+
const imageUrl = `${siteUrl}assets/og-track-studio-${page.language === 'zh-CN' ? 'zh' : 'en'}.png`
37+
const zhUrl = absoluteUrl(page.language === 'zh-CN' ? page.path : page.alternatePath)
38+
const enUrl = absoluteUrl(page.language === 'en-US' ? page.path : page.alternatePath)
39+
const head: HeadConfig[] = [
40+
['link', { rel: 'canonical', href: url }],
41+
['link', { rel: 'alternate', hreflang: 'zh-CN', href: zhUrl }],
42+
['link', { rel: 'alternate', hreflang: 'en-US', href: enUrl }],
43+
['link', { rel: 'alternate', hreflang: 'x-default', href: zhUrl }],
44+
['meta', { property: 'og:type', content: 'website' }],
45+
['meta', { property: 'og:site_name', content: 'Track Studio' }],
46+
['meta', { property: 'og:locale', content: page.language === 'zh-CN' ? 'zh_CN' : 'en_US' }],
47+
['meta', { property: 'og:title', content: title }],
48+
['meta', { property: 'og:description', content: description }],
49+
['meta', { property: 'og:url', content: url }],
50+
['meta', { property: 'og:image', content: imageUrl }],
51+
['meta', { property: 'og:image:width', content: '1200' }],
52+
['meta', { property: 'og:image:height', content: '630' }],
53+
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
54+
['meta', { name: 'twitter:title', content: title }],
55+
['meta', { name: 'twitter:description', content: description }],
56+
['meta', { name: 'twitter:image', content: imageUrl }]
57+
]
58+
59+
if (page.isHome) {
60+
head.push([
61+
'script',
62+
{ type: 'application/ld+json' },
63+
JSON.stringify({
64+
'@context': 'https://schema.org',
65+
'@type': 'SoftwareApplication',
66+
name: 'Track Studio',
67+
applicationCategory: 'MultimediaApplication',
68+
operatingSystem: 'Windows, macOS',
69+
inLanguage: page.language,
70+
url,
71+
image: imageUrl,
72+
downloadUrl: 'https://github.com/coderDJing/Track-Studio/releases/latest',
73+
description
74+
})
75+
])
76+
}
77+
78+
return head
79+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<script setup lang="ts">
2+
import { computed, onMounted, ref } from 'vue'
3+
import { useLatestRelease } from './useLatestRelease'
4+
5+
const props = defineProps<{
6+
windowsLabel: string
7+
macLabel: string
8+
otherPrefix: string
9+
}>()
10+
11+
const release = ref<ReturnType<typeof useLatestRelease> | null>(null)
12+
13+
onMounted(() => {
14+
release.value = useLatestRelease()
15+
})
16+
17+
const loading = computed(() => !release.value || release.value.loading.value)
18+
const version = computed(() => release.value?.version.value ?? '')
19+
const platform = computed(() => release.value?.platform.value ?? 'unknown')
20+
const winUrl = computed(() => release.value?.winUrl.value ?? '')
21+
const macUrl = computed(() => release.value?.macUrl.value ?? '')
22+
23+
// 主按钮给当前系统,另一个平台降级成文字链接;识别不出来时两个都给主按钮
24+
const primary = computed(() => {
25+
if (platform.value === 'mac') return ['mac'] as const
26+
if (platform.value === 'win') return ['win'] as const
27+
return ['win', 'mac'] as const
28+
})
29+
const secondary = computed(() => {
30+
if (platform.value === 'mac') return 'win' as const
31+
if (platform.value === 'win') return 'mac' as const
32+
return null
33+
})
34+
35+
const labelOf = (os: 'win' | 'mac') => (os === 'win' ? props.windowsLabel : props.macLabel)
36+
const urlOf = (os: 'win' | 'mac') => (os === 'win' ? winUrl.value : macUrl.value)
37+
</script>
38+
39+
<template>
40+
<div class="dl">
41+
<div v-if="loading" class="dl-skeleton" aria-hidden="true"></div>
42+
<template v-else>
43+
<div class="dl-row">
44+
<a
45+
v-for="os in primary"
46+
:key="os"
47+
class="dl-btn"
48+
:href="urlOf(os)"
49+
target="_blank"
50+
rel="nofollow noopener"
51+
>
52+
<span class="dl-icon-frame" aria-hidden="true">
53+
<svg
54+
v-if="os === 'win'"
55+
class="dl-icon dl-icon--win"
56+
viewBox="0 0 24 24"
57+
fill="currentColor"
58+
>
59+
<path
60+
d="M0 3.449L9.75 2.1V11.7H0V3.449zm0 17.1L9.75 21.9V12.3H0v8.249zM10.5 1.8L24 0v11.7H10.5V1.8zm0 20.4L24 24V12.3H10.5v9.9z"
61+
/>
62+
</svg>
63+
<svg v-else class="dl-icon dl-icon--mac" viewBox="4 2 16 20" fill="currentColor">
64+
<path
65+
d="M17.057 12.781c.032 2.588 2.254 3.462 2.287 3.477-.025.065-.338 1.15-1.118 2.273-.679.973-1.381 1.94-2.486 1.96-1.087.02-1.391-.651-2.629-.651-1.241 0-1.609.63-2.67.67-1.1.04-1.766-.02-2.527-1.12-1.554-2.245-2.657-6.333-1.055-9.09.795-1.373 2.215-2.248 3.76-2.268 1.171-.025 2.212.748 2.927.748.717 0 1.98-.923 3.342-.782.572.022 2.181.23 3.213 1.731-.082.051-1.922 1.112-1.902 3.33zm-2.404-7.334c.615-.747 1.026-1.783.912-2.821-.892.036-1.972.593-2.612 1.341-.571.659-1.072 1.716-.938 2.731.996.078 2.016-.491 2.638-1.251z"
66+
/>
67+
</svg>
68+
</span>
69+
<span>{{ labelOf(os) }}</span>
70+
<span v-if="version" class="dl-version">{{ version }}</span>
71+
</a>
72+
</div>
73+
<a
74+
v-if="secondary"
75+
class="dl-other"
76+
:href="urlOf(secondary)"
77+
target="_blank"
78+
rel="nofollow noopener"
79+
>
80+
{{ otherPrefix }} {{ labelOf(secondary) }}
81+
</a>
82+
</template>
83+
</div>
84+
</template>

0 commit comments

Comments
 (0)