diff --git a/apps/docs/content/docs/api-reference/frameworks/next.mdx b/apps/docs/content/docs/api-reference/frameworks/next.mdx index 714a533d..f4be8ce8 100644 --- a/apps/docs/content/docs/api-reference/frameworks/next.mdx +++ b/apps/docs/content/docs/api-reference/frameworks/next.mdx @@ -213,7 +213,7 @@ const sale = await showSummerSale(code, precomputeFlags); Example usage in `generateStaticParams`: -```ts title="app/[code]/page.tsx#next" +```ts title="app/precomputed/[code]/page.tsx#next" import { generatePermutations as generatePermutations } from 'flags/next'; export async function generateStaticParams() { diff --git a/apps/docs/content/docs/frameworks/next/precompute.mdx b/apps/docs/content/docs/frameworks/next/precompute.mdx index 1d51fc32..b13b00d0 100644 --- a/apps/docs/content/docs/frameworks/next/precompute.mdx +++ b/apps/docs/content/docs/frameworks/next/precompute.mdx @@ -114,7 +114,7 @@ export const marketingFlags = [showSummerSale, showBanner] as const; ### 2. Precompute flags in middleware -Import and pass the group of flags to the `precompute` function in middleware. Then, forward the precomputation result (`code`) to the underlying page using an URL rewrite: +Import and pass the group of flags to the `precompute` function in middleware. Then, forward the precomputation result (`code`) to the underlying page using an URL rewrite. We recommend nesting precomputed pages under a dedicated `precomputed` folder so it's clear at a glance which routes participate in precomputation: ```tsx title="proxy.ts#next" import { type NextRequest, NextResponse } from 'next/server'; @@ -131,7 +131,7 @@ export async function proxy(request: NextRequest) { // rewrites the request to include the precomputed code for this flag combination const nextUrl = new URL( - `/${code}${request.nextUrl.pathname}${request.nextUrl.search}`, + `/precomputed/${code}${request.nextUrl.pathname}${request.nextUrl.search}`, request.url, ); @@ -143,10 +143,12 @@ export async function proxy(request: NextRequest) { Next, import the feature flags you created earlier, such as `showBanner`, while providing the code from the URL and the `marketingFlags` list of flags used in the precomputation. +Place the page under `app/precomputed/[code]/page.tsx`. Keeping precomputed pages in a `precomputed` folder makes it obvious which routes are served via the precompute pattern and avoids accidentally turning every top-level route into a dynamic segment. + When the `showBanner` flag is called within this component it reads the result from the precomputation, and it does not invoke the flag's `decide` function again: -```tsx title="app/[code]/page.tsx#next" -import { marketingFlags, showSummerSale, showBanner } from '../../flags'; +```tsx title="app/precomputed/[code]/page.tsx#next" +import { marketingFlags, showSummerSale, showBanner } from '../../../flags'; type Params = Promise<{ code: string }>; export default async function Page({ params }: { params: Params }) { @@ -176,14 +178,14 @@ works with API Routes. ## Enabling ISR (optional) You can enable Incremental Static Regeneration (ISR) to cache generated pages after their initial render: -```tsx title="app/[code]/layout.tsx#next" +```tsx title="app/precomputed/[code]/layout.tsx#next" import type { ReactNode } from 'react'; export async function generateStaticParams() { @@ -202,7 +204,7 @@ In the example above, we used [`generateStaticParams`](https://nextjs.org/docs/a The `flags/next` submodule exposes the [`generatePermutations`](/docs/api-reference/frameworks/next#generatepermutations) helper function for generating pages for different combinations of flags at build time. This function is called and takes a list of flags and returns an array of strings representing each combination of flags: -```tsx title="app/[code]/page.tsx#next" +```tsx title="app/precomputed/[code]/page.tsx#next" import type { ReactNode } from 'react'; import { generatePermutations } from 'flags/next'; @@ -224,9 +226,9 @@ If you're using the Pages Router, you need to pass a flag to `generatePermutatio You also need to specify a `getStaticPaths` function which can return the permutations to generate at build time or an empty array to use ISR. -```tsx title="pages/[code]/index.tsx#next" +```tsx title="pages/precomputed/[code]/index.tsx#next" import { generatePermutations } from 'flags/next'; -import { marketingFlags, exampleFlag } from '../flags'; +import { marketingFlags, exampleFlag } from '../../flags'; export const getStaticPaths = (async () => { const codes = await generatePermutations(marketingFlags); @@ -247,7 +249,7 @@ export const getStaticProps = (async (context) => { ## Declaring available options (optional) @@ -313,19 +315,19 @@ This section shows how to adapt the precompute pattern to different scenarios. ### Precomputing a single page only -The examples above use a single top-level group of flags, which will opt all pages nested under `app/[code]` into precomputation. Instead of opting the whole application into precomputation, you can also precompute a single page only. +The examples above use a single top-level `precomputed` segment, which opts every page nested under `app/precomputed/[code]` into precomputation. Instead of opting a whole subtree into precomputation, you can also precompute a single page only by scoping the `precomputed` segment to that route. For example, if you want to precompute the `/pricing` page only: -- Move your pricing page from `app/pricing/page.tsx` to `app/pricing/[pricingCode]/page.tsx` +- Move your pricing page from `app/pricing/page.tsx` to `app/pricing/precomputed/[pricingCode]/page.tsx` - Export a `pricingFlags` array of flags from your `flags.ts` file, containing all flags used by the pricing page - Run Proxy for requests to `/pricing`, and pass `pricingFlags` to the `precompute` function -- Adjust the rewrite in Proxy to rewrite requests from `/pricing` to `/pricing/[pricingCode]` -- Use the `pricingFlags` array to access the precomputed result in `app/pricing/[pricingCode]/page.tsx` when using Flags +- Adjust the rewrite in Proxy to rewrite requests from `/pricing` to `/pricing/precomputed/[pricingCode]` +- Use the `pricingFlags` array to access the precomputed result in `app/pricing/precomputed/[pricingCode]/page.tsx` when using Flags -```tsx title="app/pricing/[pricingCode]/page.tsx#next" +```tsx title="app/pricing/precomputed/[pricingCode]/page.tsx#next" import type { ReactNode } from 'react'; import { generatePermutations } from 'flags/next'; -import { pricingFlags, discountFlag } from '../../../flags'; +import { pricingFlags, discountFlag } from '../../../../flags'; export async function generateStaticParams() { const codes = await generatePermutations(pricingFlags); @@ -340,33 +342,13 @@ export default async function Page(props: { params: Promise<{ pricingCode: strin } ``` -### Precomputing a subset of pages - -This section describes an alternative folder structure where only a part of the page tree makes use of precomputation. - -So far the examples have used a single top-level group of flags under `app/[code]`. - -Instead, you can nest precomputed flags under a folder like `app/precomputed/[code]`. This makes it clear that only those pages will have access to the precomputed flags. - - -```md -app -├─page.tsx -└─precomputed - └─[rootCode] - └─page.tsx -``` - -Adjust the rewrite in Proxy to include the `precomputed` segment. - -Note that you will need to manually maintain the paths in Proxy for which the rewrite should run. - +Keeping the `precomputed` folder convention — even when the dynamic segment is scoped to a single route — makes it obvious which routes are served via the precompute pattern. ### Multiple groups -Define multiple groups of flags to avoid unnecessarily generating permutations for flags which are not used by all pages. This control allows you to only generate permutations of the precise flags used by eacha subset of your pages. +Define multiple groups of flags to avoid unnecessarily generating permutations for flags which are not used by all pages. This control allows you to only generate permutations of the precise flags used by each subset of your pages. -For example, you can have a root group of flags which apply to all pages, and a nested group of flags which only apply to a single page or subset of pages. Create a root `[rootCode]` at the root of your application for the common flags, and a `[pricingCode]` for the flags used by the pricing page. +For example, you can have a root group of flags which apply to all pages, and a nested group of flags which only apply to a single page or subset of pages. Create a root `precomputed/[rootCode]` for the common flags, and a nested `precomputed/[pricingCode]` for the flags used by the pricing page. ```tsx title="flags.ts#next" // all available flags @@ -383,11 +365,13 @@ The file tree would look like this: ```md app -└─[rootCode] - ├─ page.tsx - └─ pricing - └─ [pricingCode] - └─ page.tsx +└─precomputed + └─[rootCode] + ├─ page.tsx + └─ pricing + └─ precomputed + └─ [pricingCode] + └─ page.tsx ``` To use this pattern, you need to adjust Proxy: diff --git a/examples/shirt-shop/app/[code]/add-to-cart.tsx b/examples/shirt-shop/app/precomputed/[code]/add-to-cart.tsx similarity index 100% rename from examples/shirt-shop/app/[code]/add-to-cart.tsx rename to examples/shirt-shop/app/precomputed/[code]/add-to-cart.tsx diff --git a/examples/shirt-shop/app/[code]/cart/order-summary.tsx b/examples/shirt-shop/app/precomputed/[code]/cart/order-summary.tsx similarity index 100% rename from examples/shirt-shop/app/[code]/cart/order-summary.tsx rename to examples/shirt-shop/app/precomputed/[code]/cart/order-summary.tsx diff --git a/examples/shirt-shop/app/[code]/cart/page.tsx b/examples/shirt-shop/app/precomputed/[code]/cart/page.tsx similarity index 100% rename from examples/shirt-shop/app/[code]/cart/page.tsx rename to examples/shirt-shop/app/precomputed/[code]/cart/page.tsx diff --git a/examples/shirt-shop/app/[code]/cart/proceed-to-checkout.tsx b/examples/shirt-shop/app/precomputed/[code]/cart/proceed-to-checkout.tsx similarity index 100% rename from examples/shirt-shop/app/[code]/cart/proceed-to-checkout.tsx rename to examples/shirt-shop/app/precomputed/[code]/cart/proceed-to-checkout.tsx diff --git a/examples/shirt-shop/app/[code]/layout.tsx b/examples/shirt-shop/app/precomputed/[code]/layout.tsx similarity index 100% rename from examples/shirt-shop/app/[code]/layout.tsx rename to examples/shirt-shop/app/precomputed/[code]/layout.tsx diff --git a/examples/shirt-shop/app/[code]/page.tsx b/examples/shirt-shop/app/precomputed/[code]/page.tsx similarity index 95% rename from examples/shirt-shop/app/[code]/page.tsx rename to examples/shirt-shop/app/precomputed/[code]/page.tsx index 7b36090c..20a20eeb 100644 --- a/examples/shirt-shop/app/[code]/page.tsx +++ b/examples/shirt-shop/app/precomputed/[code]/page.tsx @@ -1,4 +1,4 @@ -import { AddToCart } from '@/app/[code]/add-to-cart'; +import { AddToCart } from '@/app/precomputed/[code]/add-to-cart'; import { SummerSale } from '@/app/summer-sale'; import { ImageGallery } from '@/components/image-gallery'; import { Main } from '@/components/main'; diff --git a/examples/shirt-shop/proxy.ts b/examples/shirt-shop/proxy.ts index 5b7a528c..2d169937 100644 --- a/examples/shirt-shop/proxy.ts +++ b/examples/shirt-shop/proxy.ts @@ -13,9 +13,10 @@ export async function proxy(request: NextRequest) { const cartId = await getCartId(); const code = await precompute(productFlags); - // rewrites the request to the variant for this flag combination + // rewrites the request to the variant for this flag combination. The + // precomputed pages live under a `precomputed` folder by convention. const nextUrl = new URL( - `/${code}${request.nextUrl.pathname}${request.nextUrl.search}`, + `/precomputed/${code}${request.nextUrl.pathname}${request.nextUrl.search}`, request.url, ); diff --git a/examples/snippets/app/concepts/precompute/automatic/[code]/flags.tsx b/examples/snippets/app/concepts/precompute/automatic/precomputed/[code]/flags.tsx similarity index 100% rename from examples/snippets/app/concepts/precompute/automatic/[code]/flags.tsx rename to examples/snippets/app/concepts/precompute/automatic/precomputed/[code]/flags.tsx diff --git a/examples/snippets/app/concepts/precompute/automatic/[code]/page.tsx b/examples/snippets/app/concepts/precompute/automatic/precomputed/[code]/page.tsx similarity index 100% rename from examples/snippets/app/concepts/precompute/automatic/[code]/page.tsx rename to examples/snippets/app/concepts/precompute/automatic/precomputed/[code]/page.tsx diff --git a/examples/snippets/app/concepts/precompute/automatic/[code]/proxy.ts b/examples/snippets/app/concepts/precompute/automatic/precomputed/[code]/proxy.ts similarity index 59% rename from examples/snippets/app/concepts/precompute/automatic/[code]/proxy.ts rename to examples/snippets/app/concepts/precompute/automatic/precomputed/[code]/proxy.ts index 1d939af0..47b79879 100644 --- a/examples/snippets/app/concepts/precompute/automatic/[code]/proxy.ts +++ b/examples/snippets/app/concepts/precompute/automatic/precomputed/[code]/proxy.ts @@ -6,8 +6,9 @@ export async function automaticPrecomputeProxy(request: NextRequest) { // precompute the flags const code = await precompute(marketingFlags); - // rewrite the page with the code + // rewrite the page with the code, nested under a `precomputed` folder so + // it's clear which routes are served via the precompute pattern return NextResponse.rewrite( - new URL(`/concepts/precompute/automatic/${code}`, request.url), + new URL(`/concepts/precompute/automatic/precomputed/${code}`, request.url), ); } diff --git a/examples/snippets/app/examples/marketing-pages/[code]/page.tsx b/examples/snippets/app/examples/marketing-pages/precomputed/[code]/page.tsx similarity index 91% rename from examples/snippets/app/examples/marketing-pages/[code]/page.tsx rename to examples/snippets/app/examples/marketing-pages/precomputed/[code]/page.tsx index d90679e3..8d897fd6 100644 --- a/examples/snippets/app/examples/marketing-pages/[code]/page.tsx +++ b/examples/snippets/app/examples/marketing-pages/precomputed/[code]/page.tsx @@ -4,8 +4,8 @@ import { marketingAbTest, marketingFlags, secondMarketingAbTest, -} from '../flags'; -import { RegenerateIdButton } from '../regenerate-id-button'; +} from '../../flags'; +import { RegenerateIdButton } from '../../regenerate-id-button'; // Generate all permutations (all combinations of flag 1 and flag 2). export async function generateStaticParams() { diff --git a/examples/snippets/app/examples/marketing-pages/proxy.tsx b/examples/snippets/app/examples/marketing-pages/proxy.tsx index aa38b5c4..af5dd5e9 100644 --- a/examples/snippets/app/examples/marketing-pages/proxy.tsx +++ b/examples/snippets/app/examples/marketing-pages/proxy.tsx @@ -13,9 +13,10 @@ export async function marketingProxy(request: NextRequest) { // precompute the flags const code = await precompute(marketingFlags); - // rewrite the page with the code and set the cookie + // rewrite the page with the code and set the cookie. Precomputed pages + // are nested under a `precomputed` folder by convention. return NextResponse.rewrite( - new URL(`/examples/marketing-pages/${code}`, request.url), + new URL(`/examples/marketing-pages/precomputed/${code}`, request.url), { headers: { // Set the cookie on the response diff --git a/examples/snippets/app/examples/suspense-fallbacks/[code]/page.tsx b/examples/snippets/app/examples/suspense-fallbacks/precomputed/[code]/page.tsx similarity index 98% rename from examples/snippets/app/examples/suspense-fallbacks/[code]/page.tsx rename to examples/snippets/app/examples/suspense-fallbacks/precomputed/[code]/page.tsx index d4e0c604..9f4fc953 100644 --- a/examples/snippets/app/examples/suspense-fallbacks/[code]/page.tsx +++ b/examples/snippets/app/examples/suspense-fallbacks/precomputed/[code]/page.tsx @@ -2,7 +2,7 @@ import { generatePermutations } from 'flags/next'; import { cookies, headers } from 'next/headers'; import Image from 'next/image'; import { Suspense } from 'react'; -import { coreFlags, hasAuthCookieFlag } from '../flags'; +import { coreFlags, hasAuthCookieFlag } from '../../flags'; // opt into on parital prerendering for this page, which is necessary while // it's experimental, see https://nextjs.org/learn/dashboard-app/partial-prerendering diff --git a/examples/snippets/app/examples/suspense-fallbacks/proxy.tsx b/examples/snippets/app/examples/suspense-fallbacks/proxy.tsx index 6454c641..b1142b67 100644 --- a/examples/snippets/app/examples/suspense-fallbacks/proxy.tsx +++ b/examples/snippets/app/examples/suspense-fallbacks/proxy.tsx @@ -6,8 +6,9 @@ export async function pprShellsProxy(request: NextRequest) { // precompute the flags const code = await precompute(coreFlags); - // rewrite the page with the code + // rewrite the page with the code. Precomputed pages are nested under a + // `precomputed` folder by convention. return NextResponse.rewrite( - new URL(`/examples/suspense-fallbacks/${code}`, request.url), + new URL(`/examples/suspense-fallbacks/precomputed/${code}`, request.url), ); } diff --git a/examples/snippets/lib/pages-router-precomputed/proxy.ts b/examples/snippets/lib/pages-router-precomputed/proxy.ts index c907c559..88a95440 100644 --- a/examples/snippets/lib/pages-router-precomputed/proxy.ts +++ b/examples/snippets/lib/pages-router-precomputed/proxy.ts @@ -6,7 +6,11 @@ export async function pagesRouterProxy(request: NextRequest) { // precompute the flags const code = await precompute(exampleFlags); + // Precomputed pages are nested under a `precomputed` folder by convention. return NextResponse.rewrite( - new URL(`/examples/pages-router-precomputed/${code}`, request.url), + new URL( + `/examples/pages-router-precomputed/precomputed/${code}`, + request.url, + ), ); } diff --git a/examples/snippets/pages/examples/pages-router-precomputed/[code]/index.tsx b/examples/snippets/pages/examples/pages-router-precomputed/precomputed/[code]/index.tsx similarity index 100% rename from examples/snippets/pages/examples/pages-router-precomputed/[code]/index.tsx rename to examples/snippets/pages/examples/pages-router-precomputed/precomputed/[code]/index.tsx diff --git a/examples/snippets/proxy.ts b/examples/snippets/proxy.ts index cf0942c2..5e34e53f 100644 --- a/examples/snippets/proxy.ts +++ b/examples/snippets/proxy.ts @@ -1,6 +1,6 @@ import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; -import { automaticPrecomputeProxy } from './app/concepts/precompute/automatic/[code]/proxy'; +import { automaticPrecomputeProxy } from './app/concepts/precompute/automatic/precomputed/[code]/proxy'; import { manualPrecomputeProxy } from './app/concepts/precompute/manual/proxy'; import { featureFlagsInProxy } from './app/examples/feature-flags-in-proxy/proxy'; import { marketingProxy } from './app/examples/marketing-pages/proxy'; diff --git a/skills/flags-sdk/SKILL.md b/skills/flags-sdk/SKILL.md index 3feb8ad7..8581d6ad 100644 --- a/skills/flags-sdk/SKILL.md +++ b/skills/flags-sdk/SKILL.md @@ -275,8 +275,8 @@ Use precompute to keep pages static while using feature flags. Middleware evalua High-level flow: 1. Declare flags and group them in an array 2. Call `precompute(flagGroup)` in middleware, get a `code` string -3. Rewrite request to `/${code}/original-path` -4. Page reads flag values from `code`: `await myFlag(code, flagGroup)` +3. Rewrite request to `/precomputed/${code}/original-path` — always nest precomputed routes under a `precomputed` folder so it's clear which pages participate in the pattern +4. Page (at `app/precomputed/[code]/page.tsx`) reads flag values from `code`: `await myFlag(code, flagGroup)` For full implementation details, see framework-specific references: - **Next.js**: See [references/nextjs.md](references/nextjs.md) — covers proxy middleware, precompute setup, ISR, generatePermutations, multiple groups diff --git a/skills/flags-sdk/references/nextjs.md b/skills/flags-sdk/references/nextjs.md index 4c3f32f0..86654e80 100644 --- a/skills/flags-sdk/references/nextjs.md +++ b/skills/flags-sdk/references/nextjs.md @@ -175,6 +175,8 @@ Not available in Pages Router. Keep pages static while using feature flags. Proxy evaluates flags and encodes results into the URL. +Always nest precomputed pages under a `precomputed` folder (for example `app/precomputed/[code]/page.tsx`). This keeps it clear at a glance which routes participate in the precompute pattern and avoids turning every top-level route into a dynamic segment. + ### Prerequisites Set `FLAGS_SECRET` env var (32 random bytes, base64-encoded). Use a separate value for each environment (Development, Preview, Production), and mark the Preview and Production values as Sensitive. Run the generator once per environment to produce distinct values: @@ -212,6 +214,8 @@ export const marketingFlags = [showSummerSale, showBanner] as const; ### Step 2: Precompute in proxy +Rewrite into the `precomputed/` folder so the page tree clearly marks which routes are served via precompute: + ```ts // proxy.ts import { type NextRequest, NextResponse } from 'next/server'; @@ -223,7 +227,7 @@ export const config = { matcher: ['/'] }; export async function proxy(request: NextRequest) { const code = await precompute(marketingFlags); const nextUrl = new URL( - `/${code}${request.nextUrl.pathname}${request.nextUrl.search}`, + `/precomputed/${code}${request.nextUrl.pathname}${request.nextUrl.search}`, request.url, ); return NextResponse.rewrite(nextUrl, { request }); @@ -233,8 +237,8 @@ export async function proxy(request: NextRequest) { ### Step 3: Read precomputed values in page ```tsx -// app/[code]/page.tsx -import { marketingFlags, showSummerSale, showBanner } from '../../flags'; +// app/precomputed/[code]/page.tsx +import { marketingFlags, showSummerSale, showBanner } from '../../../flags'; type Params = Promise<{ code: string }>; @@ -255,7 +259,7 @@ export default async function Page({ params }: { params: Params }) { ### Step 4: Enable ISR & build time prerendering ```tsx -// app/[code]/layout.tsx +// app/precomputed/[code]/layout.tsx import { generatePermutations } from 'flags/next'; export async function generateStaticParams() { @@ -302,12 +306,12 @@ export const rootFlags = [navigationFlag, bannerFlag]; export const pricingFlags = [discountFlag]; ``` -File tree: +Each group still lives under its own `precomputed/` folder: ``` -app/[rootCode]/ +app/precomputed/[rootCode]/ page.tsx - pricing/[pricingCode]/ + pricing/precomputed/[pricingCode]/ page.tsx ```