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
- Bazel's TypeScript compilation happens before Next.js—you can't rely on Next.js to add the markers
- Webpack loaders still run during Next.js build—perfect injection point
- The
__N_SSP/__N_SSG markers are undocumented—discovered by reading Next.js router source (next/dist/shared/lib/router/router.js)
- 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:
- Run
bazel run //next.js:next_dev
- Navigate to the home page (
/)
- Open DevTools → Network tab
- Click a link to navigate to
/ssr-test
- Without the fix: No
/_next/data/... request, timestamp is empty
- With the fix:
/_next/data/... request appears, timestamp shows current time
Next.js SSR/SSG Fix for Pre-compiled TypeScript (ts_project)
Actual behavior:
When using
ts_projectto pre-compile TypeScript before Next.js processes it, client-side navigation to pages withgetServerSidePropsorgetStaticPropsfails silently. ThepagePropsarrives 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_SSPforgetServerSideProps,__N_SSGforgetStaticProps) 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.jsfiles from Bazel'sts_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)2. Next.js Configuration (
next.config.js)3. Bazel BUILD Configuration
Why This Approach?
export const __N_SSP = trueKey Points
__N_SSP/__N_SSGmarkers are undocumented—discovered by reading Next.js router source (next/dist/shared/lib/router/router.js)Applicability
This solution works for:
rules_js/rules_tsgetServerSidePropsorgetStaticPropsWorking 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:Then test client-side navigation:
bazel run //next.js:next_dev/)/ssr-test/_next/data/...request,timestampis empty/_next/data/...request appears,timestampshows current time