UNPKG

ap-ssg

Version:

A fast, modular, SEO-optimized static site generator that minifies CSS, JS, and HTML for improved performance. It also supports JSON-LD, sitemap generation, and more, making it ideal for production-ready websites.

399 lines (352 loc) 12.9 kB
const fs = require("fs-extra"); const path = require("path"); const getBuildFilePath = require("./utils/getBuildFilePath"); const htmlMinifier = require("html-minifier"); const { escapePreCode } = require("./utils/getEscapeHtml"); const addImageAltText = require("./utils/addImgAltText"); const JsonldSchemas = require("./utils/jsonldSchemas"); const removeImgClosingSlash = require("./utils/removeImgClosingSlash"); const mapJoin = require("map-join"); class HtmlGenerator { constructor(doc) { this.jsonld = new JsonldSchemas(doc); this.head = ""; this.afterBodyContent = ""; this.bodyContent = ""; this.filePath = getBuildFilePath(doc.filePath); this.title = doc?.title || "Document"; this.url = doc.url; this.canonicalUrl = doc.canonicalUrl; this.description = doc.description; this.keywords = doc.keywords; this.authorName = doc.author?.name; this.ogImage = doc.ogImage; this.twitterHandle = doc.twitterHandle; this.pageLanguage = doc.pageLanguage; this.integrations = doc.integrations; this.googleTagManager = doc?.integrations?.googleTagManager ?? ""; this.themeColor = doc.themeColor; this.pwaEnabled = doc.pwa.enabled; this.autoImgAlt = doc.autoImgAlt; this.commonCss = doc.commonCss || []; this.useCommonCss = doc.useCommonCss; this.commonJs = doc.commonJs || []; this.useCommonJs = doc.useCommonJs; // Default shouldAllowIndexing and shouldFollowLinks to true if not provided this.shouldAllowIndexing = doc.shouldAllowIndexing ?? true; // Default true this.shouldFollowLinks = doc.shouldFollowLinks ?? true; // Default true } /** * Append tags in head section of the page * @param arr */ insertHead(arr) { this.#appender(arr, "head"); } /** * Append tags in the end of page body section * @param arr */ insertBody(arr) { this.#appender(arr, "afterBodyContent"); } #appender(arr, constructorKey) { if (!Array.isArray(arr)) return ""; this[constructorKey] += mapJoin(arr, (item) => item + "\n"); } /** * Pass content to add between page body tag * @param {string} content */ setBodyContent(content) { this.bodyContent = content; } /** * Generates the Google Analytics script for inclusion in the head tag. * @returns {string} Google Analytics script as a string, or an empty string if the tracking ID is invalid. */ #getGoogleAnalytics() { const { googleAnalytics } = this.integrations; if ( googleAnalytics && typeof googleAnalytics === "string" && googleAnalytics !== "" ) { return ` <!-- Google Analytics --> <script async src="https://www.googletagmanager.com/gtag/js?id=${googleAnalytics}"></script> <script> window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag("js", new Date()); gtag("config", "${googleAnalytics}"); </script> <!-- End Google Analytics --> `; } return ""; } /** * Generates the Bing Webmaster Tools meta tag for verification. * * This function checks if a valid Bing verification ID is provided and returns * the appropriate `<meta>` tag for Bing Webmaster Tools verification. * * @returns {string} * The meta tag for Bing Webmaster Tools verification, or an empty string * if no valid Bing verification ID is provided. */ #getBingWebmaster() { const { bingWebmasters } = this.integrations; if (bingWebmasters === "" || bingWebmasters === undefined) return ""; return `<meta name="msvalidate.01" content="${bingWebmasters}" />`; } /** * Return google webmaster verification meta tag * @returns {string} */ #getGoogleWebmaster() { const { googleWebmasters } = this.integrations; if (googleWebmasters === "" || googleWebmasters === undefined) return ""; return `<meta name="google-site-verification" content="${googleWebmasters}" />`; } /** * Return hotjar tracking script * @returns {string} */ #getHotjarAnalytics() { const { hotjarAnalytics } = this.integrations; if (hotjarAnalytics === "" || hotjarAnalytics === undefined) return ""; return `<script> (function(h,o,t,j,a,r){ h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments);}; h._hjSettings={hjid:"${hotjarAnalytics}",hjsv:6}; a=o.getElementsByTagName("head")[0]; r=o.createElement("script");r.async=1; r.src=t+j+"?sv="+h._hjSettings.hjsv; a.appendChild(r); })(window,document,'https://static.hotjar.com/c/hotjar-','.js'); </script>`; } #getFacebookPixel() { const { facebookPixel: facebookPixelCode } = this.integrations; if (facebookPixelCode === "" || facebookPixelCode === undefined) return ""; return `<script> !function(f,b,e,v,n,t,s) {if(f.fbq)return;n=f.fbq=function(){n.callMethod? n.callMethod.apply(n,arguments):n.queue.push(arguments);}; if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version="2.0"; n.queue=[];t=b.createElement(e);t.async=!0; t.src=v;s=b.getElementsByTagName(e)[0]; s.parentNode.insertBefore(t,s);}(window, document,"script", "https://connect.facebook.net/en_US/fbevents.js"); fbq("init", "${facebookPixelCode}"); // Replace 'YOUR_PIXEL_ID' with your Pixel ID fbq("track", "PageView"); </script> <noscript> <img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=${facebookPixelCode}&ev=PageView&noscript=1"/> </noscript> `; } #getMicroSoftClarity() { const { microsoftClarityCode } = this.integrations; if (microsoftClarityCode === "" || microsoftClarityCode === undefined) return ""; return `<script type="text/javascript"> (function(c,l,a,r,i,t,y){ c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)}; t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i; y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y); })(window, document, "clarity", "script", "${microsoftClarityCode}"); </script> `; } #insertCommonCss() { if (!this.useCommonCss) return ""; if (!this.commonCss?.length) return ""; return mapJoin( this.commonCss, (href) => `<link rel="stylesheet" href="${href}">` ); } #insertCommonJS() { if (!this.useCommonJs) return ""; if (!this.commonJs?.length) return ""; return mapJoin(this.commonJs, (href) => `<script src="${href}"></script>`); } /** * Get head tags * @returns {string} */ #getHeadTags() { const robotsContent = [ this.shouldAllowIndexing ? "index" : "noindex", this.shouldFollowLinks ? "follow" : "nofollow", ].join(", "); return ` <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="apple-touch-icon" sizes="180x180" href="/assets/site/apple-touch-icon.png"> <link rel="icon" type="image/png" sizes="32x32" href="/assets/site/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="16x16" href="/assets/site/favicon-16x16.png"> ${ this.pwaEnabled ? '<link rel="manifest" href="/manifest.json" />' : "" } <meta name="theme-color" content="${this.themeColor}"> ${this.#insertCommonCss()} ${this.jsonld.getBlogSchema()} ${this.jsonld.getBreadCrumbs()} ${this.jsonld.getWebsiteSchema()} ${this.jsonld.getOrganizationSchema()} ${this.jsonld.getSoftwareSchema()} ${this.head} ${this.#getBingWebmaster()} ${this.#getGoogleWebmaster()} ${this.#getGoogleAnalytics()} ${this.#getHotjarAnalytics()} ${this.#getFacebookPixel()} ${this.#getMicroSoftClarity()} <title>${this.title}</title> ${ this.shouldAllowIndexing ? `<link rel="canonical" href="${this.canonicalUrl}">` : "" } <meta name="description" content="${this.description}"> ${ this.keywords ? ` <meta name="keywords" content="${this.keywords}">` : "" } ${ this.authorName ? ` <meta name="author" content="${this.authorName}">` : "" } <meta name="robots" content="${robotsContent}"> <!-- Open Graph Meta Tags --> <meta property="og:title" content="${this.title}"> <meta property="og:description" content="${this.description}"> <meta property="og:url" content="${this.url}"> <meta property="og:type" content="website"> <meta property="og:image" content="${this.ogImage}"> <meta property="og:image:alt" content="${this.title}"> <!-- Twitter Card Meta Tags --> <meta name="twitter:card" content="summary_large_image"> <meta name="twitter:title" content="${this.title}"> <meta name="twitter:description" content="${this.description}"> <meta name="twitter:image" content="${this.ogImage}"> <meta name="twitter:image:alt" content="${this.title}"> ${ this.twitterHandle ? `<meta name="twitter:site" content="${this.twitterHandle}">` : "" } `; } /** * Generates the PWA service worker registration script. * @returns {string} The script to register the service worker, or an empty string if PWA is not enabled. */ #getPWAScript() { if (!this.pwaEnabled) return ""; // Return empty string if PWA is not enabled return `<script> if ("serviceWorker" in navigator) { // Ensure the page is served over HTTPS before registering service worker navigator.serviceWorker.register("/serviceworker.js") .catch((error) => { console.error("Service worker registration failed:", error); }); } </script>`; } #getHtml() { const headGTM = this.googleTagManager !== "" ? "<script async " + `src="https://www.googletagmanager.com/gtm.js?id=${this.googleTagManager}" >` + "</script>" : ""; const bodyGTM = this.googleTagManager !== "" ? "<noscript>" + "<iframe " + `src="https://www.googletagmanager.com/ns.html?id=${this.googleTagManager}"\n ` + ' height="0" width="0" style="display:none;visibility:hidden"></iframe>' + "</noscript>" : ""; let html = ` <!DOCTYPE html> <html lang="${this.pageLanguage}"> <head> ${headGTM} ${this.#getHeadTags()} </head> <body> ${bodyGTM} ${this.bodyContent} ${this.afterBodyContent} ${this.#getPWAScript()} ${this.#insertCommonJS()} </body> </html> `; html = escapePreCode(html); if (this.autoImgAlt) { html = addImageAltText(html); } else { html = removeImgClosingSlash(html); } return html; } /** * Write file and ensure dir * @param htmlContent * @param filePath * @returns {Promise<void>} */ async writeToFile(filePath, htmlContent) { try { await fs.ensureDir(path.dirname(filePath)); await fs.writeFile(filePath, htmlContent.trim(), "utf8"); } catch (err) { console.error("Error writing the file:", err); } } /** * Generate minimized html file * @returns {Promise<boolean>} */ async generate() { try { const options = { removeComments: true, collapseWhitespace: true, removeEmptyElements: true, removeEmptyAttributes: true, minifyJS: true, // Enable minification of JS minifyCSS: true, }; // Configure UglifyJS options for function name mangling options.minifyJS = { compress: { comparisons: false, }, mangle: { toplevel: true, reserved: ["gtag"], // Reserve 'gtag' function name keep_fnames: true, }, }; const minifiedHtml = htmlMinifier.minify(this.#getHtml(), options); await this.writeToFile(this.filePath, minifiedHtml); return true; } catch (err) { return false; } } } module.exports = HtmlGenerator;