158 lines
No EOL
5 KiB
TypeScript
158 lines
No EOL
5 KiB
TypeScript
import fs from "node:fs";
|
|
import { APP_DESCRIPTIONS, APP_NAMES, AppKey, APPS } from "../src/config/apps.config";
|
|
import { ANSI } from "../src/config/apps/terminal.config";
|
|
import { NAME, TAG_LINE } from "../src/config/branding.config";
|
|
import { WALLPAPERS } from "../src/config/desktop.config";
|
|
import { BASE_URL, BUILD_DIR, DOMAIN } from "../src/config/deploy.config";
|
|
|
|
const PATHS = {
|
|
sitemapXml: BUILD_DIR + "/sitemap.xml",
|
|
robotsTxt: BUILD_DIR + "/robots.txt",
|
|
indexHtml: BUILD_DIR + "/index.html",
|
|
cname: BUILD_DIR + "/CNAME",
|
|
};
|
|
|
|
function generateSitemapXml() {
|
|
const date = new Date();
|
|
const lastModified = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
|
|
|
|
const images = WALLPAPERS.map((path) => `
|
|
<image:image>
|
|
<image:loc>${BASE_URL.slice(0, -1) + path}</image:loc>
|
|
</image:image>`
|
|
);
|
|
|
|
const pages = Object.values(APPS).map((appId) => `
|
|
<url>
|
|
<loc>${BASE_URL + appId}</loc>
|
|
<lastmod>${lastModified}</lastmod>
|
|
</url>`
|
|
);
|
|
|
|
const sitemap = `
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE xml>
|
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
|
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
|
|
<url>
|
|
<loc>${BASE_URL}</loc>
|
|
<lastmod>${lastModified}</lastmod>
|
|
${images.join("")}
|
|
</url>
|
|
${pages.join("")}
|
|
</urlset>`;
|
|
|
|
return sitemap.trim();
|
|
}
|
|
|
|
function generateRobotsTxt() {
|
|
const sitemapUrl = BASE_URL + PATHS.sitemapXml.replace(BUILD_DIR + "/", "");
|
|
|
|
return `# https://www.robotstxt.org/robotstxt.html
|
|
User-agent: *
|
|
Disallow:
|
|
Sitemap: ${sitemapUrl}`;
|
|
}
|
|
|
|
function generateCname() {
|
|
return DOMAIN;
|
|
}
|
|
|
|
function generateTemplate(html: string) {
|
|
const baseUrlRegex = /(?<=")https?:\/\/[a-z0-9-]+(\.)([a-zA-Z0-9-]+(\.))*[a-z]{2,3}\/(?=.*")/gi;
|
|
html = html.replaceAll(baseUrlRegex, BASE_URL);
|
|
|
|
const commentsRegex = /<!--.*?-->/g;
|
|
html = html.replaceAll(commentsRegex, "");
|
|
|
|
const emptyLinesRegex = /^\s*$\n/gm;
|
|
html = html.replaceAll(emptyLinesRegex, "");
|
|
|
|
const path = `${BUILD_DIR}/index.html`;
|
|
fs.writeFileSync(path, html, { flag: "w+" });
|
|
console.log(`- ${ANSI.fg.cyan}${path}${ANSI.reset}`);
|
|
|
|
return html;
|
|
}
|
|
|
|
/**
|
|
* To avoid GitHub pages rendering certain pages that are only defined by React router as a 404 page,
|
|
* we copy the content of our index file to the 404 page, letting React router properly handle routing on every page
|
|
*/
|
|
function generate404Page(template: string) {
|
|
const path = `${BUILD_DIR}/404.html`;
|
|
fs.writeFileSync(path, template, { flag: "w+" });
|
|
console.log(`- ${ANSI.fg.cyan}${path}${ANSI.reset}`);
|
|
}
|
|
|
|
/**
|
|
* Add an HTML file for every app page so they can be properly crawled and indexed
|
|
*/
|
|
function generateAppPages(template: string) {
|
|
for (const [key, value] of Object.entries(APPS)) {
|
|
const appId = value;
|
|
const appName = key in APP_NAMES ? APP_NAMES[key as AppKey] as string : appId;
|
|
const appDescription = Object.keys(APP_DESCRIPTIONS).includes(key) ? APP_DESCRIPTIONS[key as AppKey] as string : TAG_LINE;
|
|
|
|
if (appId === "index") {
|
|
console.log("Invalid app ID found: " + appId);
|
|
return;
|
|
}
|
|
|
|
let html = template;
|
|
|
|
const titleRegex = /(?<=(<title>|<meta property="og:title" content="|<meta name="twitter:title" content="))(([a-zA-Z|-]|\s)+)(?=(<\/title>|"\/?>))/g;
|
|
html = html.replaceAll(titleRegex, `${appName} | ${NAME}`);
|
|
|
|
const descriptionRegex = /(?<=(<meta name="description" content="|<meta property="og:description" content="|<meta name="twitter:description" content="))(([a-zA-Z-.]|\s)+)(?=("\/?>))/g;
|
|
html = html.replaceAll(descriptionRegex, appDescription);
|
|
|
|
const canonicalRegex = /(?<=(<link rel="canonical" href="|<meta name="twitter:url" content="|<meta property="og:url" content="))(http(s)?:\/\/[a-zA-Z-.]+\/)(?=("\/?>))/g;
|
|
html = html.replaceAll(canonicalRegex, BASE_URL + appId);
|
|
|
|
const faqRegex = /<!-- FAQ -->.*?<script type="application\/ld\+json">.*?<\/script>/gs;
|
|
html = html.replaceAll(faqRegex, "");
|
|
|
|
const path = `${BUILD_DIR}/${appId}.html`;
|
|
fs.writeFileSync(path, html, { flag: "w+" });
|
|
console.log(`- ${ANSI.fg.cyan}${path}${ANSI.reset}`);
|
|
}
|
|
}
|
|
|
|
function stage() {
|
|
try {
|
|
console.log(`${ANSI.fg.yellow}Staging build...${ANSI.reset}`);
|
|
|
|
const files: [string, () => string][] = [
|
|
[PATHS.sitemapXml, generateSitemapXml],
|
|
[PATHS.robotsTxt, generateRobotsTxt],
|
|
[PATHS.cname, generateCname],
|
|
];
|
|
|
|
files.forEach(([path, generateContent]) => {
|
|
const directory = path.substring(0, path.lastIndexOf("/"));
|
|
if (directory != "" && !fs.existsSync(directory)){
|
|
fs.mkdirSync(directory, { recursive: true });
|
|
}
|
|
|
|
fs.writeFileSync(path, generateContent(), { flag: "w+" });
|
|
console.log(`- ${ANSI.fg.cyan}${path}${ANSI.reset}`);
|
|
});
|
|
|
|
console.log(`\n${ANSI.fg.yellow}Generating pages...${ANSI.reset}`);
|
|
|
|
const html = fs.readFileSync(PATHS.indexHtml, "utf-8");
|
|
const template = generateTemplate(html);
|
|
|
|
generate404Page(template);
|
|
generateAppPages(template);
|
|
|
|
console.log(`\n${ANSI.fg.green}✓ Staging complete${ANSI.reset}`);
|
|
} catch (error) {
|
|
console.error(error);
|
|
console.log(`${ANSI.fg.red}⚠ Staging failed${ANSI.reset}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
stage(); |