diff --git a/public/init-locale.js b/public/init-locale.js
new file mode 100644
index 0000000..41b4af9
--- /dev/null
+++ b/public/init-locale.js
@@ -0,0 +1,32 @@
+(() => {
+ const canonicalLocale = (value) => {
+ const language = value?.toLowerCase().replaceAll("_", "-");
+ if (!language) return null;
+ if (/^zh(?:-|$)/.test(language)) return /^zh-(tw|hk|mo)(-|$)|-hant(?:-|$)/.test(language) ? "zh-Hant" : "zh-Hans";
+ if (/^en(?:-|$)/.test(language)) return "en";
+ if (/^(ja|jp)(?:-|$)/.test(language)) return "jp";
+ return null;
+ };
+ const parts = location.pathname.split("/");
+ const firstSegment = parts[1];
+ const pathLocale = canonicalLocale(firstSegment);
+ const isUnknownLanguagePath = !pathLocale && /^[a-z]{2,3}(?:-[a-z0-9]+)*$/i.test(firstSegment ?? "");
+ let locale = pathLocale;
+ if (!locale) {
+ try { locale = canonicalLocale(localStorage.getItem("kr-locale")); } catch {}
+ }
+ if (!locale) {
+ for (const language of navigator.languages?.length ? navigator.languages : [navigator.language]) {
+ locale = canonicalLocale(language);
+ if (locale) break;
+ }
+ }
+ locale ??= "en";
+ const path = pathLocale || isUnknownLanguagePath ? `/${parts.slice(2).join("/")}` : location.pathname;
+ const normalizedPath = path.replace(/\/$/, "");
+ const destination = `/${locale}${normalizedPath}/`;
+ const search = new URLSearchParams(location.search);
+ search.delete("lang");
+ const canonicalUrl = destination + (search.size ? `?${search}` : "") + location.hash;
+ if (location.pathname + location.search + location.hash !== canonicalUrl) location.replace(canonicalUrl);
+})();
diff --git a/public/init-theme.js b/public/init-theme.js
new file mode 100644
index 0000000..7014bf2
--- /dev/null
+++ b/public/init-theme.js
@@ -0,0 +1,12 @@
+(() => {
+ const isTheme = (value) => value === "light" || value === "system" || value === "dark";
+ let preference = "system";
+ try {
+ const cookieTheme = document.cookie.split(";").map((entry) => entry.trim()).find((entry) => entry.startsWith("kr-theme="))?.slice("kr-theme=".length);
+ const savedTheme = isTheme(cookieTheme) ? cookieTheme : localStorage.getItem("kr-theme");
+ if (isTheme(savedTheme)) preference = savedTheme;
+ } catch {}
+ const theme = preference === "system" ? (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light") : preference;
+ document.documentElement.dataset.theme = theme;
+ document.documentElement.style.colorScheme = theme;
+})();
diff --git a/public/links/blueakio.svg b/public/links/blueakio.svg
new file mode 100644
index 0000000..8eafce9
--- /dev/null
+++ b/public/links/blueakio.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/public/links/fetarute.png b/public/links/fetarute.png
new file mode 100644
index 0000000..c7fff33
Binary files /dev/null and b/public/links/fetarute.png differ
diff --git a/public/links/friendly-links-hero.png b/public/links/friendly-links-hero.png
new file mode 100644
index 0000000..19b5a81
Binary files /dev/null and b/public/links/friendly-links-hero.png differ
diff --git a/src/app/(legacy)/links/page.tsx b/src/app/(legacy)/links/page.tsx
new file mode 100644
index 0000000..7407b3e
--- /dev/null
+++ b/src/app/(legacy)/links/page.tsx
@@ -0,0 +1,13 @@
+import type { Metadata } from "next";
+import { FriendlyLinksPage } from "@/components/links/friendly-links-page";
+import { getLocaleMessages, isLocale } from "@/lib/i18n";
+
+export async function generateMetadata({ params }: { params: Promise<{ locale?: string }> }): Promise {
+ const { locale } = await params;
+ return { title: getLocaleMessages(isLocale(locale) ? locale : "zh-Hans").footer.legal[2] };
+}
+
+export default async function LinksPage({ params }: { params: Promise<{ locale?: string }> }) {
+ const { locale } = await params;
+ return ;
+}
diff --git a/src/app/[locale]/links/page.tsx b/src/app/[locale]/links/page.tsx
new file mode 100644
index 0000000..657112c
--- /dev/null
+++ b/src/app/[locale]/links/page.tsx
@@ -0,0 +1 @@
+export { default, generateMetadata } from "@/app/(legacy)/links/page";
diff --git a/src/app/globals.css b/src/app/globals.css
index bbdf3ce..fbeb7e6 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -55,6 +55,20 @@ a { color: inherit; text-decoration: none; }
.home-collage { position: relative; height: 648px; margin-top: 50px; }.home-collage-tile { position: absolute; border-radius: 13px; background: #d9d9d9; }.home-collage-wide { top: 0; left: 0; width: 41.4%; height: 359px; }.home-collage-medium { top: 0; left: 42.57%; width: 28.87%; height: 359px; }.home-collage-small { top: 376px; left: 27.28%; width: 44.3%; height: 272px; }.home-collage-bottom { top: 376px; left: 0; width: 25.83%; height: 272px; }.home-collage-tall { top: 0; right: 0; width: 27.1%; height: 648px; }
.news-page { padding: 30px 0 60px; }
+.friendly-links-hero { background: var(--policy-hero-image) center 26.71% / 100% auto no-repeat; }
+.friendly-links-page-inner { gap: 50px; }
+.friendly-links-content { width: min(1214px, 100%); margin: 0 auto; }
+.friendly-links-section + .friendly-links-section { margin-top: 50px; }
+.friendly-links-section h2 { margin: 0 0 20px; color: #181c20; font-size: 40px; font-weight: 700; line-height: 48px; }
+.friendly-links-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
+.friendly-link-card { position: relative; display: flex; align-items: stretch; min-width: 0; min-height: 186px; overflow: hidden; border: 1.1px solid #e3e6e8; border-radius: 16.42px; background: #fff; color: #181c20; transition: border-color .2s ease, box-shadow .2s ease, transform .2s ease; }
+a.friendly-link-card:hover, a.friendly-link-card:focus-visible { border-color: #8f56a2; box-shadow: 0 8px 20px rgba(40, 29, 47, .1); transform: translateY(-2px); }
+.friendly-link-card-content { display: flex; flex: 1; flex-direction: column; align-items: center; justify-content: center; gap: 16.42px; padding: 32.83px 10.94px; }
+.friendly-link-card-heading { display: flex; align-items: center; justify-content: center; gap: 16px; min-width: 0; font-size: 27.36px; font-weight: 400; line-height: normal; white-space: nowrap; }
+.friendly-link-card-heading img { width: 65.67px; height: 65.67px; overflow: hidden; border-radius: 8px; object-fit: cover; }
+.friendly-link-card-description { color: #8d949a; font-size: 16.42px; line-height: 21.89px; text-align: center; }
+.friendly-link-card-archive { position: absolute; top: 13px; right: 5px; padding: 4px 6px; border-radius: 4px; background: #e72329; color: #fff; font-size: 10px; line-height: 12px; }
+.friendly-links-page .page-bottom-nav { margin-top: 176px; }
.news-page-inner { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
.news-page-header { display: flex; justify-content: center; align-items: center; margin: 0 0 30px; }
.news-page-header h1 { margin: 0; font-size: clamp(52px, 5vw, 88px); letter-spacing: -0.07em; line-height: 1; font-weight: 800; }
@@ -104,7 +118,7 @@ a { color: inherit; text-decoration: none; }
.news-detail-body a:hover, .policy-body a:hover { color: #8f56a2; }
.policy-page { padding: 0 0 48px; background: #fff; }
.policy-page-inner { display: flex; flex-direction: column; gap: 50px; align-items: center; padding: 0 40px 48px; }
-.policy-hero { width: min(1200px, 100%); min-height: 322px; display: flex; align-items: flex-start; padding: 156px 48px 0; border-radius: 10px; background: linear-gradient(110deg, rgba(143, 86, 162, 0.82), rgba(143, 86, 162, 0.18)), url("/policy-hero.png") center / cover; overflow: hidden; }
+.policy-hero { width: min(1200px, 100%); min-height: 322px; display: flex; align-items: flex-start; padding: 156px 48px 0; border-radius: 10px; background: linear-gradient(110deg, rgba(143, 86, 162, 0.82), rgba(143, 86, 162, 0.18)), var(--policy-hero-image, url("/policy-hero.png")) center / cover; overflow: hidden; }
.policy-hero-inner { margin-left: -48px; padding: 10px 46px; background: #8f56a2; }
.policy-hero h1 { margin: 0; color: #fff; font-size: clamp(38px, 4vw, 62px); line-height: 1.3; letter-spacing: 0.25px; }
.policy-card { width: min(1200px, 100%); padding: 28px; border-radius: 26px; background: #fefdfa; box-shadow: 0 10px 15px rgba(15, 23, 42, 0.04); }
@@ -188,6 +202,9 @@ html[data-theme="dark"] .theme-toggle button:hover, html[data-theme="dark"] .the
html[data-theme="dark"] .theme-toggle button.active:hover, html[data-theme="dark"] .theme-toggle button.active:focus-visible { background: #2b3746; }
html[data-theme="dark"] .theme-toggle img { filter: brightness(0) invert(1); }
html[data-theme="dark"] .news-toolbar { border-color: rgba(255, 255, 255, 0.16); }
+html[data-theme="dark"] .friendly-links-section h2 { color: var(--ink); }
+html[data-theme="dark"] .friendly-link-card { border-color: #394553; background: #19222d; color: var(--ink); }
+html[data-theme="dark"] .friendly-link-card-description { color: #a9b5c1; }
html[data-theme="dark"] .news-filter-button, html[data-theme="dark"] .news-view-toggle { color: var(--ink); border-color: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.04); }
html[data-theme="dark"] .news-pagination-button, html[data-theme="dark"] .news-share-button { color: var(--ink); border-color: rgba(255, 255, 255, 0.2); background: #1b2531; }
html[data-theme="dark"] .news-detail-card { background: #19222d; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); }
@@ -224,6 +241,9 @@ html[data-theme="dark"] .site-footer-title, html[data-theme="dark"] .site-footer
html:not([data-theme]) .theme-toggle button.active:hover, html:not([data-theme]) .theme-toggle button.active:focus-visible { background: #2b3746; }
html:not([data-theme]) .theme-toggle img { filter: brightness(0) invert(1); }
html:not([data-theme]) .news-toolbar { border-color: rgba(255, 255, 255, 0.16); }
+ html:not([data-theme]) .friendly-links-section h2 { color: var(--ink); }
+ html:not([data-theme]) .friendly-link-card { border-color: #394553; background: #19222d; color: var(--ink); }
+ html:not([data-theme]) .friendly-link-card-description { color: #a9b5c1; }
html:not([data-theme]) .news-filter-button, html:not([data-theme]) .news-view-toggle { color: var(--ink); border-color: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.04); }
html:not([data-theme]) .news-pagination-button, html:not([data-theme]) .news-share-button { color: var(--ink); border-color: rgba(255, 255, 255, 0.2); background: #1b2531; }
html:not([data-theme]) .news-detail-card { background: #19222d; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); }
@@ -277,6 +297,7 @@ html[data-theme="dark"] .site-footer-title, html[data-theme="dark"] .site-footer
.home-collage-small { top: 202px; left: 39%; width: 61%; height: 158px; }
.home-collage-tall { display: none; }
.news-page-inner { padding: 0 16px; }
+ .friendly-links-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.news-page-header h1 { font-size: clamp(46px, 12vw, 72px); }
.news-toolbar { align-items: flex-start; flex-wrap: wrap; gap: 16px; }
.news-toolbar-right { margin-left: auto; }
@@ -337,6 +358,10 @@ html[data-theme="dark"] .site-footer-title, html[data-theme="dark"] .site-footer
.home-collage-bottom, .home-collage-small { top: 160px; height: 120px; }
.news-page { padding-top: 20px; }
+ .friendly-links-content { width: 100%; }
+ .friendly-links-section + .friendly-links-section { margin-top: 36px; }
+ .friendly-links-section h2 { margin-bottom: 16px; font-size: 30px; line-height: 1.35; }
+ .friendly-links-grid { grid-template-columns: 1fr; gap: 12px; }
.news-page-inner { padding: 0 12px; }
.news-page-header { margin-bottom: 20px; }
.news-page-header h1 { font-size: 52px; }
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index e2a664f..a3baed0 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,7 +1,16 @@
-import SiteDocument from "@/components/layout/site-document";
+import Script from "next/script";
+import "@/app/globals.css";
export { metadata, viewport } from "@/components/layout/site-document";
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
- return {children};
+ return (
+
+
+ {children}
+
+
+
+
+ );
}
diff --git a/src/components/layout/site-document.tsx b/src/components/layout/site-document.tsx
index 81814c6..d8a90f5 100644
--- a/src/components/layout/site-document.tsx
+++ b/src/components/layout/site-document.tsx
@@ -1,5 +1,4 @@
import type { Metadata, Viewport } from "next";
-import "@/app/globals.css";
export const metadata: Metadata = {
title: {
@@ -7,75 +6,10 @@ export const metadata: Metadata = {
template: "%s | 基沃托斯铁道公团 KivotosRailway",
},
description: "Kivotos Railway website",
- icons: {
- icon: "/icon/icon.svg",
- },
+ icons: { icon: "/icon/icon.svg" },
};
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
};
-
-export default function RootLayout({
- children,
-}: Readonly<{ children: React.ReactNode }>) {
- return (
-
-
- {/* Run during HTML parsing, before the body can paint. */}
-