The concrete 301-redirect plan for Substack /p/ permalinks when Sanity Check migrates to Ghost
The question
What's the URL redirect plan for Substack /p/<slug> permalinks so existing inbound links don't 404 when Sanity Check migrates from Substack to Ghost? (Ghost docs note this is a manual step.) Deliver the concrete mechanics — slug mapping, 301 implementation options (Ghost redirects.yaml vs Cloudflare), and gotchas. Context: this protects existing SEO and inbound links; Sanity Check lives at sc.raydata.co and RDCO already runs Cloudflare, so a Cloudflare redirect layer is available alongside Ghost-native redirects.
What we already know (from the vault)
- [[06-reference/research/2026-04-19-newsletter-platform-sanity-check-v3]] — the parent brief that recommended Ghost(Pro) Creator on
sc.raydata.coand flagged this exact redirect question as Open Follow-up #2 ("Ghost docs note this is a manual step"). The 21-issue archive was on Substack atsc.raydata.co(Substack custom domain); Ghost's official Substack importer brings all 21 posts + the free list. - [[08-tooling/2026-04-24-resend-setup]] —
sc.raydata.cois now an Astro site on Cloudflare Pages (subscribe endpoint is a Pages Function). This is a live wrinkle: the destination may be Ghost (per the v3 recommendation) OR the existing Cloudflare Pages site — which changes which redirect surface you use. Confirm before executing. - [[06-reference/research/2026-04-22-publishing-for-agents-spec]] — confirms
_redirectsand_headersare Cloudflare Pages concerns; if the archive ends up served by Pages (not Ghost), redirects go in a Pages_redirectsfile, not Ghost'sredirects.yaml. - [[01-projects/newsletter/revival-strategy]] — the archive (21 issues + the
sc.raydata.codomain equity) is the named asset to preserve; SEO ownership is the reason for the whole move, so link-preservation is load-bearing, not cosmetic.
What the web says
- Ghost preserves the slug; one regex rule maps every post. Substack public URLs are
/p/<slug>; Ghost post URLs are/<slug>. Ghost's official Substack migration doc gives a singleredirects.yamlrule that strips the/p/prefix, which works precisely because the importer keeps each post's slug 1:1 (docs.ghost.org/migration/substack). - The exact rule (verbatim, paste into the
301:block ofredirects.yaml):
The negative-lookahead excludes UUID-shaped paths so Ghost's own301: \/p\/(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})(.*): /$1/p/<uuid>post-preview links keep working; only legacy Substack/p/<slug>permalinks get redirected (docs.ghost.org/migration/substack). - Where to upload in Ghost: Settings → Labs in Ghost Admin. Download the existing
redirects.yamlfirst, edit in a text editor (YAML, spaces not tabs), re-upload; changes take effect immediately. Works on Ghost(Pro) managed hosting (ghost.org/tutorials/implementing-redirects). - YAML vs JSON:
redirects.yamlis current; older installs usedredirects.json(still works, not recommended). Regex is supported in the from-path (e.g.^\/blog\/([a-z0-9-]+)\/$: /$1) (ghost.org/tutorials/implementing-redirects). - Cloudflare Bulk Redirects (edge alternative): Dashboard → Bulk Redirects → create a list (CSV import of Source/Target/Status rows, or manual entry), then a Bulk Redirect Rule referencing the list. Requires the hostname to be proxied (orange-cloud) through Cloudflare. Bulk Redirects are exact-match/prefix (no regex) — so you'd load ~21 rows, one per issue, rather than one pattern (developers.cloudflare.com/rules/url-forwarding/bulk-redirects/create-dashboard).
- Comments gotcha: the regex also catches
/p/<slug>/commentsand/p/<slug>/comment/<id>and rewrites them to non-existent Ghost paths; Substack comment permalinks don't survive migration cleanly. The Ghost forum thread on this treats it as an accepted loss (forum.ghost.org — Substack redirect comments pages). - A concrete first-person writeup (alexhyett.com/newsletter/substack-to-ghost) was blocked (HTTP 403) and could not be read — flagged, not used.
Convergences and contradictions
- Convergence: Every source agrees the redirect is a manual, one-time step, and that a single
/p/-stripping rule is the correct pattern because slugs are preserved — no per-URL mapping table is needed for the post archive itself. - Contradiction / unverified: Ghost's doc does not explicitly state slug preservation; the single-regex approach only holds if the importer keeps each slug byte-for-byte. If the importer de-duplicated or altered any of the 21 slugs, that post needs an explicit override row. Verify on a sample post post-import.
- Scope contradiction with the vault: the v3 brief assumes Ghost as the destination, but [[08-tooling/2026-04-24-resend-setup]] shows
sc.raydata.cois already a Cloudflare Pages/Astro site. These can't both be the live archive host — resolve which one ownssc.raydata.cobefore choosing the redirect surface.
Synthesis for RDCO
Primary recommendation: use Ghost-native redirects.yaml with the single official regex rule — if and only if Ghost is the confirmed host for sc.raydata.co. It is one rule, covers all 21 current posts plus every future imported post automatically, correctly preserves Ghost's own preview URLs via the UUID negative-lookahead, requires zero per-URL maintenance, and works on Ghost(Pro). This beats Cloudflare Bulk Redirects for the post archive because Bulk Redirects can't do regex — you'd hand-load 21 rows and re-load on every new import, for no benefit. The slug-mapping approach is therefore "preserve, don't remap": trust the importer's 1:1 slug carry-over and strip only the /p/ prefix, then spot-check.
Reserve Cloudflare for the edge cases the Ghost post-regex doesn't cover, since sc.raydata.co is already proxied through Cloudflare. Namely: (1) /feed → /rss/ — Substack's RSS lives at /feed, Ghost's at /rss/, so feed-reader subscribers 404 without this; add it as a second line in redirects.yaml (/feed/: /rss/) or a Cloudflare rule. (2) Any apex/legacy path redirects (e.g. /archive). (3) The sanitycheck.substack.com subdomain URLs are unreachable from Ghost or Cloudflare — that host stays under Substack's control; keep the Substack publication alive (don't delete it) and set Substack's own "redirect to custom domain" so those links forward, or accept them as a bounded loss. Only inbound links pointing at sc.raydata.co/p/... are ours to 301.
If the destination turns out to be the existing Astro/Cloudflare Pages site (not Ghost), the equivalent mechanic is a Pages _redirects file with a placeholder rule: /p/:slug /:slug 301 (Pages supports :placeholder and :splat syntax). That is the Cloudflare-Pages-native analog of Ghost's regex and is the cleaner path than Bulk Redirects in that scenario, since it lives with the site build.
Ordered runbook (Ghost path):
- Run the Ghost Substack importer; confirm all 21 posts imported with slugs intact (open 2-3 posts, compare each Ghost slug to the old Substack
/p/<slug>). - In Ghost Admin → Settings → Labs, download the current
redirects.yaml. - Add the
301:block with the verbatim/p/-stripping regex above; add/feed/: /rss/on a new line under the same301:key. Spaces, not tabs. - Re-upload
redirects.yaml; it takes effect immediately. - Test with
curl -I https://sc.raydata.co/p/<a-real-old-slug>— expect301→Location: /<slug>→200. Test/feed→/rss/. Test one Ghost preview/p/<uuid>link still resolves (confirms the lookahead works). - Keep the Substack publication live (don't delete) so
substack.com-hosted links and image assets don't hard-break; set Substack's redirect-to-custom-domain option. - Re-submit the Ghost sitemap in Google Search Console and watch Coverage for
/p/404s over the next 2-4 weeks.
Open follow-ups
- Confirm the actual host of
sc.raydata.co: Ghost (v3 recommendation) or the live Astro/Cloudflare Pages site (resend-setup doc). This decidesredirects.yamlvs Pages_redirects. Blocking for execution. - Verify the Ghost importer preserved all 21 slugs byte-for-byte; if any were altered/de-duplicated, add explicit per-URL 301 overrides for those.
- Image assets: confirm whether the importer re-hosted Substack CDN images into Ghost, or if
srcstill points atsubstackcdn.com(which breaks if Substack is ever deleted). If not re-hosted, keep Substack alive or re-upload images. - Decide whether to keep the Substack publication indefinitely (protects
substack.com/p/...inbound links + hosted images) or sunset it after a link-decay window. - Read the blocked alexhyett.com writeup via an alternate route (WebArchive/text-mode) for any additional real-world gotchas not in the official docs.
Related
- [[06-reference/research/2026-04-19-newsletter-platform-sanity-check-v3]]
- [[08-tooling/2026-04-24-resend-setup]]
- [[01-projects/newsletter/revival-strategy]]
- [[06-reference/research/2026-04-22-publishing-for-agents-spec]]
Sources
Vault:
- /Users/ray/rdco-vault/06-reference/research/2026-04-19-newsletter-platform-sanity-check-v3.md
- /Users/ray/rdco-vault/08-tooling/2026-04-24-resend-setup.md
- /Users/ray/rdco-vault/01-projects/newsletter/revival-strategy.md
- /Users/ray/rdco-vault/06-reference/research/2026-04-22-publishing-for-agents-spec.md
Web:
- https://docs.ghost.org/migration/substack
- https://ghost.org/tutorials/implementing-redirects/
- https://developers.cloudflare.com/rules/url-forwarding/bulk-redirects/create-dashboard/
- https://forum.ghost.org/t/substack-redirect-comments-pages/44711
- https://www.alexhyett.com/newsletter/substack-to-ghost/ (blocked — HTTP 403, not used)