feat: accessibility enhancement
All checks were successful
Deploy / Deploy (push) Successful in 1m16s
closes #3
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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": "关闭"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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": "關閉"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
28
index.html
|
@ -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>
|
||||||
|
|
|
@ -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
|
@ -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
After Width: | Height: | Size: 6.4 KiB |
BIN
public/favicon-96x96.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
public/favicon.png
Normal file
After Width: | Height: | Size: 11 KiB |
3
public/favicon.svg
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
public/favicon.zip
Normal file
BIN
public/og-image.jpg
Normal file
After Width: | Height: | Size: 85 KiB |
54
public/robots.txt
Normal 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
|
@ -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
|
@ -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>
|
BIN
public/web-app-manifest-192x192.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
public/web-app-manifest-512x512.png
Normal file
After Width: | Height: | Size: 30 KiB |
|
@ -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 最新動態'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
97
scripts/generate-sitemap.ts
Normal 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();
|
101
scripts/generate-update-sitemaps.js
Normal 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);
|
58
src/App.tsx
|
@ -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,12 +34,36 @@ 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 />
|
||||||
|
|
||||||
|
<main className="pt-14" role="main" aria-label={t('aria.mainContent')}>
|
||||||
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/projects" element={<ProjectsPage />} />
|
<Route path="/projects" element={<ProjectsPage />} />
|
||||||
|
@ -43,11 +72,13 @@ function App() {
|
||||||
<Route path="/contributors" element={<ContributorsPage />} />
|
<Route path="/contributors" element={<ContributorsPage />} />
|
||||||
<Route path="/about" element={<AboutPage />} />
|
<Route path="/about" element={<AboutPage />} />
|
||||||
</Routes>
|
</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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
70
src/components/Breadcrumb.tsx
Normal 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;
|
15
src/components/LoadingSpinner.tsx
Normal 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;
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
<HelmetProvider>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<App />
|
<App />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
</HelmetProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
<div className="py-20">
|
<div className="py-20">
|
||||||
<About />
|
<About />
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
<div className="pb-20">
|
<div className="pb-20">
|
||||||
<Contributors />
|
<Contributors />
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
<div className="py-20 bg-gray-50 dark:bg-gray-900 transition-colors">
|
<div className="py-20 bg-gray-50 dark:bg-gray-900 transition-colors">
|
||||||
<Projects />
|
<Projects />
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
<div className="py-20">
|
<div className="py-20">
|
||||||
<Timeline />
|
<Timeline />
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|