feat: enhance SEO and localization support with meta tags and structured data

This commit is contained in:
2026-08-13 23:03:07 +08:00
parent b6a8b86a72
commit d892c9751b
8 changed files with 140 additions and 8 deletions
+38 -3
View File
@@ -6,11 +6,46 @@
<link rel="apple-touch-icon" href="/favicon.png" /> <link rel="apple-touch-icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
<title>Haoze Zhou</title> <meta name="theme-color" content="#f4f1eb" />
<meta name="description" content="Haoze Zhou — CS undergraduate at Universiti Malaya. Projects and self-hosted infrastructure." /> <title>Haoze Zhou | Computer Science Student &amp; Developer</title>
<meta name="description" content="Haoze Zhou is a Computer Science undergraduate at Universiti Malaya building software, self-hosted infrastructure, and open-source projects." />
<meta name="author" content="Haoze Zhou" />
<meta name="robots" content="index, follow, max-image-preview:large" />
<link rel="canonical" href="https://www.zhouhaoze.com/" />
<link rel="alternate" hreflang="en" href="https://www.zhouhaoze.com/" />
<link rel="alternate" hreflang="zh-CN" href="https://www.zhouhaoze.cn/" />
<link rel="alternate" hreflang="x-default" href="https://www.zhouhaoze.com/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Haoze Zhou" />
<meta property="og:title" content="Haoze Zhou | Computer Science Student &amp; Developer" />
<meta property="og:description" content="Software projects and self-hosted infrastructure by Haoze Zhou." />
<meta property="og:url" content="https://www.zhouhaoze.com/" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="zh_CN" />
<meta property="og:image" content="https://www.zhouhaoze.com/favicon.png" />
<meta property="og:image:alt" content="Haoze Zhou personal website" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="Haoze Zhou | Computer Science Student &amp; Developer" />
<meta name="twitter:description" content="Software projects and self-hosted infrastructure by Haoze Zhou." />
<meta name="twitter:image" content="https://www.zhouhaoze.com/favicon.png" />
</head> </head>
<body> <body>
<div id="app"></div> <div id="app">
<noscript>
<main>
<h1>Haoze Zhou</h1>
<p>Computer Science undergraduate at Universiti Malaya building software, open-source projects, and self-hosted infrastructure.</p>
<nav aria-label="Primary navigation">
<a href="#about">About</a>
<a href="#projects">Projects</a>
<a href="#contact">Contact</a>
</nav>
<section id="about"><h2>About</h2><p>I study Computer Science at Universiti Malaya and learn by building practical systems.</p></section>
<section id="projects"><h2>Projects</h2><p>Explore my open-source work, including AuthRouter, a unified identity-provider login gateway.</p></section>
<section id="contact"><h2>Contact</h2><p><a href="mailto:me@zhouhaoze.com">Email Haoze Zhou</a> · <a href="https://github.com/YIYI-16">GitHub</a></p></section>
</main>
</noscript>
</div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>
+1
View File
@@ -2,5 +2,6 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url> <url>
<loc>https://www.zhouhaoze.cn/</loc> <loc>https://www.zhouhaoze.cn/</loc>
<lastmod>2026-08-13</lastmod>
</url> </url>
</urlset> </urlset>
+1
View File
@@ -2,5 +2,6 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url> <url>
<loc>https://www.zhouhaoze.com/</loc> <loc>https://www.zhouhaoze.com/</loc>
<lastmod>2026-08-13</lastmod>
</url> </url>
</urlset> </urlset>
+4 -2
View File
@@ -1,5 +1,6 @@
import en from "./locales/en.json"; import en from "./locales/en.json";
import zh from "./locales/zh.json"; import zh from "./locales/zh.json";
import { updateSeo } from "./seo";
export type Lang = "en" | "zh"; export type Lang = "en" | "zh";
@@ -8,6 +9,7 @@ const dictionaries: Record<Lang, Record<string, string>> = { en, zh };
const STORAGE_KEY = "pw-lang"; const STORAGE_KEY = "pw-lang";
function detectLang(): Lang { function detectLang(): Lang {
if (window.location.hostname.endsWith(".cn")) return "zh";
const stored = localStorage.getItem(STORAGE_KEY); const stored = localStorage.getItem(STORAGE_KEY);
if (stored === "en" || stored === "zh") return stored; if (stored === "en" || stored === "zh") return stored;
return navigator.language.toLowerCase().startsWith("zh") ? "zh" : "en"; return navigator.language.toLowerCase().startsWith("zh") ? "zh" : "en";
@@ -27,7 +29,7 @@ export function t(key: string): string {
export function setLang(lang: Lang): void { export function setLang(lang: Lang): void {
currentLang = lang; currentLang = lang;
localStorage.setItem(STORAGE_KEY, lang); localStorage.setItem(STORAGE_KEY, lang);
document.documentElement.lang = lang === "zh" ? "zh-CN" : "en"; updateSeo(lang);
applyTranslations(); applyTranslations();
listeners.forEach((fn) => fn(lang)); listeners.forEach((fn) => fn(lang));
} }
@@ -53,6 +55,6 @@ export function applyTranslations(): void {
} }
export function initI18n(): void { export function initI18n(): void {
document.documentElement.lang = currentLang === "zh" ? "zh-CN" : "en"; updateSeo(currentLang);
applyTranslations(); applyTranslations();
} }
+1
View File
@@ -47,6 +47,7 @@
"projects.title": "Projects", "projects.title": "Projects",
"projects.lead": "Things I've written and maintained. Click a card for more detail.", "projects.lead": "Things I've written and maintained. Click a card for more detail.",
"projects.authrouter.tag": "Featured", "projects.authrouter.tag": "Featured",
"authrouter.imageAlt": "AuthRouter identity provider gateway project screenshot",
"projects.authrouter.title": "AuthRouter", "projects.authrouter.title": "AuthRouter",
"projects.authrouter.desc": "Consolidates multiple identity providers behind a single login endpoint, with identity mapping across providers.", "projects.authrouter.desc": "Consolidates multiple identity providers behind a single login endpoint, with identity mapping across providers.",
"projects.authrouter.detail": "Systems that connect to several identity providers often force users to jump between different login entry points, and identity data ends up scattered across them. AuthRouter merges these into one endpoint: callers integrate once, and requests get routed to the right upstream provider with identities mapped into a consistent format.", "projects.authrouter.detail": "Systems that connect to several identity providers often force users to jump between different login entry points, and identity data ends up scattered across them. AuthRouter merges these into one endpoint: callers integrate once, and requests get routed to the right upstream provider with identities mapped into a consistent format.",
+1
View File
@@ -47,6 +47,7 @@
"projects.title": "项目", "projects.title": "项目",
"projects.lead": "一些自己写的和维护的项目,点击卡片可以看更多细节。", "projects.lead": "一些自己写的和维护的项目,点击卡片可以看更多细节。",
"projects.authrouter.tag": "重点项目", "projects.authrouter.tag": "重点项目",
"authrouter.imageAlt": "AuthRouter 身份提供方网关项目截图",
"projects.authrouter.title": "AuthRouter", "projects.authrouter.title": "AuthRouter",
"projects.authrouter.desc": "把多个身份提供方(IdP)整合到一个统一的登录入口,只需要调用一个端点就能完成登录,并支持在不同身份提供方之间做身份映射。", "projects.authrouter.desc": "把多个身份提供方(IdP)整合到一个统一的登录入口,只需要调用一个端点就能完成登录,并支持在不同身份提供方之间做身份映射。",
"projects.authrouter.detail": "很多系统会同时接入好几个身份提供方,用户登录时经常要在不同入口之间跳转,身份信息也难以统一。AuthRouter 把这些入口整合成一个,调用方只需要对接一个端点,就能把登录请求路由到正确的上游,并把不同上游的身份信息映射成统一的格式。", "projects.authrouter.detail": "很多系统会同时接入好几个身份提供方,用户登录时经常要在不同入口之间跳转,身份信息也难以统一。AuthRouter 把这些入口整合成一个,调用方只需要对接一个端点,就能把登录请求路由到正确的上游,并把不同上游的身份信息映射成统一的格式。",
+2 -3
View File
@@ -288,7 +288,7 @@ function renderProjects(): void {
<article class="project-featured" data-project="${p.id}"> <article class="project-featured" data-project="${p.id}">
${ ${
p.image p.image
? `<img class="project-featured__media" src="${p.image}" alt="" loading="lazy" />` ? `<img class="project-featured__media" src="${p.image}" alt="${t(`${p.id}.imageAlt`)}" loading="lazy" />`
: `<div class="project-featured__media" data-i18n="projects.media.placeholder"></div>` : `<div class="project-featured__media" data-i18n="projects.media.placeholder"></div>`
} }
<div> <div>
@@ -323,7 +323,7 @@ function renderProjects(): void {
<h3 class="modal__title" data-i18n="${project.titleKey}"></h3> <h3 class="modal__title" data-i18n="${project.titleKey}"></h3>
${ ${
project.image project.image
? `<img class="modal__media" src="${project.image}" alt="" loading="lazy" />` ? `<img class="modal__media" src="${project.image}" alt="${t(`${project.id}.imageAlt`)}" loading="lazy" />`
: `<div class="modal__media" data-i18n="projects.media.placeholder"></div>` : `<div class="modal__media" data-i18n="projects.media.placeholder"></div>`
} }
<p class="modal__desc" data-i18n="${project.detailKey ?? project.descKey}"></p> <p class="modal__desc" data-i18n="${project.detailKey ?? project.descKey}"></p>
@@ -407,4 +407,3 @@ renderInfraStats();
// Ensure the empty i18n keys used as placeholders (e.g. tag on non-featured items) don't render "undefined" // Ensure the empty i18n keys used as placeholders (e.g. tag on non-featured items) don't render "undefined"
document.querySelectorAll<HTMLElement>('[data-i18n=""]').forEach((el) => el.remove()); document.querySelectorAll<HTMLElement>('[data-i18n=""]').forEach((el) => el.remove());
+92
View File
@@ -0,0 +1,92 @@
import type { Lang } from "./i18n";
const SEO = {
en: {
title: "Haoze Zhou | Computer Science Student & Developer",
description: "Haoze Zhou is a Computer Science undergraduate at Universiti Malaya building software, self-hosted infrastructure, and open-source projects.",
imageAlt: "Haoze Zhou personal website",
},
zh: {
title: "周昊泽 | 计算机科学学生与开发者",
description: "周昊泽是马来亚大学计算机科学本科生,专注于软件开发、自建基础设施和开源项目。",
imageAlt: "周昊泽个人网站",
},
} as const;
const EN_URL = "https://www.zhouhaoze.com/";
const ZH_URL = "https://www.zhouhaoze.cn/";
function currentUrl(): string {
return `${window.location.origin}${window.location.pathname}`;
}
function setMeta(selector: string, content: string): void {
const element = document.querySelector<HTMLMetaElement>(selector);
if (element) element.content = content;
}
export function updateSeo(lang: Lang): void {
const copy = SEO[lang];
const canonical = currentUrl();
document.title = copy.title;
document.documentElement.lang = lang === "zh" ? "zh-CN" : "en";
document.querySelector<HTMLLinkElement>('link[rel="canonical"]')?.setAttribute("href", canonical);
document.querySelector<HTMLLinkElement>('link[rel="alternate"][hreflang="en"]')?.setAttribute("href", EN_URL);
document.querySelector<HTMLLinkElement>('link[rel="alternate"][hreflang="zh-CN"]')?.setAttribute("href", ZH_URL);
document.querySelector<HTMLLinkElement>('link[rel="alternate"][hreflang="x-default"]')?.setAttribute("href", EN_URL);
setMeta('meta[name="description"]', copy.description);
setMeta('meta[property="og:title"]', copy.title);
setMeta('meta[property="og:description"]', copy.description);
setMeta('meta[property="og:url"]', canonical);
setMeta('meta[property="og:image:alt"]', copy.imageAlt);
setMeta('meta[property="og:locale"]', lang === "zh" ? "zh_CN" : "en_US");
setMeta('meta[name="twitter:title"]', copy.title);
setMeta('meta[name="twitter:description"]', copy.description);
const structuredData = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "Person",
"@id": `${canonical}#person`,
name: lang === "zh" ? "周昊泽" : "Haoze Zhou",
alternateName: lang === "zh" ? "Haoze Zhou" : "周昊泽",
url: canonical,
email: "mailto:me@zhouhaoze.com",
sameAs: ["https://github.com/YIYI-16", "https://t.me/yiyitelegram_bot"],
jobTitle: lang === "zh" ? "计算机科学本科生" : "Computer Science undergraduate",
affiliation: { "@type": "CollegeOrUniversity", name: "Universiti Malaya" },
},
{
"@type": "WebSite",
"@id": `${canonical}#website`,
url: canonical,
name: copy.title,
description: copy.description,
inLanguage: lang === "zh" ? "zh-CN" : "en",
author: { "@id": `${canonical}#person` },
},
{
"@type": "SoftwareSourceCode",
name: "AuthRouter",
description:
lang === "zh"
? "将多个身份提供方整合到统一登录入口的开源项目。"
: "An open-source gateway that unifies multiple identity providers behind one login endpoint.",
codeRepository: "https://github.com/YIYI-16/AuthRouter",
programmingLanguage: "Go",
author: { "@id": `${canonical}#person` },
},
],
};
let jsonLd = document.querySelector<HTMLScriptElement>('script[data-seo="jsonld"]');
if (!jsonLd) {
jsonLd = document.createElement("script");
jsonLd.type = "application/ld+json";
jsonLd.dataset.seo = "jsonld";
document.head.appendChild(jsonLd);
}
jsonLd.textContent = JSON.stringify(structuredData);
}