Skip to content

[FR]: Next.js Pages Router SSR fails with pre-compiled TypeScript - missing __N_SSP/__N_SSG markers #2658

@elSilveira

Description

@elSilveira

Next.js SSR/SSG Fix for Pre-compiled TypeScript (ts_project)

Actual behavior:

When using ts_project to pre-compile TypeScript before Next.js processes it, client-side navigation to pages with getServerSideProps or getStaticProps fails silently. The pageProps arrives as {} instead of the server-fetched data.

Expected behavior:
Client-side navigation should fetch data from /_next/data/... endpoints, and pageProps should contain the server-fetched data.

Root Cause

Next.js uses internal markers (__N_SSP for getServerSideProps, __N_SSG for getStaticProps) to determine if a page needs server-side data fetching during client-side navigation. These markers are injected by Next.js's SWC transform during webpack compilation.

The problem: This transform only processes source files (.tsx, .jsx)—not pre-compiled .js files from Bazel's ts_project. Without these markers, the Next.js client router skips data fetching during navigation.

The Solution

A custom webpack loader that injects the missing markers during Next.js's build:

1. Webpack Loader (ssr-marker-loader.js)

/**
 * Webpack loader to inject Next.js SSR/SSG markers into pre-compiled pages.
 */
module.exports = function ssrMarkerLoader(source) {
  // Skip non-page files
  if (
    !this.resourcePath.includes('/pages/') ||
    this.resourcePath.includes('node_modules') ||
    this.resourcePath.includes('_app') ||
    this.resourcePath.includes('_document')
  ) {
    return source;
  }

  const hasGetServerSideProps =
    /export\s+(async\s+)?function\s+getServerSideProps|exports\.getServerSideProps/.test(source);
  const hasGetStaticProps =
    /export\s+(async\s+)?function\s+getStaticProps|exports\.getStaticProps/.test(source);

  let result = source;

  if (hasGetServerSideProps && !source.includes('__N_SSP')) {
    result += '\nexports.__N_SSP = true;';
  }

  if (hasGetStaticProps && !source.includes('__N_SSG')) {
    result += '\nexports.__N_SSG = true;';
  }

  return result;
};

2. Next.js Configuration (next.config.js)

const path = require('path');

module.exports = {
  webpack: (config, { isServer }) => {
    // Only apply to client-side bundles (where markers are checked)
    if (!isServer) {
      config.module.rules.push({
        test: /\.js$/,
        include: [path.resolve(__dirname, 'pages')],
        use: [path.resolve(__dirname, 'ssr-marker-loader.js')],
      });
    }
    return config;
  },
};

3. Bazel BUILD Configuration

next(
    name = "next",
    srcs = ["//pages"],
    data = [
        "next.config.js",
        "ssr-marker-loader.js",  # Include the loader
        # ... other deps
    ],
)

Why This Approach?

Approach Verdict Reason
Manual export const __N_SSP = true Requires changes to every page, error-prone
Custom SWC transpiler in Bazel Pages may be compiled by many separate BUILD files
SWC plugin Requires Rust/WASM, complex setup
Webpack loader Runs at correct point in pipeline, automatic, zero page changes

Key Points

  1. Bazel's TypeScript compilation happens before Next.js—you can't rely on Next.js to add the markers
  2. Webpack loaders still run during Next.js build—perfect injection point
  3. The __N_SSP/__N_SSG markers are undocumented—discovered by reading Next.js router source (next/dist/shared/lib/router/router.js)
  4. This applies to any pre-compiled setup—not just Bazel (esbuild, rollup, monorepo setups, etc.)

Applicability

This solution works for:

  • ✅ Bazel with rules_js / rules_ts
  • ✅ Any monorepo that pre-compiles TypeScript before Next.js
  • ✅ Custom bundler setups (esbuild, rollup, etc.)
  • ✅ Next.js Pages Router with getServerSideProps or getStaticProps

Working Example

A complete working example is available at: https://github.com/bazelbuild/examples/tree/main/frontend/next.js

How to Test

To verify the fix works, create a page with getServerSideProps:

// pages/ssr-test.tsx
import { GetServerSideProps } from 'next';
import Link from 'next/link';

export default function SSRTest({ timestamp }: { timestamp: string }) {
  return (
    <div>
      <h1>SSR Test</h1>
      <p>Timestamp: {timestamp}</p>
      <Link href="/">Home</Link>
    </div>
  );
}

export const getServerSideProps: GetServerSideProps = async () => {
  return {
    props: { timestamp: new Date().toISOString() },
  };
};

Then test client-side navigation:

  1. Run bazel run //next.js:next_dev
  2. Navigate to the home page (/)
  3. Open DevTools → Network tab
  4. Click a link to navigate to /ssr-test
  5. Without the fix: No /_next/data/... request, timestamp is empty
  6. With the fix: /_next/data/... request appears, timestamp shows current time

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions