How to Manage Redirects in Astro with Sanity

March 12, 2026 · 8 min read

You know that sinking feeling when your marketing team urgently needs a redirect set up right now, and you're knee-deep in a critical deployment? Or when a simple URL change requires a pull request, code review, and a full CI/CD pipeline run just to update a _redirects file?
Yeah, we've all been there.
Here's the thing: redirects are simultaneously crucial for SEO and user experience, yet they're often treated as an afterthought in our content operations. A broken redirect can tank your search rankings. A missing one sends users to a 404 page. But requiring engineering intervention for every redirect change creates a bottleneck that slows everyone down.
This tutorial walks you through building a self-service redirect management system using Sanity CMS and Astro that empowers your content team to manage redirects without bothering engineering. We'll use Cloudflare Workers for deployment, create a custom Astro integration, and set up proper validation to prevent redirect chaos.
What you'll build: A production-ready redirect system where content editors can create, update, and manage redirects through Sanity Studio, with automatic deployment to your static Astro site.
Why This Architecture Matters
Before we dive into code, let's address the elephant in the room: why go through this effort when you could just manually manage a _redirects file?
Traditional approach problems:
Every redirect change requires a developer
No historical tracking or audit trail
Risk of syntax errors in manual files
No validation against duplicate sources
Content team dependency on engineering
Our Sanity + Astro approach benefits:
Content team autonomy for redirect management
Built-in validation and duplicate prevention
Version history through Sanity's revision system
No deploy pipeline required for redirect updates*
SEO-safe with proper status code handling
*Okay, technically you still need a rebuild, but you can automate that with webhooks. We'll get there.
Understanding the Architecture
Our redirect system has three main components:
Sanity Schema – Defines the redirect data structure with validation
Astro Integration – Fetches redirects during build and injects them into Astro's config
_redirectsFile – Generated automatically by Astro
Here's the flow: Content editors create redirects in Sanity Studio → Your Astro build process queries Sanity → The integration fetches redirects and updates Astro's config → Astro generates a _redirects file → Cloudflare Workers applies the redirects.
Simple, elegant, and completely hands-off for your engineering team once it's set up.

Important Context: Static vs SSR
This tutorial focuses on fully static Astro sites. If you're using server-side rendering (SSR), you'll need to implement a middleware layer to handle redirects at request time rather than build time. The Sanity schema and query logic remain the same, but the application layer differs. We're keeping this tutorial focused on the static use case, which covers most content-heavy marketing sites.
Choosing Your Schema Strategy
Sanity offers flexibility in how you model your redirects. You have two main options:
Option 1: Singleton Document with Array
Perfect for sites with fewer than 100-200 redirects. Easy to manage, quick to query, and simple to navigate in Sanity Studio.
Pros:
Single document to manage
Faster queries (one document fetch)
Easier for content teams to find
Less clutter in Studio
Cons:
Can become unwieldy with many redirects
All redirects load at once (performance consideration at scale)
Option 2: Individual Redirect Documents
Create each redirect as its own document type. Better for larger sites or when you need advanced features like scheduled redirects or redirect categories.
Pros:
Scales to thousands of redirects
Better for complex filtering/search
Can add metadata per redirect easily
More granular permissions possible
Cons:
More Studio clutter
Slightly more complex queries
For this tutorial, we're using Option 1 (singleton with array) because it's the sweet spot for most use cases. If you're managing a massive site with 200+ redirects, consider migrating to Option 2.
Step 1: Setting Up Your Sanity Redirect Schema
Let's build a robust redirect schema with proper validation. This is where the magic happen, we prevent content editors from shooting themselves in the foot with duplicate sources or invalid paths.
Here's the complete schema:
import { defineField, defineType } from "sanity";
import { LinkIcon } from "@sanity/icons";
type RedirectTypes = {
_key: string;
source: string;
destination: string;
isPermanent: boolean;
};
export const redirects = defineType({
name: "redirects",
type: "document",
title: "Redirects",
icon: LinkIcon,
fields: [
defineField({
name: "redirects",
type: "array",
description:
"Redirects are used to redirect users to a different page. This is useful for SEO purposes. Remember about good practices for redirects as they can affect SEO.",
of: [
defineField({
name: "redirect",
type: "object",
fields: [
defineField({
name: "source",
type: "string",
description: "Internal path to redirect from (e.g. /old-page)",
validation: (Rule) => [
Rule.required(),
Rule.custom((value, context) => {
if (!value) return "Source is required";
if (!value.startsWith("/"))
return "Source must be an internal path starting with /";
const redirects = (context.document?.redirects || []) as RedirectTypes[];
const currentRedirect = context.parent as RedirectTypes;
const isDuplicate = redirects.some(
(redirect) =>
redirect._key !== currentRedirect._key && redirect.source === value,
);
if (isDuplicate)
return "This source path is already used in another redirect. Source paths must be unique.";
return true;
}),
],
}),
defineField({
name: "destination",
type: "string",
description:
"Internal path (e.g. /new-page) or external URL (e.g. https://example.com)",
validation: (Rule) =>
Rule.required().custom((value) => {
if (!value) return "Destination is required";
const isInternal = value.startsWith("/");
const isExternal = value.startsWith("http://") || value.startsWith("https://");
if (!isInternal && !isExternal)
return "Destination must be an internal path starting with / or a full external URL";
return true;
}),
}),
defineField({
name: "isPermanent",
type: "boolean",
initialValue: true,
}),
],
preview: {
select: {
source: "source",
destination: "destination",
isPermanent: "isPermanent",
},
prepare({ source, destination, isPermanent }) {
return {
title: `Source: ${source}`,
subtitle: `Destination: ${destination}`,
media: isPermanent ? () => "🔒" : () => "🔄",
};
},
},
}),
],
}),
],
preview: {
prepare: () => ({
title: "Redirects",
}),
},
});Schema Deep Dive: What Makes This Robust
Let's break down the smart parts:
Duplicate Prevention:
const isDuplicate = redirects.some(
(redirect) =>
redirect._key !== currentRedirect._key && redirect.source === value,
);This custom validation checks if any other redirect already uses the same source path. It excludes the current item being edited (_key comparison) to allow updates without false positives.
Path Validation:
The schema enforces that sources must start with / (internal paths only) while destinations can be either internal (/) or external (http:// or https://). This prevents common mistakes like forgetting the leading slash.
Visual Feedback:
media: isPermanent ? () => "🔒" : () => "🔄",
The preview uses emoji to instantly communicate redirect type: 🔒 for permanent (301) and 🔄 for temporary (302). Small touch, huge UX improvement.
SEO Guidance:
The field description reminds editors about SEO implications. Never underestimate the power of good documentation in your schema.
Step 2: Creating the GROQ Query
Sanity uses GROQ to fetch data. Our query is delightfully simple:
export const REDIRECTS = defineQuery(`
*[_type == "redirects"][0] {
redirects[] {
source,
destination,
isPermanent
}
}
`);What's happening here:
*[_type == "redirects"]– Get all documents of type "redirects"[0]– Take only the first one (singleton pattern)redirects[]– Project the redirects array fieldIndividual field selection keeps payload minimal
Store this in your queries file (typically src/queries/globals.ts or similar).
Step 3: Building the Astro Integration
This is where it gets fun. We're creating a custom Astro integration that runs during the build process, fetches redirects from Sanity, and injects them into Astro's configuration.
Create a new file at src/integrations/redirects.ts:
import type { AstroIntegration } from "astro";
import { createClient } from "@sanity/client";
import { loadEnv } from "vite";
import { REDIRECTS as REDIRECTS_QUERY } from "../queries/globals";
type RedirectEntry = {
source: string;
destination: string;
isPermanent: boolean;
};
export function sanityRedirectsPlugin(): AstroIntegration {
return {
name: "sanity-redirects",
hooks: {
"astro:config:setup": async ({ updateConfig, logger }) => {
logger.info("Fetching redirects from Sanity...");
// loadEnv reads .env files the same way Vite/Astro does at build time
const env = loadEnv(process.env.NODE_ENV ?? "production", process.cwd(), "");
const projectId = env.PUBLIC_SANITY_PROJECT_ID || "sanity-project-id";
const dataset = env.PUBLIC_SANITY_DATASET || "production";
const client = createClient({
projectId,
dataset,
apiVersion: "2025-11-15",
useCdn: true,
});
let entries: RedirectEntry[] = [];
try {
const result = await client.fetch<{ redirects: RedirectEntry[] } | null>(REDIRECTS_QUERY);
entries = result?.redirects ?? [];
} catch (err) {
logger.warn(`Could not fetch redirects from Sanity: ${err}`);
}
logger.info(`Found ${entries.length} redirect(s).`);
const redirects = Object.fromEntries(
entries.map((r) => [
r.source,
{
status: (r.isPermanent ? 301 : 302) as 301 | 302,
destination: r.destination,
},
]),
);
updateConfig({ redirects });
},
},
};
}Integration Breakdown
Environment Handling:
const env = loadEnv(process.env.NODE_ENV ?? "production", process.cwd(), "");
We use Vite's loadEnv to load environment variables exactly how Astro does. This ensures consistency between local development and your build environment.
Graceful Failure:
try {
const result = await client.fetch<{ redirects: RedirectEntry[] } | null>(REDIRECTS_QUERY);
entries = result?.redirects ?? [];
} catch (err) {
logger.warn(`Could not fetch redirects from Sanity: ${err}`);
}If Sanity is unreachable during build, we log a warning and continue with zero redirects rather than crashing the entire build. This prevents Sanity outages from taking down your deployments.
Status Code Mapping:
status: (r.isPermanent ? 301 : 302) as 301 | 302,
Permanent redirects get 301 status (tells search engines to update their index), temporary ones get 302 (tells search engines to keep checking the original URL). This is SEO-critical.
Astro Config Injection:
updateConfig({ redirects });
The updateConfig hook merges our redirects into Astro's configuration, which then generates the appropriate _redirects file for Cloudflare Workers.
Step 4: Wiring It Into Astro Config
The final piece is registering your integration in astro.config.mjs:
import { sanityRedirectsPlugin } from "./src/integrations/redirects";
export default defineConfig({
// ...rest of your config
integrations: [
sanityRedirectsPlugin(),
// ...other integrations
],
});That's it! During every build, Astro will:
Run your integration's setup hook
Fetch redirects from Sanity
Inject them into the config
Generate a
_redirectsfile in your build output
Cloudflare Workers automatically picks up this file and applies your redirects.

Deployment Workflow
Here's the typical workflow once this is implemented:
Content editor creates/updates redirects in Sanity Studio
Trigger a rebuild (manual or via webhook)
Astro build runs → integration fetches redirects
Deploy to Cloudflare Workers →
_redirectsfile is appliedRedirects are live 🎉
Automating Rebuilds with Webhooks
Want to make this truly hands-off? Set up a Sanity webhook that triggers your CI/CD pipeline when the redirects document changes:
In Sanity Studio, go to "Manage" → "API" → "Webhooks"
Create a new webhook targeting your build trigger URL
Filter by
_type == "redirects"Now redirects auto-deploy on save!
Most hosting platforms (Vercel, Netlify, Cloudflare Pages) provide build hook URLs for exactly this purpose.
Extending the System
Want to level up? Consider adding:
Scheduled Redirects:
Add publishedAt and expiresAt fields to automatically enable/disable redirects based on date.
Redirect Categories:
Tag redirects by migration batch, campaign, or reason to make them easier to manage.
Bulk Import:
Accept CSV uploads for mass redirect creation during migrations.
Redirect Analytics:
Log redirect hits to understand which old URLs still receive traffic.
Redirect Testing:
Add a "Test" button in Sanity that checks if the destination URL returns a valid response.
Conclusion
Building a self-service redirect system isn't just about reducing engineering toil, though that's a nice bonus. It's about creating sustainable content operations where your team can move fast without breaking things.
With Sanity's flexible schema, Astro's powerful integration system, and Cloudflare's edge performance, you've built a redirect management system that:
Empowers content teams with autonomy
Prevents common redirect mistakes through validation
Scales to thousands of redirects
Maintains SEO best practices
Requires zero ongoing engineering maintenance
The initial setup might seem like overkill for managing a few redirects. But once you've got 50+ redirects and marketing is updating them weekly, you'll be glad you invested the time.

Build your Astro site for SEO from day one
Every Astro site we build at Lucky Media ships with clean structured data, full Core Web Vitals optimization, automatic sitemaps, and the headless CMS setup that gives your marketing team full content control. We are an Official Astro Partner.
Lucky Media is proud to be recognized as a leading Astro development agency.
FAQs
How do I handle redirects for SSR (Server-Side Rendering) Astro sites?
For SSR sites, you'll need to create Astro middleware instead of relying on the static _redirects file approach.
Important caveat: This queries Sanity on every request, so you'll want to add caching. Consider using Astro's built-in caching or a Redis layer to cache the redirects document for 5-10 minutes. Without caching, you risk hitting Sanity's rate limits and adding latency to every page load.
Can I track which redirects are actually being used?
Absolutely! Cloudflare provides analytics on redirect hits through their dashboard, but if you want more granular tracking, add a simple logging mechanism. For static sites, you can't log server-side, but you can add a client-side tracker:
Create a small API endpoint (Cloudflare Worker or serverless function) that logs redirect sources, destinations, and timestamps. Then modify your Astro integration to generate redirects that first hit your logging endpoint with a query parameter, which then redirects to the final destination.
Alternatively, use Cloudflare Workers' built-in analytics, it's simpler and doesn't require custom infrastructure.
Navigate to your Cloudflare dashboard → Workers → Analytics to see redirect hits, response times, and error rates.
What happens if I have duplicate source paths across different redirect entries?
Our schema validation prevents this! The custom validation rule checks all existing redirects and rejects any duplicate source paths.
However, this validation only runs in Sanity Studio's UI. If someone bypasses Studio (API access, migrations, etc.), you could theoretically create duplicates. As a safety net, consider adding a migration script that periodically checks for and removes duplicate sources, keeping only the most recently created one.
Should I use a singleton document with an array or individual redirect documents?
Use singleton with array (tutorial approach) if:
You have fewer than 200 redirects
Redirects are managed by a small team
You want simple, fast queries
You prioritize ease of use in Sanity Studio
Use individual redirect documents if:
You have 200+ redirects (or expect to grow there)
You need granular permissions (different teams manage different redirects)
You want to add complex metadata (created by, category, notes, approval workflow)
You need advanced filtering/searching in Studio
You're building a multi-tenant system where redirects belong to different sites
The singleton approach is simpler and faster for most use cases. If you outgrow it, migrating to individual documents is straightforward, just create a new schema type and write a migration script similar to the _redirects file import example above. Start simple, scale when needed.
Technologies

Stay up-to-date
Be updated with all news, products and tips we share!
On this page
- Why This Architecture Matters
- Understanding the Architecture
- Important Context: Static vs SSR
- Choosing Your Schema Strategy
- Step 1: Setting Up Your Sanity Redirect Schema
- Step 2: Creating the GROQ Query
- Step 3: Building the Astro Integration
- Step 4: Wiring It Into Astro Config
- Deployment Workflow
- Extending the System
- Conclusion
- Build your Astro site for SEO from day one
- FAQs

