diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 70f280b..51bafb4 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -56,7 +56,7 @@ jobs: - name: Assemble the site run: | mkdir _site - cp explorer/*.html explorer/*.css explorer/*.js explorer/map.json README.md _site/ + cp explorer/*.html explorer/*.css explorer/*.js explorer/sitemap.xml explorer/map.json README.md _site/ cp -r explorer/fonts explorer/assets _site/ - name: Install Chromium diff --git a/explorer/app.js b/explorer/app.js index e3ff1af..b847d02 100644 --- a/explorer/app.js +++ b/explorer/app.js @@ -98,7 +98,7 @@ function navigate(node, { push = true, resource = null, reveal = false } = {}) { syncOutline(); if (resource) revealResource(resource); const root = state.data.root; - document.title = node === root ? `${TITLE} · Explorer` : `${node.title} · ${TITLE}`; + document.title = node === root ? `${TITLE}: Interactive Learning Path` : `${node.title} · ${TITLE}`; if (push) { const hash = node === root ? '#/' : `#/${node.slug}`; if (location.hash !== hash) history.pushState(null, '', hash); diff --git a/explorer/index.html b/explorer/index.html index 298259f..8ce6f8f 100644 --- a/explorer/index.html +++ b/explorer/index.html @@ -4,15 +4,47 @@ - Embedded Systems Engineering Roadmap · Explorer - + Embedded Systems Engineering Roadmap: Interactive Learning Path + + + + + + + + + + + + + + +

Embedded Systems Engineering Roadmap

+
diff --git a/explorer/sitemap.xml b/explorer/sitemap.xml new file mode 100644 index 0000000..3df5959 --- /dev/null +++ b/explorer/sitemap.xml @@ -0,0 +1,6 @@ + + + + https://m3y54m.github.io/Embedded-Engineering-Roadmap/ + + diff --git a/explorer/styles.css b/explorer/styles.css index 02f23df..6f4858b 100644 --- a/explorer/styles.css +++ b/explorer/styles.css @@ -101,6 +101,36 @@ html[data-theme="light"] { display: none !important; } +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + border: 0; +} + +.noscript { + position: fixed; + inset: 0; + z-index: 10; + display: grid; + align-content: center; + justify-items: center; + gap: 12px; + padding: 24px; + background: var(--bg); + text-align: center; +} + +.noscript p { + max-width: 40rem; + margin: 0; +} + html, body { height: 100%; diff --git a/explorer/tools/render.mjs b/explorer/tools/render.mjs index a4a4fe9..9ddce82 100644 --- a/explorer/tools/render.mjs +++ b/explorer/tools/render.mjs @@ -1,15 +1,18 @@ // Renders the roadmap map to an A4 PDF and a PNG with headless Chromium, straight from the site. +// Also writes a 1200x630 social preview card (Open Graph image) that shows the PNG next to the roadmap's name. // Usage: node explorer/tools/render.mjs [rootDir] [pagePath] [outDir] // defaults: repository root, /explorer/, ./dist (needs: npm install --no-save playwright) import { createServer } from 'node:http'; import { mkdir, readFile } from 'node:fs/promises'; import { extname, join, resolve, sep } from 'node:path'; import { chromium } from 'playwright'; +import { TITLE } from '../dom.js'; const root = resolve(process.argv[2] || '.'); const pagePath = process.argv[3] || '/explorer/'; const outDir = resolve(process.argv[4] || 'dist'); const NAME = 'Embedded-Engineering-Roadmap'; +const SOCIAL_NAME = `${NAME}-social.png`; const PNG_SCALE = 2; const TYPES = { @@ -23,8 +26,49 @@ const TYPES = { '.png': 'image/png', }; +// Served by the local server so the card can load the site's font and logo from the same origin. +const socialCard = (base) => ` + + + +
+
Interactive learning path
+

${TITLE}

+

Topics, how they connect and curated learning resources for embedded engineers.

+
m3y54m.github.io/Embedded-Engineering-Roadmap
+
+ +`; + const server = createServer(async (req, res) => { const url = new URL(req.url, 'http://localhost'); + if (url.pathname === '/__social.html') { + res.writeHead(200, { 'content-type': TYPES['.html'] }).end(socialCard(pagePath)); + return; + } + if (url.pathname === '/__roadmap.png') { + try { + res.writeHead(200, { 'content-type': TYPES['.png'] }).end(await readFile(join(outDir, `${NAME}.png`))); + } catch { + res.writeHead(404).end(); + } + return; + } const file = resolve(join(root, decodeURIComponent(url.pathname), url.pathname.endsWith('/') ? 'index.html' : '')); if (file !== root && !file.startsWith(root + sep)) { res.writeHead(403).end(); @@ -71,7 +115,13 @@ try { await page.evaluate(() => window.dispatchEvent(new Event('beforeprint'))); await page.locator('.diagram').screenshot({ path: join(outDir, `${NAME}.png`) }); - console.log(`Wrote ${NAME}.pdf and ${NAME}.png (${Math.round(w * PNG_SCALE)}x${Math.round(h * PNG_SCALE)}) to ${outDir}`); + const social = await browser.newContext({ viewport: { width: 1200, height: 630 }, deviceScaleFactor: 1 }); + const card = await social.newPage(); + await card.goto(`http://127.0.0.1:${server.address().port}/__social.html`, { waitUntil: 'networkidle' }); + await card.evaluate(() => document.fonts.ready); + await card.screenshot({ path: join(outDir, SOCIAL_NAME) }); + + console.log(`Wrote ${NAME}.pdf, ${NAME}.png (${Math.round(w * PNG_SCALE)}x${Math.round(h * PNG_SCALE)}) and ${SOCIAL_NAME} to ${outDir}`); } finally { await browser.close(); server.close();