diff --git a/README.md b/README.md index 8f66ba1..672a850 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ pnpm install pnpm dev ``` -`pnpm dev` builds `dist/index.html` (including Mermaid transformation), serves `dist/`, and rebuilds when `slides.md` changes. +`pnpm dev` builds `dist/index.html` (including Mermaid transformation), +serves `dist/`, and rebuilds when `slides.md` changes. By default it uses port `8080`; if `8080` is busy it automatically uses the next available port and prints the URL. @@ -36,6 +37,29 @@ pnpm export The HTML build is written to `dist/index.html`. The PDF export is written to `dist/slides.pdf`. +## Images and sponsor logos + +Store images under `assets/` and reference them from `slides.md` +with relative paths such as `./assets/sponsors/acme.png`. + +That works in both places because: + +- local development serves `dist/index.html` +- the build copies `assets/` to `dist/assets/` +- GitHub Pages publishes the same built files from `dist/` + +Avoid absolute URLs such as `/assets/acme.png`, because a project +site is published under `/intro-to-home-networking/` rather than the +domain root. + +Standard Markdown image: + +```md +![width:220px](./assets/sponsors/acme.png) +``` + +While `pnpm dev` is running, changes to files inside `assets/` trigger a rebuild automatically. + ## GitHub Pages deployment This repo is designed to deploy from GitHub Actions to GitHub Pages. @@ -60,6 +84,11 @@ This repository follows gitflow for version control: - Feature/fix branches: branch from `develop` with naming pattern `feature/` or `fix/`. **Workflow:** + +Follow gitflow strictly for day-to-day changes: create a new feature or fix +branch from `develop` before you start editing, and avoid making working +changes directly on `develop`. + 1. Create a feature or fix branch from `develop`. 2. Make your changes, test locally with `pnpm dev`. 3. Push the branch and open a PR against `develop`. @@ -68,4 +97,4 @@ This repository follows gitflow for version control: ## Next edits -If you want to expand the deck later, add images or diagrams under an `assets/` directory and reference them from `slides.md`. \ No newline at end of file +If you want to expand the deck later, add images or diagrams under an `assets/` directory and reference them from `slides.md`. diff --git a/assets/sponsors/sponsors-desklodge.png b/assets/sponsors/sponsors-desklodge.png new file mode 100644 index 0000000..f395724 Binary files /dev/null and b/assets/sponsors/sponsors-desklodge.png differ diff --git a/assets/sponsors/sponsors-io-academy.webp b/assets/sponsors/sponsors-io-academy.webp new file mode 100644 index 0000000..a4cc157 Binary files /dev/null and b/assets/sponsors/sponsors-io-academy.webp differ diff --git a/assets/sponsors/sponsors-tuppenny-well.svg b/assets/sponsors/sponsors-tuppenny-well.svg new file mode 100644 index 0000000..a2f6ca7 --- /dev/null +++ b/assets/sponsors/sponsors-tuppenny-well.svg @@ -0,0 +1,77 @@ + + + + diff --git a/scripts/dev.mjs b/scripts/dev.mjs index 5814f0c..31f679e 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -1,16 +1,21 @@ import { execFileSync, spawn } from 'node:child_process'; import net from 'node:net'; -import { watchFile } from 'node:fs'; +import { watch, watchFile } from 'node:fs'; +import { access, readdir } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const rootDir = process.cwd(); const slidesFile = path.join(rootDir, 'slides.md'); +const assetsDir = path.join(rootDir, 'assets'); const buildScript = path.join(__dirname, 'build.mjs'); let isBuilding = false; let rebuildPending = false; +let rebuildTimer; + +const assetWatchers = new Map(); function isPortAvailable(port) { return new Promise((resolve) => { @@ -59,8 +64,90 @@ function buildDeck() { } } +function queueBuild(message) { + if (rebuildTimer) { + clearTimeout(rebuildTimer); + } + + rebuildTimer = setTimeout(() => { + console.log(`[dev] ${message}; rebuilding...`); + buildDeck(); + }, 100); +} + +async function collectAssetDirectories(directory, directories = []) { + directories.push(directory); + + const entries = await readdir(directory, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isDirectory()) { + continue; + } + + await collectAssetDirectories(path.join(directory, entry.name), directories); + } + + return directories; +} + +let syncAssetWatchersQueue = Promise.resolve(); + +function syncAssetWatchers() { + const run = async () => { + try { + await access(assetsDir); + } catch { + for (const watcher of assetWatchers.values()) { + watcher.close(); + } + + assetWatchers.clear(); + return; + } + + const directories = await collectAssetDirectories(assetsDir); + const expectedDirectories = new Set(directories); + + for (const watchedDirectory of assetWatchers.keys()) { + if (expectedDirectories.has(watchedDirectory)) { + continue; + } + + assetWatchers.get(watchedDirectory)?.close(); + assetWatchers.delete(watchedDirectory); + } + + for (const directory of directories) { + if (assetWatchers.has(directory)) { + continue; + } + + const watcher = watch(directory, (eventType, filename) => { + const changedPath = filename ? path.relative(rootDir, path.join(directory, filename.toString())) : path.relative(rootDir, directory); + queueBuild(`asset ${eventType} detected in ${changedPath}`); + + if (eventType === 'rename') { + void syncAssetWatchers(); + } + }); + + watcher.on('error', (error) => { + console.error(`[dev] Asset watcher error in ${path.relative(rootDir, directory)}:`); + console.error(error); + }); + + assetWatchers.set(directory, watcher); + } + }; + + const syncPromise = syncAssetWatchersQueue.then(run, run); + syncAssetWatchersQueue = syncPromise.catch(() => {}); + return syncPromise; +} + async function main() { buildDeck(); + await syncAssetWatchers(); const requestedPort = Number(process.env.PORT ?? '8080'); const port = await findAvailablePort(requestedPort); @@ -76,12 +163,30 @@ async function main() { watchFile(slidesFile, { interval: 500 }, (current, previous) => { if (current.mtimeMs !== previous.mtimeMs) { - console.log('[dev] slides.md changed; rebuilding...'); - buildDeck(); + queueBuild('slides.md changed'); } }); + const rootWatcher = watch(rootDir, (eventType, filename) => { + if (filename?.toString() !== 'assets') { + return; + } + + void syncAssetWatchers(); + queueBuild(`assets directory ${eventType} detected`); + }); + function shutdown(code = 0) { + if (rebuildTimer) { + clearTimeout(rebuildTimer); + } + + rootWatcher.close(); + + for (const watcher of assetWatchers.values()) { + watcher.close(); + } + server.kill('SIGTERM'); process.exit(code); } diff --git a/slides.md b/slides.md index f16f9a3..25dca99 100644 --- a/slides.md +++ b/slides.md @@ -94,17 +94,37 @@ style: | ## 1. Welcome & Context -- A brief introduction -- Our sponsors -- Rolling Q&A +### A bit about me ---- +I'm on [LinkedIn](https://www.linkedin.com/in/jamesrennison) + +### Our sponsors + +**The generous hosts of our IRL meetups** + +![width:220px](./assets/sponsors/sponsors-desklodge.png) + +**The magnificent financial contributors** + +![width:220px](./assets/sponsors/sponsors-io-academy.webp) -### Why home networking matters +**The newest sponsor and hosts of our website** + +![width:220px](./assets/sponsors/sponsors-tuppenny-well.svg) + +### Any questions? + +Rolling Q&A - please don't stand on ceremony --- -### Common pain points: ads, tracking, slow and insecure DNS +## Why home networking matters + +### Common pain points + +- Dubious hardware from your ISP +- Ads and tracking +- Slow and insecure DNS ---