Service worker:
- Strip version suffix from CACHE_PAGES and CACHE_IMAGES so cached pages
and recipe thumbs survive SW updates. Each deploy used to wipe both,
forcing a re-sync before the user could open the app offline. Build
and static caches stay version-suffixed (entries are hash-fingerprinted).
- Install precaches the offline shells: /, /rezepte, /recipes, both
offline-shell pages, /glaube, /faith, /fitness — best-effort with
Promise.allSettled so a single 5xx can't fail SW install.
- Bulk thumbnail precache now skips URLs already in CACHE_IMAGES.
Recipe thumb URLs embed a content hash, so a cache hit guarantees
identical bytes; subsequent syncs after small recipe edits no longer
redownload every image.
- Activate cleanup deletes only stale versioned build/static entries
and obsolete versioned pages/images caches.
Universal load migration:
- [recipeLang]/+layout.server.ts removed; logic in universal +layout.ts.
Session fetched from /auth/session, nulled when offline.
- [recipeLang]/+page.server.ts and season/[month]/+page.server.ts
removed; merged into universal +page.ts. Drops the __data.json
round-trip entirely for these routes — IndexedDB fallback now runs
even when the SW page cache is empty (fresh install, hash mismatch,
etc.) instead of getting blocked by a 503 from the data handler.
Other:
- /static/rezepte/thumb URLs in sync.ts and the SW thumb fallback now
use the absolute https://bocken.org origin. Dev/preview servers don't
host /static/rezepte and were 404ing on themselves; production keys
resolve to the same string so existing caches stay valid.
- Root +layout.svelte invalidateAll() now bails when !navigator.onLine.
Resume-while-offline used to refetch every load() and surface the
error page instead of the still-viewable cached content.
Bump 1.64.2 -> 1.65.2.
Collapses /errors/<n>.html and /errors/en/<n>.html into a single
prerendered page that shows both languages and reveals the right one
via <html data-lang>. Build script injects an inline bootstrap that
sets data-lang from localStorage before paint and wires the lang +
theme buttons (no Svelte hydration).
Layout already sets Cache-Control; SvelteKit throws when the same
header is set twice in the load chain, 500ing /glaube/apologetik/pro
and the parallel routes.
Add X-Robots-Tag noindex,nofollow handler in hooks.server.ts for /api,
/login, /logout, /register, /settings, /tasks, /fitness, /cospend,
/expenses, and the recipe admin/edit/add/search/favorites/to-try paths.
Header-based so the rule lives in one place and covers JSON responses.
Recipe detail pages now emit a self-canonical pointing at the bare slug —
the layout helper deliberately skipped detail pages, leaving query-param
variants (?multiplier=2, ?utm=…) as duplicate URLs in Google's index.
Per-page Seo on list pages so each ranks for its category-level query:
- Apologetik contra/pro indices now use localized heading + lede instead
of hardcoded English descriptions
- Calendar month view title includes month + rite ("April 2026 ·
Liturgical Calendar (Vetus Ordo) — Bocken")
- Recipe /category, /tag, /icon, /season hub + detail pages get
descriptions via new *_meta_description and *_meta_prefix i18n keys
(added in both DE and EN locales)
Sitemap now declares the image:image namespace and emits an entry per recipe
photo (loc, title from recipe name, caption from alt text) — Google Image
Search can discover all recipe images directly instead of relying on crawl.
Pro arg pages get Article JSON-LD (headline, claim as description, layer as
articleSection, voice names as keywords, deduped voice cites as citations)
plus BreadcrumbList. Katechese/zehn-gebote gets inline Article + Breadcrumb
with a Thing reference to "Dekalog" and CreativeWork citation of P. Martin
Ramm FSSP's Glaubenskurs.
Static-content load functions now set Cache-Control:
public,max-age=300,s-maxage=3600,stale-while-revalidate=86400 — applied to
apologetik layout, contra index/arg, pro index/arg, and the new katechese
+page.ts files. Recipe detail uses s-maxage=1800. Picked HTTP caching over
SvelteKit prerender to avoid baking session=null into the navbar of routes
shared with the auth-aware faithLang layout.
Set <html lang> from URL prefix via handle hook (was hardcoded "en" despite
mostly German content). Add Person + WebSite + SearchAction graph to root
layout — enables Google sitelinks search box and clusters identity across
git.bocken.org and github.com/AlexBocken via sameAs.
Build apologetikJsonLd.ts: contra args now emit QAPage with one suggestedAnswer
per voiced archetype, citations as CreativeWork. Build breadcrumbJsonLd.ts and
wire BreadcrumbList into recipe detail, contra args, prayer detail, and
calendar day. Calendar day also emits Event schema.
Sitemap now reads recipes directly from MongoDB to populate <lastmod> from
dateModified; static URLs use server-startup ISO date. English recipe URLs
only emitted when translation status is approved.
Add sitemap.xml route enumerating recipes, apologetik args, prayers, and
faith hubs. Drop /static/ from robots.txt — was blocking JSON-LD recipe
images from Google. Add reusable Seo component (OG/Twitter/canonical) and
wire into homepage, faith hub, recipes hub, and apologetik index.
Faith and recipe layouts now emit canonical + hreflang automatically by
swapping known lang slugs; deeper paths whose inner segments aren't safely
translatable (recipe [name], prayer [prayer], apologetik [argId]) are
skipped at the layout and may opt-in per page.
Recipe JSON-LD HowToStep names and baking instructions now resolve via
the recipes i18n table (jsonld_step / jsonld_bake / jsonld_for_duration +
existing at_temp) instead of being hardcoded German — English /recipes/
pages were emitting "Schritt N" in their schema.
Adds prerendered, JS-less, self-contained error pages for nginx
error_page use — served directly from /var/www/errors/ when the
SvelteKit upstream is unreachable or any nginx-originated 4xx/5xx
fires (including the catch-all default_server for unknown hosts).
- /errors/[status] (DE default) + /errors/en/[status] (EN), each
with a header language toggle linking absolute to bocken.org so
the switch works even on unknown-host fallbacks.
- httpStatus param matcher restricts entries to 401/403/404/500/
502/503/504; entries() drives prerender output.
- generate-error-quotes.ts looks up curated bilingual references
in the existing allioli/drb TSV bibles at prebuild time and
writes src/lib/data/errorQuotes.json.
- build-error-page.ts (postbuild) inlines all CSS, strips module
preloads/scripts, rewrites the home-link to canonical https URL,
and emits .html + .gz + .br per status under build/client/errors.
- deploy.sh syncs build/client/errors → /var/www/errors with
http:http ownership for nginx access.
Previous behavior silently deleted static/shopping/*.svg when
SHOPPING_*_NUMBER env vars were unset, then rsync --delete propagated
the deletion to the prod server — the loyalty buttons disappeared on
deploys where the env didn't reach the build (or during the brief
rm→write window of a parallel run).
Now the script exits non-zero with a clear message; deploy.sh's set -e
aborts before any destructive sync.
Replace season: number[] (months 1-12) on Recipe with seasonRanges, a
list of date ranges where each endpoint is either a fixed MM-DD or a
movable liturgical anchor (Easter, Ash Wednesday, Palm Sunday,
Pentecost, Advent I) plus a day offset. The old month list couldn't
express liturgical seasons whose boundaries shift each year (Advent,
Lent, Easter Octave, Christmas Octave) nor sub-month windows.
The shared evaluator resolves anchors against [Y-1, Y, Y+1] so spans
that wrap the calendar year boundary (e.g. christmas + 0 to
christmas + 7) match correctly on both sides. SeasonSelect was
rewritten as a controlled bind:ranges editor with a
fixed/liturgical kind toggle, anchor + offset inputs, per-row
resolved-this-year preview, and preset chips.
Run the one-time migration before deploying:
pnpm exec vite-node scripts/migrate-season-to-ranges.ts
It coalesces contiguous month runs into single fixed ranges and
merges Dec/Jan wrap into one wrapping range; the new code does not
read the legacy season field, so order matters.
The answer-rail had width:max-content with a wider max-width override
gated behind @media (min-width:760px). The width:max-content sat
outside the media query, so on mobile it inflated the rail's max-content
contribution to .arg-body's 1fr grid track. The track expanded past the
viewport (max-width:100% can't clamp during cyclic track sizing), making
.arg-body and its h2/text appear to overflow horizontally.
Move width:max-content inside the >=760px block so the desktop break-out
behaviour stays, while mobile falls back to default flex-wrap within
the column.
Split the single OfflineSyncButton into two surfaces with distinct
intents:
- OfflineSyncBanner: dismissable promo on the recipe index that
encourages first-time download (only when standalone + not yet
synced).
- OfflineSyncIndicator: small status pip overlaid on the nav logo
when offline data is available, opening a popover with sync /
clear actions.
Also fold the sync / clear actions into the UserHeader options menu so
the avatar dropdown is the canonical place to manage offline data.
Header.svelte gains a `logo_overlay` snippet slot to host the
indicator pip.
Other:
- manifest.json: prefer the theme-aware SVG as the primary install
icon and drop the redundant 512px raster (kept maskable 192px).
- scripts/deploy.sh: build locally and rsync artifacts to the
server, avoiding any pnpm/git work on the production host.
Bump 1.57.8 -> 1.58.0.
`navigator.serviceWorker.ready` resolves only when a SW is registered
and active. In `vite dev` no SW exists, so awaiting `ready` hangs the
sync forever. Gate on `navigator.serviceWorker.controller` first to
short-circuit cleanly when nothing controls the page.
Bump 1.57.7 -> 1.57.8.
Split the logo into foreground/background layers so Android can
apply system masks (circle, squircle, teardrop) and parallax
instead of rendering a flat composited PNG.
- icons/logo_{foreground,background}.png: new canonical sources
- mipmap-*/ic_launcher_{foreground,background}.png: regenerated
per density (108/162/216/324/432)
- mipmap-*/ic_launcher{,_round}.png: legacy pre-API-26 composites
- mipmap-anydpi-v26/ic_launcher.xml: background now points at
@mipmap/ic_launcher_background instead of solid white @color
- mipmap-anydpi-v26/ic_launcher_round.xml: added so round
launchers also get adaptive treatment
- drop unused @color/ic_launcher_background and the leftover
Tauri-template drawable
Tauri app: 0.5.2 -> 0.5.3.
Replaced Tauri icon source (icon.png) with new 1024px wheat-stalk
mark on dark background. Regenerated all platform variants via
`tauri icon`: macOS .icns, Windows .ico, Linux PNGs, iOS AppIcon
set, Android adaptive icon foregrounds.
Web: PWA manifest icons (192/512) and apple-touch-icon now use the
new logo. Browser-tab favicon (favicon.svg) unchanged — keeps the
theme-aware wheat mark.
Tauri app: 0.5.1 → 0.5.2.
favicon.svg now uses currentColor + prefers-color-scheme so a single
asset adapts to light/dark. Removed unused .ico/.png and 512 raster;
192 PNG kept and regenerated for apple-touch-icon and PWA maskable.
The previous wrap-in-anchor approach leaked the bocken.org link onto
unrelated page titles because Jellyfin reuses the same <h3> across
navigations and only swaps its class between .pageTitleWithDefaultLogo
and .pageTitle. Switch to click delegation so the redirect fires only
when the clicked element currently carries the logo class. Also unwrap
any legacy anchor wrappers on first mutation, and bump cursor/filter
hover styles to !important so they survive Jellyfin's own h3 rules.
Make the custom-multiplier pill behave and look like a single input zone:
- Wrapper is now a <label> so clicking anywhere focuses the input.
- Replace the explicit \"x\" submit button with a passive <span> suffix and
add a visually-hidden first-tree-order submit so no-JS Enter still
submits with the typed value (rather than the first preset pill's value).
- Wrapper cursor: text end-to-end, no pointer flicker.
- Hover/focus selector now matches the wrapper alongside the preset
buttons, and an isCustomMultiplier flag highlights the pill in primary
whenever a non-preset value is active (e.g. ?multiplier=12).
- Input uses field-sizing: content (with min/max) so the pill collapses
to fit the placeholder.
- align-items: center (was baseline) so the input doesn't sit high in
its pill.
- Tighten the multipliers row (gap 0.5rem -> 0.3rem, button min-width
2em -> 1.8em, matching paddings) so all six pills fit on one line in
the ingredients column.
Cake-form scaling no longer overwrites the base multiplier (pill buttons
+ custom input). Both factors stay independent and compose as
effectiveMultiplier = multiplier * formMultiplier, which feeds ingredient
amounts, portions, nested-recipe links, HefeSwapper, and NutritionSummary.
Pills reflect the base only; the existing cake-form badge keeps showing
the form factor whenever it deviates from 1, so the two contributions
stay visually distinct. Drop the formDriven flag, the effect that wrote
formMultiplier into multiplier, and the now-redundant
oninput=applyFormMultiplier hooks (bind:value already triggers
recomputation). resetCakeForm only resets form fields now.
Add a per-locale common dictionary at src/lib/i18n/common/{de,en}.ts and
the shim src/lib/js/commonI18n.ts. Migrate inline lang ternaries on the
homepage (welcome/sections/links), OfflineSyncButton (all label
ternaries), DatePicker (today/select date), ErrorView (Error/Fehler
eyebrow), and UserHeader (login aria/title) to use the shared dict.
The long marketing intro paragraphs on the homepage stay inline since
they're one-shot content with no drift risk and don't benefit from
per-key extraction.
Bump site version to 1.57.0 (new namespace).
Replace lang === 'en' string ternaries on the check-in, stats, workout,
exercises, history, and stats history detail pages, plus TemplateCard,
with t.<key> lookups against the fitness dictionary. Added new keys for
toast messages, body-part counts, body-fat label, clear/measure short
labels, "edit all fields", BF chart delta prefix, calorie balance and
adherence tooltips, actual/target legend labels, daily expenditure
prefix, height/birth/weight setup hint, exercise/workout/recent labels,
"starts with", and a {n}-template "X days ago" string.
URL slug ternaries (e.g. 'check-in' / 'erfassung') remain inline since
they encode route data, not UI text.
Bump site version to 1.56.2.
Migrate FavoritesFilter, IconFilter, TagFilter, FilterPanel, HefeSwapper
and the offline-shell, season/[month], icon/[icon], favorites, search,
tips-and-tricks, and index pages to use the recipes i18n dictionary.
Add corresponding keys for filter toggles, filter placeholders, yeast
toggle title, recipes-growing suffix, search "for" preposition, and
favorites count labels. Strip unused isEnglish derivations from layout,
tag, and category landing pages.
Bump site version to 1.56.1.
Bulk migration of the recipes namespace following the same pattern as
fitness/cospend/calendar/faith. Layout collapses its label-object into
t.foo lookups; NutritionSummary's 33 ternaries (incl. the
German-stem-plus-optional-e amino-acid pattern that read
`Lysin{isEnglish ? 'e' : ''}`) become straight dictionary references;
AddToFoodLogButton, IngredientsPage, to-try, search, favorites,
the index, and the small landing pages (category, tag, season, icon,
tips-and-tricks) all migrate the same way.
The recipes dict is now ~120 keys. Patterns kept intentionally:
- Long page-specific marketing copy (subheading sentences, meta
descriptions that include dynamic counts, hero alt text variants)
stays inline as `lang === 'en' ? '...' : '...'` rather than
bloating the dict with one-shot strings.
- URL slug ternaries stay inline — those are URL data, not UI text.
- The `recipes/admin/nutrition` page was deliberately skipped — admin
tooling, ~18 ternaries that are mostly admin-jargon strings used
in exactly one place.
Detail pages ([name]/+page, [name]/+error, IngredientsPage extras,
InstructionsPage, smaller components) and the admin page remain for
follow-up commits.
Two-locale recipes dictionary lands at src/lib/i18n/recipes/{de,en}.ts
with the same satisfies-based completeness enforcement as the other
namespaces. recipesI18n.ts is the slim shim — exports m, RecipesLang,
RecipesKey, plus langFromRecipeSlug / recipeSlugFromLang helpers for
the rezepte ↔ recipes URL slug mapping.
[recipeLang]/+layout.svelte's nav-label ternary chain collapses into
t.foo lookups. NutritionSummary.svelte is the heavy hitter — 33
inline isEnglish ternaries become a single dictionary load. Most
amino-acid names use a German-stem-plus-optional-e pattern in the old
code (`Lysin{isEnglish ? 'e' : ''}`) that's now just t.lysine in the
template; less clever, much more obviously translatable.
Adds prayer-name keys (sign_of_cross, pater_noster, fatima_prayer, …),
search/filter UI labels (search_prayers, clear_search, filter_by_category,
all_categories), the eastertide_badge, and the prayer-detail-only
nicene_creed / hail_mary aliases (German + Latin keep the Latin form,
English uses the English name).
Prayers index labels object collapses each name ternary into a t.foo
lookup; the language-invariant ones (Glória Patri, Credo, Ave Maria,
Salve Regina, Glória, Ánima Christi, Tantum Ergo, Angelus, Regína Cæli)
stay hardcoded as single strings since they're identical across all
three locales. The baseUrl building now uses faithSlugFromLang/prayersSlug
helpers instead of inline ternaries.
Prayer detail's prayerDefs routing table — every name field that was
isEnglish ? a : b now points at a t.* lookup. Painting captions for
the Velázquez/Murillo Angelus/Regina Cæli backgrounds become
t.painting_coronation_virgin / t.painting_annunciation. The
AngelusStreakCounter call site drops its three-way ternary in favor of
the typed `lang` derived value.
Slug-table ternaries (URL slug per locale) and the long gloriaIntro
paragraph are intentionally left inline — slugs are URL data, not UI
text, and gloriaIntro is page-unique marketing copy that doesn't
benefit from being in a shared dict.
Adds streak/angelus and Bible-modal keys to the faith dictionary, plus
the three-fragment "this catechesis is only available in German" notice
used by both katechese pages. Pluralization for day/days handled by two
explicit keys (day_singular/day_plural) chosen at the call site —
Latin's "Dies" is invariant so both keys hold the same string.
StreakCounter and AngelusStreakCounter collapse their per-component
labels objects into direct t.foo lookups; the rosary page's BibleModal
call site now passes the typed `lang` derived value (was data.lang as
plain string, didn't satisfy the tightened FaithLang prop type).
BibleModal isn't actually used in Latin context, but the dict requires
every key in every locale, so reasonable Latin equivalents got filled
in for completeness.
Three-locale faith dictionary lands at src/lib/i18n/faith/{de,en,la}.ts
with the same satisfies-based completeness enforcement we use for
fitness, cospend, and calendar. faithI18n.ts is the slim shim — exports
m, FaithLang, FaithKey, plus the URL-slug helpers (langFromFaithSlug,
faithSlugFromLang, prayersSlug, rosarySlug, calendarSlug, apologetikSlug)
needed because faith routes do bidirectional slug ↔ locale mapping that
the other namespaces don't.
[faithLang]/+layout.svelte and +page.svelte fully migrated. The
isEnglish/isLatin derived flag dance collapses into a single typed
`lang`; ten inline ternaries per file (display labels and slug
selection) become t.key lookups or slug-helper calls. The "DE" badge
condition for non-German faith locales tightened from
`isEnglish || isLatin` to `lang !== 'de'`. Apologetik latin-fallback
hops through the helpers instead of inline matchers.
Apologetik pages get the shared-label cut: all four pages (contra,
contra detail, pro, pro detail) now use t.objections, t.evidences,
t.alex_pick, t.objection_label, t.answered_by, t.voices_answering,
t.arguments_title, t.positive_case from the dict. Page-specific
marketing copy (the per-page heading/lede/eyebrow object literals)
stays inline — those strings live in exactly one place each, the
structure is already readable, and pulling them into a shared dict
would be noise.
Also: ImageUpload.svelte was the one stray cospend t() caller the
earlier codemod missed (it lives at lib/components/, outside the
codemod's --root scope). Now uses t.key with `as CospendLang` cast.
Cospend translations move to src/lib/i18n/cospend/{de,en}.ts with
satisfies-based key-set enforcement, mirroring the fitness layout
shipped earlier. cospendI18n.ts becomes the same kind of slim shim
exporting m, CospendLang, CospendKey while keeping every existing
helper (detectCospendLang, paymentCategoryName, splitDescription,
formatNextExecutionI18n, etc.) on the same surface.
Calendar gets the same treatment but with three locales (de/en/la)
and two namespaces — `ui` and the rite-1962-specific `ui1962`.
calendarI18n.ts now imports both as m / m1962, types them as
CalendarKey / Calendar1962Key, and routes t() / t1962() through
them. The 1962 fallback is per-namespace dir with file-prefixed
locale files (de_1962.ts etc.) so they can co-exist.
19 cospend route/component files and 3 calendar pages migrated to
the t.key / t1962.key syntax. Two notable hand fixes: UsersList.svelte
needed `as CospendLang` because the `lang` prop default uses an `as`
cast that breaks TS narrowing of m[lang]; and a sed pass converted
codemod-emitted t['camelCase'] to t.camelCase since the static-key
regex initially only matched snake_case.
The split + codemod scripts are now generic — split-i18n.ts takes
namespace, locales, optional marker and basename for multi-table
modules; codemod-i18n-t-to-m.ts takes module basename, fn name, and
m alias name (so t1962 / m1962 share the same machinery as t / m).
The fitness-specific one-shots are deleted, superseded.
22 files migrated from t('key', lang) function calls to direct lookups
on a derived dictionary alias: const t = $derived(m[lang]) once per
file, then t.start_period or t[card.labelKey] at the call sites.
Cleaner read at the point of use, one less argument threaded through,
and TypeScript narrows on every key access (so a typo in a literal
key now errors at the call site, not silently falls back to the key
string).
The codemod handles both ways `lang` can enter scope — derived from
the URL via detectFitnessLang, or destructured from $props() (single
or multi-line). One file aliased the i18n table to `messages` to
avoid collision with a local `const m = data.measurement`.
The deprecated `t(key, lang)` function still exists in fitnessI18n.ts
for any remaining out-of-tree call sites — can be deleted once
nothing imports it.
The fitness UI translation table previously lived as one combined
object in fitnessI18n.ts where every entry held both languages. That
hides drift (an English string can silently disappear without TypeScript
noticing) and makes adding strings a multi-edit dance.
Split into src/lib/i18n/fitness/{de,en}.ts. de.ts is the source of
truth for the key set; en.ts uses `as const satisfies
Record<keyof typeof de, string>` so any missing English translation is
a build-time error. fitnessI18n.ts now re-exports both as a typed
table m and adds FitnessLang/FitnessKey types — the existing
t/fitnessSlugs/fitnessLabels API stays so call sites don't churn.
The strict typing immediately surfaced one real bug: t('initializing_gps')
was being called from the active workout page but the key never existed
in the dictionary, so it had been rendering the literal string
'initializing_gps' through the fallback. Added the missing key in both
locales.
Tightened BodyPartCard.labelKey and the body-parts Step JSDoc to
FitnessKey instead of plain string so card data drift catches drift at
the data site, not the call site. Two dynamic-key sites (partKeyMap
fallbacks for unmapped measurement keys) are cast pragmatically.
The 360-entry split was done by a one-shot extraction script
(scripts/split-fitness-i18n.ts) — kept for re-use against
cospendI18n.ts and calendarI18n.ts in follow-up commits.
Holding any past or current calendar cell (outside an existing period
record and unless one is already ongoing) for 600ms now opens a
confirmation dialog and starts a period on that day. Same POST as the
button-driven start; just a faster gesture for back-dating today or
yesterday.
Implemented as an inline {@attach longPress(handler)} attachment that
cancels on >8px movement, suppresses iOS contextmenu, and respects
pointer cancel/leave. The held cell scales 1.18× with a growing red
ring and rounded pill border for visual feedback (reduced-motion
falls back to a static ring). Eligibility is gated client-side
(canStartOn): no read-only mode, no projection-only mode, no future
dates, and no overlap with the current period.
Two custom Leaflet actions converted to attachments: renderMap is now
a factory returning an attachment, mountMap is the attachment itself.
Four call sites updated. use:enhance left alone — still the canonical
SvelteKit form-action API.
The stats page's three streamed Promise.resolve(...).then(...) chains
now log on rejection instead of silently swallowing errors. The muscle
heatmap {#await} block gained pending and catch branches with a
lang-aware error message.
Module-level top-level await for db/scheduler init and the cache
warmup IIFE move into the canonical export const init hook. Same
ordering and non-blocking semantics; makes the lifecycle explicit
and works on environments without top-level await.
English label and variable name now match the existing ALEX_PICKS
data convention. German keeps 'Alex' Wahl' (the natural translation).
Latin updated to 'Alexandri delectus' to mirror the pick semantics.
Codemod-driven migration of 55 .svelte files from the deprecated
$app/stores module to the rune-based $app/state ($page.x → page.x,
no auto-subscription wrapper). Two custom writable() stores converted
to .svelte.ts factory functions matching the existing theme store
pattern, with consumers updated to use .value getters and the explicit
.set() method.
UserHeader.svelte's login link now guards page.url.search behind
the browser flag — search-param access throws during prerender, and
this defensive change unblocks future prerender adoption on any page
that includes the header.
Replace string-literal and template-literal hrefs across the codebase
with the modern SvelteKit 2.26+ resolve() and asset() APIs. Migration
makes route IDs explicit, type-checked against generated $app/types,
and base-path-aware. Two codemod scripts handle the bulk; remaining
ambiguous, query-bearing, and precomputed-href cases are converted
manually at the assignment sites.
- contra/pro detail pages move from #voice-X hash to /[argId]/[archId]
(and /[posArgId]/[voiceId]) optional path segments. SSR renders the
selected voice directly — no hydration flash on deep links.
- Tab onclick uses replaceState to update path without a load roundtrip.
- Add Alex's choice chip on contra detail tabs: small circular pfp on
picks, expanded label on the active tab. ALEX_PICKS map per argument.
- Answer-rail pills on contra index extend past 760px column into the
right viewport gutter when space allows; wrap otherwise.
Sync DE translation with current EN content for the 11th pro argument:
expanded claim/thesis (typology as test case), full Akedah meditation in
the hahn voice, canonical-shape argument in wright, doctrinal-development
expansion in newman. Also fixes the canon count from 66 to 73 books to
match the Catholic canon used elsewhere.
- Numbers move to left of dots (text-anchor end).
- ViewBox widened (W 700→820, H 240→320) so the converge label fits and
bigger fonts/dots have breathing room.
- Strand thickness, dot/orb radii, and label font sizes bumped.
- Replace static rings with two pulse-out ripples (4.8s period, 2.4s
offset) emanating from the orb; reduced-motion falls back to static.
- zehn-gebote: orphan `ul` rule left over from inline-toc removal.
- fitness/active: orphan .exercise-header*, .move-exercise*, .remove-exercise*,
.add-exercise-btn rules left over from rail/focus refactor.
Adds the entire /<faithLang>/{apologetik,apologetics} section:
- Landing page introducing the contra/pro split with shield/flame cards.
- Contra (objections): 23 objections, each answered by multiple archetype
voices (Aquinas, Pascal, Augustine, Lewis, Chesterton, plus Logician,
Mystic, Scientist, Pastor archetypes); index + per-argument detail pages
with archetype filter and inter-argument navigation.
- Pro (positive case): 12 arguments across three layers (supernatural,
theism, christianity) voiced by Habermas, Polkinghorne, Newman, Hart,
Lewis, Wright, Hahn, Plantinga, Eliade, Feser, Chesterton, Guénon;
cumulative-case visual + per-argument detail pages.
- DE/EN content via per-language data modules; LA stub layout 307-redirects
to English.
- Per-language slug via apologetikSlug matcher; canonical-slug enforcement
redirects mismatches.
- Shared ApologetikToc component (also reused on zehn-gebote katechese).
- CaseTabs component for contra/pro switching.
- DeepL translation script for regenerating DE data from EN source.
- Server-side scripture lookup helper.
Redesign the active-workout page around a left-rail timeline and a
focus card on the right. The rail owns the workout title, pause,
elapsed time, sync indicator, progress bar, and a reorderable chip
per exercise (drag to reorder, × to delete, starting-weight hint so
you know what to rack, green checkmark when complete). Main stage
holds a hero focus card for the active exercise plus its SetTable.
- New WorkoutRail.svelte and WorkoutFocusCard.svelte
- Active exercise pinned to top of the scrollable rail (mobile only)
- Desktop: rail grows freely; mobile: compact vertical stack
- Finish + cancel share one row; cancel is a ghost action
- Drop the old sticky bottombar; its controls moved into the rail
- ExerciseName gains `plain` prop to opt out of the detail link
- Active workout route joins the 1400px max-width whitelist
Redesign the active-workout footer as a floating glass pill that
mirrors the site header — same backdrop-blur recipe, same token set,
anchored to the bottom instead of the top. Inner controls recomposed:
icon-only pause button, dominant elapsed time, subtle divider, label +
chevron or rest-timer pill. Mount animation, hover-lift, reduced-
motion fallback.
Align the muscle picker with the site card language (matches
/fitness/check-in and /fitness/stats) and unlock the full desktop
width via the 1400px container used by nutrition/check-in.
- Sidebar card layout at ≥900px (200/620 grid, sticky)
- Larger sidebar at ≥1180px (460/720) with figures uncapped
- Tablet tier (900–1179px) stacks figures vertically inside the card
- Below 900px the card sits on top of the content column
Coop Supercard uses GS1 Data Matrix — the payload contains FNC1
separators between fields, not literal 0x1D bytes. Enable bwip-js
parsefnc so ^FNC1 placeholders in the env value produce genuine
FNC1 codewords (codeword 232), matching the physical card and
letting checkout scanners parse the GS1 element string.
Replaces the single card button with two brand-colored buttons
(Coop blue, Migros orange) that each open only their own card.
Modal now wears the brand gradient directly, drops the red cross
close button pattern from BibleModal, and scales the Data Matrix
+ linear barcode to fill the modal on phones for easy scanning.