seo: static JSON-LD, sitemap lastmod, og-image for public website (crawler-only, no UI change)

This commit is contained in:
gadgethd
2026-08-08 00:16:20 +00:00
parent 99d444e200
commit 049b063a8c
2 changed files with 55 additions and 1 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

+55 -1
View File
@@ -48,6 +48,52 @@ function buildTarget(): BuildTarget {
return 'public-website';
}
function buildJsonLd(meta: RouteMeta, routePath: string, site: SiteId): string {
const defaults = SITE_DEFAULTS[site];
const url = `${defaults.baseUrl}${routePath === '/' ? '' : routePath}`;
const siteName = defaults.siteName;
const organization = {
'@type': 'Organization',
name: siteName,
url: defaults.baseUrl,
logo: {
'@type': 'ImageObject',
url: `${defaults.baseUrl}/og-image.png`,
},
};
const graph: object[] = routePath === '/'
? [{
'@type': 'WebSite',
name: siteName,
url: defaults.baseUrl,
description: meta.description,
publisher: organization,
}]
: [
{
'@type': 'WebPage',
name: meta.title,
url,
description: meta.description,
isPartOf: { '@type': 'WebSite', name: siteName, url: defaults.baseUrl },
publisher: organization,
},
{
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: defaults.baseUrl },
{ '@type': 'ListItem', position: 2, name: meta.title.split(' — ')[0], item: url },
],
},
];
// Escape `<` so the serialized JSON can never close the script tag.
const json = JSON.stringify({ '@context': 'https://schema.org', '@graph': graph }).replace(/</g, '\\u003c');
return `<script type="application/ld+json">${json}</script>`;
}
function buildMetaTags(meta: RouteMeta, routePath: string, site: SiteId): string {
const defaults = SITE_DEFAULTS[site];
const url = `${defaults.baseUrl}${routePath === '/' ? '' : routePath}`;
@@ -68,6 +114,7 @@ function buildMetaTags(meta: RouteMeta, routePath: string, site: SiteId): string
`<meta name="twitter:title" content="${meta.title}">`,
`<meta name="twitter:description" content="${meta.description}">`,
`<meta name="twitter:image" content="${ogImage}">`,
buildJsonLd(meta, routePath, site),
].join('\n ');
}
@@ -83,11 +130,12 @@ function generateSitemapXml(site: SiteId): string {
const baseUrl = SITE_DEFAULTS[site].baseUrl;
const routes = SITEMAP_ROUTES[site];
const totalRoutes = routes.length;
const buildDate = new Date().toISOString().slice(0, 10);
const urls = routes.map((route, i) => {
const loc = route === '/' ? baseUrl + '/' : baseUrl + route;
const priority = Math.max(0.4, 1.0 - (i / totalRoutes) * 0.6).toFixed(1);
return ` <url>\n <loc>${loc}</loc>\n <priority>${priority}</priority>\n </url>`;
return ` <url>\n <loc>${loc}</loc>\n <lastmod>${buildDate}</lastmod>\n <priority>${priority}</priority>\n </url>`;
});
return [
@@ -208,6 +256,12 @@ export default function viteSeoPlugin(): Plugin {
routeHtml = routeHtml.replace(/(<meta name="twitter:title" content=")[^"]*(")/, `$1${meta.title}$2`);
routeHtml = routeHtml.replace(/(<meta name="twitter:description" content=")[^"]*(")/, `$1${meta.description}$2`);
// Replace the homepage JSON-LD block with the route-specific one
routeHtml = routeHtml.replace(
/<script type="application\/ld\+json">.*?<\/script>/,
buildJsonLd(meta, route, site),
);
// Write to route subdirectory
const routeDir = path.join(outDir, route.slice(1)); // Remove leading /
fs.mkdirSync(routeDir, { recursive: true });