Skip to content

APP: Research and implement vue ssr for ssgisr#1686

Draft
MrDirkelz wants to merge 13 commits into
mainfrom
1672-app-research-and-implement-vue-ssr-for-ssgisr
Draft

APP: Research and implement vue ssr for ssgisr#1686
MrDirkelz wants to merge 13 commits into
mainfrom
1672-app-research-and-implement-vue-ssr-for-ssgisr

Conversation

@MrDirkelz

Copy link
Copy Markdown
Collaborator

No description provided.

@MrDirkelz MrDirkelz self-assigned this Jun 8, 2026
@MrDirkelz MrDirkelz linked an issue Jun 8, 2026 that may be closed by this pull request
@MrDirkelz MrDirkelz marked this pull request as draft June 8, 2026 12:49
MrDirkelz and others added 3 commits June 19, 2026 14:42
Add a separate web/SSG build (vite-ssg) alongside the untouched native
build, so public, API-driven content is emitted as crawlable static HTML
that hydrates cleanly into the existing SPA.

- New web entry + config: src/main.web.ts, vite.config.web.ts, build:web
  script. Outputs to dist-web/ (isolated from native dist/) with NO service
  worker — the web tier is online-only by design.
- Prerender public content (article/post) pages to full static HTML,
  visible with JavaScript disabled: heading, body, hero image, and complete
  SEO metadata — title, description, absolute canonical, Open Graph, Twitter
  card, reciprocal hreflang (+ x-default), JSON-LD Article, <html lang>.
- Clean hydration: serialize the public-content snapshot into the page and
  restore it before mount; a client-only runtime (src/ssg/clientRuntime.ts)
  boots the data layer so pages hydrate into the live, interactive SPA.
- Public-tier data seam via the unauthenticated /search endpoint (anonymous
  default groups); build-time route enumeration; sitemap.xml + robots.txt.
- Signed-out shell: auth/per-user chrome gated post-mount; nav rendered as
  real <a href> links so it is crawlable. Native build behaviour unchanged.
- Use import.meta.env.SSR (not vite-ssg isClient / typeof window) to branch
  prerender vs browser, since ssgOptions.mock makes window defined in Node.

Query-driven feed/tag/home sections and web auth are deferred to Phase 2
(they need the hybrid Mango query). Specs + plan under docs/seo-strategy/.
Treat each prerendered page as a cache keyed by the data it read, so a
content change maps to the exact set of stale pages — the basis for
incremental regeneration.

- dependencyKeys.ts: coarse, language-scoped key vocabulary (doc:/tag:/
  feed:) + keysForChangedDoc / keysForRecategorization. Pure (no Vue/DOM/
  Vite deps) so the deploy-repo watcher can import it.
- dependencyCapture.ts: render-time collector on globalThis; fetchers
  report the keys they read while a page renders.
- computeAffected.ts: computeAffected(changedKeys, manifest) +
  non-destructive simulateAffected(doc, manifest, prevDoc?) with fan-out.
- vite.config.web.ts: concurrency:1 (load-bearing for capture), capture
  hooks, writes dist-web/ssg-deps.json. Scoped rebuild via SSG_ONLY_ROUTES
  (build:affected) — renders only named routes, preserves other files,
  merges the manifest, restores index.html when "/" is out of scope.
- publicContentApi.ts: report doc: keys while fetching content by slug.
- CLIs: whatChanged.ts (simulate stale set) + verifyIsolation.ts (sha256
  proof a scoped rebuild touched only intended files).
- README.md: documents the whole SSG system + the parked plan (Pinia store
  as the gateway, selector-derived keys) and why it's blocked on the hybrid
  Mango query branch (1333).

Also fixes nondeterministic route enumeration: offset pagination on /search
without a stable sort was silently capping the build at ~101 of the site's
~1934 public pages. Now a single deterministic request — full build:web is
correspondingly larger/slower.

Feed/tag/home prerendering (Phase 2b) and selector-derived keys are blocked
on branch 1333-shared-app-hybrid-mango-queries (rich public /query).

Refs #1672
… (Phase 2b)

Prerender the web tier's query-driven pages (home feeds, tag/category lists),
not just article pages, and collapse the per-component web/native branches into
one seam. Unblocked by the hybrid Mango query (branch 1333) now on main.

The seam:
- useContentQuery is now SSG-aware. Native build: unchanged useHybridQuery. Web
  prerender: onServerPrefetch -> Node-safe queryPublic() POST /query (anonymous,
  public tier), writes a hydration snapshot slice, captures dependency keys. Web
  client: seeds from the slice then hands off to the live hybrid query after
  hydration (flush:'post'). Return type stays ShallowRef<ContentDto[]> so all 17
  consumers are byte-identical.
- queryPublic.ts: Node-safe unauthenticated POST /query (can't reuse shared
  queryRemote, which needs initHybridQuery and swallows errors).
- sliceKey.ts: stable hash of caller selector + sort/limit/use_index + render
  language, excluding the volatile mangoIsPublished bounds, so build and client
  agree on the slice key.
- facetKeys.ts replaces dependencyKeys.ts: generic facet:<field>:<value>:<lang>
  keys derived identically from a query selector (capture) and a changed doc
  (watcher), language-scoped (no cross-language invalidation). Keeps
  keysForChangedDoc / keysForRecategorization for the deploy watcher + computeAffected.
- publicContent store gains slices / renderLang / storageBuckets, serialized for
  hydration.
- Per-route render language: vite config enumeration builds slug->lang + default
  maps on globalThis; main.web.ts sets the app language per route. A page's feeds
  render in the page's own language; home uses the CMS default.
- useBucketInfo is SSG-aware: storage buckets are prerendered + serialized (shared
  store read, not per-instance) so LImage builds real CDN srcset URLs in static HTML.
- HomePage renders feeds unconditionally (removed isWeb/showDynamic gate);
  ContentTile formats dates via luxon directly (db is undefined in Node).

Fix: the web build's CLIENT was booting main.ts -- the full native app, with its
service worker, Matomo analytics SW registration, and no vite-ssg snapshot
hydration -- instead of main.web.ts. The entry rewrite ran as a default-phase
transformIndexHtml hook, which fires in generateBundle after Vite has already
locked in main.ts as the entry and rewritten the script src to the hashed chunk,
so the replace matched nothing and was a silent no-op. Move it to the pre phase
(runs before build-html scans for the script entry). Also add buildTargetVirtuals
to the web config (the rebase introduced a virtual:demo-banner module it lacked).

Native/SPA build (vite.config.ts, dist/, service worker) is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MrDirkelz MrDirkelz force-pushed the 1672-app-research-and-implement-vue-ssr-for-ssgisr branch from 9541c90 to abb2b0e Compare June 19, 2026 15:00
MrDirkelz and others added 10 commits June 22, 2026 13:04
…t == native path

Replace the bespoke SSG snapshot layer with shared's own first-paint mechanism so the
web client uses the identical local-first hybrid query as native, with no-flash
hydration and far less code.

- useContentQuery: collapse the 3 web/SSR/client paths to 2. The browser (web AND
  native) returns the normal useHybridQuery; its `cache:true` seed is primed by the
  build. The Node prerender fetches once via shared `queryRemote` (anonymous -> public)
  in onServerPrefetch, renders the docs, primes the response cache via
  writeResponseCache(structuralCacheKey(...)), and captures facet keys for regen.
- useBucketInfo: same shape -- normal useHybridQuery({storage},{cache:true}) on the
  browser; the SSR prefetch primes its cache. Drops the per-instance store branch.
- main.web.ts: initHybridQuery(new HttpReq(apiUrl)) so queryRemote works in Node
  (fetch-only, no Dexie/socket). Stop coupling render-language to the store.
- vite.config.web.ts: serialize each page's `hqcache:*` entries into an inline classic
  <script> (runs before the deferred module entry) so the client seeds synchronously;
  clear them per page (concurrency:1 keeps it isolated).
- Delete queryPublic.ts (duplicated shared `queryRemote`) and sliceKey.ts (duplicated
  shared `structuralCacheKey`). Trim the publicContent store to just SingleContent's
  per-slug bySlug + hreflang (Phase 3 folds that into the cache seam too).

Incremental regeneration (facet keys / capture / manifest / scoped build:affected) is
unchanged. Native build + service worker unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… SSR)

The prerendered chrome + section titles leaked raw i18n keys (menu.home, home.newest,
...) because i18n loads messages via an {immediate:true} watch over language refs that
are empty during the Node prerender (languages come from the Dexie-backed data layer,
absent in Node).

main.web.ts now bootstraps the render language BEFORE installing i18n: in the prerender
it fetches the Language docs via shared queryRemote and fills cmsLanguages so the
immediate watch emits real messages; it serializes the render + default language docs
through vite-ssg's initialState so the client restores them before mount and the first
render matches (clean hydration). The web tier defaults to the page's own language; the
user can still switch via the language modal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…c-content snapshot

Completes the web/SSG simplification: SingleContent now uses the SAME useContentQuery
seam as every other page on both builds, so the last bespoke snapshot path is gone.

- `content` is a computed over the slug query's result (render-safe in SSR, where the
  watch-based bind never ran) + a `contentOverride` ref for in-place language switches.
  cacheId = slug makes the per-document response-cache seed safe -> no-flash hydration.
- tags + translations go through the seam too (chained after content via ssrChain), so
  TAG pages now prerender their related lists and articles prerender hreflang. The
  language list comes from the shared `cmsLanguages` instead of a separate Dexie query.
- SEO useHead runs on every build (native keeps its imperative title path; web's @unhead
  serializes into the static HTML); hreflang alternates derive from the sibling
  translations.
- main.web.ts serializes ALL languages (translations stripped from all but render +
  default) so the dropdown + hreflang have every sibling's name/code.
- DELETE app/src/stores/publicContent.ts + app/src/ssg/publicContentApi.ts (the snapshot
  store + its /search reader) -- now unused.

Verified: article/tag pages prerender content + SEO + hreflang + related lists with a
per-page response-cache seed; type-check + native build green (service worker intact).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The full ~2k-route staging build ran out of heap: each article's response-cache seed
serialized the FULL body of every sibling translation (xN languages), so a 4-language
page carried ~5 copies of the article text (~105 KB seed).

- SingleContent translations query: cacheStripFields: ["text"]. The LIVE result keeps
  the body (a language switch binds it), but the cache SEED drops it -- sibling bodies
  re-load from the live query on switch. The content query keeps its own body in the
  seed (no-flash article body). Seed ~105 KB -> ~39 KB, with only the content body left.
- main.web.ts: fetch the language list once per build instead of per page.
- build:web / build:affected: NODE_OPTIONS=--max-old-space-size=8192 headroom.

Not a leak -- hqcache is cleared per page (verified bounded at 6 keys/page); this is
peak/per-page memory. Article still prerenders content + SEO + hreflang + related lists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
These public listing pages were excluded from the prerender set (a Phase-1 precaution
from when feeds rendered blank server-side). With the SSG-aware useContentQuery seam
they now render their tile collections into static HTML like the home page.

- routes.ts: add meta.prerender to /explore + /watch.
- vite.config.web.ts: drop them from the includedRoutes exclude set (only the private
  /open, /settings, /bookmarks remain excluded). Render order is already static-first
  ([...staticRoutes, ...slugRoutes]) -- main routes render before content slugs.

Verified: /explore prerenders 11 tile collections (59 tiles) + sidebar + cache seed, no
SSR crash, no i18n leak. /watch renders whatever CATEGORISED video content exists (the
page groups videos by category); the current staging snapshot has sparse/uncategorised
video data, so it's near-empty there. type-check + native build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… routing)

The desktop sidebar's route items (Home/Explore/Watch nav items, Bookmarks, Settings)
used RouterLink in `custom` mode rendering a `<span @click="navigate">` -- no real href,
so the prerendered static site had no crawlable nav links and JS-off navigation didn't
work. Render an `<a :href="href" @click="navigate">` instead: a real, crawlable link
that still does SPA navigation on click (and opens in a new tab on modifier-click).

Search / Theme / Language / Privacy Policy / Login stay buttons -- they open modals or
run auth actions, not routes. The mobile bottom bar (MobileMenu) already used real
default-slot RouterLinks (real <a href>), so no change there.

Verified: static HTML now has <a href="/explore|/watch|/bookmarks|/settings|/">; Search
has no href. type-check + native build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…affected pages

A standalone `watch:ssg` service (run by the deploy repo, started before build:web) that
REUSES the prerender's own mechanism -- the anonymous POST /query (queryRemote) -- to detect
content changes, then regenerates exactly the affected static pages.

- src/ssg/watch.ts: every WATCH_POLL_MS, queryRemote for content with updatedTimeUtc newer than
  last seen -> changed docs -> keysForChangedDoc -> computeAffected(ssg-deps.json) -> debounced,
  lock-serialized build:affected (re-runs the queries, regenerates dist-web/*.html). lastSeen is
  stamped at launch (WATCH_SINCE override) so changes during the initial build:web are buffered
  and flushed after. Regenerates dist-web only (upload/purge = deploy repo).
- src/ssg/ssgNodeEnv.ts: minimal window/localStorage shim so luminary-shared can be imported for
  queryRemote (no Dexie/socket/IndexedDB -- REST-only).
- vite.config.web.ts: a .ssg-building lock (written at buildStart, cleared on finish) so the
  watcher never rebuilds concurrently with a build.
- package.json: watch:ssg script. tsconfig.app excludes the Node tools.

Verified e2e: polled real staging, detected the actual changed doc, ran build:affected, the page
regenerated. type-check green.

Chose polling over the socket: the socket scopes live `data` by rooms/accessMap (separate from
REST query permissions) and routes it through shared's Dexie live-sync -- extra coupling for a
"what changed since T?" service. Polling queryRemote is the public anonymous path the prerender
already uses. Trade-off: poll latency vs instant push.

TODO (v1 gaps, documented in README): DeleteCmd/redirect handling; no prev-doc state for
recategorization (old-facet page covered by the periodic full build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

APP: Research and Implement Vue-SSR for SSG/ISR

1 participant