Single Page Publishing reuses existing publication records via Publication.findLatestForDatabase(), which is a simple "newest by timestamp" query with no filtering by publish type, domain, or strategy. Combined with the fact that multiple publications can be "live" simultaneously across different domains, this creates a class of bugs where SPP can cross-pollinate publication IDs between domains and cause CMS content inconsistencies.
- Each custom domain has its own
lastPublicationIdfield - Full site publishes only update
lastPublicationIdon domains included incustomDomainsTargets(the domains selected for that publish) - SPP calls
findLatestForDatabase(databaseId)which returns the most recent publication by timestamp, regardless of which domains it was originally published to - SPP then stamps that publication ID onto its target domains via
markPublished() - CMS single-item publish (SIP) writes to "live" target directly without creating new publication records, and publishes to ALL live publications (no domain selection)
This means:
- A staging-only publish creates Publication B while production is still on Publication A
- SPP fires, grabs Publication B (newest), and stamps it onto whatever domains it targets
- The production
CustomDomainmodel'slastPublicationIdnow references a publication whose published database was created during a staging publish
When SPP overwrites a domain's lastPublicationId with the wrong publication, the domain's manifest.json (which maps page slugs to S3 asset paths) no longer matches the publication ID that the hosting layer uses to resolve CMS content. The hosting layer calls resolvePublicationIdForDomain(), gets back the wrong publication, and looks up CMS data in a database target that doesn't correspond to what's actually live on that domain.
From the customer's perspective, publishes appear to stop working. They publish a CMS item via single-item publish, see it succeed in the UI, but the live site returns 404s for those items. The domain's manifest.json publicationVersion no longer matches its customDomain.lastPublicationId, so the hosting layer resolves CMS content from a published database that doesn't correspond to what's actually live on that domain.
- Set up a site with two custom domains (e.g.,
production.example.comandstaging.example.com) - Full publish targeting only
production.example.com(creates Publication A) - Full publish targeting only
staging.example.com(creates Publication B, newer timestamp) - At this point the two domains have divergent
lastPublicationIdvalues - Do a Single Page Publish targeting
production.example.com - SPP calls
findLatestForDatabase, which returns Publication B (newest by timestamp, even though it was deployed to staging) markPublished()overwritesproduction.example.com'slastPublicationIdto Publication B- The production domain's manifest.json
publicationVersionstill references Publication A's published database - CMS single-item publishes now write to a different published database than the one the production domain's manifest expects, resulting in 404s
| Behavior | SPP | CMS Single-Item Publish (SIP) |
|---|---|---|
| Domain selection | User selects which domains to publish to | No selection; writes to ALL live publications |
| Publication records | Reuses latest via findLatestForDatabase |
Does not create or reference publication records |
| What changes | Page static assets (HTML/CSS/JS on S3) | CMS item data in the "live" target |
Updates lastPublicationId? |
Yes (this is the bug) | No |
The fundamental issue: SPP lets you target specific domains but assumes a single globally-latest publication ID is correct for all of them. CMS SIP doesn't have this problem because it doesn't touch publication IDs at all; it writes directly to the live data layer.
It shouldn't need to. SPP only changes a page's static assets on S3. It doesn't touch CMS data. The domain's existing lastPublicationId is still the correct pointer to its published database.
SPP updates lastPublicationId because it reuses the same markPublished() code path as full site publish (site.ts lines 3009-3025). After compile + upload, site.publish() calls cd.markPublished(lastPublicationId, ...) on every domain in customDomainsTargets. This code was written for full publish, where a new publication IS the correct published database. SPP rides the same path without considering that reusing a publication doesn't warrant overwriting domain metadata.
lastPublicationId on a domain is used downstream by:
resolvePublicationIdForDomain()at request time to determine which published database to serve- The publication pruner, to know which publications are still "live" and shouldn't be deleted
Proposed fix: Skip writing lastPublicationId in markPublished() when isSinglePagePublish is true. SPP could still update lastPublished (timestamp) and lastPublishedBy (user) without touching the publication pointer.
| What | File |
|---|---|
findLatestForDatabase (the problematic lookup) |
entrypoints/server/models/publication.ts |
| SPP skip-database-publishing logic | entrypoints/server/models/site.ts |
Per-domain markPublished |
entrypoints/server/models/site.ts, entrypoints/server/models/customDomain.ts |
| Domain publication resolution at request time | entrypoints/server/lib/logic/sites/resolvePublicationIdForDomain.ts |
| CMS single-item publish (separate path) | entrypoints/server/lib/logic/cms/items/publishCMSItems.ts |
| PR | Title | Merged | What it did |
|---|---|---|---|
| #63121 | [ENT-2086] Check all publications for branch publishes | 2023-12-05 | Introduced findLatestForDatabase into the site publish flow for branch publishes |
| #96880 | Work 1501 be skip cms database publishing for spp | 2026-01-15 | Added isSinglePagePublish to the skipDatabasePublishing condition, piggybacking on the branch-publish reuse path |