feat: accessibility enhancement
All checks were successful
Deploy / Deploy (push) Successful in 1m16s

closes #3
This commit is contained in:
CDN 2025-02-03 19:28:27 +08:00
parent 3e652d5e4e
commit 01c5131055
Signed by: CDN
GPG key ID: 0C656827F9F80080
31 changed files with 1121 additions and 58 deletions

View file

@ -10,15 +10,22 @@
"brand": { "brand": {
"name": "STARSET Mirror" "name": "STARSET Mirror"
}, },
"hero": { "site": {
"title": "Connecting Starset and You", "description": "STARSET Mirror, connecting STARSET and you."
"subtitle": "", },
"cta": { "home": {
"projects": "Browse Projects", "meta": {
"updates": "Check Updates" "title": "STARSET Mirror",
} "description": "STARSET Mirror, connecting STARSET and you."
},
"latestUpdates": "Latest Updates",
"featuredProjects": "Featured Projects"
}, },
"projects": { "projects": {
"meta": {
"title": "Projects - STARSET Mirror",
"description": "Explore our projects dedicated to STARSET and the community. From translations to community services, discover how we're making a difference."
},
"title": "Projects", "title": "Projects",
"tags": { "tags": {
"translation": "Translation", "translation": "Translation",
@ -34,13 +41,17 @@
} }
}, },
"updates": { "updates": {
"meta": {
"title": "Updates - STARSET Mirror",
"description": "Stay up to date with the latest news, updates, and activities from STARSET Mirror. Follow our journey in supporting the STARSET community."
},
"title": "Project Updates", "title": "Project Updates",
"loading": "Loading...", "loading": "Loading...",
"back_to_list": "Back to Updates List", "back_to_list": "Back to Updates",
"filter": { "filter": {
"all": "All Updates", "all": "All Updates",
"title": "Filter by Tag", "title": "Filter by Tags",
"search_placeholder": "Search Tag...", "search_placeholder": "Search tags...",
"no_results": "No matching tags found", "no_results": "No matching tags found",
"clear": "Clear Filter" "clear": "Clear Filter"
}, },
@ -60,6 +71,10 @@
} }
}, },
"contributors": { "contributors": {
"meta": {
"title": "Contributors - STARSET Mirror",
"description": "Meet the amazing people behind STARSET Mirror. Our contributors work tirelessly to bring STARSET closer to you."
},
"title": "Contributors", "title": "Contributors",
"tabs": { "tabs": {
"contributors": "Contributors", "contributors": "Contributors",
@ -69,6 +84,10 @@
"regular_members": "Regular Members & Community Contributors" "regular_members": "Regular Members & Community Contributors"
}, },
"about": { "about": {
"meta": {
"title": "About - STARSET Mirror",
"description": "Learn about STARSET Mirror's mission, values, and our dedication to connecting STARSET with fans worldwide."
},
"title": "About Us", "title": "About Us",
"contact": { "contact": {
"title": "Social Links" "title": "Social Links"
@ -77,6 +96,27 @@
"title": "Join Us" "title": "Join Us"
} }
}, },
"aria": {
"mainContent": "Main content",
"breadcrumb": "Page navigation",
"navigation": "Site navigation",
"menu": "Menu",
"search": "Search",
"darkMode": "Dark mode",
"language": "Language selection",
"loading": "Loading",
"error": "Error",
"closeMenu": "Close menu",
"openMenu": "Open menu"
},
"hero": {
"title": "Connecting STARSET with You",
"subtitle": "",
"cta": {
"projects": "Browse Projects",
"updates": "Latest Updates"
}
},
"theme": { "theme": {
"light": "Light", "light": "Light",
"dark": "Dark", "dark": "Dark",
@ -124,6 +164,12 @@
"icon": "Forgejo" "icon": "Forgejo"
} }
] ]
},
"common": {
"loading": "Loading...",
"error": "An error occurred",
"retry": "Retry",
"close": "Close"
} }
} }
} }

View file

@ -10,6 +10,30 @@
"brand": { "brand": {
"name": "STARSET Mirror" "name": "STARSET Mirror"
}, },
"site": {
"description": "STARSET Mirror连接星落与你。"
},
"home": {
"meta": {
"title": "STARSET Mirror",
"description": "STARSET Mirror连接星落与你"
},
"latestUpdates": "最新动态",
"featuredProjects": "精选项目"
},
"aria": {
"mainContent": "主要内容",
"breadcrumb": "页面导航",
"navigation": "网站导航",
"menu": "菜单",
"search": "搜索",
"darkMode": "深色模式",
"language": "语言选择",
"loading": "加载中",
"error": "错误",
"closeMenu": "关闭菜单",
"openMenu": "打开菜单"
},
"hero": { "hero": {
"title": "连接星落与你", "title": "连接星落与你",
"subtitle": "", "subtitle": "",
@ -19,6 +43,10 @@
} }
}, },
"projects": { "projects": {
"meta": {
"title": "项目 - STARSET Mirror",
"description": "探索我们为 STARSET 和社区打造的项目。从翻译到社区服务,了解我们如何为社区贡献力量。"
},
"title": "项目", "title": "项目",
"tags": { "tags": {
"translation": "翻译", "translation": "翻译",
@ -34,6 +62,10 @@
} }
}, },
"updates": { "updates": {
"meta": {
"title": "动态 - STARSET Mirror",
"description": "了解 STARSET Mirror 的最新动态、更新和活动。跟随我们支持 STARSET 社区的脚步。"
},
"title": "项目动态", "title": "项目动态",
"loading": "正在加载...", "loading": "正在加载...",
"back_to_list": "返回动态列表", "back_to_list": "返回动态列表",
@ -60,6 +92,10 @@
} }
}, },
"contributors": { "contributors": {
"meta": {
"title": "贡献者 - STARSET Mirror",
"description": "认识 STARSET Mirror 背后的优秀贡献者们。他们不懈努力,让 STARSET 与你更近。"
},
"title": "贡献者", "title": "贡献者",
"tabs": { "tabs": {
"contributors": "贡献者", "contributors": "贡献者",
@ -69,6 +105,10 @@
"regular_members": "项目成员 & 社区贡献者" "regular_members": "项目成员 & 社区贡献者"
}, },
"about": { "about": {
"meta": {
"title": "关于 - STARSET Mirror",
"description": "了解 STARSET Mirror 的使命、价值观,以及我们致力于连接 STARSET 与全球粉丝的愿景。"
},
"title": "关于我们", "title": "关于我们",
"contact": { "contact": {
"title": "联系方式" "title": "联系方式"
@ -124,6 +164,12 @@
"icon": "Forgejo" "icon": "Forgejo"
} }
] ]
},
"common": {
"loading": "加载中...",
"error": "发生错误",
"retry": "重试",
"close": "关闭"
} }
} }
} }

View file

@ -10,6 +10,30 @@
"brand": { "brand": {
"name": "STARSET Mirror" "name": "STARSET Mirror"
}, },
"site": {
"description": "STARSET Mirror連結星落與你。"
},
"home": {
"meta": {
"title": "STARSET Mirror",
"description": "STARSET Mirror連結星落與你。"
},
"latestUpdates": "最新動態",
"featuredProjects": "查看專案"
},
"aria": {
"mainContent": "主要內容",
"breadcrumb": "頁面導航",
"navigation": "網站導航",
"menu": "選單",
"search": "搜尋",
"darkMode": "深色模式",
"language": "語言選擇",
"loading": "載入中",
"error": "錯誤",
"closeMenu": "關閉選單",
"openMenu": "開啟選單"
},
"hero": { "hero": {
"title": "連接星落與你", "title": "連接星落與你",
"subtitle": "", "subtitle": "",
@ -124,6 +148,12 @@
"icon": "Forgejo" "icon": "Forgejo"
} }
] ]
},
"common": {
"loading": "載入中...",
"error": "發生錯誤",
"retry": "重試",
"close": "關閉"
} }
} }
} }

View file

@ -2,9 +2,35 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/png" href="/favicon.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>STARSET Mirror</title> <title>STARSET Mirror</title>
<!-- Primary Meta Tags -->
<meta name="title" content="STARSET Mirror" />
<meta name="description" content="STARSET Mirror, 连接星落与你。" />
<meta name="keywords" content="STARSET,STARSET Mirror,starsetonline,STARSET Society,STARSET 中文,STARSET 歌词,STARSET 翻译,STARSET Fans,STARSET 粉丝" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://mirror.starset.fans" />
<meta property="og:title" content="STARSET Mirror" />
<meta property="og:description" content="STARSET Mirror, 连接星落与你。" />
<meta property="og:image" content="https://mirror.starset.fans/og-image.jpg" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://mirror.starset.fans/" />
<meta property="twitter:title" content="STARSET Mirror" />
<meta property="twitter:description" content="STARSET Mirror, 连接星落与你。" />
<meta property="twitter:image" content="https://mirror.starset.fans/og-image.jpg" />
<!-- Alternate Language Versions -->
<link rel="alternate" hreflang="zh-Hans" href="https://mirror.starset.fans/" />
<link rel="alternate" hreflang="en" href="https://mirror.starset.fans" />
<link rel="alternate" hreflang="zh-Hant" href="https://mirror.starset.fans/" />
<script defer src="https://analytics.owu.one/script.js" data-website-id="1f9f0242-8bce-4883-be1d-4bb7a3aad6d0"></script> <script defer src="https://analytics.owu.one/script.js" data-website-id="1f9f0242-8bce-4883-be1d-4bb7a3aad6d0"></script>
</head> </head>
<body> <body>

View file

@ -5,7 +5,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build && tsx scripts/generate-rss.ts", "build": "vite build && tsx scripts/generate-rss.ts && tsx scripts/generate-sitemap.ts",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "preview": "vite preview"
}, },
@ -21,6 +21,7 @@
"marked": "^12.0.2", "marked": "^12.0.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-helmet-async": "^2.0.5",
"react-i18next": "^14.1.3", "react-i18next": "^14.1.3",
"react-icons": "^5.4.0", "react-icons": "^5.4.0",
"react-router-dom": "^6.28.2" "react-router-dom": "^6.28.2"
@ -45,6 +46,7 @@
"tsx": "^4.19.2", "tsx": "^4.19.2",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"typescript-eslint": "^8.22.0", "typescript-eslint": "^8.22.0",
"vite": "^5.4.14" "vite": "^5.4.14",
"xml2js": "^0.6.2"
} }
} }

50
pnpm-lock.yaml generated
View file

@ -41,6 +41,9 @@ importers:
react-dom: react-dom:
specifier: ^18.3.1 specifier: ^18.3.1
version: 18.3.1(react@18.3.1) version: 18.3.1(react@18.3.1)
react-helmet-async:
specifier: ^2.0.5
version: 2.0.5(react@18.3.1)
react-i18next: react-i18next:
specifier: ^14.1.3 specifier: ^14.1.3
version: 14.1.3(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 14.1.3(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -111,6 +114,9 @@ importers:
vite: vite:
specifier: ^5.4.14 specifier: ^5.4.14
version: 5.4.14(@types/node@22.13.0) version: 5.4.14(@types/node@22.13.0)
xml2js:
specifier: ^0.6.2
version: 0.6.2
packages: packages:
@ -1254,6 +1260,9 @@ packages:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'} engines: {node: '>=0.8.19'}
invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
is-binary-path@2.1.0: is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -1773,6 +1782,14 @@ packages:
peerDependencies: peerDependencies:
react: ^18.3.1 react: ^18.3.1
react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
react-helmet-async@2.0.5:
resolution: {integrity: sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==}
peerDependencies:
react: ^16.6.0 || ^17.0.0 || ^18.0.0
react-i18next@14.1.3: react-i18next@14.1.3:
resolution: {integrity: sha512-wZnpfunU6UIAiJ+bxwOiTmBOAaB14ha97MjOEnLGac2RJ+h/maIYXZuTHlmyqQVX1UVHmU1YDTQ5vxLmwfXTjw==} resolution: {integrity: sha512-wZnpfunU6UIAiJ+bxwOiTmBOAaB14ha97MjOEnLGac2RJ+h/maIYXZuTHlmyqQVX1UVHmU1YDTQ5vxLmwfXTjw==}
peerDependencies: peerDependencies:
@ -1870,6 +1887,9 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
shallowequal@1.1.0:
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
shebang-command@2.0.0: shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -2053,6 +2073,14 @@ packages:
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
hasBin: true hasBin: true
xml2js@0.6.2:
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
engines: {node: '>=4.0.0'}
xmlbuilder@11.0.1:
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
engines: {node: '>=4.0'}
yallist@3.1.1: yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@ -3228,6 +3256,10 @@ snapshots:
imurmurhash@0.1.4: {} imurmurhash@0.1.4: {}
invariant@2.2.4:
dependencies:
loose-envify: 1.4.0
is-binary-path@2.1.0: is-binary-path@2.1.0:
dependencies: dependencies:
binary-extensions: 2.3.0 binary-extensions: 2.3.0
@ -3789,6 +3821,15 @@ snapshots:
react: 18.3.1 react: 18.3.1
scheduler: 0.23.2 scheduler: 0.23.2
react-fast-compare@3.2.2: {}
react-helmet-async@2.0.5(react@18.3.1):
dependencies:
invariant: 2.2.4
react: 18.3.1
react-fast-compare: 3.2.2
shallowequal: 1.1.0
react-i18next@14.1.3(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): react-i18next@14.1.3(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
'@babel/runtime': 7.26.7 '@babel/runtime': 7.26.7
@ -3889,6 +3930,8 @@ snapshots:
semver@7.7.0: {} semver@7.7.0: {}
shallowequal@1.1.0: {}
shebang-command@2.0.0: shebang-command@2.0.0:
dependencies: dependencies:
shebang-regex: 3.0.0 shebang-regex: 3.0.0
@ -4062,6 +4105,13 @@ snapshots:
dependencies: dependencies:
sax: 1.4.1 sax: 1.4.1
xml2js@0.6.2:
dependencies:
sax: 1.4.1
xmlbuilder: 11.0.1
xmlbuilder@11.0.1: {}
yallist@3.1.1: {} yallist@3.1.1: {}
yaml@2.7.0: {} yaml@2.7.0: {}

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
public/favicon-96x96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

3
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/favicon.zip Normal file

Binary file not shown.

BIN
public/og-image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

54
public/robots.txt Normal file
View file

@ -0,0 +1,54 @@
User-agent: AI2Bot
User-agent: Ai2Bot-Dolma
User-agent: Amazonbot
User-agent: anthropic-ai
User-agent: Applebot
User-agent: Applebot-Extended
User-agent: Bytespider
User-agent: CCBot
User-agent: ChatGPT-User
User-agent: Claude-Web
User-agent: ClaudeBot
User-agent: cohere-ai
User-agent: cohere-training-data-crawler
User-agent: Crawlspace
User-agent: Diffbot
User-agent: DuckAssistBot
User-agent: FacebookBot
User-agent: FriendlyCrawler
User-agent: Google-Extended
User-agent: GoogleOther
User-agent: GoogleOther-Image
User-agent: GoogleOther-Video
User-agent: GPTBot
User-agent: iaskspider/2.0
User-agent: ICC-Crawler
User-agent: ImagesiftBot
User-agent: img2dataset
User-agent: ISSCyberRiskCrawler
User-agent: Kangaroo Bot
User-agent: Meta-ExternalAgent
User-agent: Meta-ExternalFetcher
User-agent: OAI-SearchBot
User-agent: omgili
User-agent: omgilibot
User-agent: PanguBot
User-agent: PerplexityBot
User-agent: PetalBot
User-agent: Scrapy
User-agent: SemrushBot-OCOB
User-agent: SemrushBot-SWA
User-agent: Sidetrade indexer bot
User-agent: Timpibot
User-agent: VelenPublicWebCrawler
User-agent: Webzio-Extended
User-agent: YouBot
Disallow: /
Disallow: /data/
# Sitemap
Sitemap: https://mirror.starset.fans/sitemap.xml
# Crawl-delay
Crawl-delay: 10

21
public/site.webmanifest Normal file
View file

@ -0,0 +1,21 @@
{
"name": "STARSET Mirror",
"short_name": "SSTMirror",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#dae9ff",
"background_color": "#dae9ff",
"display": "standalone"
}

53
public/sitemap.xml Normal file
View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<!-- Homepage -->
<url>
<loc>https://mirror.starset.fans/</loc>
<xhtml:link rel="alternate" hreflang="zh-Hans" href="https://mirror.starset.fans/"/>
<xhtml:link rel="alternate" hreflang="en" href="https://mirror.starset.fans/"/>
<xhtml:link rel="alternate" hreflang="zh-Hant" href="https://mirror.starset.fans/"/>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<!-- Projects -->
<url>
<loc>https://mirror.starset.fans/projects</loc>
<xhtml:link rel="alternate" hreflang="zh-Hans" href="https://mirror.starset.fans/projects"/>
<xhtml:link rel="alternate" hreflang="en" href="https://mirror.starset.fans/projects"/>
<xhtml:link rel="alternate" hreflang="zh-Hant" href="https://mirror.starset.fans/projects"/>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<!-- Updates -->
<url>
<loc>https://mirror.starset.fans/updates</loc>
<xhtml:link rel="alternate" hreflang="zh-Hans" href="https://mirror.starset.fans/updates"/>
<xhtml:link rel="alternate" hreflang="en" href="https://mirror.starset.fans/updates"/>
<xhtml:link rel="alternate" hreflang="zh-Hant" href="https://mirror.starset.fans/updates"/>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<!-- Contributors -->
<url>
<loc>https://mirror.starset.fans/contributors</loc>
<xhtml:link rel="alternate" hreflang="zh-Hans" href="https://mirror.starset.fans/contributors"/>
<xhtml:link rel="alternate" hreflang="en" href="https://mirror.starset.fans/contributors"/>
<xhtml:link rel="alternate" hreflang="zh-Hant" href="https://mirror.starset.fans/contributors"/>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<!-- About -->
<url>
<loc>https://mirror.starset.fans/about</loc>
<xhtml:link rel="alternate" hreflang="zh-Hans" href="https://mirror.starset.fans/about"/>
<xhtml:link rel="alternate" hreflang="en" href="https://mirror.starset.fans/about"/>
<xhtml:link rel="alternate" hreflang="zh-Hant" href="https://mirror.starset.fans/about"/>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
</urlset>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -22,14 +22,14 @@ const LANGUAGES: LanguageConfig[] = [
{ {
code: 'zh-CN', code: 'zh-CN',
dataDir: 'zh-CN', dataDir: 'zh-CN',
title: 'STARSET 镜像站更新', title: 'STARSET Mirror 项目动态',
description: 'STARSET 镜像站的最新更新' description: 'STARSET Mirror 最新动态'
}, },
{ {
code: 'zh-Hant', code: 'zh-Hant',
dataDir: 'zh-TW', dataDir: 'zh-TW',
title: 'STARSET 鏡像站更新', title: 'STARSET Mirror 專案動态',
description: 'STARSET 鏡像站的最新更新' description: 'STARSET Mirror 最新動態'
} }
]; ];

View file

@ -0,0 +1,97 @@
import { promises as fs } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import xml2js from 'xml2js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const LANGUAGES = ['en-US', 'zh-CN', 'zh-TW'];
const BASE_URL = 'mirror.starset.fans';
interface Update {
id: string;
date: string;
}
interface UpdateIndex {
updates: Update[];
}
async function getYearlyIndices(lang: string): Promise<string[]> {
const indexDir = join(__dirname, '..', 'data', lang, 'updates', 'index');
try {
const files = await fs.readdir(indexDir);
return files.filter(file => file.match(/^\d{4}\.json$/));
} catch (err) {
console.error(`Error reading index directory for ${lang}:`, err);
return [];
}
}
async function generateUpdateUrls(lang: string) {
const yearFiles = await getYearlyIndices(lang);
const urls = [];
for (const yearFile of yearFiles) {
try {
const content = await fs.readFile(
join(__dirname, '..', 'data', lang, 'updates', 'index', yearFile),
'utf-8'
);
const data = JSON.parse(content) as UpdateIndex;
if (data.updates) {
for (const update of data.updates) {
urls.push({
loc: `https://${BASE_URL}/updates/${update.id}`,
lastmod: update.date,
changefreq: 'monthly',
priority: '0.7'
});
}
}
} catch (err) {
console.error(`Error processing ${yearFile} for ${lang}:`, err);
}
}
return urls;
}
async function mergeSitemaps() {
const parser = new xml2js.Parser();
const mainSitemapPath = join(__dirname, '..', 'dist', 'sitemap.xml');
try {
const mainSitemapContent = await fs.readFile(mainSitemapPath, 'utf-8');
const mainSitemap = await parser.parseStringPromise(mainSitemapContent);
// Filter out existing update URLs while maintaining order
const existingUrls = mainSitemap.urlset.url.filter((url: any) =>
!url.loc[0].includes('/updates/'));
mainSitemap.urlset.url = existingUrls;
// Add language-specific update URLs at the end
for (const lang of LANGUAGES) {
const updateUrls = await generateUpdateUrls(lang);
mainSitemap.urlset.url.push(...updateUrls);
}
const builder = new xml2js.Builder();
const newSitemap = builder.buildObject(mainSitemap);
await fs.writeFile(mainSitemapPath, newSitemap);
console.log('Successfully updated sitemap.xml in dist directory');
} catch (err) {
console.error('Error merging sitemaps:', err);
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
console.error('Make sure the dist directory exists and contains sitemap.xml');
}
process.exit(1);
}
}
// Run the script
mergeSitemaps();

View file

@ -0,0 +1,101 @@
import { promises as fs } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import xml2js from 'xml2js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const LANGUAGES = ['en-US', 'zh-CN', 'zh-TW'];
const BASE_URL = 'starset.wiki'; // Replace with your actual domain
async function getYearlyIndices(lang) {
const indexDir = join(__dirname, '..', 'data', lang, 'updates', 'index');
try {
const files = await fs.readdir(indexDir);
return files.filter(file => file.match(/^\d{4}\.json$/));
} catch (err) {
console.error(`Error reading index directory for ${lang}:`, err);
return [];
}
}
async function generateUpdateUrls(lang) {
const yearFiles = await getYearlyIndices(lang);
const urls = [];
for (const yearFile of yearFiles) {
try {
const content = await fs.readFile(
join(__dirname, '..', 'data', lang, 'updates', 'index', yearFile),
'utf-8'
);
const data = JSON.parse(content);
if (data.updates) {
for (const update of data.updates) {
urls.push({
loc: `https://${BASE_URL}/updates/${update.id}`,
lastmod: update.date,
changefreq: 'monthly',
priority: '0.7'
});
}
}
} catch (err) {
console.error(`Error processing ${yearFile} for ${lang}:`, err);
}
}
return urls;
}
async function generateSitemap(urls) {
const sitemap = {
urlset: {
$: {
xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9'
},
url: urls
}
};
const builder = new xml2js.Builder();
return builder.buildObject(sitemap);
}
async function mergeSitemaps() {
const parser = new xml2js.Parser();
const mainSitemapPath = join(__dirname, '..', 'dist', 'sitemap.xml');
try {
const mainSitemapContent = await fs.readFile(mainSitemapPath, 'utf-8');
const mainSitemap = await parser.parseStringPromise(mainSitemapContent);
// Filter out existing update URLs while maintaining order
const existingUrls = mainSitemap.urlset.url.filter(url =>
!url.loc[0].includes('/updates/'));
mainSitemap.urlset.url = existingUrls;
// Add language-specific update URLs at the end
for (const lang of LANGUAGES) {
const updateUrls = await generateUpdateUrls(lang);
mainSitemap.urlset.url.push(...updateUrls);
}
const builder = new xml2js.Builder();
const newSitemap = builder.buildObject(mainSitemap);
await fs.writeFile(mainSitemapPath, newSitemap);
console.log('Successfully updated sitemap.xml in dist directory');
} catch (err) {
console.error('Error merging sitemaps:', err);
if (err.code === 'ENOENT') {
console.error('Make sure the dist directory exists and contains sitemap.xml');
}
}
}
// Run the script
mergeSitemaps().catch(console.error);

View file

@ -1,19 +1,24 @@
import React from 'react'; import React, { Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Helmet } from 'react-helmet-async';
import { ThemeProvider } from './contexts/ThemeContext'; import { ThemeProvider } from './contexts/ThemeContext';
import Navbar from './components/Navbar'; import Navbar from './components/Navbar';
import HomePage from './pages/HomePage'; import Breadcrumb from './components/Breadcrumb';
import ProjectsPage from './pages/ProjectsPage'; import LoadingSpinner from './components/LoadingSpinner';
import UpdatesPage from './pages/UpdatesPage';
import UpdateDetailPage from './pages/UpdateDetailPage';
import ContributorsPage from './pages/ContributorsPage';
import AboutPage from './pages/AboutPage';
import iconMap from './utils/iconMap'; import iconMap from './utils/iconMap';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
// 懒加载路由组件
const HomePage = React.lazy(() => import('./pages/HomePage'));
const ProjectsPage = React.lazy(() => import('./pages/ProjectsPage'));
const UpdatesPage = React.lazy(() => import('./pages/UpdatesPage'));
const UpdateDetailPage = React.lazy(() => import('./pages/UpdateDetailPage'));
const ContributorsPage = React.lazy(() => import('./pages/ContributorsPage'));
const AboutPage = React.lazy(() => import('./pages/AboutPage'));
function App() { function App() {
const { t } = useTranslation(); const { t, i18n } = useTranslation();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const socialLinks = t('social.links', { returnObjects: true }); const socialLinks = t('social.links', { returnObjects: true });
@ -29,25 +34,51 @@ function App() {
}; };
}, [queryClient]); }, [queryClient]);
// 构建 Schema.org 结构化数据
const schemaOrgWebsite = {
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'STARSET Mirror',
url: 'https://mirror.starset.fans',
description: t('site.description'),
inLanguage: ['zh-Hans', 'en', 'zh-Hant'],
potentialAction: {
'@type': 'SearchAction',
target: 'https://mirror.starset.fans/search?q={search_term_string}',
'query-input': 'required name=search_term_string'
}
};
return ( return (
<ThemeProvider> <ThemeProvider>
<Router> <Router>
<div className="min-h-screen bg-white dark:bg-gray-900 transition-colors"> <div className="min-h-screen bg-white dark:bg-gray-900 transition-colors">
<Helmet>
<script type="application/ld+json">
{JSON.stringify(schemaOrgWebsite)}
</script>
</Helmet>
<Navbar /> <Navbar />
<main className="pt-14"> <Breadcrumb />
<Routes>
<Route path="/" element={<HomePage />} /> <main className="pt-14" role="main" aria-label={t('aria.mainContent')}>
<Route path="/projects" element={<ProjectsPage />} /> <Suspense fallback={<LoadingSpinner />}>
<Route path="/updates" element={<UpdatesPage />} /> <Routes>
<Route path="/updates/:id" element={<UpdateDetailPage />} /> <Route path="/" element={<HomePage />} />
<Route path="/contributors" element={<ContributorsPage />} /> <Route path="/projects" element={<ProjectsPage />} />
<Route path="/about" element={<AboutPage />} /> <Route path="/updates" element={<UpdatesPage />} />
</Routes> <Route path="/updates/:id" element={<UpdateDetailPage />} />
<Route path="/contributors" element={<ContributorsPage />} />
<Route path="/about" element={<AboutPage />} />
</Routes>
</Suspense>
</main> </main>
<footer className="bg-gray-900 dark:bg-gray-950 text-white py-6">
<footer className="bg-gray-900 dark:bg-gray-950 text-white py-6" role="contentinfo">
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<p className="text-gray-400 mb-4">{t('footer.copyright')}</p> <p className="text-gray-400 mb-4"> {t('footer.copyright')}</p>
<div className="flex space-x-6"> <div className="flex space-x-6">
{socialLinks.map((link: any) => { {socialLinks.map((link: any) => {
const Icon = iconMap[link.icon as keyof typeof iconMap]; const Icon = iconMap[link.icon as keyof typeof iconMap];
@ -57,10 +88,11 @@ function App() {
href={link.url} href={link.url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="transition-colors" className={link.color}
aria-label={link.name}
title={link.name} title={link.name}
> >
<Icon className={`h-5 w-5 ${link.color}`} /> <Icon className="h-6 w-6" />
</a> </a>
); );
})} })}

View file

@ -0,0 +1,70 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
const Breadcrumb: React.FC = () => {
const { t } = useTranslation();
const location = useLocation();
const pathnames = location.pathname.split('/').filter(x => x);
// 如果是首页,不显示面包屑
if (pathnames.length === 0) {
return null;
}
return (
<nav className="bg-gray-50 dark:bg-gray-800 py-3 px-4" aria-label={t('aria.breadcrumb')}>
<ol className="list-none p-0 inline-flex">
<li className="flex items-center">
<Link
to="/"
className="text-primary hover:text-primary-dark transition-colors"
aria-label={t('nav.home')}
>
<svg
className="w-4 h-4"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" />
</svg>
</Link>
</li>
{pathnames.map((name, index) => {
const routeTo = `/${pathnames.slice(0, index + 1).join('/')}`;
const isLast = index === pathnames.length - 1;
return (
<li key={name} className="flex items-center">
<svg
className="w-4 h-4 mx-2 text-gray-400"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clipRule="evenodd"
/>
</svg>
{isLast ? (
<span className="text-gray-600 dark:text-gray-300" aria-current="page">
{t(`nav.${name}`)}
</span>
) : (
<Link
to={routeTo}
className="text-primary hover:text-primary-dark transition-colors"
>
{t(`nav.${name}`)}
</Link>
)}
</li>
);
})}
</ol>
</nav>
);
};
export default Breadcrumb;

View file

@ -0,0 +1,15 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
const LoadingSpinner: React.FC = () => {
const { t } = useTranslation();
return (
<div className="flex items-center justify-center min-h-[200px]" role="status">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-primary border-t-transparent" />
<span className="sr-only">{t('common.loading')}</span>
</div>
);
};
export default LoadingSpinner;

View file

@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { Book, Menu, X } from 'lucide-react'; import { Menu, X } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import LanguageSwitcher from './LanguageSwitcher'; import LanguageSwitcher from './LanguageSwitcher';
import ThemeSwitcher from './ThemeSwitcher'; import ThemeSwitcher from './ThemeSwitcher';
@ -28,7 +28,7 @@ const Navbar = () => {
<div className="flex justify-between items-center h-14"> <div className="flex justify-between items-center h-14">
<div className="flex items-center"> <div className="flex items-center">
<Link to="/" className="flex items-center"> <Link to="/" className="flex items-center">
<Book className="h-6 w-6 text-blue-600 dark:text-blue-400" /> <img src="/favicon.png" alt="Logo" className="h-6 w-6" />
<span className="ml-2 text-lg font-bold text-gray-900 dark:text-white">{t('brand.name')}</span> <span className="ml-2 text-lg font-bold text-gray-900 dark:text-white">{t('brand.name')}</span>
</Link> </Link>
</div> </div>

View file

@ -1,5 +1,6 @@
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { HelmetProvider } from 'react-helmet-async';
import App from './App.tsx'; import App from './App.tsx';
import './i18n'; import './i18n';
import './index.css'; import './index.css';
@ -15,8 +16,10 @@ const queryClient = new QueryClient({
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<QueryClientProvider client={queryClient}> <HelmetProvider>
<App /> <QueryClientProvider client={queryClient}>
</QueryClientProvider> <App />
</QueryClientProvider>
</HelmetProvider>
</StrictMode> </StrictMode>
); );

View file

@ -1,11 +1,94 @@
import React from 'react'; import React from 'react';
import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
import About from '../components/About'; import About from '../components/About';
const AboutPage = () => { const AboutPage = () => {
const { t, i18n } = useTranslation();
const currentLang = i18n.language;
const pageTitle = t('about.meta.title');
const pageDescription = t('about.meta.description');
// 构建页面级别的结构化数据
const schemaOrg = {
'@context': 'https://schema.org',
'@type': 'AboutPage',
name: pageTitle,
description: pageDescription,
url: 'https://mirror.starset.fans/about',
inLanguage: currentLang,
isPartOf: {
'@type': 'WebSite',
name: 'STARSET Mirror',
url: 'https://mirror.starset.fans'
},
breadcrumb: {
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
item: {
'@id': 'https://mirror.starset.fans',
name: t('nav.home')
}
},
{
'@type': 'ListItem',
position: 2,
item: {
'@id': 'https://mirror.starset.fans/projects',
name: t('nav.projects')
}
},
{
'@type': 'ListItem',
position: 3,
item: {
'@id': 'https://mirror.starset.fans/updates',
name: t('nav.updates')
}
},
{
'@type': 'ListItem',
position: 4,
item: {
'@id': 'https://mirror.starset.fans/contributors',
name: t('nav.contributors')
}
},
{
'@type': 'ListItem',
position: 5,
item: {
'@id': 'https://mirror.starset.fans/about',
name: t('nav.about')
}
}
]
}
};
return ( return (
<div className="py-20"> <>
<About /> <Helmet>
</div> <html lang={currentLang} />
<title>{pageTitle}</title>
<meta name="description" content={pageDescription} />
<meta property="og:title" content={pageTitle} />
<meta property="og:description" content={pageDescription} />
<meta property="og:type" content="website" />
<meta name="twitter:title" content={pageTitle} />
<meta name="twitter:description" content={pageDescription} />
<script type="application/ld+json">
{JSON.stringify(schemaOrg)}
</script>
</Helmet>
<div className="py-20">
<About />
</div>
</>
); );
}; };

View file

@ -1,11 +1,94 @@
import React from 'react'; import React from 'react';
import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
import Contributors from '../components/Contributors'; import Contributors from '../components/Contributors';
const ContributorsPage = () => { const ContributorsPage = () => {
const { t, i18n } = useTranslation();
const currentLang = i18n.language;
const pageTitle = t('contributors.meta.title');
const pageDescription = t('contributors.meta.description');
// 构建页面级别的结构化数据
const schemaOrg = {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: pageTitle,
description: pageDescription,
url: 'https://mirror.starset.fans/contributors',
inLanguage: currentLang,
isPartOf: {
'@type': 'WebSite',
name: 'STARSET Mirror',
url: 'https://mirror.starset.fans'
},
breadcrumb: {
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
item: {
'@id': 'https://mirror.starset.fans',
name: t('nav.home')
}
},
{
'@type': 'ListItem',
position: 2,
item: {
'@id': 'https://mirror.starset.fans/projects',
name: t('nav.projects')
}
},
{
'@type': 'ListItem',
position: 3,
item: {
'@id': 'https://mirror.starset.fans/updates',
name: t('nav.updates')
}
},
{
'@type': 'ListItem',
position: 4,
item: {
'@id': 'https://mirror.starset.fans/contributors',
name: t('nav.contributors')
}
},
{
'@type': 'ListItem',
position: 5,
item: {
'@id': 'https://mirror.starset.fans/about',
name: t('nav.about')
}
}
]
}
};
return ( return (
<div className="pb-20"> <>
<Contributors /> <Helmet>
</div> <html lang={currentLang} />
<title>{pageTitle}</title>
<meta name="description" content={pageDescription} />
<meta property="og:title" content={pageTitle} />
<meta property="og:description" content={pageDescription} />
<meta property="og:type" content="website" />
<meta name="twitter:title" content={pageTitle} />
<meta name="twitter:description" content={pageDescription} />
<script type="application/ld+json">
{JSON.stringify(schemaOrg)}
</script>
</Helmet>
<div className="pb-20">
<Contributors />
</div>
</>
); );
}; };

View file

@ -1,11 +1,93 @@
import React from 'react'; import React from 'react';
import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
import Hero from '../components/Hero'; import Hero from '../components/Hero';
const HomePage = () => { const HomePage: React.FC = () => {
const { t, i18n } = useTranslation();
const currentLang = i18n.language;
const pageTitle = t('home.meta.title');
const pageDescription = t('home.meta.description');
// 构建页面级别的结构化数据
const schemaOrg = {
'@context': 'https://schema.org',
'@type': 'WebPage',
name: pageTitle,
description: pageDescription,
url: 'https://mirror.starset.fans',
inLanguage: currentLang,
isPartOf: {
'@type': 'WebSite',
name: 'STARSET Mirror',
url: 'https://mirror.starset.fans'
},
breadcrumb: {
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
item: {
'@id': 'https://mirror.starset.fans',
name: t('nav.home')
}
},
{
'@type': 'ListItem',
position: 2,
item: {
'@id': 'https://mirror.starset.fans/projects',
name: t('nav.projects')
}
},
{
'@type': 'ListItem',
position: 3,
item: {
'@id': 'https://mirror.starset.fans/updates',
name: t('nav.updates')
}
},
{
'@type': 'ListItem',
position: 4,
item: {
'@id': 'https://mirror.starset.fans/contributors',
name: t('nav.contributors')
}
},
{
'@type': 'ListItem',
position: 5,
item: {
'@id': 'https://mirror.starset.fans/about',
name: t('nav.about')
}
}
]
}
};
return ( return (
<div> <>
<Helmet>
<html lang={currentLang} />
<title>{pageTitle}</title>
<meta name="description" content={pageDescription} />
<meta property="og:title" content={pageTitle} />
<meta property="og:description" content={pageDescription} />
<meta property="og:type" content="website" />
<meta name="twitter:title" content={pageTitle} />
<meta name="twitter:description" content={pageDescription} />
<script type="application/ld+json">
{JSON.stringify(schemaOrg)}
</script>
</Helmet>
<Hero /> <Hero />
</div> </>
); );
}; };

View file

@ -1,11 +1,94 @@
import React from 'react'; import React from 'react';
import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
import Projects from '../components/Projects'; import Projects from '../components/Projects';
const ProjectsPage = () => { const ProjectsPage = () => {
const { t, i18n } = useTranslation();
const currentLang = i18n.language;
const pageTitle = t('projects.meta.title');
const pageDescription = t('projects.meta.description');
// 构建页面级别的结构化数据
const schemaOrg = {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: pageTitle,
description: pageDescription,
url: 'https://mirror.starset.fans/projects',
inLanguage: currentLang,
isPartOf: {
'@type': 'WebSite',
name: 'STARSET Mirror',
url: 'https://mirror.starset.fans'
},
breadcrumb: {
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
item: {
'@id': 'https://mirror.starset.fans',
name: t('nav.home')
}
},
{
'@type': 'ListItem',
position: 2,
item: {
'@id': 'https://mirror.starset.fans/projects',
name: t('nav.projects')
}
},
{
'@type': 'ListItem',
position: 3,
item: {
'@id': 'https://mirror.starset.fans/updates',
name: t('nav.updates')
}
},
{
'@type': 'ListItem',
position: 4,
item: {
'@id': 'https://mirror.starset.fans/contributors',
name: t('nav.contributors')
}
},
{
'@type': 'ListItem',
position: 5,
item: {
'@id': 'https://mirror.starset.fans/about',
name: t('nav.about')
}
}
]
}
};
return ( return (
<div className="py-20 bg-gray-50 dark:bg-gray-900 transition-colors"> <>
<Projects /> <Helmet>
</div> <html lang={currentLang} />
<title>{pageTitle}</title>
<meta name="description" content={pageDescription} />
<meta property="og:title" content={pageTitle} />
<meta property="og:description" content={pageDescription} />
<meta property="og:type" content="website" />
<meta name="twitter:title" content={pageTitle} />
<meta name="twitter:description" content={pageDescription} />
<script type="application/ld+json">
{JSON.stringify(schemaOrg)}
</script>
</Helmet>
<div className="py-20 bg-gray-50 dark:bg-gray-900 transition-colors">
<Projects />
</div>
</>
); );
}; };

View file

@ -1,11 +1,94 @@
import React from 'react'; import React from 'react';
import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
import Timeline from '../components/Timeline'; import Timeline from '../components/Timeline';
const UpdatesPage = () => { const UpdatesPage = () => {
const { t, i18n } = useTranslation();
const currentLang = i18n.language;
const pageTitle = t('updates.meta.title');
const pageDescription = t('updates.meta.description');
// 构建页面级别的结构化数据
const schemaOrg = {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: pageTitle,
description: pageDescription,
url: 'https://mirror.starset.fans/updates',
inLanguage: currentLang,
isPartOf: {
'@type': 'WebSite',
name: 'STARSET Mirror',
url: 'https://mirror.starset.fans'
},
breadcrumb: {
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
item: {
'@id': 'https://mirror.starset.fans',
name: t('nav.home')
}
},
{
'@type': 'ListItem',
position: 2,
item: {
'@id': 'https://mirror.starset.fans/projects',
name: t('nav.projects')
}
},
{
'@type': 'ListItem',
position: 3,
item: {
'@id': 'https://mirror.starset.fans/updates',
name: t('nav.updates')
}
},
{
'@type': 'ListItem',
position: 4,
item: {
'@id': 'https://mirror.starset.fans/contributors',
name: t('nav.contributors')
}
},
{
'@type': 'ListItem',
position: 5,
item: {
'@id': 'https://mirror.starset.fans/about',
name: t('nav.about')
}
}
]
}
};
return ( return (
<div className="py-20"> <>
<Timeline /> <Helmet>
</div> <html lang={currentLang} />
<title>{pageTitle}</title>
<meta name="description" content={pageDescription} />
<meta property="og:title" content={pageTitle} />
<meta property="og:description" content={pageDescription} />
<meta property="og:type" content="website" />
<meta name="twitter:title" content={pageTitle} />
<meta name="twitter:description" content={pageDescription} />
<script type="application/ld+json">
{JSON.stringify(schemaOrg)}
</script>
</Helmet>
<div className="py-20">
<Timeline />
</div>
</>
); );
}; };