feat: add SEO component and integrate it across multiple pages with localised metadata
This commit is contained in:
@@ -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<HTMLMetaElement>(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<HTMLLinkElement>(`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<string, unknown>) {
|
||||
let element = document.querySelector<HTMLScriptElement>(`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
|
||||
}
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "一系列强大的、注重隐私的开发者工具集合。所有处理都在您的浏览器本地进行。",
|
||||
|
||||
@@ -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 (
|
||||
<div className="space-y-8 max-w-6xl mx-auto">
|
||||
<Seo
|
||||
title={t("seo.about.title")}
|
||||
description={t("seo.about.description")}
|
||||
path="/about"
|
||||
/>
|
||||
{/* Page Header */}
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold text-gray-900 sm:text-4xl">{t("about.title")}</h1>
|
||||
|
||||
@@ -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 (
|
||||
<div className="h-full flex gap-4 overflow-hidden">
|
||||
<Seo
|
||||
title={t("seo.bmi.title")}
|
||||
description={t("seo.bmi.description")}
|
||||
path="/bmi-calculator"
|
||||
/>
|
||||
{/* Left panel - 30% */}
|
||||
<div className="w-[30%] flex flex-col gap-4 min-h-0">
|
||||
{/* Input Form - fills remaining height */}
|
||||
|
||||
@@ -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 (
|
||||
<div className="max-w-5xl mx-auto py-12 px-4 sm:px-6">
|
||||
<Seo
|
||||
title={t("seo.contact.title")}
|
||||
description={t("seo.contact.description")}
|
||||
path="/contact"
|
||||
/>
|
||||
{/* Header */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-3xl font-bold text-gray-900 sm:text-4xl">{t("contact.title")}</h1>
|
||||
|
||||
@@ -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 (
|
||||
<div className="space-y-8 max-w-6xl mx-auto">
|
||||
<Seo
|
||||
title={t("seo.home.title")}
|
||||
description={t("seo.home.description")}
|
||||
path="/"
|
||||
/>
|
||||
{/* Page Header */}
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold text-gray-900 sm:text-4xl">{t("home.title")}</h1>
|
||||
|
||||
@@ -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<string, unknown>
|
||||
|
||||
@@ -79,6 +80,11 @@ export default function JsonGrid() {
|
||||
|
||||
return (
|
||||
<div className="h-full flex gap-4 overflow-hidden">
|
||||
<Seo
|
||||
title={t("seo.jsonGrid.title")}
|
||||
description={t("seo.jsonGrid.description")}
|
||||
path="/json-grid"
|
||||
/>
|
||||
<div className="w-[35%] bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden flex flex-col min-h-0">
|
||||
<div className="bg-slate-50 px-4 py-2 border-b border-slate-200 shrink-0">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
||||
|
||||
@@ -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 (
|
||||
<div className="h-full flex gap-4 overflow-hidden">
|
||||
<Seo
|
||||
title={t("seo.jsonViewer.title")}
|
||||
description={t("seo.jsonViewer.description")}
|
||||
path="/json-viewer"
|
||||
/>
|
||||
{/* Left panel - 30% */}
|
||||
<div className="w-[30%] flex flex-col gap-4 min-h-0">
|
||||
{/* JSON Source - fills remaining height */}
|
||||
|
||||
Reference in New Issue
Block a user