feat: accessibility enhancement
All checks were successful
Deploy / Deploy (push) Successful in 1m16s
All checks were successful
Deploy / Deploy (push) Successful in 1m16s
closes #3
This commit is contained in:
parent
3e652d5e4e
commit
01c5131055
31 changed files with 1121 additions and 58 deletions
74
src/App.tsx
74
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 { useTranslation } from 'react-i18next';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import Navbar from './components/Navbar';
|
||||
import HomePage from './pages/HomePage';
|
||||
import ProjectsPage from './pages/ProjectsPage';
|
||||
import UpdatesPage from './pages/UpdatesPage';
|
||||
import UpdateDetailPage from './pages/UpdateDetailPage';
|
||||
import ContributorsPage from './pages/ContributorsPage';
|
||||
import AboutPage from './pages/AboutPage';
|
||||
import Breadcrumb from './components/Breadcrumb';
|
||||
import LoadingSpinner from './components/LoadingSpinner';
|
||||
import iconMap from './utils/iconMap';
|
||||
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() {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const socialLinks = t('social.links', { returnObjects: true });
|
||||
|
||||
|
@ -29,25 +34,51 @@ function App() {
|
|||
};
|
||||
}, [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 (
|
||||
<ThemeProvider>
|
||||
<Router>
|
||||
<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 />
|
||||
<main className="pt-14">
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/projects" element={<ProjectsPage />} />
|
||||
<Route path="/updates" element={<UpdatesPage />} />
|
||||
<Route path="/updates/:id" element={<UpdateDetailPage />} />
|
||||
<Route path="/contributors" element={<ContributorsPage />} />
|
||||
<Route path="/about" element={<AboutPage />} />
|
||||
</Routes>
|
||||
<Breadcrumb />
|
||||
|
||||
<main className="pt-14" role="main" aria-label={t('aria.mainContent')}>
|
||||
<Suspense fallback={<LoadingSpinner />}>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/projects" element={<ProjectsPage />} />
|
||||
<Route path="/updates" element={<UpdatesPage />} />
|
||||
<Route path="/updates/:id" element={<UpdateDetailPage />} />
|
||||
<Route path="/contributors" element={<ContributorsPage />} />
|
||||
<Route path="/about" element={<AboutPage />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</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="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">
|
||||
{socialLinks.map((link: any) => {
|
||||
const Icon = iconMap[link.icon as keyof typeof iconMap];
|
||||
|
@ -57,10 +88,11 @@ function App() {
|
|||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="transition-colors"
|
||||
className={link.color}
|
||||
aria-label={link.name}
|
||||
title={link.name}
|
||||
>
|
||||
<Icon className={`h-5 w-5 ${link.color}`} />
|
||||
<Icon className="h-6 w-6" />
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
|
|
70
src/components/Breadcrumb.tsx
Normal file
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
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 { 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 LanguageSwitcher from './LanguageSwitcher';
|
||||
import ThemeSwitcher from './ThemeSwitcher';
|
||||
|
@ -28,7 +28,7 @@ const Navbar = () => {
|
|||
<div className="flex justify-between items-center h-14">
|
||||
<div 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>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { HelmetProvider } from 'react-helmet-async';
|
||||
import App from './App.tsx';
|
||||
import './i18n';
|
||||
import './index.css';
|
||||
|
@ -15,8 +16,10 @@ const queryClient = new QueryClient({
|
|||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
<HelmetProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</HelmetProvider>
|
||||
</StrictMode>
|
||||
);
|
|
@ -1,11 +1,94 @@
|
|||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import About from '../components/About';
|
||||
|
||||
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 (
|
||||
<div className="py-20">
|
||||
<About />
|
||||
</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>
|
||||
<div className="py-20">
|
||||
<About />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,11 +1,94 @@
|
|||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Contributors from '../components/Contributors';
|
||||
|
||||
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 (
|
||||
<div className="pb-20">
|
||||
<Contributors />
|
||||
</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>
|
||||
<div className="pb-20">
|
||||
<Contributors />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,11 +1,93 @@
|
|||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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 (
|
||||
<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 />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,11 +1,94 @@
|
|||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Projects from '../components/Projects';
|
||||
|
||||
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 (
|
||||
<div className="py-20 bg-gray-50 dark:bg-gray-900 transition-colors">
|
||||
<Projects />
|
||||
</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>
|
||||
<div className="py-20 bg-gray-50 dark:bg-gray-900 transition-colors">
|
||||
<Projects />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,11 +1,94 @@
|
|||
import React from 'react';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Timeline from '../components/Timeline';
|
||||
|
||||
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 (
|
||||
<div className="py-20">
|
||||
<Timeline />
|
||||
</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>
|
||||
<div className="py-20">
|
||||
<Timeline />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue