Architecture proposal · TopFans + WLS · 10 September 2026

Build once. Prerender the pages that changed.

Keep complete HTML pages in S3. Use a shared Angular build and a separate render worker to update the correct pages for main-site, Prateek, and James.

Proposed implementation, not a deployed change. Examples use your actual article post URL format. Backend events, queue, and page records described here must be added.

1. What the code does today

A WLS pipeline generates a separate Angular project for each creator. It places tenant configuration and some page data in TypeScript files. The prerender target then builds browser and server code and creates HTML for its route list.

Repository evidenceMeaning for this proposal
scripts/generate-whitelabel.jsGenerates tenant projects, environment files, cover/sub-page constants, and deployment entries for all selected WLS creators. It currently has no creator selector for generation.
generate-static-routes.mjsReads admin/posts-list-seo, builds post URLs, and writes them to the prerender route list. The WLS generator adds a creator username filter.
prerender.routes.txtContains article URLs. Articles are scheduled for prerendering. This does not verify that each deployed HTML file currently exists or contains the expected content.
post-detail-routing.module.tsThe post module already accepts :username/:type/:id. Articles are posts with type = article, not a separate /articles/:slug route.
scripts/prefetch-marketing-pages.mjsGenerates marketing data and routes as TypeScript. A new marketing slug currently becomes a build input.
server.tsUses an existing dist folder to select a project and creates global DOM objects. A shared worker needs explicit site selection and isolated render state.
deployment_script/parallel-build-and-upload.shBuilds each listed tenant, uploads the output, and requests distribution invalidation. Its upload rule applies a one-year immutable header to the output.

Verified article URL: /post/<username>/article/<encoded-id>. Reuse the existing URL builder, including its original_sk || sk handling. Do not introduce a different article URL scheme.

The workspace now declares Angular 16.2 and Universal 16.2, with an upgrade in progress. Earlier discussion referred to Angular 15. Select the render API and build settings for the version finalized in the repository.

2. The proposed system, in three parts

01 · BUILD

Shared application

CI compiles browser files and a server renderer when code changes. Start with one main-site build and one generic WLS build if their shells differ.

02 · SELECT

Backend + queue

A content publish or code release selects site-and-URL pairs. The queue holds these jobs and limits how many run at once.

03 · RENDER

Render worker

A Node worker runs the existing server build for one site and URL, creates HTML, and publishes that page to S3.

Publish content                         Release application code
       |                                          |
       |                                  Compile shared application
       |                                          |
       +--------------- Select affected URLs -----+
                                  |
                            Render queue
                                  |
                    Worker runs the built renderer
                                  |
                       Validate and publish HTML
                                  |
                       CloudFront serves the page

Use SQS for the queue and a Node worker on ECS or EC2 as an initial AWS design. The worker reads public, published content with backend credentials kept on the server. The visitor receives HTML with the correct site branding and content already present.

For the first implementation, keep the existing site buckets and distributions. Put common JavaScript and CSS in a shared release location. Configure shared asset routing once, rather than copying application files into each bucket for every release.

Shared application storage
  /releases/wls/v43/...          JavaScript, CSS, server artifact
  /releases/main-site/v43/...    Separate artifact if needed

Site storage, illustrated as three existing buckets
  main-site → HTML, theme, public config, manifest, icons
  prateek   → HTML, theme, public config, manifest, icons
  james     → HTML, theme, public config, manifest, icons

Build and render are different jobs. One code build can produce HTML for many sites. If a change affects 50,000 static pages, those pages still require rendering. This design removes repeated compilation and selects the work that is needed.

3. All cases discussed

Assume Prateek and James each have a WLS. Their public article posts can also appear on main-site. Whether a post belongs on a particular site is checked against publication and access rules.

A. Only Prateek needs a WLS rebuild today

As an immediate fix, add an explicit creator selector to the generator. Filter before fetching tenant pages and generating files. Emit a deployment file containing only Prateek. The upload script must process only that file. A missing or invalid creator must fail, never fall back to building every creator. Require an explicit all-sites mode for bulk work.

This is an interim improvement. After the new worker is available, a content or theme publish uses the smaller jobs below.

B. Create a new WLS for James

  1. Register James's verified domain, creator ID, storage destination, and selected shared application version.
  2. Publish James's theme, public configuration, manifest, and icons.
  3. Discover James's eligible public routes, including article posts, and queue their first renders.
  4. Verify the required initial pages, then activate the site.

With the current tenant-compiled code, James still needs an initial tenant build. With the generic WLS application, provisioning needs data and page renders only. Do not copy today's main-site output into James's bucket: it contains main-site assumptions and metadata. A neutral WLS release can be reused after the refactor.

C. Prateek changes colors

The backend validates and publishes Prateek's theme. Pages read the same six CSS variables before their first visible render. James and main-site keep their themes. This avoids page renders only after removing compiled and inline overrides that restore old colors. See the theme design for missing files, caching, and existing visitors.

D. Prateek creates or edits a marketing page

On publish, the backend registers the actual site and URL, then queues one render. A new page has no rendered revision, so its first render is always required. An edit creates a new desired revision. The worker publishes the page and updates the site's sitemap and any pages that link to it.

A new slug using an existing layout needs no compilation after routes and content are loaded at runtime. A brand-new Angular layout or feature still needs a code release. Reserve application paths and existing creator routes; do not let an arbitrary marketing slug take them over.

E. Prateek edits one article post

  1. The post-save API commits the published edit and records a durable event with the post identity and revision.
  2. The event handler computes the existing post URL and finds sites where the article is publicly served.
  3. It queues the same post URL in the two site contexts below.
  4. The worker renders each context using that site's branding, uploads its HTML, and refreshes its public URL in CloudFront.
Job 1: main-site → /post/prateek/article/<id>
Job 2: prateek   → /post/prateek/article/<id>

No article-detail job for James's WLS.

If a prerendered homepage, feed, or related-post section shows this article, queue that page too. Do not assume an /articles listing exists. Register dependencies for the actual pages in this application. A page rendered only in the browser does not need an HTML job, though its data cache may need a refresh.

F. Code changes all creators' article pages

  1. Build the new shared WLS and main-site artifacts, where applicable.
  2. The release explicitly declares that it affects article post detail pages.
  3. Query eligible posts with type = article. For each post, find its permitted site contexts.
  4. Queue the following renders and publish their HTML with the matching application assets.
Prateek article A → main-site + prateek
Prateek article B → main-site + prateek
James article C   → main-site + james

Other post types need rendering only if shared post code affects them. Article cards in other pages need rendering if the change affects those cards. When release scope is uncertain, select all public pages that could be affected. Angular cannot reliably infer the complete impact from a Git diff.

G. Prateek changes shared page content

If his footer changes, select Prateek's prerendered pages that use that footer. A title change in a menu can affect several pages even though only one page was edited. Track dependencies to account for this.

H. Unpublish, delete, or rename

Unpublishing removes the public HTML, refreshes listing pages and the sitemap, and clears the relevant edge caches. Mark the page withdrawn so an older queued job cannot publish it again. A rename publishes the new path and applies the chosen redirect or removal to the old path. Do not serve a stale full article after its access rules become private.

4. How the backend knows which pages to render

Add a page registry. Each record represents one URL on one site. The same Prateek article on main-site and on his WLS has two records.

siteId:              prateek
path:                /post/prateek/article/<id>
pageType:            POST_DETAIL
postType:            article
dependencies:        post:101, branding:prateek, footer:prateek
desiredRevision:     9
renderedRevision:    8
applicationVersion:  wls-v42
publicStatus:        published
publishedObject:     pages/article-101/revision-8/index.html

These are proposed fields, not existing backend names. The desired page revision should change when any relevant dependency changes. Store the input versions or a fingerprint of the published snapshot to avoid duplicate work.

TriggerSelection rule
New pageCreate a record for its site and URL. No rendered revision means it needs a first render.
Post editSelect direct post detail records and reverse dependencies for that post ID.
New article in a listingSelect pages depending on the creator's post collection, even though the new article was absent from their last render.
Footer or menu editSelect records with that site-specific footer or menu dependency.
Code release for articlesSelect post detail records with postType = article, plus other affected page types declared by the release.
Broad shared code changeSelect all eligible records for the affected application.

Record dependencies when a renderer or page service loads data. Also declare collection dependencies for feeds, pagination, and new or removed posts. Start with conservative rules: for example, an article publish can refresh the creator's registered public listing pages rather than attempt field-level optimization.

Initially, seed this registry from the existing generated route lists and the SEO post source. Use pagination or a complete export. A periodic reconciliation job compares published routes with the registry to repair missed events, add missing routes, and identify withdrawn pages.

5. What a single render job actually does

Render job
  siteId:         prateek
  path:           /post/prateek/article/<id>
  targetRevision: 9
  appVersion:     wls-v42
  reason:         post-updated
  1. Resolve the site. Read its verified hostname and public branding from the backend registry. Derive storage targets on the server, not from arbitrary event URLs.
  2. Load a consistent snapshot. Fetch the intended published post, access state, and page dependencies. Render the anonymous visitor's permitted view.
  3. Run the compiled renderer. Pass the URL, an unrendered HTML shell, site context, and page data into Angular's server rendering entry point. A renderModule-style entry point or Universal engine can return one HTML string. Bundle it with the server artifact.
  4. Keep browser startup consistent. Embed only public initial data through TransferState or an equivalent safe serialization path. The browser must use the same site and page snapshot initially, rather than compiled old data.
  5. Validate. Check expected page identity, canonical hostname, content, status, and referenced assets. Do not publish an error page, loading shell, or private article body as a successful public render.
  6. Publish safely. Write HTML to a new object. Verify the job is still current, then update the page's published pointer. Apply cache refresh rules and mark the revision complete.

Do not use ng run <tenant>:prerender for these content jobs. That is the current combined pipeline. The new worker uses an already built server artifact and must not serve the old prerendered file as its rendering template.

Serialize jobs for the same site and path. Use a durable publish event, such as a transactional outbox, retries, and a failed-job queue. An atomic revision check on the published pointer prevents a slow older job from replacing a newer page. A failed render keeps the previous valid page available, except where publication or access rules require removal.

The existing global DOM and module-level tenant state require an audit. Use isolated worker processes with one render at a time initially; move to concurrent renders only after proving that tenant data cannot cross between requests.

6. Colors without rebuilding pages

Create a public theme resource for each site, using the six existing variables:

--configBgColor          --configThemeColor
--configSecondaryBgColor --configPostTextColor
--configButtonTextColor  --configBgTextColor

Load the theme as a normal stylesheet in the document head, in the correct cascade order. Remove competing inline values and startup code that reapplies compiled colors. Cover pages, article theme controls, and ordinary page components must follow an explicit policy: the published WLS theme is authoritative unless a permitted local preview or reading mode overrides it.

The render worker receives site configuration before producing HTML. The browser can receive that public configuration inside the HTML, so a frontend API request is not required to determine the initial tenant identity or colors. A separate theme stylesheet is still a browser asset request.

ConditionRequired behavior
Strict requirement: only that WLS's colors may appearEmbed the last published colors for that site as a fallback; fresh theme CSS overrides them. If neither a valid site theme nor its fallback is available, show a neutral unavailable shell instead of the normal WLS UI.
Theme asset missingUse the same site's last valid fallback, report the failure, and retry. A generic TopFans theme does not meet the strict branding requirement.
Site identity missing or hostname unknownReject normal site rendering. Never fall back to another creator or main-site.
Visitor already has the page openNext load can obtain fresh colors. Updating an open page immediately needs an SSE/WebSocket notification and a stylesheet refresh. Offline visitors cannot receive live updates.

“Instant” needs a defined promise. A save can publish a small theme without a build, but edge invalidation and propagation are not instantaneous. For strict freshness on new requests, use a theme endpoint or behavior that bypasses caching and reads the published version. For efficient caching, use versioned theme objects with a short-lived or revalidated pointer and accept a measured propagation window.

Keep mutable theme/config resources out of stale service-worker caches. Set CloudFront minimum TTL to zero where origin revalidation rules must apply. Do not return index.html for a missing CSS or JSON resource. A raw HTTP failure is preferable to an HTML response masquerading as CSS.

Separate buckets or shared bucket?

With separate distributions and correctly configured site origins, /tenant-theme.css resolves to each site's own file. A shared distribution needs tenant-aware cache routing:

Prateek's verified hostname → siteId prateek
  /tenant-theme.css → /tenants/prateek/theme/current.css

James's verified hostname → siteId james
  /tenant-theme.css → /tenants/james/theme/current.css

Perform the rewrite before the cache lookup so the cache uses distinct paths. Validate hostnames, overwrite any client-supplied internal tenant marker, and reject attempts to request another site's internal prefix. Hostname-based routing is not a replacement for backend access control.

CloudFront KeyValueStore can hold small routing/version mappings without a function-code deployment. It should not hold full page bodies. CloudFront Functions perform routing here; the Node worker performs Angular rendering. AWS KeyValueStore documentation.

7. A feature release with 1,000 WLS sites

Build the generic WLS application once. Build main-site separately if it needs a different artifact. Upload immutable versioned assets once per artifact. Route all sites' release-asset requests to that shared location.

StrategyCompilationRemaining work
Current tenant buildsUp to 1,000 WLS buildsPrerender and upload for each WLS
Shared build copied to tenant bucketsOne WLS build1,000 asset copies plus affected HTML renders
Recommended shared asset originOne WLS buildOne shared asset upload plus affected HTML renders and publication updates

If all 1,000 sites have 50 affected pages, there are still 50,000 HTML renders. A queue spreads that work across measured worker capacity. Do not promise a fixed speed improvement before measuring compilation, data fetches, rendering, uploads, and cache refresh separately.

Keep HTML and its application version together

HTML rendered by v42 must reference explicit v42 asset paths. HTML rendered by v43 references v43. Do not redirect every asset request to whichever version is newest.

A shared release pointer alone cannot update already generated HTML. Choose how a release becomes visible:

For the first major shared-app migration, prefer complete site rollouts. Keep old code and page objects for rollback and old browser sessions. A feature flag can enable already released behavior for selected creators; if it affects server-rendered markup, flag changes must also queue the affected renders.

Optional later: render on the first request

A shared SSR origin can render missing or outdated HTML when requested and cache the result. This avoids rendering unvisited pages in advance, but adds runtime infrastructure and first-request latency. An S3-only origin cannot do this by itself. Start with publish-time workers if every public page must already exist as static HTML.

Cache policy

Use long immutable caching for versioned assets and immutable internal HTML objects. Public navigation URLs and version pointers need a freshness policy. Invalidate changed public paths, including aliases and rewritten paths where required, or select a new immutable object via an edge mapping. Cache keys must distinguish site, route, and selected revision. AWS cache-key guidance.

Audit ngsw-config.json, offline.service.js, and the active service-worker registrations. A worker serving a cached app shell can bypass new HTML and theme policies. Public prerendered routes need network behavior compatible with independent content updates, while offline pages must remain explicitly stale.

8. Implementation order and ownership

StepChangesCompletion checkpoint
1. Restrict today's pipeline
Frontend + CI
Add creator selection and unique job output paths. Filter the deployment list. Fix broad cache headers. Remove duplicate deployment-config declarations and construct validated commands instead of evaluating command strings.A Prateek request builds/uploads Prateek only. Invalid selection cannot trigger all sites.
2. Prove one-URL rendering
Frontend
Add a renderer entry point using the pinned Angular/Universal version. Use an unrendered shell and a known server artifact. Initially it may reuse existing tenant artifacts.Render one real article URL from a saved build without invoking Angular CLI compilation.
3. Supply site and page data
Frontend + backend
Introduce explicit tenant context. Replace tenant environment reads and generated content dependencies. Initialize browser state from the rendered snapshot. Replace global DOM assumptions and add runtime marketing route resolution.The generic WLS renderer produces Prateek and James pages with correct, isolated branding. Main-site still works.
4. Separate colors
Frontend + backend + infrastructure
Publish the six theme variables as a site resource. Remove stale inline/body overrides. Define missing-theme fallback and cache/service-worker rules.Prateek's color publish changes his initial page appearance without a build or another site's colors.
5. Add selective publishing
Backend + infrastructure
Seed page registry; add dependency selection, durable publish events, SQS, worker deployment, retries, revision checks, and sitemap updates.One Prateek article edit updates its two permitted site copies and registered dependent pages. James stays untouched unless a real dependency exists.
6. Share code releases
CI + infrastructure + frontend
Upload shared release assets, declare affected page types, route versioned assets, stage HTML publication, and retain rollback artifacts.An article-layout release renders all eligible article posts without per-creator Angular builds.

For build speed, also use a reproducible install with the committed lockfile, cache dependency downloads and Angular build cache with CI enabled, and preinstall recurring tools in the build image. Avoid rewriting generated files solely to change timestamps. Reuse published content snapshots rather than fetching the full SEO dataset separately for every tenant.

The parallel buildspec sets PARALLELISM, while its script reads MAX_JOBS. Align those settings and measure memory use. Keep the required --max_old_space_size=10048 for builds. Multiple builds or render processes can exceed host memory even when each process stays below its own heap limit.

Keep Angular upgrade work separate from this publishing refactor. A newer build system may improve compilation, but it does not supply page dependency selection or independent HTML publication automatically.

9. Checks before rollout

Decisions to confirm during implementation

The backend trigger implementation and live CloudFront settings were not inspected. Confirm the SEO post endpoint's completeness, public visibility rules, existing event mechanism, cache rewrites, and the authoritative domain registry. Decide the acceptable theme propagation delay and whether open pages must update live. Decide whether code releases require a single version per site or permit controlled page-by-page rollout.

No CONTEXT.md was found during the discussion. This document uses the existing terms WLS, post, article, prerender, S3, and CloudFront.