## Summary - Closes the public-page gaps from #124: each route gets its own title, description, canonical, heading, links, and JSON-LD, and unknown URLs have a real 404 document. - Adds HeyCatch and the Google tag (`G-V12DLRLPWR`) only on the production site and the iOS and Android apps. Tianji still loads on beta. - Needs the server-infra change that stops serving the app shell for missing paths. ## Test plan - [ ] Build production and confirm `/` and `/pricing/` HTML include a unique title, description, canonical, one h1, social tags, related links, and Organization JSON-LD. - [ ] Confirm a nonsense path is served as `404.html` once the nginx change is deployed. - [ ] Confirm beta and local web do not load HeyCatch or gtag, and that a production or native build does. - [ ] Open the live site after deploy and click around, then mark HeyCatch installed in its dashboard. Reviewed-on: #125
154 lines
3.7 KiB
JavaScript
154 lines
3.7 KiB
JavaScript
/**
|
|
* Sitemap lastmod comes from the page source, not from the build clock.
|
|
* Changelog uses the newest entry date. Terms use the "Last Updated" line.
|
|
* Other public pages use the last git commit that touched that file.
|
|
*/
|
|
const { execFileSync } = require('child_process');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const ORIGIN = 'https://hesychia.ai';
|
|
const INDEXNOW_KEY = '8d1667dd71c6ef137171989c26eeb632';
|
|
const INDEXNOW_HOST = 'hesychia.ai';
|
|
|
|
const llmFe = path.resolve(__dirname, '..');
|
|
const repoRoot = path.resolve(llmFe, '..');
|
|
|
|
/** Public pages only. Same URLs the site already lists for crawlers. */
|
|
const SITEMAP_PAGES = [
|
|
{
|
|
path: '/',
|
|
file: 'llm-fe/src/llm-fe/pages/Landing/LandingPage.tsx',
|
|
lastmod: 'git',
|
|
},
|
|
{
|
|
path: '/pricing/',
|
|
file: 'llm-fe/src/llm-fe/pages/Pricing/PricingPage.tsx',
|
|
lastmod: 'git',
|
|
},
|
|
{
|
|
path: '/privacy/',
|
|
file: 'llm-fe/src/llm-fe/pages/Privacy/PrivacyPage.tsx',
|
|
lastmod: 'git',
|
|
},
|
|
{
|
|
path: '/changelog/',
|
|
file: 'llm-fe/src/llm-fe/pages/Changelog/changelogEntries.ts',
|
|
lastmod: 'changelog',
|
|
},
|
|
{
|
|
path: '/terms_of_service/',
|
|
file: 'llm-fe/src/llm-fe/components/legal/TermsOfServiceDocument.tsx',
|
|
lastmod: 'terms',
|
|
},
|
|
{
|
|
path: '/account-deletion/',
|
|
file: 'llm-fe/src/llm-fe/pages/AccountDeletion/AccountDeletionPage.tsx',
|
|
lastmod: 'git',
|
|
},
|
|
];
|
|
|
|
const MONTHS = {
|
|
January: '01',
|
|
February: '02',
|
|
March: '03',
|
|
April: '04',
|
|
May: '05',
|
|
June: '06',
|
|
July: '07',
|
|
August: '08',
|
|
September: '09',
|
|
October: '10',
|
|
November: '11',
|
|
December: '12',
|
|
};
|
|
|
|
function newestChangelogDate(source) {
|
|
const dates = [...source.matchAll(/date:\s*'(\d{4}-\d{2}-\d{2})'/g)].map((match) => match[1]);
|
|
if (dates.length === 0) {
|
|
throw new Error('changelog entries have no dates');
|
|
}
|
|
return dates.sort().at(-1);
|
|
}
|
|
|
|
function termsUpdated(source) {
|
|
const match = source.match(/Last Updated:\s+([A-Z][a-z]+)\s+(\d{1,2}),\s+(\d{4})/);
|
|
if (!match) {
|
|
throw new Error('terms have no Last Updated date');
|
|
}
|
|
const month = MONTHS[match[1]];
|
|
if (!month) {
|
|
throw new Error(`unknown month in terms date: ${match[1]}`);
|
|
}
|
|
return `${match[3]}-${month}-${match[2].padStart(2, '0')}`;
|
|
}
|
|
|
|
function gitLastmod(file) {
|
|
const out = execFileSync('git', ['log', '-1', '--format=%cs', '--', file], {
|
|
cwd: repoRoot,
|
|
encoding: 'utf8',
|
|
}).trim();
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(out)) {
|
|
throw new Error(`no git commit date for ${file}`);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function lastmodFor(page) {
|
|
const abs = path.join(repoRoot, page.file);
|
|
const source = fs.readFileSync(abs, 'utf8');
|
|
if (page.lastmod === 'changelog') return newestChangelogDate(source);
|
|
if (page.lastmod === 'terms') return termsUpdated(source);
|
|
return gitLastmod(page.file);
|
|
}
|
|
|
|
function sitemapEntries() {
|
|
return SITEMAP_PAGES.map((page) => ({
|
|
loc: `${ORIGIN}${page.path}`,
|
|
lastmod: lastmodFor(page),
|
|
}));
|
|
}
|
|
|
|
function renderSitemap(entries) {
|
|
const urls = entries
|
|
.map(
|
|
(entry) => ` <url>
|
|
<loc>${entry.loc}</loc>
|
|
<lastmod>${entry.lastmod}</lastmod>
|
|
</url>`,
|
|
)
|
|
.join('\n');
|
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
${urls}
|
|
</urlset>
|
|
`;
|
|
}
|
|
|
|
function writeSitemap() {
|
|
const xml = renderSitemap(sitemapEntries());
|
|
fs.writeFileSync(path.join(llmFe, 'public', 'sitemap.xml'), xml);
|
|
return xml;
|
|
}
|
|
|
|
function indexNowBody(key, urls) {
|
|
return {
|
|
host: INDEXNOW_HOST,
|
|
key,
|
|
keyLocation: `https://${INDEXNOW_HOST}/${key}.txt`,
|
|
urlList: urls,
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
INDEXNOW_KEY,
|
|
SITEMAP_PAGES,
|
|
newestChangelogDate,
|
|
termsUpdated,
|
|
gitLastmod,
|
|
sitemapEntries,
|
|
renderSitemap,
|
|
writeSitemap,
|
|
indexNowBody,
|
|
};
|