Skip to content

Commit

Permalink
feat: Sanity-controlled redirects with Next.js middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
mathiazom committed Sep 2, 2024
1 parent d04aeba commit b140f11
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 0 deletions.
32 changes: 32 additions & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { NextRequest, NextResponse } from "next/server";
import { client } from "../studio/lib/client";
import { RedirectSparsePage } from '../studio/lib/payloads/redirect';

export async function middleware(request: NextRequest) {
const slug = request.nextUrl.pathname;
const redirect = await client.fetch<RedirectSparsePage | null>(
`*[_type == "redirect" && source.current == "${slug}"][0]{
"destination":destination.current,
permanent
}`
);
if (redirect !== null) {
return NextResponse.redirect(
new URL(redirect.destination, request.url),
redirect.permanent ? 308 : 307,
);
}
}

export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
*/
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
],
}
5 changes: 5 additions & 0 deletions studio/components/RedirectThumbnail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const RedirectThumbnail = ({ permanent }: { permanent: boolean }) => {
return <span style={{ fontSize: "1.5rem" }}>{permanent ? "⮕" : "⇢"}</span>;
};

export default RedirectThumbnail;
8 changes: 8 additions & 0 deletions studio/lib/payloads/redirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface RedirectPage extends RedirectSparsePage {
source: string;
}

export interface RedirectSparsePage {
destination: string;
permanent: boolean;
}
2 changes: 2 additions & 0 deletions studio/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import office from "./schemas/documents/office";
import compensations from "./schemas/documents/compensations";
import salaryAndBenefits from "./schemas/documents/salaryAndBenefits";
import siteSettings from "./schemas/documents/siteSettings";
import { redirect } from "./schemas/documents/redirect";

export const schema: { types: SchemaTypeDefinition[] } = {
types: [
Expand All @@ -36,5 +37,6 @@ export const schema: { types: SchemaTypeDefinition[] } = {
salaryAndBenefits,
benefit,
office,
redirect,
],
};
6 changes: 6 additions & 0 deletions studio/schemas/deskStructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import {
StackCompactIcon,
HeartIcon,
CaseIcon,
DoubleChevronRightIcon,
} from "@sanity/icons";
import { soMeLinksID } from "./documents/socialMediaProfiles";
import { companyInfoID } from "./documents/companyInfo";
import { postId } from "./documents/post";
import { legalDocumentID } from "./documents/legalDocuments";
import { compensationsId } from "./documents/compensations";
import { redirectId } from "./documents/redirect";

export default (S: StructureBuilder) =>
S.list()
Expand Down Expand Up @@ -75,4 +77,8 @@ export default (S: StructureBuilder) =>
),
]),
),
S.listItem()
.title("Redirects")
.icon(DoubleChevronRightIcon)
.child(S.documentTypeList(redirectId).title("Redirects")),
]);
80 changes: 80 additions & 0 deletions studio/schemas/documents/redirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { defineField, defineType, type Slug } from "sanity";
import { SlugRule } from "@sanity/types";
import RedirectThumbnail from "../../components/RedirectThumbnail";

const slugValidator = (rule: SlugRule) =>
rule.required().custom((value: Slug | undefined) => {
if (!value || !value.current) return "Can't be blank";
if (!value.current.startsWith("/")) {
return "The path must start with a forward slash ('/')";
}
return true;
});

export const redirectId = "redirect";

export const redirect = defineType({
name: redirectId,
title: "Redirect",
type: "document",
readOnly: (ctx) => {
/*
make permanent redirects read-only after initial publish
this is a soft guardrail that is possible to bypass
*/
return (
(ctx.document?.permanent ?? false) &&
!ctx.document?._id.startsWith("drafts.")
);
},
fields: [
defineField({
name: "source",
description: "Which url should this redirect apply for",
type: "slug",
validation: slugValidator,
}),
defineField({
name: "destination",
description: "Where should the user be redirected?",
type: "slug",
validation: slugValidator,
options: {
isUnique: () => {
/*
does not need to be unique since multiple source paths
can point to the same destination path
*/
return true;
},
},
}),
defineField({
name: "permanent",
description:
"Will this redirect exist throughout the foreseeable future?",
type: "boolean",
}),
],
initialValue: {
permanent: false,
},
preview: {
select: {
source: "source",
destination: "destination",
permanent: "permanent",
},
prepare({ source, destination, permanent }) {
const title =
source && destination
? `${source.current}${destination.current}`
: undefined;
return {
title,
subtitle: permanent ? "Permanent" : "Temporary",
media: RedirectThumbnail({ permanent }),
};
},
},
});

0 comments on commit b140f11

Please sign in to comment.