326 private links
These are definitely the rules I follow. I still not reach for <menu> yet.
in HTML5 they realized it was kinda annoying that the spec didn’t allow us to clump the terms and definitions together. So now a
is permitted as a non-semantic wrapper to help us clump those terms and definitions together:Action list should be put inside
<menu>. Thenavelement is a sectioning element whereas the`menuelement is a list element.
The client is not a thin view requesting permission to show data. The client is a node in a distributed system with its own database.
It’s overkill for simple CRUD apps with no offline or collaboration needs.
But here’s where it shines: note-taking, document editing, collaborative design tools, project management, field apps with unreliable connectivity, basically anything where data privacy is a selling point, as well as anything with real-time collaboration.
One more thing I wish someone had told me earlier: you don’t have to go all-in. I’ve had the best results using local-first for specific features within otherwise traditional apps. Offline drafts in a blog editor. Real-time collaborative notes inside a project management tool that’s otherwise standard REST.
To do so: SQLite in the browser via WebAssembly; persisted to the Origin Private File System (OPFS). The author describes the method used.
To avoid conflicts: CRDT. Yjs exists. There is also Automerge and the newer Loro.
To grasp the data: replicate rows via database replication.
PowerSync does this well from Postgres to SQLite.
Triplit is a full-stack database with sync built-in.
LiveStore use an event-based approach.
TinyBase for prototyping or small apps.
PGLite (Postgres compiled to WASM) but it has a significant bundle size and memory footprint for mobile browsers.
Often the last-write-wins (LWW) is the best strategy at the field level.
For a document body, CRDT should be used.
To book a meeting, one must verify there is no other meeting booked by someone else. " The approach I’ve landed on (after getting it wrong twice) is: validate on the server during the write-back phase, but flag violations rather than silently rejecting them. When the client pushes mutations to the server during sync, the server runs them through a constraint validation layer before applying them to Postgres". See the example.
The conflict should then be resolved by the user.
For something like inventory management where two people “buy” the last item, that window is unacceptable, and that’s exactly why I said earlier that local-first is wrong for systems requiring strong transactional consistency.
Conflict resolution works well for texts with CRDT.
See such app architecture: https://www.smashingmagazine.com/2026/05/architecture-local-first-web-development/#building-a-real-app-architecture-auth-and-migrations
Example of E2E (local, on device) encryption for https://anytype.io/
One thing to consider is migrations: Design your migrations to be additive. New columns with defaults. New tables. Don’t rename or drop columns unless you absolutely must, because users running old app versions will still be syncing data, and your server needs to handle the mismatch. I learned this the hard way when I dropped a column that an older client was still writing to, which caused silent sync failures for about 200 users over a weekend.
Performance are awesome (< 10ms for read or writes). The initial sync is where the cost occurs.
The architecture can be tested with Playwright and context.setOffline(true).
I’m excited about where this is going. PGlite (full Postgres in the browser) feels like a glimpse of a future where the client/server data layer distinction just dissolves. You write SQL, it runs everywhere, sync is a runtime concern rather than an architectural decision. We’re not there yet, but you can see it from here.
There is also no standard for a sync engine. Migrating away a sync engine is not trivial. I’m also worried about the complexity budget. Local-first adds real architectural complexity: sync engines, conflict resolution, client-side migrations, partial replication, and auth at the sync boundary.
Adding query string for a referral (with ?via for example)
- can broke the URL such as
https://int10h.org/oldschool-pc-fonts/fontlist/?foo. Altering a URL gives you a new URL. The new URL could point to a completely different resource, or to no resource at all, even if the alteration is as small as adding a seemingly harmless query string. - There is already the HTTP Referrer header for that. It's governed by the Referrer-Policy
Another post describe this problem: https://chrismorgan.info/no-query-strings
There are a number of serialization libraries that outperform JSON in NodeJS.
It's important to avoid generating extraneous garbage when doing these kinds of benchmarks.
It's important to provide an appropriately sized buffer when performing serialization.
If you care about serialization performance, consider using a different programming language with better tradeoffs.
With 3 columns and CSS grid.
Validate the wrapped HTML
Ein Text-Adventure-Lernspiel für die Datenbanksprache SQL
Step into a smoky 80s detective agency, question suspects with SQL queries, and crack the case one statement at a time.
We can use the oklch (OK L C H) and adjust the lightness and chroma from the hue, thus generating colors.
Creating colors from oklch is tedious, but they can derivated from other color codes from [#007ab8](https://shaarli.lyokolux.space/./add-tag/007ab8) calc(l - 0.05) c h)
Tip: very light and very dark lightness levels have a tendency to look oversaturated.. a lot of color palettes look more cohesive when you reduce the chroma slightly as you move away from the middle-range of lightness towards the edges.
Example: https://codepen.io/cferdinandi/pen/zxoYpeL
This example is good for dynamic colors.
Following the basics of anchor positioning, there are many more use cases for this layout specification. Note this is not supported by browsers yet.
Logical properties can be used for the alignment.
position-try-fallbacks can be defined by the developer with @position-try. It accepts few rules: inset properties, margins, sizing, self-alignment, position-anchor, position-area.
For a website build
For local development, this has been a big win. Re-rendering all the HTML pages used to take about 15 seconds, but with a warm cache it takes 0.06 seconds. That’s a 200× speedup that I feel every time I hit save, and it’s made working on this site a smoother and more satisfying experience.
body:has(li:hover) li:not(:hover){
opacity:0.5;
transition: opacity 500ms;
}
Highlight the hovered element of a list by hiding the others
About the <picture> syntax which the author finds great.
On the contrary, the srcset and sizes attributes on one image are hard to use. "So, we are left describing the all of the sizes that an element will be, across every breakpoint and container query, as a single string, in an HTML attribute. How disgusting."
A tool is neede to write media queries in the sizes attribute.
How is a tool supposed to generate (min-width: 1340px) 257px, (min-width: 1040px) calc(24.64vw - 68px), (min-width: 360px) calc(28.64vw - 17px), 80px though?
The author proposes to bury this attribute and use sizes="auto". The developer can rely on loading="lazy" to load the images after the layout is computed, thus the browser know the size in advance.
sizes values remain a must have for images displayed way up at the top of the page... well sizes="100vw" is a good fit for it, isn't it?
All other images? loading="lazy" sizes="auto"
The idea seems good. Another ID format: 16 bytes like UUID v4 and v7. The key difference is they are monotonic, but that's something UUID v7 and ULID can have.
The structure:
Timestamp: 8 chars · ms since epoch · good until year 6028
Counter: 6 chars · randomly seeded · monotonic
Random: 7 chars · cryptographically secure randomness
The landing page uses catchy things without real correlation: 23M SparkIDs/sec in Rust. Ok but with which hardware?
hashing: SHA-512
Password or one time code hashing: Argo2id
Key derivation: SHAKE256 or HKDF-SHA512
Api key: prefix + version + Base32LowerCase.encode(UUID || 32-byte secret); hash function: SHAKE256 with a 512 bit output, or SHA3-512 or SHA-512
Encryption: AES-256 has too short nonces, jey / nonce reuse is catastrophic and it's also lacking context commitment. XChaCha20-Poly1305 lacks context commitment, that's why ChaCha20-BLAKE3 is recommended.
Encrypting secrets: use a Key Management Service
Symmetric Key signature: HMAC-SHA512
Asymmetric key signature: ML-DSA-65, or ML-DSA-87
JSON Web TOkens: ML-DSA-65 for asymmetric signatures or HMAC-SHA512 for symmetric signatures
End-to-end encryption
While you probably need more advanced protocols for your specific use case (e.g. The Messaging Layer Security (MLS) Protocol, RFC 9420, for messaging), basic end-to-end encryption to a public key has been standardized in RFC 9180 - Hybrid Public Key Encryption (HPKE). In this context hybrid means that we combine both symmetric and asymmetric cryptography.
Therefore, I recommend the following algorithms for use with HPKE to encrypt data to a public key:
- KEM: X-Wing
- AEAD: AES-256-GCM
- KDF: HKDF-SHA512
TLS
Today, the only quantum-resistant key exchange algorithm available for TLS is the hybrid X25519MLKEM768. Ensure that your load balancers / reverse proxies support it.
# the api key generation
fn hash_api_key(api_key_id: Uuid, version i16, organization_id: Uuid, secret: &[u8]) -> [u8; API_KEY_HASH_SIZE] {
let mut hasher = sha3::Sha3_512::new();
hasher.write(api_key_id.as_bytes());
hasher.write(&version.to_le_bytes());
hasher.write(organization_id.as_bytes());
hasher.write(secret);
return hasher.sum();
}
the storage in the database
CREATE TABLE api_keys (
id UUID PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
name TEXT NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE,
version SMALLINT NOT NULL,
secret_hash BYTEA NOT NULL,
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
UNIQUE (name, organization_id)
);
CREATE INDEX index_api_keys_on_organization_id_and_expires_at ON api_keys (organization_id, expires_at);