Skip to content

Astro SSR

Copy it into a new project:

Terminal window
pnpm dlx degit antstanley/blogwright/examples/astro-ssr my-site

Astro with @astrojs/node in standalone mode, running on a Lambda Web Function. Fingerprinted /_astro/ assets still serve from S3.

Framework Mode Blogwright config Source
Astro + @astrojs/node Server-rendered ssr examples/astro-ssr
  1. Copy the example and start a repository:

    Terminal window
    pnpm dlx degit antstanley/blogwright/examples/astro-ssr my-site
    cd my-site && git init
  2. Set siteName and region in config/production.jsonc. For SSR, region must be us-east-1, us-east-2, us-west-2, ap-northeast-1 or eu-west-1.

  3. Add Blogwright, then commit everything, including pnpm-lock.yaml - the builder installs with --frozen-lockfile:

    Terminal window
    pnpm add -D blogwright
    git add -A && git commit -m "Start from the blogwright example"
  4. Create the infrastructure: pnpm exec blogwright bootstrap

  5. Build and publish: pnpm exec blogwright deploy

  • config/production.jsonc - packages all of dist/, not dist/server/. @astrojs/node finds its files by walking up to a directory named server, and fails to start when the entry sits at the package root, so entryPoint is server/lambda.mjs.
  • astro.config.mjs - output: 'server', the Node adapter in standalone mode, and vite.ssr.noExternal: true, which bundles every dependency into dist/server so the package needs no node_modules.
  • site in astro.config.mjs is the public origin. The app receives the endpoint’s domain as Host, so build absolute URLs from Astro.site.
  • lambda.mjs - the entry shim. It maps AWS_LAMBDA_HTTP_ENDPOINT onto HOST and PORT, then imports entry.mjs. The build copies it into dist/server/.
config/production.jsonc
// Blogwright config for this example. Paths are relative to the repository
// root, so they assume this directory IS the repository - see README.md.
{
"region": "us-east-1",
"siteName": "astro-ssr",
"ssr": {
// The whole of dist/: @astrojs/node finds its files by walking up to a
// directory named "server", so the package must keep dist/'s layout.
"packageDir": "dist",
"entryPoint": "server/lambda.mjs",
"staticDir": "dist/client",
"staticPaths": ["/_astro/*", "/favicon.svg"],
},
}
astro.config.mjs
import node from '@astrojs/node';
import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
// The app sees the Web Function endpoint's domain as Host, so absolute URLs
// are built from `site` (Astro.site), not Astro.url.
site: 'https://example.com',
vite: {
// Bundle every dependency into dist/server, so the package Blogwright
// zips is self-contained: Lambda loads nothing outside it.
ssr: { noExternal: true },
},
});
lambda.mjs
// Entry shim for Lambda Web Functions. Lambda passes the listen address in
// AWS_LAMBDA_HTTP_ENDPOINT; Astro's standalone server reads HOST and PORT.
const endpoint = process.env.AWS_LAMBDA_HTTP_ENDPOINT;
const colon = endpoint.lastIndexOf(':');
process.env.HOST = endpoint.slice(0, colon);
process.env.PORT = endpoint.slice(colon + 1);
await import('./entry.mjs');
package.json
{
"name": "blogwright-example-astro-ssr",
"private": true,
"type": "module",
"packageManager": "pnpm@11.25.0",
"scripts": {
"dev": "astro dev",
"build": "astro build && cp lambda.mjs dist/server/",
"preview": "astro preview"
},
"devDependencies": {
"@astrojs/node": "^11.1.7",
"astro": "^7.3.8"
}
}

Run it the way Lambda does - from a copy outside the project, so nothing resolves from its own node_modules:

Terminal window
pnpm build
cp -R dist /tmp/pkg && cd /tmp/pkg
AWS_LAMBDA_HTTP_ENDPOINT=127.0.0.1:8080 node server/lambda.mjs
curl -i http://127.0.0.1:8080/

The server must start listening within 10 seconds, and find dist -type l must print nothing: the build agent rejects a package that contains a symbolic link.