platforms
platforms copied to clipboard
how to invalidate all a pages of a [domain] route
I'm currently building an application that is based on this repo with two applications in a monorepo where users can create sites on a separate dashboard app.
When a user updates e.g. his theme data I need to purge the cache for all pages of his subdomain or domain. My current implementation looks like this
I use radix themes as a base color provider for tailwind. Radix Ui uses a Theme Provider that adds html attributes to a div that wrapps the whole application that looks like this:
<div
data-is-root-theme="true"
data-accent-color="green"
data-gray-color="slate"
data-has-background="false"
data-panel-background="solid"
data-radius="medium"
data-scaling="100%"
class="radix-themes"
/>
Here's my layout that feeds the values from the database into the Theme provider:
import type { ReactNode } from "react";
import type { Metadata } from "next";
import { notFound, redirect } from "next/navigation";
import { Theme } from "@radix-ui/themes";
import { getPublicSiteData } from "@acme/api/src/fetchers/site";
import { CompanyThemeSchema } from "@acme/api/src/schemas/custom/theme";
import { prisma } from "@acme/db/src/db";
import { Navbar } from "~/components/nav/navbar";
import { Footer } from "~/components/sections/Footer";
/////////////////////////////////////////////////
// GENERATE STATIC PARAMS
/////////////////////////////////////////////////
export async function generateStaticParams() {
const allSites = await prisma.site.findMany({
select: {
subdomain: true,
customDomain: true,
},
});
const allPaths = allSites
.flatMap(({ subdomain, customDomain }) => [
subdomain && {
domain: `${subdomain}.${process.env.NEXT_RFK_ONLINE_ROOT_DOMAIN}`,
},
customDomain && {
domain: customDomain,
},
])
.filter((value): value is RootParamsType =>
Boolean((value as RootParamsType)?.domain),
);
return allPaths;
}
export interface RootParamsType {
domain: string;
}
/////////////////////////////////////////////////
// GENERATE METADATA
/////////////////////////////////////////////////
export async function generateMetadata({
params,
}: {
params: RootParamsType;
}): Promise<Metadata | null> {
const domain = decodeURIComponent(params.domain);
const { data } = await getPublicSiteData(domain);
if (!data) {
return null;
}
const {
name: title,
description,
image,
logo,
} = data as {
name: string;
description: string;
image: string;
logo: string;
};
return {
title,
description,
openGraph: {
title,
description,
images: [image],
},
twitter: {
card: "summary_large_image",
title,
description,
images: [image],
creator: "@vercel",
},
icons: [logo],
metadataBase: new URL(`https://${domain}`),
// Optional: Set canonical URL to custom domain if it exists
// ...(params.domain.endsWith(`.${process.env.NEXT_RFK_ONLINE_ROOT_DOMAIN}`) &&
// data.customDomain && {
// alternates: {
// canonical: `https://${data.customDomain}`,
// },
// }),
};
}
/////////////////////////////////////////////////
// LAYOUT
/////////////////////////////////////////////////
export default async function SiteLayout({
params,
children,
}: {
params: RootParamsType;
children: ReactNode;
}) {
const domain = decodeURIComponent(params.domain);
const { data, error } = await getPublicSiteData(domain);
if (!data || error) {
notFound();
}
// Optional: Redirect to custom domain if it exists
if (
domain.endsWith(`.${process.env.NEXT_RFK_ONLINE_ROOT_DOMAIN}`) &&
data.customDomain &&
process.env.REDIRECT_TO_CUSTOM_DOMAIN_IF_EXISTS === "true"
) {
return redirect(`https://${data.customDomain}`);
}
const theme = CompanyThemeSchema.parse(data.theme);
return (
<Theme {...theme}>
<Navbar />
<main className="min-h-screen">{children}</main>
<Footer
email={data.company?.email}
phoneNumber={data.company?.phoneNumber}
mobilePhone={data.company?.mobilePhone}
/>
</Theme>
);
}
The fetcher looks like this:
///////////////////////////////////////////////////////
// GET PUBLIC SITE DATA
///////////////////////////////////////////////////////
export async function getPublicSiteData(domain: string) {
return await unstable_cache(
async () => {
return serverApi(null, async (api) => api.site.findPublicSite(domain));
},
[`${domain}-metadata`],
{ tags: [`${domain}-metadata`] },
)();
}
export type GetSiteDataReturnType = RouterOutputs["site"]["findPublicSite"];
Which calls a trpc endpoint server side
export const siteRouter = createTRPCRouter({
findPublicSite: publicProcedure
.input(z.string())
.query(async ({ input: domain, ctx }) => {
const subdomain = extractSubdomain(domain);
const site = await ctx.prisma.site.findUnique({
where: subdomain ? { subdomain } : { customDomain: domain },
include: {
company: {
include: {
city: {
include: {
district: {
include: {
state: {
include: {
country: true,
},
},
},
},
},
},
},
},
theme: true,
},
});
return site;
}),
})
the revalidate site route handler looks like this:
import { revalidatePath, revalidateTag } from "next/cache";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { z } from "zod";
///////////////////////////////////////////
// TYPES
///////////////////////////////////////////
export type RevalidateThemeInputInputType = z.infer<
typeof RevalidateThemeInputSchema
>;
export type RevalidateThemeRequestType = z.infer<
typeof RevalidateThemeRequestSchema
>;
///////////////////////////////////////////
// SCHEMAS
///////////////////////////////////////////
export const RevalidateThemeInputSchema = z.object({
domain: z.string(),
});
export const RevalidateThemeRequestSchema = RevalidateThemeInputSchema.extend({
secret: z.literal(process.env.REVALIDATION_SECRET),
});
const PATHS_TO_REVALIDATE = [
"/",
"/wissenswertes",
"/wissenswertes/[slug]",
"/kontakt",
"/impressum",
"/cookies",
"/datenschutz",
"/beruf",
];
//////////////////////////////////////////////
// ROUTE HANDLERS
//////////////////////////////////////////////
export async function POST(req: NextRequest) {
const body = (await req.json()) as unknown;
const parsedRequest = RevalidateThemeRequestSchema.safeParse(body);
if (!parsedRequest.success) {
console.error(parsedRequest.error);
return NextResponse.json({
status: 400,
body: {
message: "Invalid request body",
errors: parsedRequest.error,
},
});
}
const { domain } = parsedRequest.data;
// Revalidate metadata so the site data is re-fetched
const tag = `${domain}-metadata`;
revalidateTag(tag);
console.log("Revalidating tags: ", tag);
// Revalidate all paths so each page gets the new theme data
PATHS_TO_REVALIDATE.forEach((path) => {
console.log("Revalidating paths: ", `${domain}${path}`);
revalidatePath(`/${domain}${path}`, "layout");
revalidatePath(`/${domain}${path}`, "page");
});
return NextResponse.json({
status: 200,
body: {
message: "OK",
},
});
}
which is called with this fetcher in every endpoint that changes theme data:
//////////////////////////////////////////////
// FETCHER
//////////////////////////////////////////////
export async function revalidateTheme(data: RevalidateThemeInputInputType) {
await fetch(
`${process.env.NODE_ENV === "development" ? "http" : "https"}://${
process.env.NEXT_RFK_ONLINE_ROOT_DOMAIN
}/api/webhooks/revalidateTheme`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...data,
secret: process.env.REVALIDATION_SECRET,
}),
},
);
}
Everything is working so far except that the theme data is not updated on every page of the given domain when revalidation is triggered.
For example on https://mydomain
the theme data looks like this after reloading the page:
<div
data-accent-color="green"
...
/>
and on other pages like https://mydomain/subpage
the theme data still has the old color:
<div
data-accent-color="tomato"
...
/>
So how would I go about this?
The main page is updated correctly but all other paths keep having the old theme data. I assume this si because the theme is added in the root layout and therefore only the page in the same directory is purged from the cache and rebuilt with the new site data.
It also seems that the revalidatePath(
/${domain}${path}, "layout");
is not doing anything. I assume this is because I use an absolute url paht like my-subdomain.at-my-domain.at/mypath
and not the file system path as mentioned in the docs.
I also tried using the dynamic segment like [domain]/mypath
but also no luck.
Any Ideas how this could be fixed so the theme is updated on all subpages as well?