From 8a24dd2a49dbba788e583ca52a28acc09a972b88 Mon Sep 17 00:00:00 2001 From: PauloJeunon Date: Thu, 2 Jul 2026 15:11:12 -0300 Subject: [PATCH] Migrate base URL from archive.sparkpost.com to docs.sparkpost.com Renames the site's canonical host to docs.sparkpost.com and 301-redirects the old archive.sparkpost.com host to it, while keeping the site noindex so it doesn't compete with bird.com in search. - next-sitemap.js: default siteUrl -> docs.sparkpost.com (robots.txt keeps Disallow: /; sitemap/Host now emit docs.sparkpost.com) - netlify.toml: add forced host-based 301 archive.sparkpost.com/* -> docs.sparkpost.com/:splat as the first redirect rule; point CSP font-src at docs.sparkpost.com; refresh the X-Robots-Tag comment - Remove the temporary host-conditional-noindex edge function (support.sparkpost.com is now off Netlify via the CloudFront cutover) and revert the tsconfig.json netlify exclude it required noindex stays intact at all three layers (robots.txt, , X-Robots-Tag). Co-Authored-By: Claude Opus 4.8 (1M context) --- netlify.toml | 23 +++-- .../host-conditional-noindex.ts | 99 ------------------- next-sitemap.js | 2 +- tsconfig.json | 2 +- 4 files changed, 19 insertions(+), 107 deletions(-) delete mode 100644 netlify/edge-functions/host-conditional-noindex.ts diff --git a/netlify.toml b/netlify.toml index 99a583815..ce4feb657 100644 --- a/netlify.toml +++ b/netlify.toml @@ -9,11 +9,11 @@ package = "@netlify/plugin-nextjs" for = "/*" [headers.values] - # Applies to every host this deploy serves. The host-conditional-noindex - # edge function strips this header (and the noindex tag) for - # support.sparkpost.com requests during the CloudFront migration gap, so - # only archive.sparkpost.com (and deploy previews) actually receive it. - # See netlify/edge-functions/host-conditional-noindex.ts. + # Applies to every host this deploy serves (docs.sparkpost.com, the + # archive.sparkpost.com host before it 301s away, and deploy previews). + # The docs site is intentionally kept out of search so it doesn't compete + # with bird.com — belt-and-braces with the noindex (seo.tsx) and + # robots.txt Disallow: / (next-sitemap.js). X-Robots-Tag = "noindex, nofollow" Strict-Transport-Security = ''' @@ -54,7 +54,7 @@ package = "@netlify/plugin-nextjs" 'self' data: fonts.gstatic.com - archive.sparkpost.com; + docs.sparkpost.com; connect-src * data: @@ -74,6 +74,17 @@ package = "@netlify/plugin-nextjs" 'none'; ''' +# Permanently redirect the old archived host to its new home. This deploy +# serves both hostnames; matching on the full archive.sparkpost.com URL sends +# every archive request (path + query preserved via :splat) to the equivalent +# docs.sparkpost.com path. Listed first and forced so it wins over the +# path-based rules below, which then apply once the request is on docs. +[[redirects]] + from = "https://archive.sparkpost.com/*" + to = "https://docs.sparkpost.com/:splat" + status = 301 + force = true + # A redirect rule with many of the supported properties [[redirects]] from = "/" diff --git a/netlify/edge-functions/host-conditional-noindex.ts b/netlify/edge-functions/host-conditional-noindex.ts deleted file mode 100644 index b1e1a3dbc..000000000 --- a/netlify/edge-functions/host-conditional-noindex.ts +++ /dev/null @@ -1,99 +0,0 @@ -// Temporary edge function for the support.sparkpost.com → archive.sparkpost.com -// migration. Until the CloudFront redirect distribution is live (then DNS for -// support.sparkpost.com flips off Netlify), this single Netlify deploy serves -// both hostnames. The desired SEO behavior is asymmetric: -// -// archive.sparkpost.com: served as built — noindex , X-Robots-Tag, and -// Disallow: / in robots.txt all intact, so search engines drop the archive. -// support.sparkpost.com: keep search engines indexing as before, so existing -// link equity is preserved until the CloudFront layer can 301 it to bird.com. -// -// Without this function, the noindex signals (set in seo.tsx, netlify.toml, and -// next-sitemap.js) would apply to both hostnames equally — which would start -// deindexing support.sparkpost.com before the 301s exist, losing link equity -// that would otherwise transfer to bird.com via the CloudFront redirects. -// -// This function differentiates by Host header: -// - support.sparkpost.com: override X-Robots-Tag with "all", strip the noindex -// from HTML responses, and override /robots.txt with a permissive -// version. -// - archive.sparkpost.com or any other host (deploy previews, *.netlify.app): -// pass through unmodified. -// -// REMOVE THIS FUNCTION (and the netlify.toml registration) once the CloudFront -// cutover is complete — support.sparkpost.com will no longer hit Netlify, so the -// hostname-conditional logic becomes dead code. - -import type { Context } from '@netlify/edge-functions'; - -const INDEX_HOST = 'support.sparkpost.com'; - -// Match in either attribute order -// and with whitespace variations. Only the noindex/nofollow robots meta is -// targeted — any other meta tags (description, og:*, viewport, etc.) are left -// alone. -const NOINDEX_META_PATTERNS: RegExp[] = [ - /]*name=["']robots["'][^>]*content=["'][^"']*noindex[^"']*["'][^>]*\/?>/gi, - /]*content=["'][^"']*noindex[^"']*["'][^>]*name=["']robots["'][^>]*\/?>/gi, -]; - -export default async (request: Request, context: Context): Promise => { - const host = request.headers.get('host') ?? ''; - - // Only act on support.sparkpost.com. Every other host — including - // archive.sparkpost.com, *.netlify.app deploy URLs, and deploy previews — - // passes through with whatever the build emits (i.e. noindex stays on). - if (host !== INDEX_HOST) return; - - const url = new URL(request.url); - - // Override /robots.txt with a permissive version so Google can crawl - // support.sparkpost.com normally. Without this, the built robots.txt's - // `Disallow: /` would block Google from re-crawling, which would prevent it - // from seeing the 301s once CloudFront comes online. - if (url.pathname === '/robots.txt') { - return new Response('User-agent: *\n', { - status: 200, - headers: { 'Content-Type': 'text/plain; charset=utf-8' }, - }); - } - - // For everything else, fetch the response normally, then mutate it. - const response = await context.next(); - - // Override the X-Robots-Tag set in netlify.toml. `delete()` on this header is - // silently ignored — Netlify re-applies the netlify.toml [[headers]] block - // after the edge function returns, but it respects values WE set. So we - // overwrite with "all" (canonical "ignore any prior noindex; index normally"), - // which Google treats as equivalent to no header at all. - response.headers.set('X-Robots-Tag', 'all'); - - // Only HTML responses can carry the noindex tag. Skip non-HTML - // (images, JS, CSS, JSON) to avoid pointlessly buffering large bodies. - const contentType = response.headers.get('content-type') ?? ''; - if (!contentType.includes('text/html')) { - return response; - } - - let body = await response.text(); - for (const pattern of NOINDEX_META_PATTERNS) { - body = body.replace(pattern, ''); - } - - // Rebuild the response with the mutated body. content-length is dropped so - // Netlify recomputes it. content-encoding is dropped because response.text() - // already decoded any gzip/brotli the origin sent — leaving the header on - // would tell the client to decompress a now-plain-text body and fail. - const headers = new Headers(response.headers); - headers.delete('content-length'); - headers.delete('content-encoding'); - return new Response(body, { - status: response.status, - statusText: response.statusText, - headers, - }); -}; - -export const config = { - path: '/*', -}; diff --git a/next-sitemap.js b/next-sitemap.js index 73f28ed3d..491f464f3 100644 --- a/next-sitemap.js +++ b/next-sitemap.js @@ -1,5 +1,5 @@ module.exports = { - siteUrl: process.env.SITE_URL || 'https://archive.sparkpost.com', + siteUrl: process.env.SITE_URL || 'https://docs.sparkpost.com', generateRobotsTxt: true, sitemapSize: 5000, robotsTxtOptions: { diff --git a/tsconfig.json b/tsconfig.json index 7e38daddc..cce6df6b1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,5 +24,5 @@ "@context/*": ["context/*"] }, "include": ["next-env.d.ts", "yaml.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "netlify"] + "exclude": ["node_modules"] }