Precompute
So far we relied on server side rendering for our feature flags. But we can actually do better.
Manual approach
We could just create app/home-a/page.tsx
and app/home-b/page.tsx
and use Routing Middleware to determine which one to render.
Do not make these changes, this is just for educational purposes.
import { NextResponse, type NextRequest } from 'next/server';
import { homeFlag } from './flags.ts';
export const config = { matcher: ['/'] };
export async function middleware(request: NextRequest) {
const home = await homeFlag();
// Determine which version to show based on the feature flag
const version = home ? '/home-b' : '/home-a';
// Rewrite the request to the appropriate version
const nextUrl = new URL(version, request.url);
return NextResponse.rewrite(nextUrl);
}
The manual approach breaks down when you want to:
- combine multiple feature flags on a single, static page
- use a single feature flag across multiple pages
Precomputing
Make these changes, we're back to editing.
Export an array of flags we want to precompute.
import { flag, dedupe } from "flags/next";
type Entities = {
browser: "Safari" | "Chrome";
};
const identify = dedupe(({ headers, cookies }): Entities => {
const ua = headers.get("user-agent") || "";
const isSafari = ua.includes("Safari") && !ua.includes("Chrome");
return { browser: isSafari ? "Safari" : "Chrome" };
});
export const enableSummerSale = flag<boolean, Entities>({
key: "summer-sale",
description: "Shows a bright yellow banner for a 20% discount",
defaultValue: false,
identify,
decide({ entities }) {
return entities?.browser !== "Safari";
},
});
export const productFlags = [
enableSummerSale
] as const;
Precompute flags in middleware
import { type NextRequest, NextResponse } from "next/server";
import { precompute } from "flags/next";
import { productFlags } from "@/flags";
export const config = { matcher: "/" };
export async function middleware(request: NextRequest) {
const code = await precompute(productFlags);
// rewrites the request to the variant for this flag combination
const nextUrl = new URL(
`/${code}${request.nextUrl.pathname}${request.nextUrl.search}`,
request.url
);
return NextResponse.rewrite(nextUrl, { request });
}
Rename the app/(main)
folder to app/[code]
and update all imports.
This dynamic route segment called [code]
will hold the precomputed code.
Edit the page to access the precomputed code.
import { ImageGallery } from "@/components/image-gallery";
import { ProductDetails } from "@/components/product-detail-page/product-details";
import { ProductHeader } from "@/components/product-detail-page/product-header";
import { AddToCart } from "@/app/[code]/add-to-cart";
import { ColorPicker } from "@/components/product-detail-page/color-picker";
import { SizePicker } from "@/components/product-detail-page/size-picker";
import { ProductDetailPageProvider } from "@/components/utils/product-detail-page-context";
import { Main } from "@/components/main";
import { enableSummerSale, productFlags } from "@/flags";
import { SummerSaleBanner } from "@/components/banners/summer-sale-banner";
export default async function Page(props: {
params: Promise<{ code: string }>;
}) {
const { code } = await props.params;
const summerBanner = await enableSummerSale(code, productFlags);
return (
<ProductDetailPageProvider>
{summerBanner ? <SummerSaleBanner /> : null}
<Main>
<div className="lg:grid lg:auto-rows-min lg:grid-cols-12 lg:gap-x-8">
<ProductHeader />
<ImageGallery />
<div className="mt-8 lg:col-span-5">
<ColorPicker />
<SizePicker />
<AddToCart />
<ProductDetails />
</div>
</div>
</Main>
</ProductDetailPageProvider>
);
}
We can now opt into Incremental Static Regeneration (ISR) for this page.
This means each variant will only ever be rendered on demand at request time once and can then stay cached in the CDN.
import { ImageGallery } from "@/components/image-gallery";
import { ProductDetails } from "@/components/product-detail-page/product-details";
import { ProductHeader } from "@/components/product-detail-page/product-header";
import { AddToCart } from "@/app/[code]/add-to-cart";
import { ColorPicker } from "@/components/product-detail-page/color-picker";
import { SizePicker } from "@/components/product-detail-page/size-picker";
import { ProductDetailPageProvider } from "@/components/utils/product-detail-page-context";
import { Main } from "@/components/main";
import { enableSummerSale, productFlags } from "@/flags";
import { SummerSaleBanner } from "@/components/banners/summer-sale-banner";
export async function generateStaticParams() {
// returning an empty array is enough to enable ISR
return [];
}
export default async function Page(props: {
params: Promise<{ code: string }>;
}) {
const { code } = await props.params;
const summerBanner = await enableSummerSale(code, productFlags);
return (
<ProductDetailPageProvider>
{summerBanner ? <SummerSaleBanner /> : null}
<Main>
<div className="lg:grid lg:auto-rows-min lg:grid-cols-12 lg:gap-x-8">
<ProductHeader />
<ImageGallery />
<div className="mt-8 lg:col-span-5">
<ColorPicker />
<SizePicker />
<AddToCart />
<ProductDetails />
</div>
</div>
</Main>
</ProductDetailPageProvider>
);
}
We can futher optimize this by opting into build time generation of pages.
import { ImageGallery } from "@/components/image-gallery";
import { ProductDetails } from "@/components/product-detail-page/product-details";
import { ProductHeader } from "@/components/product-detail-page/product-header";
import { AddToCart } from "@/app/[code]/add-to-cart";
import { ColorPicker } from "@/components/product-detail-page/color-picker";
import { SizePicker } from "@/components/product-detail-page/size-picker";
import { ProductDetailPageProvider } from "@/components/utils/product-detail-page-context";
import { Main } from "@/components/main";
import { enableSummerSale, productFlags } from "@/flags";
import { SummerSaleBanner } from "@/components/banners/summer-sale-banner";
import { generatePermutations } from 'flags/next';
export async function generateStaticParams() {
const codes = await generatePermutations(productFlags);
return codes.map((code) => ({ code }));
}
export default async function Page(props: {
params: Promise<{ code: string }>;
}) {
const { code } = await props.params;
const summerBanner = await enableSummerSale(code, productFlags);
return (
<ProductDetailPageProvider>
{summerBanner ? <SummerSaleBanner /> : null}
<Main>
<div className="lg:grid lg:auto-rows-min lg:grid-cols-12 lg:gap-x-8">
<ProductHeader />
<ImageGallery />
<div className="mt-8 lg:col-span-5">
<ColorPicker />
<SizePicker />
<AddToCart />
<ProductDetails />
</div>
</div>
</Main>
</ProductDetailPageProvider>
);
}