feat: add SEO component and integrate it across multiple pages with localised metadata
@@ -397,66 +397,79 @@ packages:
|
|||||||
resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==}
|
resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm-musleabihf@4.55.1':
|
'@rollup/rollup-linux-arm-musleabihf@4.55.1':
|
||||||
resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==}
|
resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-gnu@4.55.1':
|
'@rollup/rollup-linux-arm64-gnu@4.55.1':
|
||||||
resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==}
|
resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-musl@4.55.1':
|
'@rollup/rollup-linux-arm64-musl@4.55.1':
|
||||||
resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==}
|
resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-loong64-gnu@4.55.1':
|
'@rollup/rollup-linux-loong64-gnu@4.55.1':
|
||||||
resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==}
|
resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-loong64-musl@4.55.1':
|
'@rollup/rollup-linux-loong64-musl@4.55.1':
|
||||||
resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==}
|
resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-gnu@4.55.1':
|
'@rollup/rollup-linux-ppc64-gnu@4.55.1':
|
||||||
resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==}
|
resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-musl@4.55.1':
|
'@rollup/rollup-linux-ppc64-musl@4.55.1':
|
||||||
resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==}
|
resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-gnu@4.55.1':
|
'@rollup/rollup-linux-riscv64-gnu@4.55.1':
|
||||||
resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==}
|
resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-musl@4.55.1':
|
'@rollup/rollup-linux-riscv64-musl@4.55.1':
|
||||||
resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==}
|
resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-s390x-gnu@4.55.1':
|
'@rollup/rollup-linux-s390x-gnu@4.55.1':
|
||||||
resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==}
|
resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-gnu@4.55.1':
|
'@rollup/rollup-linux-x64-gnu@4.55.1':
|
||||||
resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==}
|
resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-musl@4.55.1':
|
'@rollup/rollup-linux-x64-musl@4.55.1':
|
||||||
resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==}
|
resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-openbsd-x64@4.55.1':
|
'@rollup/rollup-openbsd-x64@4.55.1':
|
||||||
resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==}
|
resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==}
|
||||||
@@ -532,24 +545,28 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
|
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
|
||||||
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
|
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
|
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
|
||||||
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
|
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
|
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
|
||||||
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
|
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
|
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
|
||||||
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
|
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
|
||||||
@@ -887,24 +904,28 @@ packages:
|
|||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
lightningcss-linux-arm64-musl@1.30.2:
|
lightningcss-linux-arm64-musl@1.30.2:
|
||||||
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
lightningcss-linux-x64-gnu@1.30.2:
|
lightningcss-linux-x64-gnu@1.30.2:
|
||||||
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
lightningcss-linux-x64-musl@1.30.2:
|
lightningcss-linux-x64-musl@1.30.2:
|
||||||
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
lightningcss-win32-arm64-msvc@1.30.2:
|
lightningcss-win32-arm64-msvc@1.30.2:
|
||||||
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<!-- Homepage -->
|
<!-- Homepage -->
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dev-lab.onixbyte.dev/</loc>
|
<loc>https://dev-lab.onixbyte.dev/</loc>
|
||||||
<lastmod>2024-01-01</lastmod>
|
<lastmod>2026-02-24</lastmod>
|
||||||
<changefreq>weekly</changefreq>
|
<changefreq>weekly</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
@@ -12,7 +12,15 @@
|
|||||||
<!-- JSON Viewer -->
|
<!-- JSON Viewer -->
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dev-lab.onixbyte.dev/json-viewer</loc>
|
<loc>https://dev-lab.onixbyte.dev/json-viewer</loc>
|
||||||
<lastmod>2024-01-01</lastmod>
|
<lastmod>2026-02-24</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.9</priority>
|
||||||
|
</url>
|
||||||
|
|
||||||
|
<!-- JSON Grid -->
|
||||||
|
<url>
|
||||||
|
<loc>https://dev-lab.onixbyte.dev/json-grid</loc>
|
||||||
|
<lastmod>2026-02-24</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
@@ -20,7 +28,7 @@
|
|||||||
<!-- BMI Calculator -->
|
<!-- BMI Calculator -->
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dev-lab.onixbyte.dev/bmi-calculator</loc>
|
<loc>https://dev-lab.onixbyte.dev/bmi-calculator</loc>
|
||||||
<lastmod>2024-01-01</lastmod>
|
<lastmod>2026-02-24</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.9</priority>
|
<priority>0.9</priority>
|
||||||
</url>
|
</url>
|
||||||
@@ -28,7 +36,7 @@
|
|||||||
<!-- About -->
|
<!-- About -->
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dev-lab.onixbyte.dev/about</loc>
|
<loc>https://dev-lab.onixbyte.dev/about</loc>
|
||||||
<lastmod>2024-01-01</lastmod>
|
<lastmod>2026-02-24</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.7</priority>
|
<priority>0.7</priority>
|
||||||
</url>
|
</url>
|
||||||
@@ -36,7 +44,7 @@
|
|||||||
<!-- Contact -->
|
<!-- Contact -->
|
||||||
<url>
|
<url>
|
||||||
<loc>https://dev-lab.onixbyte.dev/contact</loc>
|
<loc>https://dev-lab.onixbyte.dev/contact</loc>
|
||||||
<lastmod>2024-01-01</lastmod>
|
<lastmod>2026-02-24</lastmod>
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>0.6</priority>
|
<priority>0.6</priority>
|
||||||
</url>
|
</url>
|
||||||
|
|||||||
@@ -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)",
|
"english": "English (Great Britain)",
|
||||||
"chinese": "简体中文"
|
"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": {
|
"home": {
|
||||||
"title": "DevLab",
|
"title": "DevLab",
|
||||||
"description": "A collection of powerful, privacy-focused developer tools. All processing happens locally in your browser.",
|
"description": "A collection of powerful, privacy-focused developer tools. All processing happens locally in your browser.",
|
||||||
|
|||||||
@@ -14,6 +14,32 @@
|
|||||||
"english": "English (Great Britain)",
|
"english": "English (Great Britain)",
|
||||||
"chinese": "简体中文"
|
"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": {
|
"home": {
|
||||||
"title": "DevLab",
|
"title": "DevLab",
|
||||||
"description": "一系列强大的、注重隐私的开发者工具集合。所有处理都在您的浏览器本地进行。",
|
"description": "一系列强大的、注重隐私的开发者工具集合。所有处理都在您的浏览器本地进行。",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import Seo from "@/components/seo"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* About page component that displays information about the DevLab application.
|
* About page component that displays information about the DevLab application.
|
||||||
@@ -8,6 +9,11 @@ export default function About() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8 max-w-6xl mx-auto">
|
<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 */}
|
{/* Page Header */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 sm:text-4xl">{t("about.title")}</h1>
|
<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 { useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import Seo from "@/components/seo"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BMI Calculator page component that displays the BMI calculator tool.
|
* BMI Calculator page component that displays the BMI calculator tool.
|
||||||
@@ -72,6 +73,11 @@ export default function BmiCalculator() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex gap-4 overflow-hidden">
|
<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% */}
|
{/* Left panel - 30% */}
|
||||||
<div className="w-[30%] flex flex-col gap-4 min-h-0">
|
<div className="w-[30%] flex flex-col gap-4 min-h-0">
|
||||||
{/* Input Form - fills remaining height */}
|
{/* Input Form - fills remaining height */}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import Seo from "@/components/seo"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contact page component that encourages manual GitHub Issue submission.
|
* Contact page component that encourages manual GitHub Issue submission.
|
||||||
@@ -38,6 +39,11 @@ ${message}
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-5xl mx-auto py-12 px-4 sm:px-6">
|
<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 */}
|
{/* Header */}
|
||||||
<div className="text-center mb-12">
|
<div className="text-center mb-12">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 sm:text-4xl">{t("contact.title")}</h1>
|
<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 { Link } from "react-router-dom"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import Seo from "@/components/seo"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Home page component that displays the main landing content.
|
* Home page component that displays the main landing content.
|
||||||
@@ -9,6 +10,11 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8 max-w-6xl mx-auto">
|
<div className="space-y-8 max-w-6xl mx-auto">
|
||||||
|
<Seo
|
||||||
|
title={t("seo.home.title")}
|
||||||
|
description={t("seo.home.description")}
|
||||||
|
path="/"
|
||||||
|
/>
|
||||||
{/* Page Header */}
|
{/* Page Header */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 sm:text-4xl">{t("home.title")}</h1>
|
<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 { useCallback, useMemo, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import JsonCodeEditor from "@/components/json-code-editor"
|
import JsonCodeEditor from "@/components/json-code-editor"
|
||||||
|
import Seo from "@/components/seo"
|
||||||
|
|
||||||
type RowRecord = Record<string, unknown>
|
type RowRecord = Record<string, unknown>
|
||||||
|
|
||||||
@@ -79,6 +80,11 @@ export default function JsonGrid() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex gap-4 overflow-hidden">
|
<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="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">
|
<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">
|
<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 jp from "jsonpath"
|
||||||
import JsonTreeNode from "@/components/json-tree-node"
|
import JsonTreeNode from "@/components/json-tree-node"
|
||||||
import JsonCodeEditor from "@/components/json-code-editor"
|
import JsonCodeEditor from "@/components/json-code-editor"
|
||||||
|
import Seo from "@/components/seo"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON Viewer page component that displays the JSON visualisation tool in DevLab.
|
* JSON Viewer page component that displays the JSON visualisation tool in DevLab.
|
||||||
@@ -91,6 +92,11 @@ export default function JsonViewer() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex gap-4 overflow-hidden">
|
<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% */}
|
{/* Left panel - 30% */}
|
||||||
<div className="w-[30%] flex flex-col gap-4 min-h-0">
|
<div className="w-[30%] flex flex-col gap-4 min-h-0">
|
||||||
{/* JSON Source - fills remaining height */}
|
{/* JSON Source - fills remaining height */}
|
||||||
|
|||||||