/** * Generator script which creates stub per-entity pages for SEO. */ import * as fs from "fs"; import "../js/parser.js"; import "../js/utils.js"; import "../js/utils-dataloader.js"; import "../js/render.js"; import "../js/render-dice.js"; import * as ut from "./util.js"; const IS_DEV_MODE = true; // !!process.env.VET_SEO_IS_DEV_MODE; // N.b.: disabled as all known deployments are "dev" const BASE_SITE_URL = process.env.VET_BASE_SITE_URL || "https://5e.tools/"; const LOG_EVERY = 1000; // Certain stakeholders prefer less logspam const isSkipUaEtc = !!process.env.VET_SEO_IS_SKIP_UA_ETC; const isOnlyVanilla = !!process.env.VET_SEO_IS_ONLY_VANILLA; const version = ut.readJson("package.json").version; const lastMod = (() => { const date = new Date(); return `${date.getFullYear()}-${`${date.getMonth() + 1}`.padStart(2, "0")}-${`${date.getDate()}`.padStart(2, "0")}`; })(); const baseSitemapData = (() => { const out = {}; // Scrape all the links from navigation.js -- avoid any unofficial HTML files which might exist const navText = fs.readFileSync("./js/navigation.js", "utf-8"); navText.replace(/(?:"([^"]+\.html)"|'([^']+)\.html'|`([^`]+)\.html`)/gi, (...m) => { const str = m[1] || m[2] || m[3]; if (str.includes("${")) return; out[str] = true; }); return out; })(); const getTemplate = ({page, name, source, hash, img, textStyle, isFluff}) => ` 5etools${img ? `` : ""}
Loading...
`; const getTemplateDev = ({page, name, source, hash, img, textStyle, isFluff}) => ` 5etools ${img ? `` : ""}
Loading...
`; const toGenerate = [ { page: "spells", pGetEntityMetas: async () => { const entities = (await DataUtil.spell.pLoadAll()) .filter(({source}) => !isSkipUaEtc || !SourceUtil.isNonstandardSourceWotc(source)) .filter(({source}) => !isOnlyVanilla || Parser.SOURCES_VANILLA.has(source)); return entities.pSerialAwaitMap(async ent => ({entity: ent, fluff: await Renderer.spell.pGetFluff(ent)})); }, style: 1, isFluff: 1, }, { page: "bestiary", pGetEntityMetas: async () => { const entities = (await DataUtil.monster.pLoadAll()) .filter(({source}) => !isSkipUaEtc || !SourceUtil.isNonstandardSourceWotc(source)) .filter(({source}) => !isOnlyVanilla || Parser.SOURCES_VANILLA.has(source)); return entities.pSerialAwaitMap(async ent => ({ entity: ent, fluff: await Renderer.monster.pGetFluff(ent), img: Renderer.monster.hasToken(ent) ? Renderer.monster.getTokenUrl(ent) : null, })); }, style: 2, isFluff: 1, }, { page: "items", pGetEntityMetas: async () => { const entities = (await Renderer.item.pBuildList()).filter(it => !it._isItemGroup) .filter(it => !isSkipUaEtc || !SourceUtil.isNonstandardSourceWotc(it.source)) .filter(it => !isOnlyVanilla || Parser.SOURCES_VANILLA.has(it.source)); return entities.pSerialAwaitMap(async ent => ({entity: ent, fluff: await Renderer.item.pGetFluff(ent)})); }, style: 1, isFluff: 1, }, // TODO expand this as required ]; const siteMapData = {}; async function main () { ut.patchLoadJson(); let total = 0; console.log(`Generating SEO pages...`); await Promise.all(toGenerate.map(async meta => { try { fs.mkdirSync(`./${meta.page}`, { recursive: true }); } catch (err) { if (err.code !== "EEXIST") throw err; } const entityMetas = await meta.pGetEntityMetas(); const builder = UrlUtil.URL_TO_HASH_BUILDER[`${meta.page}.html`]; entityMetas.forEach(({entity, fluff, img}) => { let offset = 0; let html; let path; while (true) { const hash = builder(entity); const sluggedHash = UrlUtil.getSluggedHash(hash); path = `${meta.page}/${sluggedHash}${offset ? `-${offset}` : ""}.html`; if (siteMapData[path]) { ++offset; continue; } if (!img && fluff?.images?.length) { img = Renderer.utils.getEntryMediaUrl(fluff.images[0], "href", "img"); } html = (IS_DEV_MODE ? getTemplateDev : getTemplate)({ page: meta.page, name: entity.name, source: entity.source, hash, img, textStyle: meta.style, isFluff: meta.isFluff, }); siteMapData[path] = true; break; } if (offset > 0) console.warn(`\tDeduplicated URL using suffix: ${path}`); fs.writeFileSync(`./${path}`, html, "utf-8"); total++; if (total % LOG_EVERY === 0) console.log(`Wrote ${total} files...`); }); })); console.log(`Wrote ${total} files.`); let sitemapLinkCount = 0; let sitemap = `\n`; sitemap += `\n`; sitemap += ` ${BASE_SITE_URL} ${lastMod} monthly \n`; sitemapLinkCount++; Object.keys(baseSitemapData).forEach(url => { sitemap += ` ${BASE_SITE_URL}${url} ${lastMod} monthly \n`; sitemapLinkCount++; }); Object.keys(siteMapData).forEach(url => { sitemap += ` ${BASE_SITE_URL}${url} ${lastMod} weekly \n`; sitemapLinkCount++; }); sitemap += `\n`; fs.writeFileSync("./sitemap.xml", sitemap, "utf-8"); console.log(`Wrote ${sitemapLinkCount.toLocaleString()} URL${sitemapLinkCount === 1 ? "" : "s"} to sitemap.xml`); ut.unpatchLoadJson(); } main().then(() => console.log(`SEO page generation complete.`)).catch(e => console.error(e));