From 16282155c351ddfb37d96e4e531484cfca2d91c5 Mon Sep 17 00:00:00 2001 From: zihluwang Date: Tue, 24 Feb 2026 10:32:17 +0800 Subject: [PATCH] feat: add SEO component and integrate it across multiple pages with localised metadata --- pnpm-lock.yaml | 21 +++++ public/sitemap.xml | 18 ++-- src/components/seo/index.tsx | 88 ++++++++++++++++++++ src/init/i18n/locales/BritishEnglish.json | 26 ++++++ src/init/i18n/locales/SimplifiedChinese.json | 26 ++++++ src/page/about/index.tsx | 6 ++ src/page/bmi-calculator/index.tsx | 6 ++ src/page/contact/index.tsx | 6 ++ src/page/home/index.tsx | 6 ++ src/page/json-grid/index.tsx | 6 ++ src/page/json-viewer/index.tsx | 6 ++ 11 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 src/components/seo/index.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26a2d54..c443094 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -397,66 +397,79 @@ packages: resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.55.1': resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.55.1': resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.55.1': resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.55.1': resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.55.1': resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.55.1': resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.55.1': resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.55.1': resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.55.1': resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.55.1': resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.55.1': resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.55.1': resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.55.1': resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} @@ -532,24 +545,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} @@ -887,24 +904,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} diff --git a/public/sitemap.xml b/public/sitemap.xml index 7662efa..baf97f6 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -4,7 +4,7 @@ https://dev-lab.onixbyte.dev/ - 2024-01-01 + 2026-02-24 weekly 1.0 @@ -12,7 +12,15 @@ https://dev-lab.onixbyte.dev/json-viewer - 2024-01-01 + 2026-02-24 + monthly + 0.9 + + + + + https://dev-lab.onixbyte.dev/json-grid + 2026-02-24 monthly 0.9 @@ -20,7 +28,7 @@ https://dev-lab.onixbyte.dev/bmi-calculator - 2024-01-01 + 2026-02-24 monthly 0.9 @@ -28,7 +36,7 @@ https://dev-lab.onixbyte.dev/about - 2024-01-01 + 2026-02-24 monthly 0.7 @@ -36,7 +44,7 @@ https://dev-lab.onixbyte.dev/contact - 2024-01-01 + 2026-02-24 monthly 0.6 diff --git a/src/components/seo/index.tsx b/src/components/seo/index.tsx new file mode 100644 index 0000000..5fd430c --- /dev/null +++ b/src/components/seo/index.tsx @@ -0,0 +1,88 @@ +import { useEffect } from "react" +import { useTranslation } from "react-i18next" + +type SeoProps = { + title: string + description: string + path: string +} + +const SITE_URL = "https://dev-lab.onixbyte.dev" +const DEFAULT_IMAGE = `${SITE_URL}/onixbyte.svg` + +function setMetaTag(selector: string, attr: string, value: string) { + let element = document.querySelector(selector) + if (!element) { + element = document.createElement("meta") + const [key, keyValue] = selector.replace("meta[", "").replace("]", "").split("=") + element.setAttribute(key, keyValue.replace(/"/g, "")) + document.head.appendChild(element) + } + element.setAttribute(attr, value) +} + +function setLink(rel: string, href: string) { + let element = document.querySelector(`link[rel="${rel}"]`) + if (!element) { + element = document.createElement("link") + element.rel = rel + document.head.appendChild(element) + } + element.href = href +} + +function setJsonLd(id: string, payload: Record) { + let element = document.querySelector(`script[data-seo="${id}"]`) + if (!element) { + element = document.createElement("script") + element.type = "application/ld+json" + element.setAttribute("data-seo", id) + document.head.appendChild(element) + } + element.text = JSON.stringify(payload) +} + +export default function Seo({ title, description, path }: SeoProps) { + const { i18n } = useTranslation() + const url = `${SITE_URL}${path}` + const locale = i18n.language === "zh" ? "zh-CN" : "en-GB" + + const jsonLd = { + "@context": "https://schema.org", + "@type": "WebPage", + name: title, + description, + url, + inLanguage: locale, + isPartOf: { + "@type": "WebSite", + name: "DevLab", + url: SITE_URL, + }, + } + + useEffect(() => { + document.title = title + setMetaTag("meta[name=\"title\"]", "content", title) + setMetaTag("meta[name=\"description\"]", "content", description) + setLink("canonical", url) + + setMetaTag("meta[property=\"og:type\"]", "content", "website") + setMetaTag("meta[property=\"og:url\"]", "content", url) + setMetaTag("meta[property=\"og:title\"]", "content", title) + setMetaTag("meta[property=\"og:description\"]", "content", description) + setMetaTag("meta[property=\"og:image\"]", "content", DEFAULT_IMAGE) + setMetaTag("meta[property=\"og:locale\"]", "content", locale) + setMetaTag("meta[property=\"og:site_name\"]", "content", "DevLab") + + setMetaTag("meta[property=\"twitter:card\"]", "content", "summary_large_image") + setMetaTag("meta[property=\"twitter:url\"]", "content", url) + setMetaTag("meta[property=\"twitter:title\"]", "content", title) + setMetaTag("meta[property=\"twitter:description\"]", "content", description) + setMetaTag("meta[property=\"twitter:image\"]", "content", DEFAULT_IMAGE) + + setJsonLd("webpage", jsonLd) + }, [title, description, url, locale, jsonLd]) + + return null +} diff --git a/src/init/i18n/locales/BritishEnglish.json b/src/init/i18n/locales/BritishEnglish.json index 478e08c..6781cc5 100644 --- a/src/init/i18n/locales/BritishEnglish.json +++ b/src/init/i18n/locales/BritishEnglish.json @@ -14,6 +14,32 @@ "english": "English (Great Britain)", "chinese": "简体中文" }, + "seo": { + "home": { + "title": "DevLab - Free Developer Tools", + "description": "A collection of powerful, privacy-focused developer tools. JSON Viewer with JSONPath queries, JSON Grid, BMI Calculator, and more." + }, + "jsonViewer": { + "title": "JSON Viewer - JSONPath Queries", + "description": "Visualise JSON and query data using JSONPath. Inspect structures, filter results, and export CSV locally in your browser." + }, + "jsonGrid": { + "title": "json-grid - JSON Array to Table", + "description": "Convert JSON arrays into a clean, sortable table view and export as CSV. All processing happens locally." + }, + "bmi": { + "title": "BMI Calculator - Instant Results", + "description": "Calculate your Body Mass Index (BMI) with instant feedback and category guidance." + }, + "about": { + "title": "About DevLab", + "description": "Learn about DevLab, an open source, privacy-focused toolkit for developers." + }, + "contact": { + "title": "Contact DevLab", + "description": "Get in touch via GitHub Issues for support, bug reports, or feature requests." + } + }, "home": { "title": "DevLab", "description": "A collection of powerful, privacy-focused developer tools. All processing happens locally in your browser.", diff --git a/src/init/i18n/locales/SimplifiedChinese.json b/src/init/i18n/locales/SimplifiedChinese.json index c0dfd1b..7793e1a 100644 --- a/src/init/i18n/locales/SimplifiedChinese.json +++ b/src/init/i18n/locales/SimplifiedChinese.json @@ -14,6 +14,32 @@ "english": "English (Great Britain)", "chinese": "简体中文" }, + "seo": { + "home": { + "title": "DevLab - 免费开发者工具", + "description": "一系列强大的、注重隐私的开发者工具集合,包含 JSON 查看器、json-grid、BMI 计算器等。" + }, + "jsonViewer": { + "title": "JSON 查看器 - JSONPath 查询", + "description": "使用 JSONPath 可视化并查询 JSON 数据,筛选结果并导出 CSV,全程在浏览器本地完成。" + }, + "jsonGrid": { + "title": "json-grid - JSON 数组转表格", + "description": "将 JSON 数组转换为清晰的表格视图并导出 CSV,所有处理均在本地完成。" + }, + "bmi": { + "title": "BMI 计算器 - 即时结果", + "description": "快速计算 BMI,并获得分类与建议。" + }, + "about": { + "title": "关于 DevLab", + "description": "了解 DevLab:一个开源且注重隐私的开发者工具集合。" + }, + "contact": { + "title": "联系 DevLab", + "description": "通过 GitHub Issues 反馈问题或提交功能需求。" + } + }, "home": { "title": "DevLab", "description": "一系列强大的、注重隐私的开发者工具集合。所有处理都在您的浏览器本地进行。", diff --git a/src/page/about/index.tsx b/src/page/about/index.tsx index d9bd874..727e25e 100644 --- a/src/page/about/index.tsx +++ b/src/page/about/index.tsx @@ -1,4 +1,5 @@ import { useTranslation } from "react-i18next" +import Seo from "@/components/seo" /** * About page component that displays information about the DevLab application. @@ -8,6 +9,11 @@ export default function About() { return (
+ {/* Page Header */}

{t("about.title")}

diff --git a/src/page/bmi-calculator/index.tsx b/src/page/bmi-calculator/index.tsx index 267a00b..cca041e 100644 --- a/src/page/bmi-calculator/index.tsx +++ b/src/page/bmi-calculator/index.tsx @@ -1,5 +1,6 @@ import { useState } from "react" import { useTranslation } from "react-i18next" +import Seo from "@/components/seo" /** * BMI Calculator page component that displays the BMI calculator tool. @@ -72,6 +73,11 @@ export default function BmiCalculator() { return (
+ {/* Left panel - 30% */}
{/* Input Form - fills remaining height */} diff --git a/src/page/contact/index.tsx b/src/page/contact/index.tsx index 9f74b60..5f391b4 100644 --- a/src/page/contact/index.tsx +++ b/src/page/contact/index.tsx @@ -1,5 +1,6 @@ import React from "react" import { useTranslation } from "react-i18next" +import Seo from "@/components/seo" /** * Contact page component that encourages manual GitHub Issue submission. @@ -38,6 +39,11 @@ ${message} return (
+ {/* Header */}

{t("contact.title")}

diff --git a/src/page/home/index.tsx b/src/page/home/index.tsx index 60d7c76..841f592 100644 --- a/src/page/home/index.tsx +++ b/src/page/home/index.tsx @@ -1,5 +1,6 @@ import { Link } from "react-router-dom" import { useTranslation } from "react-i18next" +import Seo from "@/components/seo" /** * Home page component that displays the main landing content. @@ -9,6 +10,11 @@ export default function Home() { return (
+ {/* Page Header */}

{t("home.title")}

diff --git a/src/page/json-grid/index.tsx b/src/page/json-grid/index.tsx index a9351c2..a60a040 100644 --- a/src/page/json-grid/index.tsx +++ b/src/page/json-grid/index.tsx @@ -1,6 +1,7 @@ import { useCallback, useMemo, useState } from "react" import { useTranslation } from "react-i18next" import JsonCodeEditor from "@/components/json-code-editor" +import Seo from "@/components/seo" type RowRecord = Record @@ -79,6 +80,11 @@ export default function JsonGrid() { return (
+
diff --git a/src/page/json-viewer/index.tsx b/src/page/json-viewer/index.tsx index 263b437..6a6560e 100644 --- a/src/page/json-viewer/index.tsx +++ b/src/page/json-viewer/index.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next" import jp from "jsonpath" import JsonTreeNode from "@/components/json-tree-node" import JsonCodeEditor from "@/components/json-code-editor" +import Seo from "@/components/seo" /** * JSON Viewer page component that displays the JSON visualisation tool in DevLab. @@ -91,6 +92,11 @@ export default function JsonViewer() { return (
+ {/* Left panel - 30% */}
{/* JSON Source - fills remaining height */}