/**
* 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 ? `` : ""}`;
const getTemplateDev = ({page, name, source, hash, img, textStyle, isFluff}) => `
5etools
${img ? `` : ""}
`;
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));