Modern web applications introduced powerful features that were once impossible on the web: virtual DOM, component state, client-side routing, offline support, Service Workers, and installable apps. These capabilities make today's web fast and interactive.
However, they also introduce new challenges. One of the biggest is SEO. Search engines may not fully execute client-side JavaScript, making it difficult to index single-page applications correctly.
Server-side rendering
Server-side rendering (SSR) renders a page on the server, converts it to HTML, and sends the generated markup to the client. This allows search engines to index the page without relying on JavaScript execution.
In Next.js, static pages are generated during the build process. If a requested route does not exist, Next.js automatically returns a 404 response.
Dynamic routes are different.
Dynamic pages
A dynamic page is a route template whose content depends on request parameters. For example, the route /catalog/[id] matches URLs such as /catalog/88.
Because these pages are generated at runtime, Next.js cannot know in advance which URLs are valid.
Create a file named pages/[id]/index.js:
import ChildComponent from '/components/ChildComponent';
const DynamicPage = () => (
<div className='container'>
<ChildComponent />
</div>
);
export default DynamicPage;
The folder name in square brackets ([id]) defines a dynamic route parameter. For a request to somesite.com/example, the value of id will be "example".
Suppose ChildComponent fetches data. What happens if the requested resource does not exist?
If a request to somesite.com/wrong-address returns 404, Next.js still renders the page because it has no way to know whether the missing data should produce a 404 page or an empty component.
Handling errors on the client
A simple solution is to move data fetching to the parent component and render an error page when the request fails.
import React from 'react';
import ChildComponent from '/components/ChildComponent';
import Error from '/pages/404';
const DynamicPage = () => {
// Error state.
const [is_error, toggle_error] = React.useState(false);
// Fetch data after the component mounts.
React.useEffect(() => {
fetch_data();
}, []);
// Data request.
const fetch_data = () => {
// ...
}
return(
<div className='container'>
{
is_error ?
<Error /> : // Render the 404 page when an error occurs.
<ChildComponent /> // Otherwise render the page content.
}
</div>
);
}
export default DynamicPage;
This works for missing resources, but there is another problem.
If the API returns 500, 403, or another error, the user still sees the 404 page.
Handling multiple error codes
Next.js provides a built-in error component that can display any HTTP status code.
import React from 'react';
import NextError from 'next/error';
import ChildComponent from '/components/ChildComponent';
import Error from '/pages/404';
const DynamicPage = () => {
const [error_code, change_error_code] = React.useState(null);
React.useEffect(() => {
fetch_data();
}, []);
const fetch_data = () => {
// ...
}
// Render the appropriate component based on the error code.
if (error_code) {
// Show the custom 404 page.
if (error_code === 404) {
return(
<div className='container'>
<Error />
</div>
);
// Show the default Next.js error page for all other errors.
} else {
return(
<div className='container'>
<NextError statusCode={error_code} />
</div>
);
}
// No error: render the page normally.
} else {
return(
<div className='container'>
<ChildComponent />
</div>
);
}
}
export default DynamicPage;
The is_error state is no longer needed. The error_code state contains all the required information.
At first glance, this seems complete. Users now see the correct error page.
Unfortunately, search engines still do not.
Why search engines still index the page
Search engines index pages based on the HTTP status code, not on the React component that is rendered.
Even if the page displays a 404 component, the server still returns 200 OK. As far as the search engine is concerned, the page exists.
This becomes a problem when:
- Product IDs or slugs change.
- Products are removed from a catalog.
- URL structure changes.
- Old links remain indexed.
A sitemap helps, but it cannot replace correct HTTP status codes.
The correct solution
Client-side JavaScript cannot change the HTTP response status after the page has been sent.
Instead, the status code must be set before rendering.
Next.js provides getServerSideProps(), which runs on the server before the page is rendered. It has access to the response object, allowing you to set the correct HTTP status code.
import NextError from 'next/error';
import Error from '/pages/404';
import ChildComponent from '/components/ChildComponent';
const DynamicPage = ({
statusCode = 200,
data: null,
}) => {
// Render the page when the request succeeds.
if (fetch_res.status >= 200 && fetch_res.status < 300) {
return (
<ChildComponent data={data} /> // Data is already available before rendering.
);
// Render the custom 404 page.
} else if (statusCode === 404) return <Error />;
// Render the default Next.js error page.
else return <NextError statusCode={statusCode} />;
};
// Runs on the server before rendering.
export async function getServerSideProps(context) {
// Read the dynamic route parameter.
const { id } = context.query;
// Request data.
const fetch_res = await fetch(/* ...url */);
// Parse the response.
const fetch_json = await fetch_res.json();
let statusCode = 200;
// Check the HTTP status.
if (fetch_res.status < 200 || fetch_res.status >= 300) {
// Set the HTTP status returned by the server.
context.res.statusCode = fetch_res.status;
// Pass the status code to the page component.
statusCode = fetch_res.status;
}
return {
props: {
statusCode,
data: fetch_json, // Pass fetched data to the page.
},
};
}
export default DynamicPage;
Conclusion
Rendering a 404 component on the client is not enough for SEO.
To return the correct HTTP status code, handle data fetching in getServerSideProps() and update context.res.statusCode before the response is sent.
This ensures:
- Users see the correct error page.
- Search engines receive the correct HTTP status.
- Invalid URLs are removed from search indexes over time.