APP: Research and implement vue ssr for ssgisr#1686
Draft
MrDirkelz wants to merge 13 commits into
Draft
Conversation
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>
9541c90 to
abb2b0e
Compare
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.