Compare commits

...

10 commits

Author SHA1 Message Date
7c2f5f39fb Links 2025-05-10 2025-05-11 01:28:37 +02:00
1f0e2b63c6 Ideas 2025-05-10 2025-05-11 01:27:47 +02:00
68d2981914 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!
2025-05-10 23:31:12 +02:00
9366be0499 Pad the bottom of the page 2025-02-16 23:57:51 +01:00
22365ff977 Add tight spacing class for use with lists 2025-02-16 23:56:49 +01:00
9f15da0835 Format links page
Group links by date and style them like cards.
2025-02-16 23:54:10 +01:00
8a1313eb6e Add convenience scripts for development
Add watch-ts, watch-site and serve scripts as convenient shortcuts for
running the development services.
2025-02-16 23:50:11 +01:00
aecc648fdd Links 2025-02-16 2025-02-16 18:38:12 +01:00
9d27566b66 Resolve src refs for img and script
Handle absolute to relative resolution of the src attribute for img and
script elements.
2025-02-03 13:42:37 +01:00
07a7386e1d Move scripts and stylesheets into /assets
To keep things organised put the assets supporting pages into
sub-folders in /assets.
2025-02-03 13:32:15 +01:00
18 changed files with 419 additions and 29 deletions

View file

@ -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

View file

@ -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": {

View file

@ -18,8 +18,9 @@ source.addEventListener("reload", () => location.reload());`;
<meta charset="utf-8" />
<title>{props.title}</title>
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" href="/style.css" />
<script type="module" defer="" src="viewport.js" />
<link rel="stylesheet" href="/assets/styles/font.css" />
<link rel="stylesheet" href="/assets/styles/base.css" />
<script type="module" defer="" src="/assets/scripts/viewport.js" />
{ reloadScript }
</head>
<body>

View file

@ -20,10 +20,22 @@
- Please stop using framesets. They are horrible UX, mkay?
- One MB is 1 million bytes.
- Stop using arbitrary linear scales for wide ranged things like volume. Use dB! For brightness too!
- Why your site should have dark mode (and light mode).
- Detailed breakdown of the CSS 3D perspective script.
- Dark patterns
- YouTube's subscriber notification setting requires enabling notifications for the app on mobile to ring the bell.
- Discord giving force feeding you free nitro for a week
- Android phone asking "want to set firefox as default" but then the "don't ask me again" changes the choice.
- Using a hardware passkey on windows with separate device requiring you to click "cancel" to use it.
- Every website saying "we value your privacy".
- The travel app asking for location permission every time I open it, despite not needing it.
- Apps that when you open a link that takes you to the app, don't take you back to where you where when you press back. YouTube and X does this.
- Ask before writing a long irrelevant angry rant. You might just have the details wrong.
- Projects
- Buddhabrot renderer.
- Wooden Drawing Board.
- Blender to CSS export script.
- Page Flip animation for web like turn.js
- Art
- Flying Hornwitser papercraft.
- Safiria papercraft

View file

@ -44,6 +44,7 @@
"tags": ["webrings", "history"],
"description": "A brief history of Webrings",
"url": "https://brisray.com/web/webring-history.htm",
"read": "2025-01-05",
"author": "Ray Thomas",
},
{
@ -214,7 +215,7 @@
"title": "The Small Website Discoverability Crisis",
"url": "https://www.marginalia.nu/log/19-website-discoverability-crisis/",
"tags": ["article", "small web", "discoverability", "bookmarks", "links"],
"description": "Victor argues a solution to the small web discoverability problem lies in sharing bookmarks.",
"description": "Viktor argues a solution to the small web discoverability problem lies in sharing bookmarks.",
"read": "2025-01-24",
"author": "Viktor Lofgren",
},
@ -283,6 +284,130 @@
"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": "Its 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 dont 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": "Its 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": "GitHub Actions Feels Bad",
"url": "https://www.youtube.com/watch?v=9qljpi5jiMQ",
"tags": ["video", "github actions", "ci"],
"description": "Critique of GitHub Actions involving a brief history of how it emerged, the shortcomings of it particularly with how it refuses to accept that it's a package manager, and examples of bad design and code in it.",
"read": "2025-02-17",
"author": "fasterthanlime",
},
{
"title": "Reality has a surprising amount of detail",
"url": "http://johnsalvatier.org/blog/2017/reality-has-a-surprising-amount-of-detail",
"tags": ["article", "detailed oriented thinking", "reality"],
"description": "An observation of the surprising amounts of detail embedded in the structure of reality. These aren't immediately obvious or easy to perceive, they are easy to miss.",
"read": "2025-02-21",
"author": "John Salvatier",
},
{
"title": "Media for Thinking the Unthinkable",
"url": "https://www.youtube.com/watch?v=oUaOucZRlmE",
"tags": ["talk", "ux"],
"description": "Demonstration of interfaces that interactively show what system are doing, and how this is vital to understanding those systems.",
"read": "2025-02-23",
"author": "Bret Victor",
},
{
"title": "The Computer Revolution Hasn't Happened Yet - OOPSLA 97 Keynote",
"url": "https://www.youtube.com/watch?v=aYT2se94eU0",
"tags": ["talk", "programming", "system design"],
"description": "Talk about how system design is still something humans are trying to figure out and as a result the systems we have today aren't the revolution that computers promised to be. Especially how the current systems like C++ have optimized themselves into a corner rather than innovate something better.",
"read": "2025-03-10",
"author": "Alan Kay",
},
{
"title": "State of Text Rendering 2024",
"url": "https://behdad.org/text2024/",
"tags": ["article", "text rendering", "font"],
"description": "Overview of the various open source text rendering libraries, tools and capabilities in 2024.",
"read": "2025-03-21",
"author": "Behdad Esfahbod",
},
{
"title": "The Raster Tragedy at Low-Resolution Revisited: Opportunities and Challenges beyond “Delta-Hinting”",
"url": "http://rastertragedy.com/",
"tags": ["resource", "text rendering", "font"],
"description": "A comprehensive resource on the challenges with rendering fonts to pixels.",
"author": "Beat Stamm",
},
{
"title": "Reflections: The ecosystem is moving",
"url": "https://signal.org/blog/the-ecosystem-is-moving/",
"tags": ["article", "interoperability", "decentralised systems", "federation", "software ecosystems"],
"description": "Reflections on how federated systems get stuck and unable to meaningfully update their protocols, as distributing updates to the installed systems become virtually impossible on internet scale systems. Key examples being the incredibly slow adoption of IPv6 and encryption for email.",
"read": "2025-05-10",
"author": "moxie0",
},
{
"title": "Tech Companies Apparently Do Not Understand Why We Dislike AI",
"url": "https://soatok.blog/2025/05/04/tech-companies-apparently-do-not-understand-why-we-dislike-ai/",
"tags": ["article", "ai", "ethics"],
"description": "The real problem with AI is not the hypothetical doomsday prophesies or the singularity, but rather the real ethical problems with the sort of detrimental societal behaviours it enables and the industries it displaces.",
"read": "2025-05-10",
"author": "Soatok",
},
{
"title": "",
"url": "",
@ -294,6 +419,7 @@
],
"to_read": [
"https://bitbashing.io/gc-for-systems-programmers.html",
"https://freetype.org/freetype2/docs/hinting/text-rendering-general.html",
],
"authors": {
"Ray Thomas": {
@ -356,6 +482,39 @@
"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"],
},
"fasterthanlime": {
"urls": ["https://fasterthanli.me/", "https://www.youtube.com/@fasterthanlime"],
},
"John Salvatier": {
"urls": ["http://johnsalvatier.org/"],
},
"Bret Victor": {
"urls": ["https://worrydream.com/"],
},
"Behdad Esfahbod": {
"urls": ["https://behdad.org/"],
},
"Beat Stamm": {
"urls": ["http://rastertragedy.com/RTRBSt.htm#About"],
},
"moxie0": {
"urls": ["https://github.com/moxie0"],
},
"Soatok": {
"urls": ["https://soatok.blog/"],
},
"": {
"urls": [""],
},

View file

@ -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 <>
<hgroup>
<h4>
<a href={link.url}>{link.title}</a>
{" "}
{link.author ? <span class="no-break"> {link.author}</span> : null}
</h4>
<p>
{link.tags.join(", ")}
{link.read ? <>
, read: <time>{link.read}</time>
</> : null}
</p>
</hgroup>
{link.quote ? <blockquote>
{link.quote}
</blockquote> : null}
{link.description ? <p>
{link.description}
</p> : null}
{link.altUrls ? <p>
Also available at {link.altUrls.map(url => <a href={url}>{url}</a>)}.
</p> : null}
{link.related ? <p>
Related <a href={link.related}>{link.related}</a>.
</p> : null}
{link.via ? <p>
Via <a href={link.via}>{link.via}</a>.
</p> : 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<T, K>(items: Iterable<T>, 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: <BasePage title={title}>
<main>
<h1>{title}</h1>
<ul>
{ data.links.map(link => <li><Link link={link} /></li>)}
<p>
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.
</p>
{byYearMonth.map(([year, months]) => <section>
<h2>{year}</h2>
{months.map(([yearMonth, links]) => <section>
<h3>{monthNames[Number.parseInt(yearMonth.slice(5))]}</h3>
<ul role="list">
{links.map(link => <li class="link">
<Link link={link} />
</li>)}
</ul>
</section>)}
</section>)}
<h2>
Resources and other things
</h2>
<ul role="list">
{other.map(link => <li class="link">
<Link link={link} />
</li>)}
</ul>
</main>
</BasePage>

View file

@ -19,7 +19,7 @@ const content = <>
<p>
This has some interesting advantages:
</p>
<ul>
<ul class="tight">
<li>
HTML content is only generated when it's changed, rather than being redone on every request.
</li>

View file

@ -25,6 +25,16 @@ suite("function resolveRefs", () => {
assert.equal((el as Element).attributes.get("href"), "../alt/page.html");
});
test("script src", () => {
const el = resolveRefs(<script src="/assets/script.js"></script>, "/subdir");
assert.equal((el as Element).attributes.get("src"), "../assets/script.js");
});
test("img src", () => {
const el = resolveRefs(<img src="/assets/image.png"></img>, "/subdir");
assert.equal((el as Element).attributes.get("src"), "../assets/image.png");
});
test("nested element", () => {
const el = resolveRefs(<div>Content with <a href="/page.html">Link</a></div>, "/");
assert.equal((el.childNodes[1] as Element).attributes.get("href"), "page.html");

View file

@ -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[] = [];

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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;
}
@ -39,29 +43,43 @@ textarea:not([rows]) {
/* Overall styling */
html {
color-scheme: light dark;
font-family: sans-serif;
font-family: "Comic Neue", sans-serif;
overflow-wrap: break-word;
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 {
font-family: "Comic Neue", sans-serif;
margin-block-start: 1.25em;
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);
}
.tight {
--block-space: 0.35em;
}
ol, ul {
blockquote {
padding-inline: 1.25em;
}
blockquote::before {
content: "“";
}
blockquote::after {
content: "”";
}
:is(ol, ul):not([role]) {
padding-inline-start: 1.25em;
}
@ -80,8 +98,8 @@ ol, ul {
/* Base Page Layout */
body {
max-width: 50rem;
padding: 0;
margin-block: 0;
padding-inline: 0.5em;
padding-block-end: 2em;
margin-inline: auto;
}
@ -94,7 +112,7 @@ body {
background-color: grey;
}
/* index */
/* about */
.author {
display: grid;
grid-template-columns: auto 1fr;
@ -102,13 +120,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;

View file

@ -0,0 +1,71 @@
@font-face {
font-family: 'Comic Neue';
src:
local('Comic Neue Light'),
local('ComicNeue-Light'),
url('/assets/fonts/ComicNeue-Light.woff2') format('woff2')
;
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Comic Neue';
src:
local('Comic Neue Italic'),
local('ComicNeue-Italic'),
url('/assets/fonts/ComicNeue-Italic.woff2') format('woff2')
;
font-weight: normal;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Comic Neue';
src:
local('Comic Neue Bold'),
local('ComicNeue-Bold'),
url('/assets/fonts/ComicNeue-Bold.woff2') format('woff2')
;
font-weight: bold;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Comic Neue';
src:
local('Comic Neue Bold Italic'),
local('ComicNeue-BoldItalic'),
url('/assets/fonts/ComicNeue-BoldItalic.woff2') format('woff2')
;
font-weight: bold;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Comic Neue';
src:
local('Comic Neue Light Italic'),
local('ComicNeue-LightItalic'),
url('/assets/fonts/ComicNeue-LightItalic.woff2') format('woff2')
;
font-weight: 300;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Comic Neue';
src:
local('Comic Neue Regular'),
local('ComicNeue-Regular'),
url('/assets/fonts/ComicNeue-Regular.woff2') format('woff2')
;
font-weight: normal;
font-style: normal;
font-display: swap;
}