Next.js Discord

Discord Forum

Need page-level response header mutations in App Router (similar to getServerSideProps )

Unanswered
Najmus Sakib posted this in #help-forum
Open in Discord
## Problem

We are building a no-code website/page builder where users can configure page-level settings, such as:

- SEO metadata
- Custom scripts
- Page verification tags
- X-Frame-Options
- Content-Security-Policy
- Caching logic
- Other response headers

Each page can have different values, driven by user configuration stored in the database.

7 Replies

## Current Setup

- App Router (app/)
- Page data is fetched in a Server Component
- Metadata is generated using generateMetadata()
- Page-level config is stored in DB

This works well for metadata, but response headers are the problem.

---

## The Core Issue

In the App Router, response headers like X-Frame-Options and Content-Security-Policy cannot be modified at the page level.

They can only be set via:
- next.config.ts (static)
- Middleware / proxy (global or route-based, but not page-data-aware)

However, our use case requires dynamic, per-page response headers, based on user-defined configuration.

---

## What Worked Before (Pages Router)

In the Pages Router, this was handled using:

export async function getServerSideProps({ res }) {
  res.setHeader("X-Frame-Options", "DENY")
  return { props: {} }
}

## This allowed us to:

- Fetch page-level config
- Mutate response headers dynamically
- Render the page

This pattern no longer exists in the App Router.
## What We Tried
1. Fetching the same data multiple times
We currently fetch page data in:

- proxy.ts (to set headers)
- page.ts
- generateMetadata()
export async function generateMetadata({
  params,
}: {
  params: Promise<ParamsType>;
}): Promise<Metadata> {
  const parameters = await params;
  const slug = getSlugFromParams(parameters.slug);
  const res = await getPageDataForRender(slug);

  return getSeoMeta(res.data.page.pageInfo);
}

export default async function Page({
  params,
  searchParams,
}: {
  params: Promise<ParamsType>;
  searchParams: Promise<SearchParamsType>;
}) {
  const parameters = await params;
  const locale = parameters.locale;
  const qs = await searchParams;
  const slug = getSlugFromParams(parameters.slug);

  const ver = qs.ver;
  const draft = qs.draft;

  let res, initialSiteData;

  try {
    [res, initialSiteData] = await Promise.all([
      getPageDataForRender(slug, ver, draft),
      getSiteInitialData(),
    ]);
  } catch {
    return notFound();
  }

  const data = res?.data;

  if (!data) {
    return notFound();
  }
  return (
    <PageBuilder
      pageData={data.page}
      initialSiteData={initialSiteData?.data}
      locale={locale}
    />
  );
}


export const getPageDataForRender = React.cache(function getPageDataForRender(
  slug: string,
  ver?: string,
  draft?: string
) {
  return httpClient.get<PageRenderData>(`/builder/v2/pages/${slug}`, {
    params: {
      ver,
      draft,
    },
  });
});
We tried deduplicating calls using React.cache(), but:

- The cache only works within the React render context

- Middleware/proxy runs outside that context

- Result: the API is called twice (sometimes 3 times)

2. AsyncLocalStorage (Node.js)

Since:

- We are self-hosting

- Using Node.js runtime only

- No Edge runtime requirements

We considered using AsyncLocalStorage to share request-scoped data between:

- Proxy / middleware

- Server Components

However, this requires further R&D, and it’s unclear whether this is a supported or recommended pattern in Next.js.

## Why This Matters
Even though:

- Calls are server-to-server

- Latency is relatively small (≈5–10ms)

This is a page builder product, and:

- Page-level response headers are a core feature

- Extra backend calls are something we want to avoid if possible

## Questions
1. Is there an official or recommended way in the App Router to:

- Dynamically mutate response headers per page

- Based on data fetched from a database?

2. Is the lack of page-level header control a fundamental limitation due to:

- React Server Components?

- Streaming architecture?

3. Are approaches like:

- AsyncLocalStorage

- Request-scoped context

- Custom Node.js server hooks
considered acceptable or supported for this use case?
I have tried modifying the headers object directly and inject custom data to headers() and it worked great
i doubt its recommended but it works for me as long as you do it very early in the render tree
Yes. The page-level header control is a fundamental limitation due to RSC specifically the streaming architecture since you need the header to be sent first before the body of the response. Not to mention that generateMetadata and RSC renders happens simultaneously and each are wrapped by Suspense
This is a powerful disadvantage in the App router framework and is valid if you want to stay in the pages router since it will give you all the benefit you need like setting per-page response headers