Modify the HTML Response from a Next.js Custom Server

Posted on Tue Jun 11 2024

Next.js is a popular framework for creating React.js frontend applications. It is especially useful for improving SEO for website while keeping SPA like application architecture.

Problem

Next.js offers a custom server implementation. This is often required for custom handling of incoming requests. This could be redirecting based on the user session or CSRF implementation. Sometimes you may want to modify the HTML responses for a Next.js rendered page. This could be especially useful for injecting scripts or meta tags based on headers. Something like this is requested in issue.

Solution

First, you need to disable compression in your Next.js project config. Compression is often already performed by cloud computing frontend servers or an external service like Cloudflare.

In this example, we are using an Express.js based custom server. Here we are modifying all HTML output(s) and injecting a custom meta tag, when suitable.

1import next from 'next';
2import Express from 'express';
3
4const server = express();
5const app = next({ dev: true });
6const handle = app.getRequestHandler();
7
8server.all('*', async (req, res) => {
9  const isHTMLRequest =
10    req.method === 'GET' &&
11    req.accepts(['json', 'html']) === 'html' &&
12    !req.path.includes('/_next/');
13
14  if (isHTMLRequest) {
15    // get tag value
16    const value = await getMetaTagValue();
17
18    const _resEnd = res.end.bind(res);
19    res.end = function (payload) {
20      if (!payload) return _resEnd(payload);
21
22      const modifiedPayload = payload.replace(
23        '</head>',
24        `<meta name="custom-data" content="${value}" /></head>`,
25      );
26
27      // ensure that correct content length
28      // is set, so content is not trimmed
29      res.setHeader('Content-Length', Buffer.byteLength(modifiedPayload));
30
31      return _resEnd(modifiedPayload);
32    };
33  }
34
35  return handle(req, res);
36});
37
38app
39  .prepare()
40  .then(() => {
41    server.listen(3000);
42  })
43  .catch((error: Error) => {
44    console.error(error);
45  });

Also notice that we are resetting content length, so modified HTML content is not trimmed.