Reading list
+Articles and links I’ve found worth keeping, grouped by category.
+diff --git a/README.md b/README.md index 57dcf67..97ce69b 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,32 @@ vanilla JavaScript. ## Pages - `index.html` — home page +- `reading.html` — reading list page (curated links grouped by category, + rendered from `data/reading-list.js`) - `contact.html` — contact page (opens the visitor's mail client with the form fields pre-filled via a `mailto:` link) +## Reading list + +The reading list is a single data file: `data/reading-list.js`. The page code +renders whatever is in it, so it can grow to hundreds of entries without code +changes. + +To add a link, edit `data/reading-list.js` only — add an entry with a `title`, +a `url`, a `category`, and an optional one-line `note`: + +```js +{ + category: "Engineering", + title: "A new article", + url: "https://example.com/article", + note: "Optional one-liner shown under the title.", +}, +``` + +Categories appear on the page in the order they are first used; entries keep +the order they are listed in. + ## Development Serve the directory with any static file server, e.g.: @@ -31,10 +54,13 @@ npm test ```text index.html Home page +reading.html Reading list page contact.html Contact page css/style.css Global + responsive styles +data/reading-list.js Curated reading list data (edit to add links) js/mailto.js Pure mailto: URL builder (unit tested) js/contact.js Contact form wiring (browser + tests) +js/reading-list.js Reading list renderer: data file -> grouped HTML (tested) tests/ Node built-in test suite .gitea/workflows/ ci.yml runs `npm test` on PRs and pushes to main ``` diff --git a/contact.html b/contact.html index d7d37fc..f442c69 100644 --- a/contact.html +++ b/contact.html @@ -12,6 +12,7 @@ My Blog
diff --git a/css/style.css b/css/style.css index 0dc0a2f..eef36d8 100644 --- a/css/style.css +++ b/css/style.css @@ -120,6 +120,59 @@ button[type="submit"]:hover { filter: brightness(1.1); } +/* --- Reading list --- */ +.reading-category { + margin-bottom: 2rem; +} + +.reading-category h2 { + font-size: 1.1rem; + border-bottom: 1px solid var(--border); + padding-bottom: 0.35rem; + margin-bottom: 0.75rem; +} + +.reading-list { + margin: 0; + padding: 0; + list-style: none; + display: grid; + gap: 0.75rem; +} + +.reading-entry { + display: grid; + gap: 0.1rem; +} + +.reading-link { + color: var(--accent); + text-decoration: none; + overflow-wrap: anywhere; +} + +.reading-link:hover, +.reading-link:focus { + text-decoration: underline; +} + +.reading-note { + margin: 0; + color: var(--muted); + font-size: 0.9rem; +} + +.reading-empty { + color: var(--muted); +} + +/* Two columns once there is room — keeps long lists scannable. */ +@media (min-width: 40rem) { + .reading-list { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + /* --- Footer --- */ .site-footer { border-top: 1px solid var(--border); diff --git a/data/reading-list.js b/data/reading-list.js new file mode 100644 index 0000000..fd06a7c --- /dev/null +++ b/data/reading-list.js @@ -0,0 +1,82 @@ +/** + * Curated reading list — the single source of truth for the reading list page. + * + * Each entry: + * title — link text shown on the page (required) + * url — absolute http(s) URL the title links to (required) + * note — optional one-line note shown under the title + * category — heading the entry is grouped under (required) + * + * Entries appear on the page in the order they are listed here, and categories + * appear in the order they are first used. + * + * To add a link, edit this file only — no page or code changes are needed. + */ +export const READING_LIST = [ + // Design + { + category: "Design", + title: "Refactoring UI", + url: "https://www.refactoringui.com/", + note: "Practical visual-design tactics for developers.", + }, + { + category: "Design", + title: "A List Apart", + url: "https://alistapart.com/", + note: "Long-running essays on web design and development.", + }, + { + category: "Design", + title: "Laws of UX", + url: "https://lawsofux.com/", + note: "Design principles as quick-reference cards.", + }, + + // Engineering + { + category: "Engineering", + title: "The Twelve-Factor App", + url: "https://12factor.net/", + note: "Methodology for building modern, portable web services.", + }, + { + category: "Engineering", + title: "MDN Web Docs", + url: "https://developer.mozilla.org/", + note: "The web reference I reach for first.", + }, + { + category: "Engineering", + title: "Refactoring Guru", + url: "https://refactoring.guru/", + note: "Design patterns and refactoring, with diagrams.", + }, + { + category: "Engineering", + title: "How to Ask Questions the Smart Way", + url: "http://www.catb.org/esr/faqs/smart-questions.html", + }, + + // Typography + { + category: "Typography", + title: "Butterick's Practical Typography", + url: "https://practicaltypography.com/", + note: "Typography rules, explained in plain English.", + }, + { + category: "Typography", + title: "Fonts in Use", + url: "https://fontsinuse.com/", + note: "Real-world examples of type in action.", + }, + + // Writing + { + category: "Writing", + title: "Paul Graham's Essays", + url: "http://www.paulgraham.com/articles.html", + note: "Essays on startups, writing, and thinking.", + }, +]; diff --git a/index.html b/index.html index 28aa417..7fe5e75 100644 --- a/index.html +++ b/index.html @@ -12,6 +12,7 @@ My Blog diff --git a/js/reading-list.js b/js/reading-list.js new file mode 100644 index 0000000..e7a94e4 --- /dev/null +++ b/js/reading-list.js @@ -0,0 +1,111 @@ +/** + * Reading list renderer — turns the curated reading list data file into + * category-grouped HTML. Pure string builder (no DOM), so it can be unit + * tested in Node; the browser wiring at the bottom is guarded accordingly. + */ +import { READING_LIST } from "../data/reading-list.js"; + +/** HTML-escape a value so it is safe to embed in markup. */ +export function escapeHtml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +/** True when the value is an absolute http(s) URL. */ +export function isValidUrl(value) { + try { + const url = new URL(String(value)); + return url.protocol === "http:" || url.protocol === "https:"; + } catch { + return false; + } +} + +/** True when the entry has everything needed to render. */ +export function isValidEntry(entry) { + return Boolean( + entry && + typeof entry.title === "string" && + entry.title.length > 0 && + isValidUrl(entry.url) + ); +} + +/** + * Group valid entries by category, preserving first-seen category order and + * entry order within each category. Entries without a category fall under + * "Uncategorized". Invalid entries are skipped. + */ +export function groupByCategory(entries) { + const groups = new Map(); + for (const entry of entries) { + if (!isValidEntry(entry)) continue; + const category = entry.category || "Uncategorized"; + if (!groups.has(category)) groups.set(category, []); + groups.get(category).push(entry); + } + return groups; +} + +/** Slugify a category name into a stable, unique HTML id. */ +function slugify(value) { + return value + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); +} + +/** Render one entry: a title link plus an optional one-line note. */ +function renderEntry(entry) { + const note = entry.note + ? `${escapeHtml(entry.note)}
` + : ""; + return ( + `The reading list is empty.
'; + } + + const usedIds = new Set(); + const sections = []; + for (const [category, items] of groups) { + let id = slugify(category) || "category"; + while (usedIds.has(id)) id = `${id}-${usedIds.size + 1}`; + usedIds.add(id); + + sections.push( + `Articles and links I’ve found worth keeping, grouped by category.
+