From fe51d7840d61d6e4e2d35377a71fc5ba10abcafe Mon Sep 17 00:00:00 2001 From: Abdullah Kaya Date: Sat, 1 Aug 2026 03:00:39 +0300 Subject: [PATCH 1/3] fix(content): restore spaces swallowed before inline elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Astro follows JSX whitespace rules, so a newline between prose text and an adjacent inline element is dropped rather than collapsed to a space. Prettier reflows long sentences and moves inline tags onto their own line, at which point the space silently disappears from the rendered page. The source still reads correctly and typecheck, lint and the build all stay green, so these went unnoticed. Nine occurrences shipped joined text: - "See ourPrivacy Policy" in the cookie banner — on all 17 pages - "visit our website atlibredb.org" (privacy policy) - "processed by Meta under theirPrivacy Policy" (privacy policy) - "the file you openis the sequence" (database architecture) - "log and fsync-dbefore the commit" (database reliability) - "without the magic.One core, three lenses" (database hero) - "credentials from your.env file" and "read from your.env file" (docker compose) Each is fixed with an explicit {' '} — already the idiom used elsewhere in this repo — or where the line would otherwise exceed the print width and Prettier would wrap the expression onto three lines. Add src/lib/prose-whitespace.test.ts to the gate so this cannot regress. It scans the .astro sources for prose ending a line immediately followed by a content-bearing inline element with no explicit space, and was verified by reverting each of the eight fix sites in turn and confirming it fails. is deliberately out of scope: it is used for badges and icon dots inside `flex ... gap-*` rows, where the missing text-node space is invisible. Co-Authored-By: Claude Opus 5 (1M context) --- src/components/CookieConsent.astro | 4 +- .../DatabaseArchitectureSection.astro | 2 +- .../sections/DatabaseReliabilitySection.astro | 2 +- src/components/sections/DatabaseSection.astro | 2 +- .../sections/DockerComposeSection.astro | 6 +- src/lib/prose-whitespace.test.ts | 74 +++++++++++++++++++ src/pages/privacy-policy.astro | 4 +- 7 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 src/lib/prose-whitespace.test.ts diff --git a/src/components/CookieConsent.astro b/src/components/CookieConsent.astro index 9d41eda..db23f80 100644 --- a/src/components/CookieConsent.astro +++ b/src/components/CookieConsent.astro @@ -12,7 +12,9 @@

- -- We use cookies to analyze site traffic and improve your experience. See our + + -- We use cookies to analyze site traffic and improve your experience. See our Privacy Policy for details. diff --git a/src/components/sections/DatabaseArchitectureSection.astro b/src/components/sections/DatabaseArchitectureSection.astro index 27507cb..b2844fa 100644 --- a/src/components/sections/DatabaseArchitectureSection.astro +++ b/src/components/sections/DatabaseArchitectureSection.astro @@ -83,7 +83,7 @@ const lenses = [

The WAL is the database

- Most databases keep a write-ahead log separate from data files. LibreDB does not: the file you open + Most databases keep a write-ahead log separate from data files. LibreDB does not: the file you open{' '} is the sequence of every committed transaction, replayed into an in-memory sorted array on open. No separate data file, no buffer pool — the same lineage as Redis AOF mode. Two boxes instead of three, which is how the kernel stays small enough to understand. diff --git a/src/components/sections/DatabaseReliabilitySection.astro b/src/components/sections/DatabaseReliabilitySection.astro index 8480383..a5307c6 100644 --- a/src/components/sections/DatabaseReliabilitySection.astro +++ b/src/components/sections/DatabaseReliabilitySection.astro @@ -56,7 +56,7 @@ const notForYet = [

A write-ahead log, fsync-d before commit

- A transaction that returns has been written to a length-framed, CRC-32 checksummed log and fsync-d + A transaction that returns has been written to a length-framed, CRC-32 checksummed log and fsync-d{' '} before the commit becomes visible. A crash can only ever damage the last, un-fsync-d record — which recovery detects and truncates, leaving a valid committed prefix.

diff --git a/src/components/sections/DatabaseSection.astro b/src/components/sections/DatabaseSection.astro index dc4b366..f4c4e36 100644 --- a/src/components/sections/DatabaseSection.astro +++ b/src/components/sections/DatabaseSection.astro @@ -47,7 +47,7 @@ const principles = [ you can read in one sitting.

- Multi-model without the magic. + Multi-model without the magic.{' '} One core, three lenses, every line tested. A small, readable, embeddable database in TypeScript — built on one idea: a database can be powerful and still be understood by opening its source.

diff --git a/src/components/sections/DockerComposeSection.astro b/src/components/sections/DockerComposeSection.astro index 4ad6285..7032ca0 100644 --- a/src/components/sections/DockerComposeSection.astro +++ b/src/components/sections/DockerComposeSection.astro @@ -157,8 +157,8 @@ const envGroups = [

- That's it — LibreDB Studio is now running at - http://localhost:3000. Log in with the admin credentials from your + That's it — LibreDB Studio is now running at{' '} + http://localhost:3000. Log in with the admin credentials from your{' '} .env file.

@@ -213,7 +213,7 @@ const envGroups = [

Environment variable reference

- Every supported variable, grouped as in the file. Secrets are read from your + Every supported variable, grouped as in the file. Secrets are read from your{' '} .env file via ${'{VAR}'} interpolation — never hardcoded in the compose file.

diff --git a/src/lib/prose-whitespace.test.ts b/src/lib/prose-whitespace.test.ts new file mode 100644 index 0000000..6c53e49 --- /dev/null +++ b/src/lib/prose-whitespace.test.ts @@ -0,0 +1,74 @@ +import { test, expect } from 'bun:test'; +import { Glob } from 'bun'; +import { readFileSync } from 'node:fs'; + +// Guards against a silent, recurring content bug. +// +// Astro follows JSX whitespace rules: a newline between prose text and an +// adjacent inline element is DROPPED, not collapsed to a space. Prettier +// reflows long sentences and will happily move an inline tag onto its own +// line — at which point the space disappears from the rendered page and +// "See our Privacy Policy" ships as "See ourPrivacy Policy". Nothing +// in typecheck, lint or the build complains, and the source still reads +// correctly, so it is very easy to miss in review. +// +// The fix at each site is an explicit `{' '}` or ` ` at the end of the +// text line. This test fails if a new occurrence appears. +// +// Scope: only content-bearing inline elements (a, em, code, strong, ...). +// `` is deliberately excluded — it is used here for badges and icon +// dots that sit in `flex ... gap-*` rows, where the missing text-node space +// is invisible and intentional. Adjacency of a closing inline tag followed by +// prose is likewise context-dependent (flex gaps again) and is not checked. + +const INLINE = String.raw`(?:a|em|code|strong|abbr|kbd|b|i|mark|time)`; + +/** Line ends mid-sentence: a word character or sentence punctuation, and not markup. */ +const PROSE_TAIL = new RegExp(String.raw`[\p{L}\p{N},;:—–)]$`, 'u'); +/** Already carries an explicit space, or is not prose at all. */ +const SAFE_TAIL = /(?:\{' '\}| | |>|"|'|=|\{|\()$/; +// The tag may be alone on its line with attributes below it, so end-of-line +// counts as a boundary just like whitespace or `>`. +const OPENS_INLINE = new RegExp(String.raw`^<${INLINE}(?:[\s>]|$)`); + +interface Offence { + file: string; + line: number; + text: string; + next: string; +} + +function scan(file: string): Offence[] { + const lines = readFileSync(file, 'utf8').split('\n'); + // Skip the component script (frontmatter) — it is TypeScript, not markup. + let start = 0; + if (lines[0]?.trim() === '---') { + const end = lines.indexOf('---', 1); + if (end > 0) start = end + 1; + } + + const out: Offence[] = []; + for (let i = start; i < lines.length - 1; i++) { + const cur = lines[i]!.replace(/\s+$/, ''); + const next = lines[i + 1]!.replace(/^\s+/, ''); + const bare = cur.replace(/^\s+/, ''); + if (!bare || !next) continue; + if (bare.startsWith('