feat(explorer): improve SEO metadata, add social preview card and sitemap

This commit is contained in:
Meysam Parvizi
2026-10-14 01:46:01 +02:00
parent 9d83d84f8b
commit e9946aa71a
6 changed files with 123 additions and 5 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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);
+34 -2
View File
@@ -4,15 +4,47 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; font-src 'self'; img-src 'self' data:; connect-src 'self' https://raw.githubusercontent.com; base-uri 'none'; form-action 'none'; object-src 'none'">
<title>Embedded Systems Engineering Roadmap · Explorer</title>
<meta name="description" content="Interactive map of the Embedded Systems Engineering Roadmap: explore topics, how they connect, and their learning resources.">
<title>Embedded Systems Engineering Roadmap: Interactive Learning Path</title>
<meta name="description" content="Interactive roadmap for embedded engineers: microcontrollers, RTOS, embedded Linux, protocols, debugging and curated learning resources.">
<meta name="google-site-verification" content="y5W2YH3fkl_7vDqWiUHVUK4hPvV7orvVwVRzIqTAgFc">
<meta name="robots" content="index, follow, max-image-preview:large">
<link rel="canonical" href="https://m3y54m.github.io/Embedded-Engineering-Roadmap/">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Embedded Systems Engineering Roadmap">
<meta property="og:title" content="Embedded Systems Engineering Roadmap: Interactive Learning Path">
<meta property="og:description" content="Interactive roadmap for embedded engineers: microcontrollers, RTOS, embedded Linux, protocols, debugging and curated learning resources.">
<meta property="og:url" content="https://m3y54m.github.io/Embedded-Engineering-Roadmap/">
<meta property="og:image" content="https://m3y54m.github.io/Embedded-Engineering-Roadmap/Embedded-Engineering-Roadmap-social.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Embedded Systems Engineering Roadmap with a preview of the roadmap map">
<meta name="twitter:card" content="summary_large_image">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Embedded Systems Engineering Roadmap",
"url": "https://m3y54m.github.io/Embedded-Engineering-Roadmap/",
"description": "Interactive roadmap for embedded engineers: microcontrollers, RTOS, embedded Linux, protocols, debugging and curated learning resources.",
"inLanguage": "en",
"license": "https://creativecommons.org/licenses/by-sa/4.0/",
"creator": { "@type": "Person", "name": "m3y54m", "url": "https://github.com/m3y54m" },
"sameAs": ["https://github.com/m3y54m/Embedded-Engineering-Roadmap"]
}
</script>
<link rel="icon" href="assets/logo.svg" type="image/svg+xml">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="map.css">
<script type="module" src="app.js"></script>
</head>
<body>
<h1 class="sr-only">Embedded Systems Engineering Roadmap</h1>
<noscript>
<div class="noscript">
<p>A learning roadmap for embedded systems engineers: electronics, programming languages, microcontrollers, interfaces and protocols, operating systems and RTOS, embedded Linux, debugging, testing and security, each with curated learning resources.</p>
<p>The interactive map needs JavaScript. Without it, read the <a href="https://github.com/m3y54m/Embedded-Engineering-Roadmap#readme">roadmap on GitHub</a> or download it as a <a href="Embedded-Engineering-Roadmap.pdf">PDF</a> or <a href="Embedded-Engineering-Roadmap.png">PNG</a>.</p>
</div>
</noscript>
<div class="app">
<header class="topbar">
<a class="brand" href="#/" aria-label="Roadmap overview">
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://m3y54m.github.io/Embedded-Engineering-Roadmap/</loc>
</url>
</urlset>
+30
View File
@@ -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%;
+51 -1
View File
@@ -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) => `<!doctype html>
<html lang="en">
<meta charset="utf-8">
<style>
@font-face { font-family: "Sofia Sans Semi Condensed"; src: url("${base}fonts/SofiaSansSemiCondensed.woff2") format("woff2"); font-weight: 1 1000; }
* { box-sizing: border-box; margin: 0; }
body {
width: 1200px; height: 630px; overflow: hidden; display: flex; align-items: center; justify-content: space-between; gap: 40px; padding: 0 72px 0 80px;
font-family: "Sofia Sans Semi Condensed", "Arial Narrow", sans-serif; color: #e9edf6;
background: radial-gradient(900px 600px at 8% -10%, rgba(91, 103, 255, 0.28), transparent 62%), radial-gradient(800px 600px at 100% 110%, rgba(20, 184, 166, 0.2), transparent 60%), #060914;
}
.copy { flex: 1; }
.brand { display: flex; align-items: center; gap: 16px; font-size: 28px; letter-spacing: 0.06em; text-transform: uppercase; color: #94a3b8; }
.brand img { width: 56px; height: 56px; }
h1 { margin-top: 28px; font-size: 72px; line-height: 1.04; font-weight: 800; }
p { margin-top: 24px; max-width: 560px; font-size: 32px; line-height: 1.3; color: #b6c0d4; }
.url { margin-top: 36px; font-size: 26px; color: #8fa8ff; }
.map { flex: none; height: 540px; border-radius: 14px; border: 1px solid rgba(148, 163, 184, 0.3); box-shadow: 0 24px 64px rgba(0, 0, 0, 0.55); }
</style>
<div class="copy">
<div class="brand"><img src="${base}assets/logo.svg" alt="">Interactive learning path</div>
<h1>${TITLE}</h1>
<p>Topics, how they connect and curated learning resources for embedded engineers.</p>
<div class="url">m3y54m.github.io/Embedded-Engineering-Roadmap</div>
</div>
<img class="map" src="/__roadmap.png" alt="">
`;
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();