Edit

renderPage()

Environment: server

The renderPage() function renders your pages (e.g. +Page) on the server-side.

⚠️

We recommend using vike(app) instead of renderPage(). Because renderPage() doesn't support middlewares added by Vike extensions (+middleware). See also: renderPage() vs vike(app).

From the perspective of a server, Vike is just a middleware:

// +serverEntry.js
 
// renderPage() doesn't depend on Node.js and can be used within any JavaScript environment:
// Node.js, AWS, Cloudflare, Vercel, Deno, Bun, Lagon, ...
import { renderPage } from 'vike/server'
 
// Any server: Express.js, Cloudflare Worker, AWS Lambda Function, Fastify, Hono, Nitro, ...
server.addMiddleware({
  method: 'GET',
  route: '*', // catch-all
  async handler(request) {
    const pageContextInit = { urlOriginal: request.url }
    const pageContext = await renderPage(pageContextInit)
    // `body` is the HTML of the page with a route matching pageContextInit.urlOriginal
    const { body, statusCode, headers } = pageContext.httpResponse
    const response = { body, statusCode, headers }
    return response
  }
})
// +serverEntry.ts
 
// renderPage() doesn't depend on Node.js and can be used within any JavaScript environment:
// Node.js, AWS, Cloudflare, Vercel, Deno, Bun, Lagon, ...
import { renderPage } from 'vike/server'
 
// Any server: Express.js, Cloudflare Worker, AWS Lambda Function, Fastify, Hono, Nitro, ...
server.addMiddleware({
  method: 'GET',
  route: '*', // catch-all
  async handler(request: Request) {
    const pageContextInit = { urlOriginal: request.url }
    const pageContext = await renderPage(pageContextInit)
    // `body` is the HTML of the page with a route matching pageContextInit.urlOriginal
    const { body, statusCode, headers } = pageContext.httpResponse
    const response = { body, statusCode, headers }
    return response
  }
})

You can also run your server yourself (e.g. $ node server/index.js) instead of using +serverEntry.js, see Without +serverEntry.js.

Alternatively, instead of using renderPage(), you can pre-render your pages and remove the need for a production server (and deploy to a static host instead).

Usage

Define your server at +serverEntry.js and use renderPage() as the last middleware.

Don't define +server.js, see API > +serverEntry > With(out) +server.js.

// +serverEntry.js
 
// In this example we use Express.js but we could use any other server framework
import express from 'express'
import assert from 'node:assert'
import { fileURLToPath } from 'node:url'
import { getGlobalContext } from 'vike'
import { renderPage } from 'vike/server'
 
// Create an Express.js server
const app = express()
 
// Dev/prod middleware
if (import.meta.env.DEV) {
  // Vite's development middleware (HMR, transpiling, static assets, ...)
  const { devMiddleware } = await getGlobalContext()
  assert(devMiddleware)
  app.use(devMiddleware)
} else {
  // In production, we need to serve our static assets ourselves.
  // (In dev, Vite's middleware serves our static assets.)
  app.use(express.static(fileURLToPath(new URL('../client', import.meta.url))))
}
 
// ...
// Other middlewares (authentication, REST/GraphQL/RPC middleware, ...)
// ...
 
// SSR middleware.
// Note: it should always be the last middleware, because it's a catch-all
// middleware that supersedes any middleware placed after it.
app.get('/{*vikeCatchAll}', async (req, res) => {
  const pageContextInit = {
    // Required: the URL of the page
    urlOriginal: req.originalUrl,
 
    // Optional: the HTTP Headers
    headersOriginal: req.headers,
 
    // Optional: information about the logged-in user (when using an
    // Express.js authentication middleware that defines `req.user`).
    user: req.user
 
    // ... we can provide any additional information about the request here ...
  }
 
  const pageContext = await renderPage(pageContextInit)
 
  const { body, statusCode, headers } = pageContext.httpResponse
  headers.forEach(([name, value]) => {
    // There can be several Set-Cookie headers: res.setHeader() would only keep the last one
    if (name.toLowerCase() === 'set-cookie') res.appendHeader(name, value)
    else res.setHeader(name, value)
  })
  res.status(statusCode).send(body)
})
 
const port = 3000
app.listen(port)
console.log(`Server running at http://localhost:${port}`)
// +serverEntry.ts
 
// In this example we use Express.js but we could use any other server framework
import express from 'express'
import assert from 'node:assert'
import { fileURLToPath } from 'node:url'
import { getGlobalContext } from 'vike'
import { renderPage } from 'vike/server'
import type { Request, Response } from 'express'
 
// Create an Express.js server
const app = express()
 
// Dev/prod middleware
if (import.meta.env.DEV) {
  // Vite's development middleware (HMR, transpiling, static assets, ...)
  const { devMiddleware } = await getGlobalContext()
  assert(devMiddleware)
  app.use(devMiddleware)
} else {
  // In production, we need to serve our static assets ourselves.
  // (In dev, Vite's middleware serves our static assets.)
  app.use(express.static(fileURLToPath(new URL('../client', import.meta.url))))
}
 
// ...
// Other middlewares (authentication, REST/GraphQL/RPC middleware, ...)
// ...
 
// SSR middleware.
// Note: it should always be the last middleware, because it's a catch-all
// middleware that supersedes any middleware placed after it.
app.get('/{*vikeCatchAll}', async (req: Request, res: Response) => {
  const pageContextInit = {
    // Required: the URL of the page
    urlOriginal: req.originalUrl,
 
    // Optional: the HTTP Headers
    headersOriginal: req.headers,
 
    // Optional: information about the logged-in user (when using an
    // Express.js authentication middleware that defines `req.user`).
    user: req.user
 
    // ... we can provide any additional information about the request here ...
  }
 
  const pageContext = await renderPage(pageContextInit)
 
  const { body, statusCode, headers } = pageContext.httpResponse
  headers.forEach(([name, value]) => {
    // There can be several Set-Cookie headers: res.setHeader() would only keep the last one
    if (name.toLowerCase() === 'set-cookie') res.appendHeader(name, value)
    else res.setHeader(name, value)
  })
  res.status(statusCode).send(body)
})
 
const port = 3000
app.listen(port)
console.log(`Server running at http://localhost:${port}`)
# Development: Vike runs your server with Vite and restarts it whenever you modify it
$ vike dev
# Build: +serverEntry.ts => dist/server/index.mjs
$ vike build
# Production
$ node dist/server/index.mjs

The pageContext.httpResponse.body value is the HTML string returned by the onRenderHtml() hook with additional <script> and <style> tags automatically injected by Vike.

You can control where and what Vike injects using API > injectFilter().

For HTML streams use httpResponse.pipe() instead of pageContext.httpResponse.body, see HTML Streaming. Same for binary and streamed non-HTML content.

Optionally, you can use pageContext.httpResponse.earlyHints for adding early hints (103 Early Hint), see Guides > Preloading > Early hints.

The renderPage() function doesn't depend on Node.js and you can use renderPage() (and therefore embed Vike) anywhere:

  • Any server environment (Express.js, Hattip, Deno, Fastify, Vite's development server, Node.js's HTTP server, ...)
  • Any deployment provider (AWS, Cloudflare Workers, Vercel, ...)

renderPage() vs vike(app)

There are two fundamental differences:

  • vike(app) supports and automatically installs +middleware, while renderPage() doesn't support +middleware.

    This means that, if you use a Vike extension that uses +middleware, then use vike(app) instead of renderPage().

    In principle, it's possible to use renderPage() together with +middleware, but we don't recommend it.

  • vike(app) is zero-config while renderPage() is lower-level.
vike(app)renderPage()
+middleware✅❌
ServersHono, Express, Fastify, H3, ElysiaAny
IntegrationOne lineYour own catch-all route
Custom pageContext propertiesWith +onCreatePageContextPassed to renderPage()
pageContext.req and pageContext.resSet for youPassed to renderPage() (if needed)
HTTP responseSent for youSent by you

vike(app) and renderPage() are interchangeable: you can use either one, regardless of whether you use +server.js, +serverEntry.js, or run your server directly (e.g. $ node server/index.js).

That said, if you use a Vike extension that uses +middleware, you cannot (easily) use renderPage() — you must use vike(app) (or use globalContext.middlewares if your server doesn't have a vike(app) adapter).

See also:

Without +serverEntry.js

You can also run your server yourself (e.g. $ node server/index.js) instead of defining +server.js/+serverEntry.js.

Unless you have a reason, we generally recommend using +server.js/+serverEntry.js as it provides a better development experience:

+server.js/+serverEntry.js$ node server/index.js
Server code (1)Processed by ViteNot processed by Vite
Server restart (2)Fine-grainedCoarse (unnecessary reloads)
  1. Vite processes your server code: import.meta.env is available (e.g. import.meta.env.DEV and the variables of your .env files), and so are path aliases and Vite plugins.

    While modern runtimes such as Bun and recent Node.js versions can run TypeScript, they don't apply your Vite configuration.

  2. Vike restarts your server process only when a file imported by +serverEntry.js changes: modifying a page doesn't restart your server (the page is updated with HMR).

    If you run the server yourself, you'll need a watcher (e.g. node --watch or nodemon) which doesn't know about Vite's module graph.

// server/index.js
 
import express from 'express'
import { fileURLToPath } from 'node:url'
import { createDevMiddleware } from 'vike/server'
 
const app = express()
 
if (process.env.NODE_ENV === 'production') {
  app.use(express.static(fileURLToPath(new URL('../dist/client', import.meta.url))))
} else {
  // Vite's development middleware (HMR, transpiling, static assets, ...)
  const root = fileURLToPath(new URL('..', import.meta.url))
  const { devMiddleware } = await createDevMiddleware({ root })
  app.use(devMiddleware)
}
 
// ...
// Other middlewares and the SSR middleware, as in the example above
// ...
 
app.listen(3000)

See also:

See also