/** * CRA serves one index.html for every route. A canonical in that shared file * would point every page at the homepage. After `react-scripts build`, write a * copy per route whose canonical href is that route's own absolute URL. */ const fs = require('fs'); const path = require('path'); const { ORIGIN, OG_IMAGE, PAGE_PATHS, h1ForPath, titleForPath, descriptionForPath } = require('../src/llm-fe/utils/canonicals'); const { relatedNavHtml } = require('../src/llm-fe/utils/internalLinks'); const { proseHtml } = require('../src/llm-fe/utils/pageProse'); const { jsonLdHtml } = require('../src/llm-fe/utils/structuredData'); const CANONICAL_RE = //i; const H1_RE = /]*>[\s\S]*?<\/h1>/i; const TITLE_RE = /[^<]*<\/title>/i; const DESCRIPTION_RE = /<meta\s+name=["']description["']\s+content=["'][^"']*["']\s*\/?>/i; const RELATED_NAV_RE = /<nav\s+aria-label=["']Related pages["']>[\s\S]*?<\/nav>/i; const PROSE_RE = /<div id="crawler-prose">[\s\S]*?<\/div>/i; const JSONLD_SOURCE = '<script type="application\\/ld\\+json">[\\s\\S]*?<\\/script>'; /** * @param {string} html * @param {'property' | 'name'} attr * @param {string} key * @param {string} content * @returns {string} */ function replaceSocial(html, attr, key, content) { const re = new RegExp( `<meta\\s+${attr}=["']${key}["']\\s+content=["'][^"']*["']\\s*\\/?>`, 'i', ); if (!re.test(html)) { throw new Error(`missing meta ${attr}="${key}"`); } return html.replace(re, () => `<meta ${attr}="${key}" content="${content}" />`); } /** * @param {string} homeHtml * @param {string} pathname * @returns {string} */ function htmlForPath(homeHtml, pathname) { const found = homeHtml.match(new RegExp(CANONICAL_RE.source, 'gi')); if (!found || found.length !== 1) { throw new Error( `expected exactly one <link rel="canonical"> in index.html, found ${found ? found.length : 0}`, ); } const url = `${ORIGIN}${pathname}`; let next = homeHtml.replace(CANONICAL_RE, `<link rel="canonical" href="${url}" />`); const h1s = next.match(/<h1\b/gi); if (!h1s || h1s.length !== 1) { throw new Error(`expected exactly one <h1> in index.html, found ${h1s ? h1s.length : 0}`); } const headline = h1ForPath(pathname); if (!headline) { throw new Error(`no h1 for ${pathname}`); } next = next.replace(H1_RE, `<h1>${headline}</h1>`); if (next.match(/<h1\b/gi).length !== 1) { throw new Error(`stamped ${pathname} does not have exactly one h1`); } const titles = next.match(/<title\b/gi); if (!titles || titles.length !== 1) { throw new Error(`expected exactly one <title> in index.html, found ${titles ? titles.length : 0}`); } const title = titleForPath(pathname); if (!title) { throw new Error(`no title for ${pathname}`); } next = next.replace(TITLE_RE, `<title>${title}`); if (next.match(/`); if (next.match(/ prose); } else { next = next.replace(/<\/h1>/i, () => `${prose}`); } const nav = relatedNavHtml(pathname); if (RELATED_NAV_RE.test(next)) { next = next.replace(RELATED_NAV_RE, () => nav); } else { next = next.replace(/<\/h1>/i, () => `${nav}`); } if (next.match(/ { if (replaced) return ''; replaced = true; return block; }); } else { next = next.replace(/<\/head>/i, () => `${block}\n`); } const scripts = next.match(/