From 07a7386e1d84cbc6a51df86b661a805b4d0b8d76 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Mon, 3 Feb 2025 13:32:15 +0100 Subject: [PATCH 01/10] Move scripts and stylesheets into /assets To keep things organised put the assets supporting pages into sub-folders in /assets. --- src/components/BasePage.tsx | 4 ++-- web/{ => assets/scripts}/viewport.js | 0 web/{style.css => assets/styles/base.css} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename web/{ => assets/scripts}/viewport.js (100%) rename web/{style.css => assets/styles/base.css} (100%) diff --git a/src/components/BasePage.tsx b/src/components/BasePage.tsx index 13674d9..6d50cf5 100644 --- a/src/components/BasePage.tsx +++ b/src/components/BasePage.tsx @@ -18,8 +18,8 @@ source.addEventListener("reload", () => location.reload());`; {props.title} - - , "/subdir"); + assert.equal((el as Element).attributes.get("src"), "../assets/script.js"); + }); + + test("img src", () => { + const el = resolveRefs(, "/subdir"); + assert.equal((el as Element).attributes.get("src"), "../assets/image.png"); + }); + test("nested element", () => { const el = resolveRefs(
Content with Link
, "/"); assert.equal((el.childNodes[1] as Element).attributes.get("href"), "page.html"); diff --git a/src/utils/resolve-refs.ts b/src/utils/resolve-refs.ts index 7bdba49..2c19dd0 100644 --- a/src/utils/resolve-refs.ts +++ b/src/utils/resolve-refs.ts @@ -8,6 +8,13 @@ function shallowCopyElement(element: Element) { return copy; } +const resolvedAttributes = new Map([ + ["a", "href"], + ["img", "src"], + ["link", "href"], + ["script", "src"], +]); + /** Resolves absolute href attributes in a and link elements the Node tree into relative references from the given directory. @@ -21,20 +28,18 @@ export function resolveRefs(node: Node, dir: string) { } let resolvedNode = node; const name = node.name; - if ( - (name === "link" || name === "a") - && node.attributes.has("href") - ) { - const original = node.attributes.get("href")! + let attribute = resolvedAttributes.get(name); + if (attribute && node.attributes.has(attribute)) { + const original = node.attributes.get(attribute)! if (/^[a-z][a-z+.-]*:/i.test(original)) { // Ignore refs that start with a URI scheme. /* node:coverage ignore next 3 */ } else if (!original.startsWith("/")) { - console.log(`Warning: found relative href to ${original}`); + console.log(`Warning: found relative ${attribute} to ${original}`); } else { const ref = posix.relative(dir, original); resolvedNode = shallowCopyElement(node); - resolvedNode.attributes.set("href", ref); + resolvedNode.attributes.set(attribute, ref); } } const resolvedChildren: Node[] = []; From aecc648fdd4c2b935dd0b7bb56b77c925b73b0c3 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 16 Feb 2025 18:38:12 +0100 Subject: [PATCH 03/10] Links 2025-02-16 --- src/content/links.jsonc | 73 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/content/links.jsonc b/src/content/links.jsonc index 57c542b..ec57b22 100644 --- a/src/content/links.jsonc +++ b/src/content/links.jsonc @@ -283,6 +283,67 @@ "read": "2025-02-02", "author": "David and Felipe", }, + { + "title": "IndieWeb", + "url": "https://indieweb.org/", + "tags": ["resource", "community", "small web"], + "description": "A community of independent and personal websites with a focus on owning your content and identity.", + }, + { + "title": "It’s about being, not having", + "url": "https://sive.rs/being", + "tags": ["article", "life goals", "learning"], + "description": "How living is about what you want to be and do, and not what you have in the moment.", + "quote": "When you sign up to run a marathon, you don’t want a taxi to take you to the finish line.", + "read": "2025-02-03", + "author": "Derke Sivers", + }, + { + "title": "Low-tech Magazine", + "url": "https://solar.lowtechmagazine.com/", + "tags": ["magazine", "solar power", "low tech"], + "description": "A beautiful magazine on low tech solutions and solar power that practise what it preaches.", + }, + { + "title": "Quietism", + "url": "https://tommorris.org/posts/2018/quietism/", + "tags": ["article", "small web", "maintenance tax"], + "description": "A personal website breaking from lack of maintenance, and thoughts about why not to have a website.", + "read": "2025-02-03", + "author": "Tom Morris", + }, + { + "title": "Hart contracts, not smart contracts", + "url": "https://tommorris.org/posts/2020/hart-contracts-not-smart-contracts/", + "tags": ["article", "blockchain", "smart contracts", "contract law", "philosophy"], + "description": "Why smart contracts cannot fulfil a useful purpose in the wider society.", + "read": "2025-02-03", + "author": "Tom Morris", + }, + { + "title": "Liber Indigo: The Affordances of Magic", + "url": "https://www.youtube.com/playlist?list=PLsfH1Ahi4SzE-QmrsrtyZubGmi66iP45V", + "tags": ["video", "philosophy", "materialism"], + "description": "How we are shaped by the material and philosophical landscape around us.", + "read": "2025-02-12", + "author": "Justin C Kirkwood", + }, + { + "title": "The hardest working font in Manhattan", + "url": "https://aresluna.org/the-hardest-working-font-in-manhattan/", + "tags": ["article", "font", "monoline font", "gorton"], + "description": "Deep dive into a surprisingly common routing monoline font that crops up everywhere, but isn't widely known or named.", + "read": "2025-02-15", + "author": "Marcin Wichary", + }, + { + "title": "It’s a knowledge problem! Or is it?", + "url": "https://josvisser.substack.com/p/its-a-knowledge-problem-or-is-it", + "tags": ["article", "group behaviour", "management"], + "description": "When teams do bad work it likely isn't because they don't know it's bad.", + "read": "2025-02-15", + "author": "Jos Visser", + }, { "title": "", "url": "", @@ -356,6 +417,18 @@ "David and Felipe": { "urls": ["https://hallofdreams.org/"], }, + "Tom Morris": { + "urls": ["https://tommorris.org/"], + }, + "Justin C Kirkwood": { + "urls": ["https://www.justinckirkwood.net/", "https://www.youtube.com/@liber-indigo"], + }, + "Marcin Wichary": { + "urls": ["https://aresluna.org/"], + }, + "Jos Visser": { + "urls": ["https://substack.com/@josvisser"], + }, "": { "urls": [""], }, From 8a1313eb6edc3fdcfb12ccbe4c00888a333a3ca0 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 16 Feb 2025 19:24:29 +0100 Subject: [PATCH 04/10] Add convenience scripts for development Add watch-ts, watch-site and serve scripts as convenient shortcuts for running the development services. --- Readme.md | 12 ++++++++++++ package.json | 3 +++ 2 files changed, 15 insertions(+) diff --git a/Readme.md b/Readme.md index 11660c2..40f646b 100644 --- a/Readme.md +++ b/Readme.md @@ -1,3 +1,15 @@ # hornwitser.no The code behind hornwitser.no + +## Development + +For regular development run: +- `pnpm watch-ts` to transpile the TypeScript code to build/node and watch for changes. +- `pnpm watch-site` to host the site on http://localhost:8080 and reload the server on changes. +- `pnpm test` to run the tests and report coverage. + +Other scripts available are: +- `pnpm prepare` to transpile the TypeScript code to build/node +- `pnpm build` to build the static resources to build/web. +- `pnpm serve` to host the website on http://localhost:8080 diff --git a/package.json b/package.json index fe91023..427faba 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,10 @@ "main": "build/node/index.js", "scripts": { "prepare": "tsc", + "watch-ts": "tsc --watch", + "watch-site": "node --enable-source-maps build/node/cli.js watch", "build": "node --enable-source-maps build/node/cli.js build", + "serve": "node --enable-source-maps build/node/cli.js serve", "test": "node --test --enable-source-maps --experimental-test-coverage" }, "dependencies": { From 9f15da083542d10ce0377ffa0d02c4ba5851bcd9 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 16 Feb 2025 23:54:10 +0100 Subject: [PATCH 05/10] Format links page Group links by date and style them like cards. --- src/content/links.tsx | 98 ++++++++++++++++++++++++++++++++++++-- web/assets/styles/base.css | 43 +++++++++++++---- 2 files changed, 127 insertions(+), 14 deletions(-) diff --git a/src/content/links.tsx b/src/content/links.tsx index c432367..13c0ff7 100644 --- a/src/content/links.tsx +++ b/src/content/links.tsx @@ -12,6 +12,7 @@ interface LinkData { description?: string, read?: string, author?: string, + quote?: string, } interface Data { links: LinkData[]; @@ -23,13 +24,81 @@ interface Data { function Link(props: { link: LinkData }) { const link = props.link; return <> - {link.title} - {" "} - {link.tags.join(", ")} +
+

+ {link.title} + {link.author ? – {link.author} : null} +

+

+ {link.tags.join(", ")} + {link.read ? <> + , read: + : null} +

+
+ {link.quote ?
+ {link.quote} +
: null} + {link.description ?

+ {link.description} +

: null} + {link.altUrls ?

+ Also available at {link.altUrls.map(url => {url})}. +

: null} + {link.related ?

+ Related {link.related}. +

: null} + {link.via ?

+ Via {link.via}. +

: null} } const data: Data = eval(`(${readFileSync("src/content/links.jsonc", "utf8")})`); data.links.pop(); // Remove template at the end + +function compare(a: string, b: string) { + return Number(a > b) - Number(b > a); +} + +function* groupBy(items: Iterable, keyFn: (item: T) => K) { + let oldKey: K = Symbol() as any; + let group: T[] | undefined = undefined; + for (const item of items) { + let newKey = keyFn(item); + if (!Object.is(newKey, oldKey)) { + if (group) { + yield [oldKey, group] as [K, T[]]; + } + group = []; + oldKey = newKey; + } + group!.push(item); + } + if (group) { + yield [oldKey, group] as [K, T[]]; + } +} + +const byDate = data.links.filter(link => link.read).sort((a, b) => -compare(a.read!, b.read!)); +const byMonth = groupBy(byDate, link => link.read!.slice(0, 7)); +const byYearMonth = [...groupBy(byMonth, month => month[0].slice(0, 4))]; +const other = data.links.filter(link => !link.read); +const monthNames = [ + "", // padding to make mapping start at 1 + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + const title = "Links!"; export const links: Page = { title, @@ -37,8 +106,27 @@ export const links: Page = { content:

{title}

-
    - { data.links.map(link =>
  • )} +

    + Here be interesting things I've read, watched, listened to or otherwise found useful as resources over the years. These are ordered by when I read them as that seems most practical to me. +

    + {byYearMonth.map(([year, months]) =>
    +

    {year}

    + {months.map(([yearMonth, links]) =>
    +

    {monthNames[Number.parseInt(yearMonth.slice(5))]}

    +
      + {links.map(link => )} +
    +
    )} +
    )} +

    + Resources and other things +

    +
      + {other.map(link => )}
diff --git a/web/assets/styles/base.css b/web/assets/styles/base.css index 47b3f38..9ef6354 100644 --- a/web/assets/styles/base.css +++ b/web/assets/styles/base.css @@ -14,6 +14,10 @@ html { text-size-adjust: none; } +:is(ul, ol)[role="list"] { + list-style: none; +} + h1, h2, h3, h4, button, input, label { line-height: 1.1; } @@ -44,12 +48,12 @@ html { scrollbar-gutter: stable; } -hgroup h1 { - margin-bottom: 0.1em; +hgroup :is(h1, h2, h3, h4) { + margin-block-start: 0; + margin-block-end: 0.1em; } hgroup p { font-style: italic; - margin-bottom: 1em; } h1, h2, h3, h4 { @@ -57,11 +61,21 @@ h1, h2, h3, h4 { margin-block-end: 0.5em; } -:is(p, ol, ul) + :is(p, ol, ul) { - padding-block-start: 1em; +:is(hgroup, p, blockquote, ol, ul, li) + :is(hgroup, blockquote, p, ol, ul, li) { + margin-block-start: var(--block-space, 1em); } -ol, ul { +blockquote { + padding-inline: 1.25em; +} +blockquote::before { + content: "“"; +} +blockquote::after { + content: "”"; +} + +:is(ol, ul):not([role]) { padding-inline-start: 1.25em; } @@ -102,13 +116,24 @@ body { gap: 1em; margin-block: 1em; } -.author h1 { - margin-block-start: 0; -} .author p { margin-block-end: 0; } +/* links */ +.no-break { + display: inline-block; + max-width: 100%; +} +.link { + background-color: color-mix(in oklab, Canvas 90%, white); + padding: 0.5em; + border-radius: 0.5em; +} +.link>* { + --block-space: 0.5em; +} + /* sandbox */ .sandbox-inset-3d { contain: paint; From 22365ff97797ff171c85370f7224eabb42918f08 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 16 Feb 2025 23:56:49 +0100 Subject: [PATCH 06/10] Add tight spacing class for use with lists --- src/content/projects/my-site.tsx | 2 +- web/assets/styles/base.css | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/content/projects/my-site.tsx b/src/content/projects/my-site.tsx index 98e5482..47d95e5 100644 --- a/src/content/projects/my-site.tsx +++ b/src/content/projects/my-site.tsx @@ -19,7 +19,7 @@ const content = <>

This has some interesting advantages:

-
    +
    • HTML content is only generated when it's changed, rather than being redone on every request.
    • diff --git a/web/assets/styles/base.css b/web/assets/styles/base.css index 9ef6354..40b77db 100644 --- a/web/assets/styles/base.css +++ b/web/assets/styles/base.css @@ -64,6 +64,9 @@ h1, h2, h3, h4 { :is(hgroup, p, blockquote, ol, ul, li) + :is(hgroup, blockquote, p, ol, ul, li) { margin-block-start: var(--block-space, 1em); } +.tight { + --block-space: 0.35em; +} blockquote { padding-inline: 1.25em; From 9366be04995bad6fa89f83a7712992e6b8815182 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sun, 16 Feb 2025 23:57:51 +0100 Subject: [PATCH 07/10] Pad the bottom of the page --- web/assets/styles/base.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/assets/styles/base.css b/web/assets/styles/base.css index 40b77db..edabd0f 100644 --- a/web/assets/styles/base.css +++ b/web/assets/styles/base.css @@ -97,8 +97,8 @@ blockquote::after { /* Base Page Layout */ body { max-width: 50rem; - padding: 0; - margin-block: 0; + padding-inline: 0.5em; + padding-block-end: 2em; margin-inline: auto; } @@ -111,7 +111,7 @@ body { background-color: grey; } -/* index */ +/* about */ .author { display: grid; grid-template-columns: auto 1fr; From 68d2981914fedec70285a9f0f119f93ddd3ff5b8 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sat, 10 May 2025 23:31:12 +0200 Subject: [PATCH 08/10] Typeset in Comic Neue The Comic Neue font provides just the kind of playful and witty tones that I want my website to communicate. This is not where you go for serious business! --- src/components/BasePage.tsx | 1 + web/assets/fonts/ComicNeue-Bold.woff2 | Bin 0 -> 20716 bytes web/assets/fonts/ComicNeue-BoldItalic.woff2 | Bin 0 -> 21660 bytes web/assets/fonts/ComicNeue-Italic.woff2 | Bin 0 -> 22408 bytes web/assets/fonts/ComicNeue-Light.woff2 | Bin 0 -> 22220 bytes web/assets/fonts/ComicNeue-LightItalic.woff2 | Bin 0 -> 22652 bytes web/assets/fonts/ComicNeue-Regular.woff2 | Bin 0 -> 22480 bytes web/assets/styles/base.css | 3 +- web/assets/styles/font.css | 71 +++++++++++++++++++ 9 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 web/assets/fonts/ComicNeue-Bold.woff2 create mode 100644 web/assets/fonts/ComicNeue-BoldItalic.woff2 create mode 100644 web/assets/fonts/ComicNeue-Italic.woff2 create mode 100644 web/assets/fonts/ComicNeue-Light.woff2 create mode 100644 web/assets/fonts/ComicNeue-LightItalic.woff2 create mode 100644 web/assets/fonts/ComicNeue-Regular.woff2 create mode 100644 web/assets/styles/font.css diff --git a/src/components/BasePage.tsx b/src/components/BasePage.tsx index 6d50cf5..8e2ec7f 100644 --- a/src/components/BasePage.tsx +++ b/src/components/BasePage.tsx @@ -18,6 +18,7 @@ source.addEventListener("reload", () => location.reload());`; {props.title} +