Implementing redirects
Let's go through best practices for implementing redirects with Next.js and Sanity.
You will create a redirect system that:
- Is configured with documents in Sanity Studio
- Can be managed by your content team
- Won't create a maintenance headache later
Let's start with your redirect schema type first. You want to make this as editor friendly as possible. The goal is to build a non-technical solution that can be managed by your content team, and output to your Next.js configuration.
Below is a simplified document schema, which you'll make much smarter with validation rules later in the lesson.
import { defineField, defineType } from "sanity";import { LinkIcon } from "@sanity/icons";
export const redirectType = defineType({ name: "redirect", title: "Redirect", type: "document", icon: LinkIcon, fields: [ defineField({ name: "source", type: "string", }), defineField({ name: "destination", type: "string", }), defineField({ name: "permanent", type: "boolean", initialValue: true, }), defineField({ name: "isEnabled", description: "Toggle this redirect on or off", type: "boolean", initialValue: true, }), ],});
Don't forget to register it to your Studio schema types
// ...all other importsimport { redirectType } from "./redirectType";
export const schema: { types: SchemaTypeDefinition[] } = { types: [ // ...all other schema types redirectType, ],};
The redirect documents created in Sanity Studio will need to be queried into our Next.js config file.
queries.ts
to include a GROQ query for redirect documents// ...all other queries
export const REDIRECTS_QUERY = defineQuery(` *[_type == "redirect" && isEnabled == true] { source, destination, permanent }`);
import { client } from "./client";import { REDIRECTS_QUERY } from "./queries";
export async function fetchRedirects() { return client.fetch(REDIRECTS_QUERY);}
Since you've added schema types and a new query to the application, don't forget to generate Types.
npm run typegen
- Vercel has a limit of 1,024 redirects in Next.js config.
- For large numbers of redirects (1000+), use a custom middleware solution instead. See Vercel's documentation on managing redirects at scale for more details.
Now we can use Next.js's built-in redirects configuration in next.config.ts
. This allows us to define redirects that will be applied at build time. Note that redirects defined in next.config.ts
run before any middleware, should you use it in the future.
next.config.ts
file to include redirects// ...other importsimport { fetchRedirects } from "@/sanity/lib/fetchRedirects";
const nextConfig: NextConfig = { // ...other config async redirects() { return await fetchRedirects(); },};
export default nextConfig;
Validation is critical as invalid redirects can break future builds. Without validation, authors could publish a redirect that prevents your application from deploying—or create a deployment with circular redirects.
- Source paths must start with
/
- Never create circular redirects like
A
->B
->A
Here's the validation logic, yes it's a bit complex but it's worth it to avoid hours of debugging, when your build breaks because of a missing slash.
source
field in the redirectType
schemaimport { defineField, defineType, SanityDocumentLike } from "sanity";import { LinkIcon } from "@sanity/icons";
function isValidInternalPath(value: string | undefined) { if (!value) { return "Value is required"; } else if (!value.startsWith("/")) { return "Internal paths must start with /"; } else if (/[^a-zA-Z0-9\-_/:]/.test(value)) { return "Source path contains invalid characters"; } else if (/:[^/]+:/.test(value)) { return "Parameters can only contain one : directly after /"; } else if ( value.split("/").some((part) => part.includes(":") && !part.startsWith(":")) ) { return "The : character can only appear directly after /"; } return true;}
function isValidUrl(value: string | undefined) { try { new URL(value || ""); return true; } catch { return "Invalid URL"; }}
export const redirectType = defineType({ name: "redirect", title: "Redirect", type: "document", icon: LinkIcon, validation: (Rule) => Rule.custom((doc: SanityDocumentLike | undefined) => { if (doc && doc.source === doc.destination) { return ["source", "destination"].map((field) => ({ message: "Source and destination cannot be the same", path: [field], })); }
return true; }), fields: [ defineField({ name: "source", type: "string", validation: (Rule) => Rule.required().custom(isValidInternalPath), }), defineField({ name: "destination", type: "string", validation: (Rule) => Rule.required().custom((value: string | undefined) => { const urlValidation = isValidUrl(value); const pathValidation = isValidInternalPath(value);
if (urlValidation === true || pathValidation === true) { return true; } return typeof urlValidation === "boolean" ? urlValidation : pathValidation; }), }), defineField({ name: "permanent", description: "Should the redirect be permanent (301) or temporary (302)", type: "boolean", initialValue: true, }), defineField({ name: "isEnabled", description: "Toggle this redirect on or off", type: "boolean", initialValue: true, }), ],});
The additional validation logic now thoroughly checks:
- If the
source
is a valid internal path - If the
destination
is a valid URL, or valid internal path - If the
source
anddestination
values are different
- Keep an eye on redirect chains, they can cause "too many redirects" errors
- Clean up old redirects periodically
- Consider logging redirects if you need to track usage
- Adjust the cache duration based on how often you update redirects
Next up, you'll learn how to generate Open Graph images using Tailwind CSS and Vercel edge functions.