修改页面标题路由

This commit is contained in:
2026-08-22 22:40:25 +08:00
parent 4e778d895b
commit 65322c20c1
11 changed files with 137 additions and 15 deletions
+4 -1
View File
@@ -4,7 +4,10 @@ import { SiteFooter } from "@/components/layout/site-footer";
import { SiteHeader } from "@/components/layout/site-header"; import { SiteHeader } from "@/components/layout/site-header";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Kivotos Railway", title: {
default: "基沃托斯铁道公团 KivotosRailway",
template: "%s | 基沃托斯铁道公团 KivotosRailway",
},
description: "Kivotos Railway website", description: "Kivotos Railway website",
icons: { icons: {
icon: "/icon/icon.svg", icon: "/icon/icon.svg",
+14
View File
@@ -1,8 +1,22 @@
import type { Metadata } from "next";
import Link from "next/link"; import Link from "next/link";
import { getNewsBySlug, markdownToHtml } from "@/data/news"; import { getNewsBySlug, markdownToHtml } from "@/data/news";
import { PageBreadcrumb } from "@/components/layout/page-breadcrumb"; import { PageBreadcrumb } from "@/components/layout/page-breadcrumb";
import { NewsShare } from "@/components/news/news-share"; import { NewsShare } from "@/components/news/news-share";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const item = await getNewsBySlug(slug);
return {
title: item?.title ?? "新闻",
};
}
export default async function NewsDetailPage({ export default async function NewsDetailPage({
params, params,
}: { }: {
+5 -1
View File
@@ -1,8 +1,12 @@
import Link from "next/link"; import type { Metadata } from "next";
import { getNewsList } from "@/data/news"; import { getNewsList } from "@/data/news";
import { PageBreadcrumb } from "@/components/layout/page-breadcrumb"; import { PageBreadcrumb } from "@/components/layout/page-breadcrumb";
import { NewsList } from "@/components/news/news-list"; import { NewsList } from "@/components/news/news-list";
export const metadata: Metadata = {
title: "新闻",
};
export default async function NewsPage() { export default async function NewsPage() {
const news = await getNewsList(); const news = await getNewsList();
+2 -1
View File
@@ -1,8 +1,9 @@
import Link from "next/link"; import Link from "next/link";
import { getLocaleMessages } from "@/lib/i18n";
export function PageBreadcrumb({ items }: { items: Array<{ label: string; href?: string }> }) { export function PageBreadcrumb({ items }: { items: Array<{ label: string; href?: string }> }) {
return ( return (
<nav className="page-breadcrumb" aria-label="页面位置"> <nav className="page-breadcrumb" aria-label={getLocaleMessages("zh-CN").common.pagePosition}>
{items.map((item, index) => ( {items.map((item, index) => (
<span className="page-breadcrumb-item" key={`${item.label}-${index}`}> <span className="page-breadcrumb-item" key={`${item.label}-${index}`}>
{index > 0 && <span className="page-breadcrumb-separator" aria-hidden="true">›</span>} {index > 0 && <span className="page-breadcrumb-separator" aria-hidden="true">›</span>}
+17 -1
View File
@@ -10,7 +10,23 @@ export function SiteHeader() {
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => { useEffect(() => {
const syncLocale = (nextLocale: Locale) => setLocale(nextLocale); const updateDocumentTitle = (nextLocale: Locale) => {
const messages = getLocaleMessages(nextLocale);
const pathname = window.location.pathname;
const currentTitle = document.title.split(" | ")[0];
const pageLabel = pathname === "/"
? ""
: pathname === "/news"
? messages.common.news
: currentTitle;
document.title = pageLabel ? `${pageLabel} | ${messages.common.siteTitle}` : messages.common.siteTitle;
};
const syncLocale = (nextLocale: Locale) => {
setLocale(nextLocale);
updateDocumentTitle(nextLocale);
};
const handleLocaleChange = (event: Event) => { const handleLocaleChange = (event: Event) => {
const nextLocale = (event as CustomEvent<{ locale?: Locale }>).detail?.locale ?? getInitialLocale(); const nextLocale = (event as CustomEvent<{ locale?: Locale }>).detail?.locale ?? getInitialLocale();
+17 -3
View File
@@ -1,11 +1,25 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useEffect, useState } from "react";
import type { NewsItem } from "@/data/news"; import type { NewsItem } from "@/data/news";
import { getInitialLocale, getLocaleMessages, type Locale } from "@/lib/i18n";
export function NewsList({ news }: { news: NewsItem[] }) { export function NewsList({ news }: { news: NewsItem[] }) {
const [activeCategory, setActiveCategory] = useState<string | null>(null); const [activeCategory, setActiveCategory] = useState<string | null>(null);
const [locale, setLocale] = useState<Locale>("zh-CN");
const messages = getLocaleMessages(locale);
useEffect(() => {
const syncLocale = () => setLocale(getInitialLocale());
syncLocale();
window.addEventListener("kr-locale-change", syncLocale);
window.addEventListener("storage", syncLocale);
return () => {
window.removeEventListener("kr-locale-change", syncLocale);
window.removeEventListener("storage", syncLocale);
};
}, []);
const categories = Array.from(new Set(news.map((item) => item.category).filter(Boolean))); const categories = Array.from(new Set(news.map((item) => item.category).filter(Boolean)));
const visibleNews = activeCategory const visibleNews = activeCategory
? news.filter((item) => item.category === activeCategory) ? news.filter((item) => item.category === activeCategory)
@@ -20,7 +34,7 @@ export function NewsList({ news }: { news: NewsItem[] }) {
className={`news-filter-button${activeCategory === null ? " active" : ""}`} className={`news-filter-button${activeCategory === null ? " active" : ""}`}
onClick={() => setActiveCategory(null)} onClick={() => setActiveCategory(null)}
> >
全部 {messages.common.all}
</button> </button>
{categories.map((category) => ( {categories.map((category) => (
<button <button
@@ -34,7 +48,7 @@ export function NewsList({ news }: { news: NewsItem[] }) {
))} ))}
</div> </div>
<div className="news-toolbar-right"> <div className="news-toolbar-right">
<button type="button" className="news-view-toggle" aria-label="列表视图">☰</button> <button type="button" className="news-view-toggle" aria-label={messages.common.listView}>☰</button>
</div> </div>
</div> </div>
+20 -6
View File
@@ -1,9 +1,23 @@
"use client"; "use client";
import { useState } from "react"; import { useEffect, useState } from "react";
import { getInitialLocale, getLocaleMessages, type Locale } from "@/lib/i18n";
export function NewsShare({ title }: { title: string }) { export function NewsShare({ title }: { title: string }) {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [locale, setLocale] = useState<Locale>("zh-CN");
const messages = getLocaleMessages(locale);
useEffect(() => {
const syncLocale = () => setLocale(getInitialLocale());
syncLocale();
window.addEventListener("kr-locale-change", syncLocale);
window.addEventListener("storage", syncLocale);
return () => {
window.removeEventListener("kr-locale-change", syncLocale);
window.removeEventListener("storage", syncLocale);
};
}, []);
const getPageUrl = () => window.location.href; const getPageUrl = () => window.location.href;
@@ -27,13 +41,13 @@ export function NewsShare({ title }: { title: string }) {
return ( return (
<section className="news-share" aria-labelledby="news-share-title"> <section className="news-share" aria-labelledby="news-share-title">
<h2 id="news-share-title">分享</h2> <h2 id="news-share-title">{messages.common.share}</h2>
<div className="news-share-actions"> <div className="news-share-actions">
<button type="button" className="news-share-button" onClick={copyLink} aria-label="复制链接"> <button type="button" className="news-share-button" onClick={copyLink} aria-label={messages.common.copyLink}>
{copied ? "已复制" : "复制链接"} {copied ? messages.common.copied : messages.common.copyLink}
</button> </button>
<button type="button" className="news-share-button news-share-x" onClick={shareOnX} aria-label="分享到 X">X</button> <button type="button" className="news-share-button news-share-x" onClick={shareOnX} aria-label={messages.common.shareX}>X</button>
<button type="button" className="news-share-button news-share-qq" onClick={shareOnQq} aria-label="分享到 QQ">QQ</button> <button type="button" className="news-share-button news-share-qq" onClick={shareOnQq} aria-label={messages.common.shareQq}>QQ</button>
</div> </div>
</section> </section>
); );
+15 -1
View File
@@ -5,12 +5,26 @@
}, },
"home": { "home": {
"kicker": "KIVOTOS RAILWAY / MINECRAFT SERVER", "kicker": "KIVOTOS RAILWAY / MINECRAFT SERVER",
"title": ["Connecting every journey", "by rail"], "title": ["Railways", "connect us"],
"subtitle": "A Minecraft railway world built and expanded by players together.", "subtitle": "A Minecraft railway world built and expanded by players together.",
"cta": "Explore KivotosRailway", "cta": "Explore KivotosRailway",
"latest": "Latest News", "latest": "Latest News",
"more": "View more" "more": "View more"
}, },
"common": {
"siteTitle": "KivotosRailway",
"home": "Home",
"news": "News",
"share": "Share",
"copyLink": "Copy link",
"copied": "Copied",
"shareX": "Share on X",
"shareQq": "Share on QQ",
"all": "All",
"listView": "List view",
"pagePosition": "Page position",
"backToNews": "Back to news list"
},
"footer": { "footer": {
"follow": "Follow us", "follow": "Follow us",
"social": ["Weibo", "WeChat", "X", "LinkedIn", "Video"], "social": ["Weibo", "WeChat", "X", "LinkedIn", "Video"],
+14
View File
@@ -11,6 +11,20 @@
"latest": "最新ニュース", "latest": "最新ニュース",
"more": "もっと見る" "more": "もっと見る"
}, },
"common": {
"siteTitle": "KivotosRailway",
"home": "ホーム",
"news": "ニュース",
"share": "共有",
"copyLink": "リンクをコピー",
"copied": "コピーしました",
"shareX": "Xで共有",
"shareQq": "QQで共有",
"all": "すべて",
"listView": "リスト表示",
"pagePosition": "ページの位置",
"backToNews": "ニュース一覧へ戻る"
},
"footer": { "footer": {
"follow": "フォローする", "follow": "フォローする",
"social": ["Weibo", "WeChat", "X", "LinkedIn", "動画"], "social": ["Weibo", "WeChat", "X", "LinkedIn", "動画"],
+15 -1
View File
@@ -11,6 +11,20 @@
"latest": "最新资讯", "latest": "最新资讯",
"more": "浏览更多" "more": "浏览更多"
}, },
"common": {
"siteTitle": "基沃托斯铁道公团 KivotosRailway",
"home": "首页",
"news": "新闻",
"share": "分享",
"copyLink": "复制链接",
"copied": "已复制",
"shareX": "分享到 X",
"shareQq": "分享到 QQ",
"all": "全部",
"listView": "列表视图",
"pagePosition": "页面位置",
"backToNews": "返回新闻列表"
},
"footer": { "footer": {
"follow": "关注我们", "follow": "关注我们",
"social": ["微博", "微信", "X", "LinkedIn", "视频"], "social": ["微博", "微信", "X", "LinkedIn", "视频"],
@@ -22,6 +36,6 @@
"legal": ["KR Wiki", "状态监控", "友情链接"], "legal": ["KR Wiki", "状态监控", "友情链接"],
"description": "Minecraft 是 Mojang Studios 的注册商标,基沃托斯铁道公团与 Mojang Studios 及微软公司、网易公司没有从属、授权或合作关系。站内提及的城市、地区、组织、品牌及相关设定,除特别说明外,均为虚构内容,不指向任何现实机构或商业实体。", "description": "Minecraft 是 Mojang Studios 的注册商标,基沃托斯铁道公团与 Mojang Studios 及微软公司、网易公司没有从属、授权或合作关系。站内提及的城市、地区、组织、品牌及相关设定,除特别说明外,均为虚构内容,不指向任何现实机构或商业实体。",
"copyright": "© 2024 - 2026 基沃托斯铁道公团 KivotosRailway", "copyright": "© 2024 - 2026 基沃托斯铁道公团 KivotosRailway",
"icp": "桂ICP备2025054269号-5" "icp": ""
} }
} }
+14
View File
@@ -11,6 +11,20 @@
"latest": "最新資訊", "latest": "最新資訊",
"more": "瀏覽更多" "more": "瀏覽更多"
}, },
"common": {
"siteTitle": "基沃托斯鐵道公團 KivotosRailway",
"home": "首頁",
"news": "新聞",
"share": "分享",
"copyLink": "複製連結",
"copied": "已複製",
"shareX": "分享到 X",
"shareQq": "分享到 QQ",
"all": "全部",
"listView": "列表視圖",
"pagePosition": "頁面位置",
"backToNews": "返回新聞列表"
},
"footer": { "footer": {
"follow": "關注我們", "follow": "關注我們",
"social": ["微博", "微信", "X", "LinkedIn", "影片"], "social": ["微博", "微信", "X", "LinkedIn", "影片"],