Configuration reference
blogwright reads one JSONC config file per environment. Only siteName is required - everything else has a default or is optional. This page lists every key in the schema: its type, default, validation rules, and what it controls.
Config files
Section titled “Config files”Config is loaded from config/<env>.jsonc at the repo root of the consuming site, falling back to ops.config.jsonc. Pass --config <path> to use an explicit file instead - it then becomes the only candidate. The repo root is found by walking up from the invocation directory to the nearest .git or .jj, so the CLI works from any subdirectory.
The environment defaults to production; pass another as the positional [env] or --env (see Environments). If no candidate file exists for the environment, the CLI errors and lists the paths it looked for.
Files are JSONC: // line comments, /* */ block comments, and trailing commas are all accepted.
The minimal config:
{ "region": "us-east-1", "siteName": "example", // required - the stable slug in every AWS resource name}blogwright init writes this file for you interactively. Without a TTY, write it by hand - the two lines above are enough to bootstrap and deploy.
How overrides merge
Section titled “How overrides merge”Your file is merged over the defaults. The nested objects microvm (including microvm.idle), retention, seo, and paths merge key-by-key, so you can override a single nested field and keep the rest of the defaults. Scalars and arrays replace their default outright - setting sourceIgnore (or cacheControl) replaces the default list, it does not extend it. Blocks with no default - ssr, aws and headers - are taken from each environment’s file as written: an environment without the block gets none of it.
Core identity
Section titled “Core identity”siteName
Section titled “siteName”string · required
Stable slug used in every derived AWS resource name. Must be lowercase alphanumeric/dashes (^[a-z0-9-]+$). The per-environment S3 bucket is named <env>-<siteName>-<accountId> and must fit S3’s 63-character limit - the CLI rejects the combination otherwise, with a pointer to shorten the environment or site name.
region
Section titled “region”string · optional · default: "us-east-1"
Primary AWS region for S3, the builder MicroVM, and CloudWatch logs. ACM certificates and CloudFront resources are always managed in us-east-1 regardless of this setting (CloudFront requires it), including the CloudFront access-log group.
domain
Section titled “domain”string · optional · no default
Custom domain for the site. May also be supplied per-invocation with --domain, which takes precedence over the config value. When set, bootstrap requests an ACM certificate and prints the validation CNAMEs - see Custom domains. When unset, the site is served from its CloudFront domain.
aliases
Section titled “aliases”string[] · default []
Additional hostnames that serve the site alongside domain, which stays the primary (canonical) name: an apex plus its www., or unrelated domains such as example.org. An alias need not be a subdomain of the primary and may live in a different DNS zone or at a different registrar. Requires domain (from the config or --domain); not valid for the preview environment. Each entry is a lowercase hostname with at least two labels - no scheme, port, path or wildcard - and none repeats domain or another alias. At most 10 hostnames in all (domain plus aliases), the default ACM per-certificate name limit.
{ "domain": "example.com", "aliases": ["www.example.com", "example.org"],}One ACM certificate covers every name, and the distribution lists each as an alias. Removing an entry never detaches it on its own: see Multiple domains.
redirectToPrimary
Section titled “redirectToPrimary”boolean · default false
By default every hostname serves the same content and the site’s canonical URLs identify the primary. When true, a request for an alias answers 301 with the same path and query on https://<domain>. The redirect runs in the CloudFront routing function, so it does not reach the origin. On a server-rendered site (ssr) the same redirect runs as a redirect-only function attached to every behavior; the application’s own routing is untouched. Not valid for the preview environment.
redirects
Section titled “redirects”object[] · default []
Path redirects answered by the CloudFront routing function with a real HTTP redirect, ahead of the directory-index rewrite. Each entry has from, to and an optional status (301 by default; 302, 307 and 308 are also allowed).
{ "redirects": [ { "from": "/about.php", "to": "/about/" }, { "from": "/old-blog/*", "to": "/news/:splat", "status": 301 }, ],}frombegins with/; a trailing slash is not significant, so/amatches requests for/aand/a/. Matching is case-sensitive, against the path as the viewer sent it (percent-encoded, not decoded). Afromending in/*is a prefix match: the rest of the path replaces:splatinto(valid once, only in a prefix redirect). An exactfrombeats a prefix, and the longest prefix wins.tois a path on the same host (one leading/) or an absolutehttps://URL.fromandtoare printable ASCII with no whitespace: write anything else percent-encoded. Neither may hold a fragment.- The request’s query string is carried over; if
tohas its own query, the request’s is appended with&. - A relative
toanswers a relativelocationon the host that was requested. WithredirectToPrimary, an alias request whose path has a redirect reaches the final URL on the primary in one hop. - Rejected at config load or context creation, before any AWS call: bad shapes, duplicate
from, a redirect to itself, a two-entry loop, a prefix redirect whosetofalls under its own prefix, more than 500 entries, and a map whose compiled router function exceeds CloudFront’s 10,240-byte limit. Longer cycles are not detected.
bootstrap and deploy publish the function when the compiled source changes; an unchanged set changes nothing. deploy publishes after the new content is live. An SSR site gets the redirect-only function attached by bootstrap; deploy warns if that has not happened yet. rollback does not touch redirects, because they belong to the environment’s config. The preview environment rejects redirects: the shared preview router does not apply them.
manageDns
Section titled “manageDns”boolean · default false
When true, bootstrap creates the ACM validation records and the A/AAAA alias records that point each hostname at the distribution, for every hostname whose hosted zone is in the account’s Route 53. Hostnames with no zone in the account are printed as guidance instead, so one site can mix Route 53 and registrar-hosted names. Not valid for the preview environment, which always uses Route 53.
string · optional · no default
Value of the app tag applied to every AWS resource blogwright creates (alongside an environment tag). When unset it falls back to the domain, then to the repo directory name - always something a human can trace back to the project from a billing or resource listing. Site files get the same tags as S3 object tags.
githubRepo
Section titled “githubRepo”string · optional · no default
GitHub owner/repo, used to scope the trust policy of the GitHub OIDC deploy roles that bootstrap (role <env>-<siteName>-gh) and preview bootstrap (role preview-<siteName>-gh) provision. When unset, bootstrap skips the OIDC role; preview bootstrap always creates one, so the preview stack requires this key. See CI deploys with GitHub OIDC and PR previews.
object · optional · no default
Which AWS credentials this environment uses, and which account it accepts. Both keys are optional, and the block is left out entirely when neither is set. Unknown keys are refused.
"aws": { "profile": "mysite-prod", "accountId": "123456789012" // deploys refuse credentials for any other account}aws.profile
Section titled “aws.profile”string · optional · no default
A named profile from your shared AWS files (~/.aws/config and ~/.aws/credentials, or AWS_CONFIG_FILE and AWS_SHARED_CREDENTIALS_FILE). Every command resolves credentials in this order, and the first match wins:
--profile <name>AWS_PROFILE- Credentials exported into the environment -
AWS_ACCESS_KEY_IDwithAWS_SECRET_ACCESS_KEY,AWS_WEB_IDENTITY_TOKEN_FILE, or a container credentials URI - as CI exports them aws.profile- The default AWS provider chain
So a committed aws.profile does not break CI that exports credentials: exported credentials win over it - only the exported credentials are used, never a [default] profile from your AWS files - and the command says so (ignoring aws.profile "...": environment credentials are set). A runner that uses an EC2 instance role exports nothing, so there the profile still applies and is refused if the runner’s AWS files do not define it; export the instance credentials first with eval "$(aws configure export-credentials --format env)". A named profile that your AWS files do not define stops the command before any AWS call, listing the profiles that do exist. Only the profile’s credentials are used - never its region; the deploy region is always region.
aws.accountId
Section titled “aws.accountId”string · optional · no default
The only AWS account this environment’s commands accept: exactly 12 digits, quoted (a JSON number would drop leading zeros, so a number is refused). Every command calls sts:GetCallerIdentity before it loads any state and refuses credentials for any other account:
config/production.jsonc pins AWS account 123456789012, but the credentials in usebelong to 210987654321 (AdminRole, profile "personal" from AWS_PROFILE).Switch credentials with --profile, or change aws.accountId.Because the check comes before the state bucket (<env>-<siteName>-<accountId>) is read, the wrong credentials can never make status report an empty stack or bootstrap build a second one in the wrong account. There is no flag to bypass the pin. Under an endpoint override (--endpoint or AWS_ENDPOINT_URL) the check is skipped, because an emulator answers with its own account. blogwright init can detect and write this for you; blogwright whoami shows whether the current credentials match.
MicroVM builder
Section titled “MicroVM builder”Sizing and lifecycle of the Lambda MicroVM that runs pnpm install && pnpm build on every deploy.
{ "microvm": { "memory": 4, "maxDurationSeconds": 1800, "idle": { "autoResumeEnabled": false, "maxIdleDurationSeconds": 300, "suspendedDurationSeconds": 120, }, },}microvm.memory
Section titled “microvm.memory”number · optional · default: 4
Builder memory in GB. Must be one of 0.5, 1, 2, 4, 8. Applied to the builder image (converted to a minimum-MiB resource setting), so changing it takes effect through the image, not per-deploy.
microvm.maxDurationSeconds
Section titled “microvm.maxDurationSeconds”number · optional · default: 1800
Maximum run duration of a builder MicroVM, in seconds. Must be between 1 and 28800 (8 hours). Passed to each launch as its maximum duration; the CLI’s own wait deadline for a build is this value plus a 60-second grace period, anchored at VM launch.
microvm.idle
Section titled “microvm.idle”object · optional
Idle policy passed verbatim to each builder MicroVM launch:
autoResumeEnabled-boolean, defaultfalse. Whether the service may automatically resume a suspended MicroVM.maxIdleDurationSeconds-number, default300. How long the MicroVM may sit idle.suspendedDurationSeconds-number, default120. How long it stays suspended.
Log retention
Section titled “Log retention”CloudWatch log-group retention, in days. Both values must be positive.
retention.microvmDays
Section titled “retention.microvmDays”number · optional · default: 365
Retention for the builder MicroVM log group (/aws/lambda/microvms/<env>-<siteName>-builder), which holds the build logs that blogwright logs <hash> reads.
retention.cloudfrontDays
Section titled “retention.cloudfrontDays”number · optional · default: 90
Retention for the CloudFront access-log group (/<siteName>/<env>/cloudfront). This log group always lives in us-east-1, because CloudFront’s vended log delivery only exists there.
retention.webFunctionDays
Section titled “retention.webFunctionDays”number · optional · default: 90
Retention for the two log groups an SSR environment’s Web Function writes to (/<siteName>/<env>/web and its in-Region companion /<siteName>/<env>/web.local). Inert when ssr is absent.
Deploy source and invalidation
Section titled “Deploy source and invalidation”Each deploy zips the repo (honoring .gitignore), builds it in the MicroVM, syncs the output to S3, and invalidates only the CloudFront paths that actually changed.
sourceIgnore
Section titled “sourceIgnore”string[] · optional · default: [".jj/", ".git/", "node_modules/", "dist/", ".astro/"]
Extra path prefixes excluded from the source zip, on top of what .gitignore already excludes. Setting this replaces the default list - include the defaults you still want.
sourceInclude
Section titled “sourceInclude”string[] · optional · default: []
Paths zipped into the deploy source even when gitignored - for artifacts a pre-deploy step builds outside the MicroVM (a wasm bundle, generated assets). Each entry must exist and be non-empty at deploy time, so a forgotten pre-build fails fast instead of shipping a broken site. Entries must be repo-relative, with no leading / and no .. segments.
{ "sourceInclude": ["web/src/lib/pkg/"], // built by CI before `blogwright deploy`}See Monorepos for the pattern this supports.
invalidationMaxPaths
Section titled “invalidationMaxPaths”number · optional · default: 1000
If a deploy changes more CloudFront paths than this, the CLI invalidates /* instead of listing every path. Deploys that change fewer paths invalidate only the changed ones; an identical redeploy invalidates nothing.
CloudFront serving
Section titled “CloudFront serving”defaultRootObject
Section titled “defaultRootObject”string · optional · default: "index.html"
The CloudFront distribution’s default root object - the object served for requests to the bare origin root.
boolean · optional · default: false
Single-page app mode: CloudFront serves /index.html with a 200 for unknown paths (client-side routing) instead of the 404 page. Applies to the main distribution at creation. Previews are unaffected - the shared preview distribution’s error responses cannot be host-routed. See Single-page apps.
Cache-Control (cacheControl)
Section titled “Cache-Control (cacheControl)”{ "match": string | string[], "value": string }[] · optional · default: the two rules below
The Cache-Control header the builder writes on each file it uploads, chosen by the file’s site-relative URL path (/index.html, /_astro/app.1a2b.js), never its S3 key - so one list serves production and previews. Rules are checked in order and the first match wins; a file no rule matches is uploaded without Cache-Control. Setting the key replaces the defaults outright, so end your list with a ** catch-all; [] writes no header at all.
{ "cacheControl": [ // The default rules { "match": ["/_astro/**", "/_app/immutable/**"], "value": "public, max-age=31536000, immutable" }, { "match": "**", "value": "public, max-age=0, s-maxage=86400, must-revalidate" }, ],}match- one pattern or a non-empty list. A pattern starts with/(or**) and matches a whole path:*and?stay within one path segment, and**- which must be a whole segment - spans any number of them (**/*.htmlmatches/index.htmltoo). Match a directory URL through itsindex.html.value- the header value, verbatim: comma-separated directives such aspublic, max-age=0, s-maxage=604800, must-revalidate.
Validation rejects, at load, a malformed rule or value and any pattern that can never match: one that cannot match a built file’s path (no leading / or **, a trailing /, an empty segment, a . or .. segment, a percent-encoded character such as %20, ** inside a segment, or the unsupported […], {…} and \), and one an earlier rule always matches first - a repeated pattern, anything after ** or **/*, anything under an earlier literal /<dir>/** or /<dir>/**/*, or a literal path an earlier pattern matches. A pattern that an earlier pattern of the same rule already covers, by the same shapes, is rejected as redundant. Patterns match the decoded path, so write non-ASCII names as themselves (/blog/café/*.html).
When the rules change, the next build to each target re-uploads every file (an implicit --refresh) so unchanged files carry the new header too. Browser (max-age) versus edge (s-maxage) lifetimes, the trade-offs of a long s-maxage, and how error responses are cached are covered in Caching and Cache-Control.
Response headers (headers)
Section titled “Response headers (headers)”object · optional · no default
HTTP response headers CloudFront adds to every response: security headers such as Content-Security-Policy and Strict-Transport-Security, plus any other header you name. These cannot come from the site itself - frame-ancestors is ignored in a <meta http-equiv> Content-Security-Policy, and HSTS is only honoured as a real header - so blogwright manages a CloudFront response headers policy named <env>-<siteName>-headers and attaches it to every cache behavior of the environment’s distribution. Pages, directory routes and hashed assets are all served through those behaviors, so they carry the headers. The 404 page is served as a custom error response of the same default behavior; CloudFront is expected to add the policy’s headers to it too, but see what has not been verified.
{ "headers": { "security": { "hsts": true, // or { "maxAgeSeconds": 63072000, "includeSubdomains": true, "preload": true } "contentTypeOptions": true, // X-Content-Type-Options: nosniff "frameOptions": "deny", "referrerPolicy": "no-referrer", "contentSecurityPolicy": "default-src 'self'; frame-ancestors 'none'; object-src 'none'", }, "custom": [ { "name": "Cross-Origin-Opener-Policy", "value": "same-origin" }, { "name": "Cross-Origin-Resource-Policy", "value": "same-origin" }, { "name": "Permissions-Policy", "value": "accelerometer=(), camera=(), geolocation=()" }, ], },}| Field | Header | Validation | Default |
|---|---|---|---|
security.hsts |
Strict-Transport-Security |
true, false, or an object of maxAgeSeconds (integer 0-2147483647), includeSubdomains and preload (booleans) |
absent; true means max-age=31536000, no includeSubDomains, no preload |
security.contentTypeOptions |
X-Content-Type-Options: nosniff |
boolean | false |
security.frameOptions |
X-Frame-Options |
deny or sameorigin |
absent |
security.referrerPolicy |
Referrer-Policy |
no-referrer, no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, or unsafe-url |
absent |
security.contentSecurityPolicy |
Content-Security-Policy |
non-empty string, no control characters | absent |
custom |
any other header | array of { "name", "value" }: names are HTTP header names (RFC 7230 tokens) of at most 256 characters, with no case-insensitive duplicates and none of the five security headers above; values are strings of at most 1,783 characters with no control characters, and may be empty (the header is sent with no value); names and values together at most 10,240 characters |
[] |
Unknown keys are refused at every level, and "headers": null is refused like any other non-object block, as for ssr and aws: to have no headers, leave the key out. The limits checked when the config loads are CloudFront’s fixed ones. Two of CloudFront’s limits are adjustable quotas, so they are not checked at load, because your account may have raised them: at most 10 custom headers and 1,783 characters of Content-Security-Policy by default. A policy over your account’s quota fails at bootstrap or deploy with an error that names the quota; shorten the config, or request a higher quota in Service Quotas.
Every header overrides a header of the same name from the origin, so the configured value is the one viewers receive - including over a header an SSR application sets itself.
How changes apply. bootstrap reconciles the whole policy: it creates the policy and attaches it to the distribution’s cache behaviors, updates it in place when the config changes, and - when the headers key is removed, or the block adds no header at all - detaches it and deletes it. None of this needs a new distribution, and no invalidation: CloudFront adds the headers as each response leaves the edge, cached responses included. If a run was interrupted after CloudFront created the policy but before blogwright recorded it, the next bootstrap finds the policy by its name and adopts it.
deploy keeps an attached policy’s headers in step with the config before it builds, and promote before it copies, so adding, changing or removing a header takes effect on the next deploy, promote or bootstrap, as long as at least one header remains. It updates the policy only, never the distribution. Three changes need bootstrap, and a deploy that finds one pending warns and names the command to run:
- Adding
headersfor the first time: the policy has to be created and attached. - A policy that a bootstrap created but did not attach, for example because the bootstrap stopped with an error.
deployknows this from whatbootstraprecorded in state, so it makes no extra AWS call; a policy detached by hand in the console is not detected. - Removing the key, or removing the last header so the block adds nothing: the policy has to be detached and deleted.
Updating the policy needs cloudfront:GetResponseHeadersPolicy and cloudfront:UpdateResponseHeadersPolicy, which the GitHub OIDC deploy role carries for this policy only.
When the key is removed, CloudFront may refuse the delete because the policy is still in use. If that is only because CloudFront has not yet caught up with the detach, blogwright records that the delete is pending and the next bootstrap finishes it. If another distribution also uses the policy, blogwright says so once, names the distribution, and stops managing the policy; detach and delete it in the CloudFront console if nothing needs it.
Policies attached by hand. A cache behavior takes only one response headers policy.
- On a static site, blogwright leaves a policy you attached by hand alone while
headersis unset. Onceheadersis set,bootstraprefuses to replace it: the error names the policy and the cache behavior. Move the headers you need intoheaders, detach the old policy in the CloudFront console, and runbootstrapagain. Blogwright refuses rather than replaces because a replaced policy would be lost for good: nothing would restore it ifheaderswere removed later. - In SSR mode blogwright reconciles every cache behavior on each
bootstrap, so a hand-attached policy is replaced by the configured one, or removed whenheadersis unset - configure the headers here instead.
Upgrading an SSR site. Before this version, a policy attached by hand to an SSR distribution survived a bootstrap until some other change rewrote the cache behavior. From this version on, the first bootstrap removes it, whether or not headers is set. Copy its headers into headers before you upgrade.
What has not been verified against AWS
Section titled “What has not been verified against AWS”The headers support is covered by unit tests against recorded API shapes; it has not yet been run against a live CloudFront distribution. Until it has, treat these as expectations rather than guarantees:
- That every listed header appears on
/, on a directory route such as/about/, on the 404 page and on a hashed asset, afterbootstrapand again after a laterdeploy. The 404 page is a custom error response; CloudFront is expected to apply the default behavior’s response headers policy to it, but this has not been checked. - That a second
bootstrapordeployfinds no drift in the policy CloudFront returns, and so makes no update. - That CloudFront accepts the policy document as blogwright writes it, including the order of its elements.
- That deleting the policy right after detaching it succeeds, rather than waiting for the next
bootstrap. - Whether CloudFront accepts header values with non-ASCII characters. The config check allows them; if CloudFront refuses one,
bootstrapordeployfails with CloudFront’s error.
Headers apply to the preview stack too, from the preview config, at preview bootstrap; preview deploy never changes them, because one distribution serves every pull request.
Server rendering (ssr)
Section titled “Server rendering (ssr)”object · optional · no default
ssr turns the environment from a static site into a Lambda Web Function deployment: the MicroVM build’s output directory is packaged as a .zip, stored as a versioned object in the site bucket, and published as an immutable Web Function revision that the site’s CloudFront distribution serves as a private origin. The whole contract - packaging, revisions, caching, limits - is in Server-rendered sites.
{ "region": "us-east-1", "siteName": "example", "ssr": { "packageDir": "dist", "entryPoint": "server/entry.mjs", // Blogwright generates the entry shim: Astro's server reads HOST and PORT. "shim": { "host": "HOST", "port": "PORT" }, "staticDir": "dist/client", "staticPaths": ["/_astro/*", "/favicon.svg"], // Only if cacheable responses vary by token: // "cacheKeyHeaders": ["Authorization"], // Literal, non-secret values the server reads at runtime: "env": { "SITE_URL": "https://example.com" }, },}Presence of the key enables the mode. ssr and spa: true are mutually exclusive, and with ssr set, region must be one of the Web Function launch regions: us-east-1, us-east-2, us-west-2, ap-northeast-1, eu-west-1. Preview stacks do not support SSR - preview bootstrap and preview deploy refuse.
| Field | Validation | Default |
|---|---|---|
packageDir |
Repo-relative, non-empty, no .. segments |
paths.dist |
entryPoint |
Non-empty relative path inside the package, / separators, no .., no leading / |
index.js |
shim |
Object with port (required) and host (optional): the environment variable names the server reads its listen address from. Each matches [A-Za-z][A-Za-z0-9_]*, is not Lambda-reserved, is not also set in env, and the two differ. With shim set, entryPoint cannot be named blogwright-entry.mjs |
none |
staticDir |
Repo-relative, no ..; must be supplied together with staticPaths. Files publish at their path relative to it, so it must mirror the URL layout |
none |
staticPaths |
Array of CloudFront path patterns, each beginning /; must be non-empty when staticDir is set, and empty without it |
[] |
cacheKeyHeaders |
HTTP header names (RFC 7230 tokens), no case-insensitive duplicates, at most 10, at most 128 characters each, joined list under 1,024 characters, none CloudFront owns or strips | [] |
env |
Map of [A-Za-z][A-Za-z0-9_]* keys to non-empty strings; literal values only, never secrets; Lambda-reserved names rejected |
{} |
timeoutSeconds |
Integer 3-900 | 30 |
maxConcurrencyPerEnvironment |
Integer 1-128 | 64 |
env keys Lambda reserves are rejected at config load, naming the key: the AWS_* credential, region, session and Lambda-runtime names, plus AWS_LAMBDA_NODEJS_ENTRYPOINT (which points at entryPoint instead, so the two can never disagree). cacheKeyHeaders rejects Host, Accept-Encoding, the connection-scoped headers CloudFront removes, and any name beginning lambda-web-. CloudFront’s limits are enforced at load: 10 headers, 128 characters per header name, and a comma-joined name list strictly under 1,024 characters.
With shim set, Blogwright generates the entry shim: the package gains blogwright-entry.mjs beside entryPoint, which copies the host and port from AWS_LAMBDA_HTTP_ENDPOINT into the named variables and then imports entryPoint, and the revision starts the shim. entryPoint is then the framework’s own server entry, not a hand-written shim. See The entry shim.
Revision-scoped settings (entryPoint, shim, env, timeoutSeconds, maxConcurrencyPerEnvironment) take effect at the next deploy, which creates a revision whenever any of them changed even if the built output did not. bootstrap never cuts one.
How SSR changes the other settings
Section titled “How SSR changes the other settings”defaultRootObjectis inert - the distribution’s root object is empty, because the application answers/.invalidationMaxPathsapplies only to a deploy that publishesstaticDirchanges without a new revision; a new revision invalidates/*.seois inert - Blogwright writes norobots.txtorsitemap.xml; the application serves them, or the build puts them instaticDirand lists them instaticPaths. Aseokey written in the config document with a non-offvalue warns once per deploy.regionis restricted to the launch regions above.
robots.txt and sitemap.xml policy, with environment-aware defaults: production is indexable with a sitemap; every other environment - including previews - blocks crawlers and skips the sitemap.
seo.robots
Section titled “seo.robots”"auto" | "index" | "noindex" | "off" · optional · default: "auto"
How the deploy pipeline writes robots.txt into the published site:
auto(default) - production gets an indexablerobots.txt(Allow: /, plus aSitemap:line when the sitemap is on); every other environment getsDisallow: /.index/noindex- force either policy regardless of environment.off- don’t managerobots.txtat all; whatever the build produced is left alone.
Preview deploys always count as non-production for auto, even when the preview stack deploys from a production config.
seo.robotsContent
Section titled “seo.robotsContent”string · optional · no default
Explicit robots.txt body, published verbatim in place of the generated one. Ignored when seo.robots is "off".
seo.sitemap
Section titled “seo.sitemap”"auto" | "on" | "off" · optional · default: "auto"
sitemap.xml generation, built from the site’s HTML pages with absolute URLs. auto turns it on in production and off everywhere else; on/off force it. Requires a resolvable site origin - the custom domain, or the CloudFront domain otherwise.
Site layout (paths)
Section titled “Site layout (paths)”Repo-relative paths describing the consuming site’s layout. The defaults match a stock Astro project; override them when yours differs.
// A SvelteKit SPA in a monorepo subdirectory:{ "spa": true, "paths": { "app": "web", "dist": "web/build" },}paths.app
Section titled “paths.app”string · optional · default: "."
Directory the MicroVM builds in - pnpm install && pnpm build run here. "." for an app at the repo root; "web" for a monorepo subdirectory. Must be repo-relative with no .. segments.
paths.dist
Section titled “paths.dist”string · optional · default: "dist"
Built output directory the MicroVM publishes, relative to the repo root (not to paths.app). Same repo-relative validation as paths.app.
paths.publicDir
Section titled “paths.publicDir”string · optional · default: "public"
The static-asset directory served at the site root (Astro’s public/). The pds commands write the OAuth client documents and the standard.site well-known file at protocol-fixed locations under this directory - their URL paths are part of the OAuth client id and the standard.site spec, so only the directory root varies.
paths.content
Section titled “paths.content”string · optional · default: "src/content/blog"
Content-collection directory pds sync enumerates for posts.
paths.atprotoJson
Section titled “paths.atprotoJson”string · optional · default: "src/data/atproto.json"
JSON file the site imports to render its document <link> tags, written by pds init.
standard.site publishing (pds)
Section titled “standard.site publishing (pds)”object · optional · no default
AT Protocol / standard.site publishing. When the section is absent, publishing is entirely inert. { "name": "My Blog" } is enough to enable it - see Publishing to standard.site.
{ "pds": { "name": "My Blog", "description": "Notes on things", // optional "handleResolver": "https://public.api.bsky.app", // optional; the default "secretName": "example/atproto", // optional; defaults to <siteName>/atproto },}pds.name
Section titled “pds.name”string · required (within pds)
Publication display name - the name on the site.standard.publication record. Must be non-blank.
pds.description
Section titled “pds.description”string · optional · no default
Optional publication description.
pds.handleResolver
Section titled “pds.handleResolver”string · optional · default: "https://public.api.bsky.app"
Resolver pds login uses to turn a handle into a DID. Must be an https: URL. Unused when logging in with a bare DID - the PDS endpoint itself is always discovered from the DID document during OAuth.
pds.secretName
Section titled “pds.secretName”string · optional · default: "<siteName>/atproto"
Name of the Secrets Manager secret holding the OAuth client key and session. Limited to the characters A-Z a-z 0-9 _ / + = . @ -.
Optional analytics plugin (analytics)
Section titled “Optional analytics plugin (analytics)”Settings for the analytics plugin. The block is inert when
the plugin is not installed. After blogwright plugin add analytics, an absent
block or {} uses these defaults:
| Key | Default | Rule |
|---|---|---|
tableBucket |
<env>-<siteName>-analytics |
3–63 lowercase letters, digits or dashes |
namespace |
web |
lowercase letters, digits or underscores, nonempty |
table |
page_views |
same identifier rule |
bots |
flag |
flag includes bots in queries; filter excludes them by default; both retain rows |
saltSecretName |
<siteName>/<env>/analytics-salt |
nonempty Secrets Manager name characters |
dashboard.port |
4317 |
integer 1024–65535; loopback server only |
Unknown analytics keys, including unknown dashboard keys, are rejected when that
plugin is dispatched. The wizard asks the four literal-default settings; write
bucket/salt overrides by hand. Their defaults carry the environment, and explicit
shared names can remove that separation. The pipeline’s two diagnostic log groups
have fixed 365-day retention; retention.cloudfrontDays controls the site’s
CloudWatch copy and bounds optional backfill, not Iceberg row expiry.
Validation summary
Section titled “Validation summary”The CLI validates site-owned settings at load time. PDS owns the last three checks below and runs them on PDS dispatch; malformed PDS config no longer rejects unrelated built-in commands. Post-deploy PDS sync failures remain warnings:
| Key | Rule |
|---|---|
siteName |
required; ^[a-z0-9-]+$; derived bucket name ≤ 63 chars |
region |
must be non-empty |
microvm.memory |
one of 0.5, 1, 2, 4, 8 |
microvm.maxDurationSeconds |
1–28800 |
retention.*Days |
positive |
ssr block |
object; unknown keys rejected; field rules in the table above |
ssr + spa |
mutually exclusive |
ssr + region |
region must be a Web Function launch region |
ssr.env keys |
reserved Lambda names rejected |
ssr.shim |
port required, host optional; variable names, not reserved, not in ssr.env, distinct |
cacheControl |
array of { match, value }; unknown keys rejected; match non-empty; value a valid Cache-Control value |
cacheControl[].match |
starts with / or **; no trailing /, empty segment, . or .. segment, percent-encoded character, partial-segment **, [, ], {, } or \; not already matched by an earlier rule, or by an earlier pattern of the same rule |
headers block |
object; unknown keys rejected at every level; field rules in the headers table |
headers.security.hsts |
true, false, or { maxAgeSeconds, includeSubdomains, preload }; maxAgeSeconds an integer 0-2147483647 |
headers.security.frameOptions |
deny or sameorigin |
headers.security.referrerPolicy |
one of CloudFront’s eight Referrer-Policy values |
headers.security.contentSecurityPolicy |
non-empty, no control characters; the length quota (1,783 by default) is left to CloudFront |
headers.custom[] |
RFC 7230 names ≤ 256 characters, no case-insensitive duplicates, not a headers.security header; values ≤ 1,783 characters (empty allowed), no control characters; names and values together ≤ 10,240 characters; the entry-count quota (10 by default) is left to CloudFront |
seo.robots |
auto, index, noindex, or off |
seo.sitemap |
auto, on, or off |
sourceInclude[] |
repo-relative, no leading /, no .. |
paths.app, paths.dist |
non-empty, repo-relative, no .. |
pds.name |
non-blank when pds is present |
pds.handleResolver |
valid https: URL |
pds.secretName |
optional; characters [\w/+=.@-] only when supplied |
For the resource names derived from siteName and the environment, see the architecture reference. For the flags that interact with config (--config, --env, --domain), see the CLI reference.