feat(website): add search-intent titles, FAQ schema, and breadcrumbs
All checks were successful
CI — Website / Website — typecheck, build (pull_request) Successful in 1m2s

Give Google unique snippets and FAQ rich results, and keep robots/sitemap from being cached as HTML.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john kevin asprec
2026-09-04 07:08:57 +08:00
parent 3150e16248
commit 03ad49f523
19 changed files with 347 additions and 37 deletions

View File

@@ -7,8 +7,64 @@ const distHtml = join(root, 'dist', 'index.html');
const { origin, routes } = JSON.parse(
readFileSync(join(root, 'src', 'seo.routes.json'), 'utf8'),
);
const faq = JSON.parse(readFileSync(join(root, 'src', 'faq.json'), 'utf8'));
function apply(html, { title, description, url, robots }) {
const CHROME_STORE =
'https://chromewebstore.google.com/detail/bagpcheidbkfgijnnmolnkgagibbjfnk';
const VSCODE_MARKETPLACE =
'https://marketplace.visualstudio.com/items?itemName=JuanKibin.lexai-vscode';
const REPO = 'https://git.juankibin.space/kibin/LexAI';
const homeDescription = routes.find((r) => r.path === '/')?.description ?? '';
function softwareGraph() {
return {
'@type': 'SoftwareApplication',
name: 'LexAI',
applicationCategory: 'BrowserApplication',
applicationSubCategory: 'Writing assistance',
operatingSystem: 'Chrome, VS Code, Windows, macOS, Linux',
url: `${origin}/`,
downloadUrl: [CHROME_STORE, VSCODE_MARKETPLACE],
license: `${REPO}/src/branch/main/LICENSE`,
sameAs: [REPO, CHROME_STORE, VSCODE_MARKETPLACE],
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
};
}
function jsonLd(route) {
const url = route.path === '/' ? `${origin}/` : `${origin}${route.path}`;
const graph = [
{
'@type': 'WebSite',
name: 'LexAI',
url: `${origin}/`,
description: homeDescription,
},
softwareGraph(),
];
if (route.path === '/') {
graph.push({
'@type': 'FAQPage',
mainEntity: faq.map((item) => ({
'@type': 'Question',
name: item.q,
acceptedAnswer: { '@type': 'Answer', text: item.a },
})),
});
} else {
graph.push({
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: `${origin}/` },
{ '@type': 'ListItem', position: 2, name: route.title, item: url },
],
});
}
return JSON.stringify({ '@context': 'https://schema.org', '@graph': graph });
}
function apply(html, { title, description, url, robots, ld }) {
let out = html;
out = out.replace(/<title>[\s\S]*?<\/title>/, `<title>${title}</title>`);
out = out.replace(
@@ -28,6 +84,10 @@ function apply(html, { title, description, url, robots }) {
`$1${description}$2`,
);
out = out.replace(/(<link\s+rel="canonical"\s+href=")[^"]*(")/, `$1${url}$2`);
out = out.replace(
/<script type="application\/ld\+json">[\s\S]*?<\/script>/,
`<script type="application/ld+json">${ld}</script>`,
);
return out;
}
@@ -50,6 +110,7 @@ for (const route of routes) {
description: route.description,
url,
robots: 'index, follow',
ld: jsonLd(route),
}),
);
written.push(file);
@@ -62,8 +123,30 @@ writeFileSync(
description: 'That address is not a LexAI page.',
url: `${origin}/`,
robots: 'noindex, follow',
ld: JSON.stringify({
'@context': 'https://schema.org',
'@graph': [{ '@type': 'WebSite', name: 'LexAI', url: `${origin}/` }, softwareGraph()],
}),
}),
);
written.push('404.html');
const today = new Date().toISOString().slice(0, 10);
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${routes
.map(
(r, i) => ` <url>
<loc>${r.path === '/' ? `${origin}/` : `${origin}${r.path}`}</loc>
<lastmod>${today}</lastmod>
<changefreq>${i < 2 ? 'weekly' : 'monthly'}</changefreq>
<priority>${r.path === '/' ? '1.0' : r.path === '/docs' ? '0.8' : '0.5'}</priority>
</url>`,
)
.join('\n')}
</urlset>
`;
writeFileSync(join(root, 'dist', 'sitemap.xml'), sitemap);
written.push('sitemap.xml');
console.log('SEO HTML written for', written.join(', '));