Skip to content

Commit f10391c

Browse files
committed
更新网站元数据和SEO设置,添加社交卡片图像
1 parent 170c1b3 commit f10391c

4 files changed

Lines changed: 136 additions & 2 deletions

File tree

website/index.html

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,34 @@
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>website</title>
7+
<meta name="theme-color" content="#050008" />
8+
<meta name="robots" content="index,follow" />
9+
<meta name="author" content="coderDJing" />
10+
<meta name="application-name" content="BPM Sniffer" />
11+
<title>BPM Sniffer · Real-time system audio BPM detector</title>
12+
<meta
13+
name="description"
14+
content="BPM Sniffer is a lightweight Windows BPM detector that listens to system audio, keeps the numbers stable with vivid visuals, and ships OTA updates plus bilingual UI."
15+
/>
16+
<meta name="keywords" content="BPM Sniffer,BPM detector,beat detection,Windows DJ tool,system audio analyzer" />
17+
<link rel="canonical" href="https://coderDJing.github.io/bpm-sniffer/" />
18+
<meta property="og:title" content="BPM Sniffer · Real-time system audio BPM detector" />
19+
<meta
20+
property="og:description"
21+
content="Real-time system audio BPM detector for Windows 10+. Lightweight, open source, OTA-ready, and bilingual."
22+
/>
23+
<meta property="og:type" content="website" />
24+
<meta property="og:url" content="https://coderDJing.github.io/bpm-sniffer/" />
25+
<meta property="og:image" content="/social-card.png" />
26+
<meta property="og:site_name" content="BPM Sniffer" />
27+
<meta property="og:locale" content="en_US" />
28+
<meta name="twitter:card" content="summary_large_image" />
29+
<meta name="twitter:title" content="BPM Sniffer · Real-time system audio BPM detector" />
30+
<meta
31+
name="twitter:description"
32+
content="Windows BPM detector that reads system audio, stabilizes beats, and stays sharp with OTA updates."
33+
/>
34+
<meta name="twitter:image" content="/social-card.png" />
835
</head>
936
<body>
1037
<div id="app"></div>

website/public/social-card.png

12.6 KB
Loading

website/src/App.vue

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,33 @@ const releaseNotesExcerpt = computed(() => {
4949
.slice(0, 3)
5050
})
5151
52+
const SITE_NAME = 'BPM Sniffer'
53+
const FALLBACK_SITE_URL = 'https://coderDJing.github.io/bpm-sniffer/'
54+
const envSiteUrlRaw = (import.meta.env as Record<string, string | undefined>).VITE_SITE_URL
55+
const envSiteUrl = envSiteUrlRaw ? envSiteUrlRaw.trim() : ''
56+
const ensureTrailingSlash = (value: string) => {
57+
if (!value) return '/'
58+
return value.endsWith('/') ? value : `${value}/`
59+
}
60+
const resolveRuntimeUrl = () => {
61+
if (envSiteUrl) return envSiteUrl
62+
if (typeof window !== 'undefined') {
63+
const { origin, pathname } = window.location
64+
let normalizedPath = pathname
65+
if (normalizedPath.endsWith('index.html')) {
66+
normalizedPath = normalizedPath.slice(0, -'index.html'.length)
67+
}
68+
if (!normalizedPath.endsWith('/')) {
69+
normalizedPath = `${normalizedPath}/`
70+
}
71+
return `${origin}${normalizedPath}`
72+
}
73+
return FALLBACK_SITE_URL
74+
}
75+
const canonicalUrl = ensureTrailingSlash(resolveRuntimeUrl() || FALLBACK_SITE_URL)
76+
const canonicalBase = canonicalUrl.replace(/\/$/, '')
77+
const socialCardUrl = canonicalBase ? `${canonicalBase}/social-card.png` : `${canonicalUrl}social-card.png`
78+
5279
type SiteLang = 'zh' | 'en'
5380
type FeatureEntry = { title: string; detail: string }
5481
type StepEntry = { label: string; title: string; detail: string }
@@ -63,6 +90,13 @@ type DemoEntry = {
6390
floatingOff: string
6491
}
6592
93+
type SeoEntry = {
94+
title: string
95+
description: string
96+
keywords: string[]
97+
locale: string
98+
}
99+
66100
type TranslationEntry = {
67101
eyebrow: string
68102
heroTitle: string
@@ -82,6 +116,7 @@ type TranslationEntry = {
82116
steps: StepEntry[]
83117
langToggleLabel: string
84118
demo: DemoEntry
119+
seo: SeoEntry
85120
}
86121
87122
const translations: Record<SiteLang, TranslationEntry> = {
@@ -122,6 +157,13 @@ const translations: Record<SiteLang, TranslationEntry> = {
122157
pinOff: '置顶',
123158
floatingOn: '悬浮中',
124159
floatingOff: '悬浮球'
160+
},
161+
seo: {
162+
title: 'BPM Sniffer · 系统音频实时 BPM 侦测工具',
163+
description:
164+
'BPM Sniffer 是一款面向 Windows 10+ 的系统音频 BPM 检测工具,安装即用、零驱动依赖,提供稳定数值、可视化与 OTA 更新。',
165+
keywords: ['BPM Sniffer', 'BPM 检测', '节拍侦测', '系统音频', 'DJ 工具'],
166+
locale: 'zh_CN'
125167
}
126168
},
127169
en: {
@@ -170,6 +212,13 @@ const translations: Record<SiteLang, TranslationEntry> = {
170212
pinOff: 'Pin window',
171213
floatingOn: 'Floating',
172214
floatingOff: 'Floating widget'
215+
},
216+
seo: {
217+
title: 'BPM Sniffer · Real-time system audio BPM detector',
218+
description:
219+
'BPM Sniffer is a lightweight Windows BPM detector that listens to any system audio, keeps the BPM steady with visuals, and updates itself over the air.',
220+
keywords: ['BPM Sniffer', 'BPM detector', 'beat detection', 'system audio', 'DJ tool'],
221+
locale: 'en_US'
173222
}
174223
}
175224
}
@@ -216,11 +265,61 @@ const releaseDateText = computed(() => {
216265
}
217266
})
218267
const demoI18n = computed(() => localized.value.demo)
268+
const seoMeta = computed(() => localized.value.seo)
219269
220270
function toggleLang() {
221271
lang.value = lang.value === 'zh' ? 'en' : 'zh'
222272
}
223273
274+
function upsertMeta(attribute: 'name' | 'property', key: string, value: string) {
275+
if (typeof document === 'undefined') return
276+
const head = document.head || document.querySelector('head')
277+
if (!head) return
278+
let element = head.querySelector<HTMLMetaElement>(`meta[${attribute}="${key}"]`)
279+
if (!element) {
280+
element = document.createElement('meta')
281+
element.setAttribute(attribute, key)
282+
head.appendChild(element)
283+
}
284+
element.setAttribute('content', value)
285+
}
286+
287+
function upsertLink(rel: string, href: string) {
288+
if (typeof document === 'undefined') return
289+
const head = document.head || document.querySelector('head')
290+
if (!head) return
291+
let link = head.querySelector<HTMLLinkElement>(`link[rel="${rel}"]`)
292+
if (!link) {
293+
link = document.createElement('link')
294+
link.setAttribute('rel', rel)
295+
head.appendChild(link)
296+
}
297+
link.setAttribute('href', href)
298+
}
299+
300+
function applySeoMeta() {
301+
if (typeof document === 'undefined') return
302+
const meta = seoMeta.value
303+
const keywords = (meta.keywords || []).join(', ')
304+
document.title = meta.title
305+
document.documentElement.lang = lang.value
306+
upsertMeta('name', 'description', meta.description)
307+
if (keywords) {
308+
upsertMeta('name', 'keywords', keywords)
309+
}
310+
upsertMeta('property', 'og:title', meta.title)
311+
upsertMeta('property', 'og:description', meta.description)
312+
upsertMeta('property', 'og:locale', meta.locale)
313+
upsertMeta('property', 'og:url', canonicalUrl)
314+
upsertMeta('property', 'og:image', socialCardUrl)
315+
upsertMeta('property', 'og:site_name', SITE_NAME)
316+
upsertMeta('name', 'twitter:card', 'summary_large_image')
317+
upsertMeta('name', 'twitter:title', meta.title)
318+
upsertMeta('name', 'twitter:description', meta.description)
319+
upsertMeta('name', 'twitter:image', socialCardUrl)
320+
upsertLink('canonical', canonicalUrl)
321+
}
322+
224323
watch(lang, (val) => {
225324
if (typeof window !== 'undefined') {
226325
try {
@@ -231,6 +330,14 @@ watch(lang, (val) => {
231330
}
232331
})
233332
333+
watch(
334+
lang,
335+
() => {
336+
applySeoMeta()
337+
},
338+
{ immediate: true }
339+
)
340+
234341
onMounted(() => {
235342
fetchLatestRelease()
236343
})

website/src/components/BpmDemo.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { computed, onMounted, onUnmounted, ref, watch, withDefaults } from 'vue'
2+
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
33
import VizPanel from './bpm/VizPanel.vue'
44
import refreshIcon from '../assets/bpm/refresh.png'
55
import sunIcon from '../assets/bpm/sun.png'

0 commit comments

Comments
 (0)