Edit

vike-photon

Version history: CHANGELOG.md
Examples: examples/
Source code: GitHub > vikejs/vike-photon

The vike-photon extension integrates Vike with any JavaScript server (Express.js, Hono, Fastify, ...) and deployment provider (Cloudflare, Vercel, self-hosting, ...) in a seamless fashion. It's powered by Photon.

The entire documentation for using Photon is contained on this page, Deploy > Cloudflare and Deploy > Vercel.

Photon is currently in beta — you can use it in production, but expect breaking changes more frequently than usual.

Alternatively, instead of using a server, you can pre-render your pages and deploy to a static host.

If you want more control over server integration, see Integration > Server integration > Manual integration.

Get started

New app

Use vike.dev/new to scaffold a new Vike app that uses Photon.

Add to existing app

If you're using vike-server then migrate to vike-photon.

This guide is for self-hosted servers. For deploying to Cloudflare or Vercel, see:

1. Install

npm i vike-photon
// pages/+config.js
 
import vikePhoton from 'vike-photon/config'
 
export const config = {
  extends: [vikePhoton] 
}
// pages/+config.ts
 
import type { Config } from 'vike/types'
import vikePhoton from 'vike-photon/config'
 
export const config = {
  extends: [vikePhoton]
} satisfies Config

2. Scripts

Update your production script in package.json:

// package.json
 
"scripts": {
  "dev": "vike dev",
  "build": "vike build",
  "prod": "vike build && node ./dist/server/index.mjs"
}

3. Server (optional)

By default, Photon uses a built-in server that supports basic features like SSR. If you need additional server functionalities (e.g. file uploads or API routes), then create your own server.

Server

By default, Photon uses a built-in server that supports basic features like SSR. If you need additional server functionalities (e.g. file uploads or API routes), then create your own server:

// pages/+config.js
 
import vikePhoton from 'vike-photon/config'
 
export default {
  extends: [vikePhoton],
  photon: {
    server: 'server/index.js'
  } 
}
// pages/+config.ts
 
import type { Config } from 'vike/types'
import vikePhoton from 'vike-photon/config'
 
export default {
  extends: [vikePhoton],
  photon: {
    server: 'server/index.ts'
  }
} satisfies Config
npm i hono @photonjs/hono
// server/index.js
 
import { Hono } from 'hono'
import { apply, serve } from '@photonjs/hono'
 
function startServer() {
  const app = new Hono()
  apply(app)
  return serve(app)
}
 
export default startServer()
// server/index.ts
 
import { Hono } from 'hono'
import { apply, serve } from '@photonjs/hono'
 
function startServer() {
  const app = new Hono()
  apply(app)
  return serve(app)
}
 
export default startServer()
⚠️

Photon's Cloudflare integration (@photonjs/cloudflare) currently only supports Hono and Hattip. (Support for more servers is work-in-progress.)

Where:

  • apply() installs the middleware of Vike and Vike extensions.
  • serve() attaches your server to Node.js, Cloudflare, Netlify, Vercel, Deno, Bun, ...

    See serve() for a list of options.

Note that:

  • Vike is automatically added to your server — no need to manually integrate renderPage().
  • Some Vike extensions may also automatically add server middlewares.
  • Static files are automatically served.

Deployment

Node/Bun/Deno

In production, run $ node ./dist/server/index.mjs (or Bun/Deno).

Vercel

See Deploy > Vercel.

Cloudflare

See Deploy > Cloudflare.

serve()

The serve() function enables you to define a single server entry while being able to target multiple environments (e.g. Node.js and Cloudflare have different server attachment styles).

serve(app, {
  // Server port, defaults to 3000. It's ignored in Cloudflare and Vercel Edge (there
  // isn't any server in serverless deployment).
  port: 3000,
 
  hostname: 'localhost', // default value
 
  // Called once the server is accepting connections
  onReady() {
    console.log('Server is ready.')
  },
 
  // Called once the server is created
  onCreate(server) {
    // `server` type depends on your runtime:
    //   Node.js:  Server ('node:http') by default. It's an HTTPS or HTTP2 server
    //             if the `createServer` option was provided (see below).
    //   Deno:     return of Deno.Serve (experimental support)
    //   Bun:      return of Bun.Serve  (experimental support)
    // `server` is `undefined` in Cloudflare and Vercel Edge (there
    // isn't any server in serverless deployment)
  },
  // ⚠️  The following two options are available only when running on Node.js
  // [Node.js] Can be one of:
  //     import { createServer } from 'node:http'
  //     import { createServer } from 'node:https'
  //     import { createSecureServer as createServer } from 'node:http2'
  createServer, // [Node.js] Options forwarded to `createServer()`
  serverOptions: {
    // For example SSL/TLS key and certificate for HTTPS
    key: fs.readFileSync('/etc/letsencrypt/live/example.com/privkey.pem'),
    cert: fs.readFileSync('/etc/letsencrypt/live/example.com/fullchain.pem')
    // ... other createServer() options ...
  },
 
  // 👉 Other options are passed down as-is to the target environment
 
  // For example, you can define all @hono/node-serve options here, such as:
  fetch,
  overrideGlobalObjects: false
 
  // ... any options of your target environment ...
})
serve(app, {
  // Server port, defaults to 3000. It's ignored in Cloudflare and Vercel Edge (there
  // isn't any server in serverless deployment).
  port: 3000,
 
  hostname: 'localhost', // default value
 
  // Called once the server is accepting connections
  onReady() {
    console.log('Server is ready.')
  },
 
  // Called once the server is created
  onCreate(server) {
    // `server` type depends on your runtime:
    //   Node.js:  Server ('node:http') by default. It's an HTTPS or HTTP2 server
    //             if the `createServer` option was provided (see below).
    //   Deno:     return of Deno.Serve (experimental support)
    //   Bun:      return of Bun.Serve  (experimental support)
 
    // `server` is `undefined` in Cloudflare and Vercel Edge (there
    // isn't any server in serverless deployment)
  },
 
 
  // ⚠️  The following two options are available only when running on Node.js
 
  // [Node.js] Can be one of:
  //     import { createServer } from 'node:http'
  //     import { createServer } from 'node:https'
  //     import { createSecureServer as createServer } from 'node:http2'
  createServer,
 
  // [Node.js] Options forwarded to `createServer()`
  serverOptions: {
    // For example SSL/TLS key and certificate for HTTPS
    key: fs.readFileSync('/etc/letsencrypt/live/example.com/privkey.pem'),
    cert: fs.readFileSync('/etc/letsencrypt/live/example.com/fullchain.pem'),
    // ... other createServer() options ...
  },
 
 
  // 👉 Other options are passed down as-is to the target environment
 
  // For example, you can define all @hono/node-serve options here, such as:
  fetch,
  overrideGlobalObjects: false,
 
  // ... any options of your target environment ...
})

pageContext.runtime

You can use pageContext.runtime to access the HTTP request object, for example:

// pages/product/@id/+data.js
 
export async function data(pageContext) {
  // Using pageContext.runtime.req to access the logged-in user
  const { user } = pageContext.runtime.req
  // ...
}
// pages/product/@id/+data.ts
 
import type { PageContextServer } from 'vike/types'
 
export type Data = Awaited<ReturnType<typeof data>>
export async function data(pageContext: PageContextServer) {
  // Using pageContext.runtime.req to access the logged-in user
  const { user } = pageContext.runtime.req
  // ...
}
 
declare global {
  namespace Vike {
    interface Photon {
      // 👉 Pick your server (for correct pageContext.runtime type)
      server: 'hono' // | 'express' | 'fastify' | 'hattip' | 'srvx' | 'elysia' | 'h3'
    }
  }
}

For convenience, you can define pageContext.user as an alias of pageContext.runtime.req.user — see Integration > Authentication > pageContext.user.

See also:

Standalone

With standalone: true, the build output directory (dist/) contains everything needed for deployment. This means that, in production, only the dist/ directory is required (you can remove node_modules/ and skip $ npm install).

Skipping node_modules/ massively reduces disk usage in production, which can be important, for example to reduce the size of Docker images.

// +config.js
 
import vikePhoton from 'vike-photon/config'
 
export const config = {
  // ...
  extends: [vikePhoton],
  photon: {
    server: {
      id: 'server/index.js'
    },
    standalone: true
  }
}
// +config.ts
 
import type { Config } from 'vike/types'
import vikePhoton from 'vike-photon/config'
 
export const config = {
  // ...
  extends: [vikePhoton],
  photon: {
    server: {
      id: 'server/index.ts'
    },
    standalone: true
  }
} satisfies Config

Or with options:

export const config = {
  photon: {
    standalone: {
      // Default values
      bundle: false, // true => bundle output (usually a single file)
      minify: false, // true => minify output
      trace: true, // false => disable dependency tracing
      external: [] // List of packages to exclude from bundling
    }
  }
}
import type { Config } from 'vike/types'
 
export const config = {
  photon: {
    standalone: {
      // Default values
      bundle: false, // true => bundle output (usually a single file)
      minify: false, // true => minify output
      trace: true, // false => disable dependency tracing
      external: [] // List of packages to exclude from bundling
    }
  }
} satisfies Config

With bundle: true and minify: true you can significantly reduce size, but we recommend against this because it makes debugging more difficult (stack traces are harder to read). Adding source maps doesn't solve the issue, because dist/ will now include source map files and may even end up larger.

It's an advanced feature, more powerful than just using a bundler.

If a package throws ENOENT: no such file or directory then either add it to external or ask the package maintainers to fix the issue.

The issue is that some packages (only few in our experience) reference files in a dynamic fashion that isn't statically analyzable and, consequently, cannot be included in the standalone bundle.

// ❌ Photon cannot trace the file reference:
fs.readFile('hello.' + someCondition ? 'js' : 'wasm')
 
// ✅ Photon can trace this:
someCondition ? fs.readFile('hello.js') : fs.readFile('hello.wasm')

HMR

If you change a server file, the server code is automatically updated: the next HTTP response will be generated by the latest code. No full server reload is required.

This is experimental and doesn't always work: vike-photon sometimes still triggers a full server reload.

If HMR isn't what you want (for example if you modify the database connection) you can manually trigger a full server reload by pressing r + enter.

Compression

In production, Photon compresses all Vike responses.

You can disable it, or enable it only for static files:

// pages/+config.js
 
import vikePhoton from 'vike-photon/config'
 
export default {
  extends: [vikePhoton],
  photon: {
    // Disable compression
    compress: false
    // Only compress static files
    compress: 'static'
  } 
}
// pages/+config.ts
 
import type { Config } from 'vike/types'
import vikePhoton from 'vike-photon/config'
 
export default {
  extends: [vikePhoton],
  photon: {
    // Disable compression
    compress: false,
    // Only compress static files
    compress: 'static'
  }
} satisfies Config

Static files

In production, Photon uses @universal-middleware/sirv to serve static files.

In development, static files are served by Vite's development server.

You can disable it, or set options:

// pages/+config.js
 
import vikePhoton from 'vike-photon/config'
 
export default {
  extends: [vikePhoton],
  photon: {
    // Don't serve static files
    static: false
    // Settings
    static: { maxAge: 3600, /* ... */ }
  } 
}
// pages/+config.ts
 
import type { Config } from 'vike/types'
import vikePhoton from 'vike-photon/config'
 
export default {
  extends: [vikePhoton],
  photon: {
    // Don't serve static files
    static: false,
    // Settings
    static: { maxAge: 3600, /* ... */ }
  }
} satisfies Config

All sirv options are supported.

HTTPS

If you want to use HTTPS in development, pass createServer and the HTTPS certificates to serve():

import { createServer } from 'node:https'
/* Or with HTTP2:
import { createSecureServer as createServer } from 'node:http2'
*/
 
serve(app, {
  createServer,
 
  // [Node.js] Options forwarded to `createServer()`
  serverOptions: {
    // For example SSL/TLS key and certificate for HTTPS
    key: fs.readFileSync('/etc/letsencrypt/live/example.com/privkey.pem'),
    cert: fs.readFileSync('/etc/letsencrypt/live/example.com/fullchain.pem')
    // ... other createServer() options ...
  }
})
import { createServer } from 'node:https'
/* Or with HTTP2:
import { createSecureServer as createServer } from 'node:http2'
*/
 
serve(app, {
  createServer,
 
  // [Node.js] Options forwarded to `createServer()`
  serverOptions: {
    // For example SSL/TLS key and certificate for HTTPS
    key: fs.readFileSync('/etc/letsencrypt/live/example.com/privkey.pem'),
    cert: fs.readFileSync('/etc/letsencrypt/live/example.com/fullchain.pem')
    // ... other createServer() options ...
  },
})

See also