Unit Tests / test (pull_request) Successful in 37s
Crawlers were reading one shared shell, so titles, descriptions, links, and markup were missing or the same on every URL. HeyCatch and Google Analytics load only on the production site and the iOS and Android apps. Co-authored-by: Cursor <cursoragent@cursor.com>
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,
|
|
};
|