diff --git a/.env b/.env new file mode 100644 index 0000000..cf16acd --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +DOCRAPTOR_API_KEY="SXrftL1JWvCnWw0AkI9e" +DOCRAPTOR_TEST="true" diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..049d7a7 --- /dev/null +++ b/.env.template @@ -0,0 +1,2 @@ +DOCRAPTOR_API_KEY="" +DOCRAPTOR_TEST="true" diff --git a/build-pdf.mjs b/build-pdf.mjs new file mode 100644 index 0000000..5c3641f --- /dev/null +++ b/build-pdf.mjs @@ -0,0 +1,126 @@ +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import dotenv from 'dotenv'; +import { globby } from 'globby'; +import fetch from 'node-fetch'; +import rehypeInline from 'rehype-inline'; +import rehypeParse from 'rehype-parse'; +import rehypeStringify from 'rehype-stringify'; +import { unified } from 'unified'; + +dotenv.config(); +const docraptorApiKey = process.env.DOCRAPTOR_API_KEY; +const docraptorTest = process.env.DOCRAPTOR_TEST; +const currentDir = path.dirname(fileURLToPath(import.meta.url)); +const distDir = path.join(currentDir, 'dist', 'pdf'); +const pages = await globby('dist/**/*.html'); + +/** + * Inline Assets + * Uses rehype/unified to inline assets like CSS and JS. + */ +const inlineAssets = unified() + .use(rehypeParse, { fragment: false }) + .use(rehypeInline, { + js: true, + css: true, + images: true, + imports: true, + svgElements: false, + }) + .use(rehypeStringify); + +/** + * Get an HTML file and prepare it for DocRaptor + * + * @param {string} htmlPath - the HTML file to load + * @returns {string} the contents of the HTML file with inlined CSS + */ +const getHTML = async (htmlPath) => { + // Grab the HTML file contents as a string + const rawHTML = await fs.readFile(htmlPath, 'utf8'); + // Change the CSS URI to a path so it can be inlined + const updatedCSS = rawHTML.replace('/style.css', 'dist/style.css'); + // Change any image URLs to paths so they can be inlined + const updatedImages = updatedCSS.replace('/images/', 'dist/images/'); + // Inline the CSS + return String(await inlineAssets.process(updatedImages)); +}; + +/** + * Generate a PDF using DocRaptor + * + * @see https://docraptor.com/documentation/api + * @param {string} html - passed to docraptor + * @param {string} slug - used for better error logging only + * @returns {Buffer} + */ +const getPDF = async (html, slug) => { + if (!docraptorApiKey) throw new Error('Missing DocRaptor API Key'); + // Send HTML to DocRaptor to generate PDF + const pdfRes = await fetch('https://docraptor.com/docs', { + method: 'POST', + headers: { + Authorization: `Basic ${Buffer.from(docraptorApiKey).toString('base64')}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + test: docraptorTest, + document_content: html, + type: 'pdf', + prince_options: { + profile: 'PDF/UA-1', // Adds accessibility features like tagging + }, + }), + }); + if (!pdfRes.ok) + throw new Error( + `${slug}: ${pdfRes.status} ${pdfRes.statusText} ${await pdfRes.text()}`, + ); + // Extract the PDF from the response and return it + const blob = await pdfRes.blob(); + return Buffer.from(await blob.arrayBuffer(), 'binary'); +}; + +/** + * Create a PDF from an HTML file + * + * @param {string} htmlPath - the HTML file to load + */ +const generatePDF = async (htmlPath) => { + // Strip `dist/` and `/index.html` from htmlPath + let slug = htmlPath.slice(5, -11); + // Special case for the root HTML file + if (htmlPath === 'dist/index.html') slug = 'index'; + // Create relative HTML path and PDF write destination + const htmlPathCWD = path.join(currentDir, htmlPath); + const pdfSlug = slug.replace('/', '-'); + const pdfPath = path.join(distDir, `${pdfSlug}.pdf`); + // Load the HTML file + const html = await getHTML(htmlPathCWD); + // Generate the PDF with DocRaptor + const pdf = await getPDF(html, slug); + // Save the PDF to a file + await fs.writeFile(pdfPath, pdf); + console.log(`[PDF] Writing dist/pdf/${pdfSlug}.pdf`); +}; + +/** + * Build All PDFs + * This script will generate a PDF from every HTML file in the `/dist` folder by + * passing the HTML to DocRaptor and saving the files to `/dist/pdf`. + */ +const buildAllPDFs = async () => { + // Create the output directory if it doesn't exist + await fs.mkdir(distDir, { recursive: true }); + // Loop over the list of pages to generate an individual PDF for each page + await Promise.all( + pages.map(async (page) => { + await generatePDF(page); + }), + ); +}; + +buildAllPDFs(); diff --git a/package.json b/package.json index d48d31e..1d4c212 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,10 @@ "license": "UNLICENSED", "scripts": { "start": "yarn watch", - "build": "run-p build:*", + "build": "run-s build:*", "build:content": "eleventy", "build:css": "sass src:dist", + "build:pdf": "node build-pdf.mjs", "watch": "run-p watch:*", "watch:content": "eleventy --serve", "watch:css": "sass --watch src:dist", @@ -21,12 +22,19 @@ }, "devDependencies": { "@11ty/eleventy": "2.0.1", + "dotenv": "^16.3.1", + "globby": "^13.2.2", "markdown-it-footnote": "3.0.3", + "node-fetch": "^3.3.2", "npm-run-all": "4.1.5", "prettier": "3.0.1", + "rehype-inline": "^2.0.0", + "rehype-parse": "^8.0.4", + "rehype-stringify": "^9.0.3", "sass": "1.65.1", "stylelint": "15.10.2", "stylelint-config-prettier": "9.0.5", - "stylelint-config-spaceninja": "13.0.1" + "stylelint-config-spaceninja": "13.0.1", + "unified": "^10.1.2" } } diff --git a/src/_includes/layout.njk b/src/_includes/layout.njk index 88ebdd1..3b783ac 100644 --- a/src/_includes/layout.njk +++ b/src/_includes/layout.njk @@ -9,7 +9,6 @@ -