Johannes Klauß
Johannes Klauß

Reputation: 11020

How to access next.js rendered HTML via custom server

I want a server side generated page in next.js to be served as a file. So I wanted to grab the rendered content inside a custom server.js file:

const express = require('express');
const next = require('next');

const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== 'production';
const app = next({dev});
const handle = app.getRequestHandler();

app.prepare().then(() => {
  const server = express();

  server.get('/', async (req, res) => {
    const nextResponse = await app.renderToHTML(req, res, '/', req.query);

    console.log('nextResponse', nextResponse);
    console.log('res.body', res.body);
  });

  server.get('*', (req, res) => {
    return handle(req, res);
  });

  server.listen(port, (err) => {
    if (err) throw err;
    console.log(`> Ready on http://localhost:${port}`);
  });
});

Oddly enough every console.log returns null or undefined. I thought that renderToHTML would just return the rendered HTML string. Is there any way to do this?

Upvotes: 2

Views: 4187

Answers (3)

李龙龙
李龙龙

Reputation: 1

Reference Gregory Jakubowicz solution, nextjs 14.2.5 no handleCompression method, but can use the new handleCatchallRenderRequest instead of, The following code snippet has been tested locally:

    const _handleCatchallRenderRequest =
      nextServer.handleCatchallRenderRequest.bind(nextServer);

    nextServer.handleCatchallRenderRequest = (
      req: any,
      res: any,
      parsedUrl: any,
    ) => {
      _handleCatchallRenderRequest(req, res, parsedUrl);
      const _resEnd = res._res.end.bind(res._res);

      res._res.end = function (payload: any) {
        // payload contains the html content
        return _resEnd(payload);
      };
    };

Upvotes: 0

I adapted Nikola Knežević solution for NextJS 13.2.3, and it does the job :

const { parse } = require('url')
const next = require('next')
const express = require('express')

const dev = process.env.NODE_ENV !== 'production'
const hostname = 'localhost'
const port = 3000
// when using middleware `hostname` and `port` must be provided below
const app = next({ dev, hostname, port, dir: '<absolute root path to your project>' })
const handle = app.getRequestHandler()

app.prepare().then(() => {
    const server = express()

    server.get('*', async (req, res) => {
        const nextServer = await app.getServer()
        const _handleCompression = nextServer.handleCompression.bind(nextServer)

        nextServer.handleCompression = (req, res) => {
            _handleCompression(req, res)
            const _resEnd = res._res.end.bind(res._res)

            res._res.end = function (payload) {
                // payload contains the html content
                return _resEnd(payload)
            }
        }

        return handle(req, res, parsedUrl)
    })

    server.listen(port, (err) => {
        if (err) {
            throw err
        }
        console.log(`> Ready on http://localhost:${port}`)
    })
})

Upvotes: 0

Nikola Knežević
Nikola Knežević

Reputation: 97

This one is a bit tricky but achievable.

The idea is to override res.end function in order to catch rendered HTML there. The tricky part is that Next.js gzips and streams response using the compression library that's overriding res.end somewhere in the middle of the handle function.

The compression library is initialized using the handleCompression function of the Next.js's Server object (which is accessible using the app.getServer()), so that function needs to get overridden too.

So it should be looking something like this:

const { parse } = require('url');
const next = require('next');
const express = require('express');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });

const port = process.env.PORT || 3000;

const handle = app.getRequestHandler();

app.prepare()
.then(() => {
    const server = express();

    server.get('*', async (req, res) => {
        const parsedUrl = parse(req.url, true);

        const nextServer = await app.getServer();
        const _handleCompression = nodeServer.handleCompression.bind(nodeServer);

        nextServer.handleCompression = (req, res) => {
            _handleCompression(req, res);

            const _resEnd = res.end.bind(res)
            res.end = function (payload) {
                console.log('Rendered HTML: ', payload);

                return _resEnd(payload);
            }
        }

        return handle(req, res, parsedUrl);
    });

    server.listen(port, err => {
        if (err) throw err;
        console.log('> Ready on http://localhost:' + port);
    });
});

After you get rendered HTML you don't have to use the saved _resEnd function. Feel free to manipulate, serve as a file, or whatever you want with it.

Upvotes: 1

Related Questions