Skip to content

feat(canvas): per-domain landing & 404 page overrides - #1996

Merged
2witstudios merged 6 commits into
masterfrom
pu/moving-docs-to-puiblished-sites
Jul 11, 2026
Merged

2witstudios merged 6 commits into
masterfrom
pu/moving-docs-to-puiblished-sites

Conversation

@2witstudios

@2witstudios 2witstudios commented Jul 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Generalizes the pagespace.ai/prism-style platform-owned custom domain mechanism into a self-service feature: a drive owner can now give a custom domain its own landing (root) page and 404 page, instead of every domain on a drive being a byte-for-byte identical mirror.
  • Adds publishLandingPageId/publishNotFoundPageId to custom_domains (migration 0198_skinny_prowler.sql), with pure resolvers (resolveHostRootCopies, resolveBackfillRootCopy) deciding which page's artifact backs a given host's root/404, override-wins precedence.
  • Wires the mirror pipeline (apps/web/src/lib/canvas/custom-domain-mirror.ts, publish-page.ts), the domain PATCH API, and a settings-UI picker (apps/web/src/app/dashboard/[driveId]/settings/domains/page.tsx).
  • Includes a verified fallback to the drive-wide 404 when an override page fails to render (e.g. trashed after being set) — caught and fixed via an independent code review during development.
  • PATCH validates the entire request payload before any mutation runs, so a mixed payload (e.g. a valid isPrimary change alongside an invalid publishLandingPageId) is rejected atomically instead of partially applying.
  • Adds tasks/docs-routing-domain-landing-pages-epic.md, the plan this work grew out of (routing pagespace.ai/docs to published Canvas content the same way /prism already works — that routing change itself lives in the separate PageSpace-Deploy repo and ships independently of this PR).

Test plan

  • bunx turbo run typecheck — clean across the monorepo (post-merge with master)
  • bunx turbo run build — apps/web production build succeeds
  • Full affected test suites (packages/lib/src/canvas, apps/web/src/lib/canvas, apps/web/src/app/api/drives) — 1140+ tests passing, including new regression coverage for the 404-fallback fix and the mixed-payload validation-ordering fix
  • Manual verification of the new landing/404 pickers in the Domains & Publishing settings UI (not yet driven in a browser)

🤖 Generated with Claude Code

https://claude.ai/code/session_011vSSxCSyGbgjVXWihK9eEC

Summary by CodeRabbit

  • New Features

    • Added per-domain landing page and 404 page overrides in domain settings.
    • Override pages can be selected, cleared, validated, and independently configured.
    • Custom domains now automatically mirror the appropriate landing and 404 content.
    • Added fallback behavior to preserve standard 404 pages when custom rendering is unavailable.
  • Bug Fixes

    • Improved domain publishing and backfill behavior to prevent stale root pages.
    • Prevented invalid domain updates from partially applying.
    • Unpublishing pages now correctly updates affected custom-domain content.

2witstudios and others added 2 commits July 10, 2026 22:07
Generalizes the pagespace.ai/prism-style platform-owned custom domain
mechanism into a self-service feature: any drive owner can now give a
custom domain its own landing (root) page and 404 page, instead of every
domain on a drive being a byte-for-byte identical mirror.

- packages/db/src/schema/custom-domains.ts: nullable
  publishLandingPageId/publishNotFoundPageId columns (migration 0193)
- packages/lib/src/canvas/custom-domain-mirror.ts: pure resolvers
  (resolveHostRootCopies, resolveBackfillRootCopy) deciding which page's
  artifact backs a given host's root/404, with override-wins precedence
- apps/web/src/lib/canvas/custom-domain-mirror.ts +
  apps/web/src/lib/canvas/publish-page.ts: mirror pipeline wiring,
  including a verified fallback to the drive-wide 404 when an override
  page fails to render (e.g. trashed after being set)
- apps/web/src/app/api/drives/[driveId]/domains/[domainId]/route.ts:
  PATCH accepts publishLandingPageId/publishNotFoundPageId
- apps/web/src/app/dashboard/[driveId]/settings/domains/page.tsx:
  per-domain landing/404 page pickers in the Domains & Publishing
  settings UI

Also adds the docs-routing-domain-landing-pages-epic plan, which covers
the originating motivation: routing pagespace.ai/docs to published
Canvas content the same way pagespace.ai/prism already works (that
routing change itself lives in the separate PageSpace-Deploy repo and
ships independently).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vSSxCSyGbgjVXWihK9eEC
# Conflicts:
#	packages/db/drizzle/meta/0193_snapshot.json
#	packages/db/drizzle/meta/_journal.json
@coderabbitai

coderabbitai Bot commented Jul 11, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ae44dafc-0269-4db6-bd82-7e37f5f2f9bf

📥 Commits

Reviewing files that changed from the base of the PR and between f8140ec and 47604e7.

📒 Files selected for processing (5)
  • apps/web/src/app/api/drives/[driveId]/domains/[domainId]/__tests__/route.test.ts
  • apps/web/src/app/api/drives/[driveId]/domains/[domainId]/route.ts
  • apps/web/src/lib/canvas/__tests__/custom-domain-mirror.test.ts
  • apps/web/src/lib/canvas/custom-domain-mirror.ts
  • tasks/docs-routing-domain-landing-pages-epic.md
✅ Files skipped from review due to trivial changes (1)
  • tasks/docs-routing-domain-landing-pages-epic.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/web/src/app/api/drives/[driveId]/domains/[domainId]/route.ts
  • apps/web/src/app/api/drives/[driveId]/domains/[domainId]/tests/route.test.ts
  • apps/web/src/lib/canvas/custom-domain-mirror.ts
  • apps/web/src/lib/canvas/tests/custom-domain-mirror.test.ts

📝 Walkthrough

Walkthrough

Per-domain landing and 404 page overrides were added across database schema, API validation, publishing mirrors, certificate flows, and the Domains settings UI. Custom-domain site files now support host-specific override rendering with fallback behavior.

Changes

Domain publishing overrides

Layer / File(s) Summary
Schema and storage contracts
packages/db/..., apps/web/src/lib/canvas/published-storage.ts, apps/web/src/lib/canvas/__tests__/published-storage.test.ts
Custom domains store nullable landing and 404 page references, while published site-file APIs use a generic prefix.
Root-copy planning rules
packages/lib/src/canvas/custom-domain-mirror.ts, packages/lib/src/canvas/__tests__/custom-domain-mirror.test.ts
Root-copy helpers apply landing-page override precedence during publishing and backfills.
Override-aware mirroring
apps/web/src/lib/canvas/custom-domain-mirror.ts, apps/web/src/lib/canvas/__tests__/custom-domain-mirror.test.ts
Mirroring renders per-host 404 overrides, falls back to the drive-wide 404, and updates root/deletion behavior for landing overrides.
Publishing and domain API integration
apps/web/src/lib/canvas/publish-page.ts, apps/web/src/lib/canvas/reconcile-cert.ts, apps/web/src/app/api/drives/..., apps/web/src/app/api/pages/...
Publishing, verification, certificate activation, registration, unpublishing, and domain PATCH operations pass override rendering and persist override changes.
Settings controls
apps/web/src/app/dashboard/[driveId]/settings/domains/page.tsx
Active domains expose Landing page and 404 page pickers with mutation handlers, saving states, and action locking.
Documentation and model notes
packages/lib/src/canvas/primary-host.ts, plan.md, tasks/docs-routing-domain-landing-pages-epic.md
Documentation records root/404 divergence, routing work, implementation requirements, and acceptance criteria.

Schema snapshot updates

Layer / File(s) Summary
Generated schema snapshot
packages/db/drizzle/meta/0198_snapshot.json, packages/db/drizzle/meta/_journal.json
Generated metadata records additional tables and fields, machine naming changes, enum updates, and custom-domain override columns.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DomainSettings
  participant DomainPatchAPI
  participant CustomDomainMirror
  participant PublishedStorage
  DomainSettings->>DomainPatchAPI: PATCH landing or 404 page override
  DomainPatchAPI->>CustomDomainMirror: re-mirror affected host
  CustomDomainMirror->>PublishedStorage: render or copy host artifacts
  PublishedStorage-->>DomainSettings: updated domain state after revalidation
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: per-domain landing and 404 page overrides for canvas domains.
Docstring Coverage ✅ Passed Docstring coverage is 94.74% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pu/moving-docs-to-puiblished-sites

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 23d19215df

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"breakpoints": true
},
{
"idx": 197,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore the 0193 Drizzle snapshot

Adding the 0197 journal entry while this diff deletes/renames packages/db/drizzle/meta/0193_snapshot.json leaves the existing idx 193 journal entry with no corresponding snapshot file. Drizzle Kit relies on these historical snapshots when generating future migrations, so a checkout after this commit can fail or produce invalid diffs when it tries to load 0193_snapshot.json; please keep 0193_snapshot.json and add 0197_snapshot.json as a new file instead of moving the old snapshot.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1a79f21 — restored packages/db/drizzle/meta/0193_snapshot.json from master rather than leaving it deleted. You're right that this would have broken drizzle-kit generate for anyone checking out this history later; I confirmed by re-running bun run db:generate afterward, which now correctly reports "No schema changes, nothing to migrate" instead of re-diffing against a missing snapshot.

include404: !overrideRendered,
includeSiteFiles: true,
});
const allCopies = rootCopy ? [...copies, rootCopy] : copies;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear stale custom-domain roots when no replacement exists

When a host previously had a landing override and the override is reset while the drive default root is absent/unpublished (or a new override points at an unpublished page), rootCopy is null, so this backfill only copies paths/site files and leaves the old published/<host>/index.html in place. The API then reports the landing page reset even though the domain root continues serving the prior override until some later unpublish deletes it; delete the host root when there is no replacement root copy.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1a79f21 — mirrorDriveToCustomHost now deletes the host's stale root object (published/<host>/index.html) whenever the backfill resolves no valid root copy, instead of silently leaving prior content live. Added 3 regression tests covering: override reset with no drive home page, override pointing at an unpublished page, and the positive case (root copy resolves → no delete).

2witstudios and others added 2 commits July 10, 2026 22:57
# Conflicts:
#	packages/db/drizzle/meta/0197_snapshot.json
#	packages/db/drizzle/meta/_journal.json
- Restore packages/db/drizzle/meta/0193_snapshot.json, which was deleted
  instead of preserved during an earlier merge-conflict resolution.
  Drizzle Kit needs every historical snapshot to generate future
  migrations correctly — deleting one breaks the chain for anyone who
  checks out this history later, even though the migration itself
  (0193_awesome_titania, from master) still applies fine today.
- mirrorDriveToCustomHost: clear a custom-domain host's stale root
  object when the backfill resolves no valid root copy (override reset
  with no drive-wide home page, or a new override pointing at a page
  that isn't published yet). Previously the backfill just skipped the
  root copy in that case, leaving prior content (an old override, or an
  old home-page mirror) silently still live at that host's "/" even
  though the API had already reported the change as applied.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vSSxCSyGbgjVXWihK9eEC

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
apps/web/src/app/dashboard/[driveId]/settings/domains/page.tsx (2)

294-339: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting a shared domain-override PATCH helper.

handleSetDomainLandingPage and handleSetDomainNotFoundPage are nearly identical — same fetch logic, error handling, and revalidation, differing only in the field name, saving-state setter, and toast messages. Extracting a generic helper would reduce duplication and make future override fields trivial to add.

This is a minor maintainability improvement and can be deferred.

♻️ Proposed shared helper
+async function handleSetDomainOverride(
+  driveId: string,
+  domainId: string,
+  field: 'publishLandingPageId' | 'publishNotFoundPageId',
+  pageId: string | null,
+  setSaving: (id: string | null) => void,
+  savingGuard: string | null,
+  mutateDomains: () => Promise<void>,
+) {
+  if (savingGuard) return;
+  setSaving(domainId);
+  try {
+    const res = await fetchWithAuth(`/api/drives/${driveId}/domains/${domainId}`, {
+      method: 'PATCH',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ [field]: pageId }),
+    });
+    if (!res.ok) {
+      const data = await res.json().catch(() => ({})) as { error?: string };
+      toast.error(data.error ?? `Failed to update ${field === 'publishLandingPageId' ? 'landing page' : '404 page'}`);
+      return;
+    }
+    await mutateDomains();
+    const label = field === 'publishLandingPageId' ? 'Landing page' : '404 page';
+    toast.success(pageId ? `${label} updated` : `${label} reset to the drive default`);
+  } catch {
+    toast.error(`Failed to update ${field === 'publishLandingPageId' ? 'landing page' : '404 page'}`);
+  } finally {
+    setSaving(null);
+  }
+}

Then the two handlers become one-liners:

 const handleSetDomainLandingPage = async (domainId: string, pageId: string | null) => {
-  if (savingLandingPageId) return;
-  setSavingLandingPageId(domainId);
-  try {
-    const res = await fetchWithAuth(`/api/drives/${driveId}/domains/${domainId}`, {
-      method: 'PATCH',
-      headers: { 'Content-Type': 'application/json' },
-      body: JSON.stringify({ publishLandingPageId: pageId }),
-    });
-    if (!res.ok) {
-      const data = await res.json().catch(() => ({})) as { error?: string };
-      toast.error(data.error ?? 'Failed to update landing page');
-      return;
-    }
-    await mutateDomains();
-    toast.success(pageId ? 'Landing page updated' : 'Landing page reset to the drive default');
-  } catch {
-    toast.error('Failed to update landing page');
-  } finally {
-    setSavingLandingPageId(null);
-  }
+  handleSetDomainOverride(driveId, domainId, 'publishLandingPageId', pageId, setSavingLandingPageId, savingLandingPageId, mutateDomains);
 };

 const handleSetDomainNotFoundPage = async (domainId: string, pageId: string | null) => {
-  if (savingNotFoundPageId) return;
-  setSavingNotFoundPageId(domainId);
-  try {
-    const res = await fetchWithAuth(`/api/drives/${driveId}/domains/${domainId}`, {
-      method: 'PATCH',
-      headers: { 'Content-Type': 'application/json' },
-      body: JSON.stringify({ publishNotFoundPageId: pageId }),
-    });
-    if (!res.ok) {
-      const data = await res.json().catch(() => ({})) as { error?: string };
-      toast.error(data.error ?? 'Failed to update 404 page');
-      return;
-    }
-    await mutateDomains();
-    toast.success(pageId ? '404 page updated' : '404 page reset to the drive default');
-  } catch {
-    toast.error('Failed to update 404 page');
-  } finally {
-    setSavingNotFoundPageId(null);
-  }
+  handleSetDomainOverride(driveId, domainId, 'publishNotFoundPageId', pageId, setSavingNotFoundPageId, savingNotFoundPageId, mutateDomains);
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/dashboard/`[driveId]/settings/domains/page.tsx around lines
294 - 339, Extract the duplicated PATCH, error handling, domain revalidation,
and saving-state cleanup from handleSetDomainLandingPage and
handleSetDomainNotFoundPage into a shared helper that accepts the override field
name, page ID, saving-state setter, and success/error messages. Refactor both
handlers to call this helper while preserving their existing payloads and toast
behavior.

1091-1117: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No per-row saving indicator for page override pickers.

When a landing or 404 page override is being saved, the PagePickerPopover is disabled via anyBusy but shows no spinner or loading state on the affected row. Other per-row actions (verify, cert, remove, primary) all have individual is* booleans that render a Loader2 spinner. The new savingLandingPageId/savingNotFoundPageId states are only consumed in anyBusy and never reach DomainRow as per-row flags.

This is a minor UX gap — users get no visual feedback on which domain's picker is saving. Consider passing isSavingLandingPage/isSavingNotFoundPage booleans to DomainRow and showing a spinner in the picker label area.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/dashboard/`[driveId]/settings/domains/page.tsx around lines
1091 - 1117, Expose per-domain saving flags from the page override save state
through DomainRow, deriving isSavingLandingPage and isSavingNotFoundPage by
comparing the saving IDs with domain.id. Pass these flags from the parent and
render a Loader2 spinner beside each corresponding picker label while saving,
while retaining the existing disabled behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/app/api/drives/`[driveId]/domains/[domainId]/route.ts:
- Around line 78-159: Validate publishLandingPageId and publishNotFoundPageId
before any database mutation, including the primary-domain transaction in the
PATCH handler. Refactor the update flow so primary and override fields are
applied atomically in one transaction, or otherwise ensure all validation
completes before committing; preserve the existing Canvas/drive ownership checks
in isValidDriveNotFoundPage. Add a regression test covering { isPrimary: true,
publishLandingPageId: "invalid" } that asserts a 400 response and no
primary-domain change.

In `@tasks/docs-routing-domain-landing-pages-epic.md`:
- Line 88: The PATCH update flow must not treat the domain update as complete
while `mirrorDriveToCustomHost` is only running in an unawaited catch chain.
Update the relevant PATCH handler to either await the single-domain re-mirror
before returning success or enqueue it through the existing publish-job
mechanism, matching other publish-triggering endpoints, and preserve appropriate
error handling.
- Around line 109-112: The documentation should distinguish hard deletion from
soft-trash behavior: retain onDelete: 'set null' for deleted pages, but
explicitly require resolver fallback and test coverage when an override page is
trashed, since the FK does not clear trashed references. Update the
mirror-pipeline and API validation test requirements accordingly.

---

Nitpick comments:
In `@apps/web/src/app/dashboard/`[driveId]/settings/domains/page.tsx:
- Around line 294-339: Extract the duplicated PATCH, error handling, domain
revalidation, and saving-state cleanup from handleSetDomainLandingPage and
handleSetDomainNotFoundPage into a shared helper that accepts the override field
name, page ID, saving-state setter, and success/error messages. Refactor both
handlers to call this helper while preserving their existing payloads and toast
behavior.
- Around line 1091-1117: Expose per-domain saving flags from the page override
save state through DomainRow, deriving isSavingLandingPage and
isSavingNotFoundPage by comparing the saving IDs with domain.id. Pass these
flags from the parent and render a Loader2 spinner beside each corresponding
picker label while saving, while retaining the existing disabled behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 410cf86f-2c36-4883-8eb2-f87eb2d3c6a2

📥 Commits

Reviewing files that changed from the base of the PR and between 40c9361 and f8140ec.

📒 Files selected for processing (26)
  • apps/web/src/app/api/drives/[driveId]/domains/[domainId]/__tests__/route.test.ts
  • apps/web/src/app/api/drives/[driveId]/domains/[domainId]/cert/refresh/__tests__/route.test.ts
  • apps/web/src/app/api/drives/[driveId]/domains/[domainId]/route.ts
  • apps/web/src/app/api/drives/[driveId]/domains/[domainId]/verify/__tests__/route.test.ts
  • apps/web/src/app/api/drives/[driveId]/domains/[domainId]/verify/route.ts
  • apps/web/src/app/api/drives/[driveId]/domains/__tests__/route.test.ts
  • apps/web/src/app/api/drives/[driveId]/domains/route.ts
  • apps/web/src/app/api/pages/[pageId]/publish/route.ts
  • apps/web/src/app/dashboard/[driveId]/settings/domains/page.tsx
  • apps/web/src/lib/canvas/__tests__/custom-domain-mirror.test.ts
  • apps/web/src/lib/canvas/__tests__/publish-page.test.ts
  • apps/web/src/lib/canvas/__tests__/published-storage.test.ts
  • apps/web/src/lib/canvas/__tests__/reconcile-cert.test.ts
  • apps/web/src/lib/canvas/custom-domain-mirror.ts
  • apps/web/src/lib/canvas/publish-page.ts
  • apps/web/src/lib/canvas/published-storage.ts
  • apps/web/src/lib/canvas/reconcile-cert.ts
  • packages/db/drizzle/0198_skinny_prowler.sql
  • packages/db/drizzle/meta/0198_snapshot.json
  • packages/db/drizzle/meta/_journal.json
  • packages/db/src/schema/custom-domains.ts
  • packages/lib/src/canvas/__tests__/custom-domain-mirror.test.ts
  • packages/lib/src/canvas/custom-domain-mirror.ts
  • packages/lib/src/canvas/primary-host.ts
  • plan.md
  • tasks/docs-routing-domain-landing-pages-epic.md

Comment thread apps/web/src/app/api/drives/[driveId]/domains/[domainId]/route.ts Outdated
Comment thread tasks/docs-routing-domain-landing-pages-epic.md
Comment thread tasks/docs-routing-domain-landing-pages-epic.md
…ailure

Independent review caught a regression in the stale-root-delete added for
the prior fix: the pre-existing homeRootExists probe's catch-all swallows
ANY error (not just 404), not just genuine absence. That was harmless
before (a null rootCopy was a pure no-op) but is now destructive (null
rootCopy triggers a delete) — a transient S3/Tigris read failure would
delete a live, currently-correct custom-domain homepage until the next
backfill happens to succeed.

Fixed by tracking probe failure separately from confirmed absence, and
skipping the delete (not just the copy) when the probe itself failed —
matching the existing fail-safe precedent in reconcile-cert.ts ("a
DNS-valid domain must not lose its content because a read hit a
transient blip").

Also documented (not fixed, out of scope for this PR) a related but
lower-severity finding: mirrorDriveToCustomHost has no per-host locking
across its several unsynchronized call sites, so a sufficiently
adversarial interleaving of two overlapping calls could still delete
freshly-written root content. This predates the stale-root delete (the
copy path has the same shape) and is self-healing on the next backfill;
a proper fix needs per-host serialization, a larger change than this
feature's scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vSSxCSyGbgjVXWihK9eEC
@2witstudios

Copy link
Copy Markdown
Owner Author

Ran an independent deep-review pass on the diff after the two Codex fixes above and it caught a regression I introduced in the stale-root-delete fix itself: the pre-existing homeRootExists probe's catch-all swallows any S3 read error, not just a genuine 404 — previously harmless (null rootCopy was a no-op), but the new delete-on-null-rootCopy logic would have treated a transient S3/Tigris blip the same as "confirmed absent," deleting a live, correct custom-domain homepage.

Fixed in 450ef65 — probe failure is now tracked separately from confirmed absence, and the delete is skipped (not just the copy) when the probe itself failed, matching the existing fail-safe precedent already established in reconcile-cert.ts. Added a regression test for it.

Also documented (not fixed) a related, lower-severity finding from the same pass: mirrorDriveToCustomHost has no per-host locking across its several unsynchronized call sites, so a sufficiently adversarial interleaving could still race a delete against a fresher write. This predates the new delete logic (the copy path has the same shape already) and self-heals on the next backfill; a proper fix needs per-host serialization, which is a larger change than this PR's scope — left as a code comment for whoever picks it up next.

…rror

CodeRabbit review on PR #1996:
- A mixed payload like { isPrimary: true, publishLandingPageId: "invalid" }
  could commit the isPrimary transaction (audit log, page re-render) before
  the landing-page validation ran, so a 400 response could still carry a
  real, persisted primary-domain change. All validation now runs before
  either mutation block. Added a regression test covering this exact case.
- The landing/404 override block called mirrorDriveToCustomHost(...).catch()
  without awaiting, inconsistent with the isPrimary block's awaited
  republishDriveCanonical/regeneratePublishedSiteFiles calls. Now awaited
  with the same try/catch/log pattern.
- epic.md: clarified that onDelete: 'set null' only fires on hard delete,
  not soft-trash — the trashed-page case relies on the resolver-level
  fallback and API validation, both of which need their own test coverage.
@2witstudios

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@2witstudios
2witstudios merged commit a1cc449 into master Jul 11, 2026
10 checks passed
@2witstudios
2witstudios deleted the pu/moving-docs-to-puiblished-sites branch July 13, 2026 13:57
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.

1 participant