@projectdochelp/s3te
v3.4.9
Published
CLI, render core, AWS adapter, and testkit for S3TemplateEngine projects
Maintainers
Readme
S3TemplateEngine
S3TemplateEngine is a lightweight serverless template engine for people who want to keep writing HTML and still publish through AWS. You write templates and assets, S3TE renders and publishes the static result.
This README is the user guide for the rewrite generation. The deeper implementation specs still live in docs/, but this file is intentionally written for users first.
Table of Contents
Motivation
AWS S3 and CloudFront are a great platform for websites: cheap, fast and low-maintenance. The annoying part usually starts before hosting. You still need reusable HTML, a safe deployment flow, and maybe a way for editors to maintain content without turning your project into a full framework.
S3TemplateEngine is for you if you want to keep writing HTML by hand, but still want to:
- reuse snippets like headers, navigation and footer blocks
- generate many pages from one template
- publish multiple languages or variants from one project
- keep AWS hosting simple and low-cost
- optionally let editors maintain content in Webiny
- deploy without hand-editing Lambda settings or uploading ZIP files yourself
Support
If S3TE saves you time and you want to buy me a tea, you can do that here:
If you need help, found a bug, or want to contribute, open an issue or pull request on GitHub.
Concept
S3TE keeps one simple promise: you write source templates, S3TE turns them into static files, AWS serves the result.

The code bucket receives your variant source files from sourceDir: .html, .part, CSS, JavaScript, images, manifests and everything else you publish for that variant. Files from partDir are uploaded too, but they are treated as reusable template fragments, not as public website assets.
The website bucket contains the finished result that visitors actually receive through CloudFront. That split is what makes incremental rendering, generated pages and safe re-deploys possible.
s3te deploy loads the validated project configuration, packages the AWS runtime, creates or updates one persistent CloudFormation environment stack, creates one temporary CloudFormation deploy stack for packaging artifacts, synchronizes your current source files into the code bucket, and removes the temporary stack again after the real deploy run.
That source sync is not limited to Lambda code. It includes your .html, .part, CSS, JavaScript, images and other project files so the running AWS stack can react to source changes inside the code bucket.
The persistent environment stack contains the long-lived AWS resources such as buckets, Lambda functions, DynamoDB tables, CloudFront distributions and the runtime manifest parameter. The temporary deploy stack exists only so CloudFormation can consume the packaged Lambda artifacts cleanly.
If optional runtime features are enabled in s3te.config.json, S3TE extends the deployed AWS runtime accordingly. sitemap is added to the main environment stack. Webiny is deployed as a separate option stack so retrofitting CMS support does not require CloudFront resources to move with it.
Installation (AWS)
This section is only about the AWS things you need before you touch S3TE. The actual click-by-click screens are best left to the official AWS documentation, because the console changes over time. The goal here is to tell you exactly what S3TE needs from AWS, why it needs it, and which official page gets you there.
| Item | Why S3TE needs it | Official guide |
| --- | --- | --- |
| AWS account | S3TE deploys into your own AWS account. | Create an AWS account |
| Daily-work AWS access | s3te deploy needs credentials that can create CloudFormation stacks and related resources. | Create an IAM user, Manage access keys |
| AWS CLI v2 | The S3TE CLI shells out to the official aws CLI. | Install AWS CLI, Get started with AWS CLI |
| Domain name you control | CloudFront and TLS only make sense for domains you can point to AWS. | Use your registrar of choice |
| ACM certificate in us-east-1 | CloudFront requires its public certificate in us-east-1, and the certificate must cover every alias S3TE will derive for that environment. | Public certificates in ACM |
| Optional Route53 hosted zone | Needed only if S3TE should create DNS alias records automatically. | Create a public hosted zone |
- Create the AWS account.
- Stop using the root user for daily work.
- Create one deployment identity for yourself.
- Install AWS CLI v2 locally.
- Run
aws configureand verify it withaws sts get-caller-identity. - Request the ACM certificate in
us-east-1. - If you want automatic DNS records, create or locate the Route53 hosted zone.
For a first personal setup, the easiest route is usually one IAM user with console access and access keys. If you already work with AWS Identity Center or another federated login, that is fine too. S3TE uses the standard AWS credential chain.
Installation (VSCode)
This section is only about the local editing experience. S3TE does not require VSCode, but VSCode is the reference editor workflow and the easiest path for most users.
| Tool | Why you want it | Official guide | | --- | --- | --- | | Visual Studio Code | Comfortable editor with integrated terminal and extension support. | VS Code setup overview, Get started with VS Code | | Node.js 20 or newer | Required for the S3TE CLI and local rendering. | Download Node.js |
Open VSCode, open the integrated terminal, and run:
node --version
npm --versionIf both commands print a version number, your local machine is ready for S3TE.
Installation (S3TE)
This is the S3TE-specific part. No AWS console links, no editor tutorial, just the steps that actually create and run an S3TE project.
mkdir mywebsite
cd mywebsitenpm install --save-dev @projectdochelp/s3teWith the local package installed, initialize the project like this:
npx s3te init --project-name mywebsite --base-url example.comYou can safely run s3te init more than once. If npm install already created a minimal package.json, s3te init extends it with the missing S3TE defaults and scripts instead of failing. An existing s3te.config.json is completed with missing scaffold defaults, explicit --project-name and --base-url values are refreshed on re-run, and the generated schema file is updated to the current package version. Existing content files and templates stay untouched unless you use --force.
If you want a one-shot scaffold without installing first, and @projectdochelp/s3te is already published on npm, this also works:
npx --package @projectdochelp/s3te s3te init --project-name mywebsite --base-url example.comThe default scaffold creates:
mywebsite/
package.json
s3te.config.json
.github/
workflows/
s3te-sync.yml
app/
part/
head.part
website/
index.html
offline/
content/
en.json
schemas/
s3te.config.schema.json
tests/
project.test.mjs
.vscode/
extensions.jsonThe generated .github/workflows/s3te-sync.yml is the default CI path for GitHub-based source publishing into the S3TE code buckets. It is scaffolded once and then left alone on later s3te init runs unless you use --force.
The most important fields for a first deployment are:
{
"environments": {
"dev": {
"awsRegion": "eu-central-1",
"stackPrefix": "DEV",
"certificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/replace-me",
"route53HostedZoneId": "Z1234567890"
}
},
"variants": {
"website": {
"languages": {
"en": {
"baseUrl": "example.com",
"cloudFrontAliases": ["example.com", "www.example.com"]
}
}
}
}
}route53HostedZoneId is optional. Leave it out if you want to manage DNS yourself.
Use plain hostnames in baseUrl and cloudFrontAliases, not full URLs. If your config contains a prod environment plus additional environments such as test or stage, S3TE keeps the prod hostname unchanged and derives non-production hostnames like this:
- apex host:
example.com->test.example.com - first-level subdomain:
app.example.com->test-app.example.com - deeper host:
admin.app.example.com->test-admin.app.example.com
Your ACM certificate must cover the final derived aliases of the environment you deploy. Example:
*.example.comcoverstest.example.com*.example.comalso coverstest-app.example.com*.example.comdoes not covertest-admin.app.example.com- for deeper aliases like
test-admin.app.example.com, add a SAN such as*.app.example.com, the exact hostname, or use a differentcertificateArnfor that environment
npx s3te validate
npx s3te render --env dev
npx s3te test
npx s3te doctor --env dev
npx s3te deploy --env devrender writes the local preview into offline/S3TELocal/preview/dev/....
doctor --env <name> now also checks whether the configured ACM certificate covers the CloudFront aliases that S3TE derives for that environment. For that check, the AWS identity running doctor needs permission to call acm:DescribeCertificate for the configured certificate ARN.
deploy creates or updates the persistent environment stack, uses a temporary deploy stack for packaged Lambda artifacts, synchronizes the source project into the code bucket, and removes the temporary stack again when the deploy finishes.
After the first successful deploy, use s3te sync --env dev for regular template, partial, asset and source updates when the infrastructure itself did not change.
If you left route53HostedZoneId out of the config, the last DNS step stays manual: point your domain at the created CloudFront distribution after deploy.
Use this step if your team wants GitHub pushes to publish project sources into the S3TE code bucket instead of running s3te sync locally.
s3te init already scaffolded .github/workflows/s3te-sync.yml for that path.
That workflow is meant for source publishing only:
- it validates the project
- it reads the selected environment from GitHub and resolves the matching AWS region from
s3te.config.json - it uploads every configured variant into its own S3TE code bucket
- the resulting S3 events trigger the deployed Lambda pipeline in AWS
Use a full deploy only when the infrastructure, environment config, or runtime package changes.
GitHub preparation checklist:
- Push the project to GitHub together with
.github/workflows/s3te-sync.yml. - Make sure GitHub Actions are allowed for the repository or organization.
- Run the first real
npx s3te deploy --env <name>so the code buckets already exist. - In AWS IAM, create an access key for a CI user that may sync only the S3TE code buckets for that environment.
- In GitHub open
Settings -> Secrets and variables -> Actions -> Variables. - Add these repository variables:
S3TE_ENVIRONMENTUse the exact environment name froms3te.config.json, for exampledev,test, orprod. This is required for push-based sync when your project has more than one configured environment.S3TE_GIT_BRANCHoptional Use the branch that should trigger the sync job, for examplemain.
- In GitHub open
Settings -> Secrets and variables -> Actions -> Secrets. - Add these repository secrets:
AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY
- Leave
.github/workflows/s3te-sync.ymlunchanged unless you want a custom CI flow. The scaffolded workflow already reads:- the environment from the manual
workflow_dispatchinput when provided - otherwise from
S3TE_ENVIRONMENT - otherwise automatically from
s3te.config.jsonwhen exactly one environment exists - the branch from
S3TE_GIT_BRANCHor defaults tomain - the AWS region from
s3te.config.json
- the environment from the manual
You do not have to store bucket names, source folders, part folders, or AWS regions in GitHub variables. s3te sync resolves all of that from s3te.config.json.
For projects with multiple environments such as test and prod, the simplest setup is usually one workflow file per target environment, for example:
.github/workflows/s3te-sync-test.ymlwithnpx s3te sync --env test.github/workflows/s3te-sync-prod.ymlwithnpx s3te sync --env prod
First verification in GitHub:
- Open the
Actionstab in the repository. - Select
S3TE Sync. - Start it once manually with
Run workflow. - Check that the run reaches the
Configure AWS credentials,Validate project, andSync project sources to the S3TE code bucketssteps without error.
Where to get the AWS values:
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYIn the AWS console openIAM -> Users -> <your-ci-user> -> Security credentials -> Create access key. Save both values immediately. The secret access key is shown only once. AWS documents the credential options and access-key handling here: AWS security credentials, Manage access keys for IAM users.S3TE_ENVIRONMENTThis is the environment key from yours3te.config.json, for exampletestorprod. For repositories with multiple environments, set this variable or pass the environment manually when startingworkflow_dispatch.- AWS region
You do not need to copy this into GitHub. The workflow reads
environments.<name>.awsRegiondirectly froms3te.config.json.
What gets uploaded where:
- For each variant, S3TE stages
partDirintopart/andsourceDirinto<variant>/. - Then S3TE syncs that staged tree into the resolved code bucket for that variant and environment.
- In the deployed runtime, files below
<variant>/whose extension is inrendering.renderExtensionsare rendered. By default that means.html,.htmand.part. - Every other file below
<variant>/is copied unchanged into the public output bucket, with the<variant>/prefix removed. Dot-folders such as.well-known/, image folders such asgfx/, files likesite.webmanifest, and root-level icons insidesourceDirare valid assets. - Empty folders are not published, because S3 stores objects, not real directories.
With your example config this means:
test+website:app/partandapp/websitego totest-website-code-soptest+app:app/part-appandapp/appgo totest-app-code-sopprod+website:app/partandapp/websitego towebsite-code-sopprod+app:app/part-appandapp/appgo toapp-code-sop
For the prod + app case, these source files publish like this:
app/app/index.htmlis rendered fromapp-code-sop:app/index.htmltoapp-sop:index.htmlapp/app/site.webmanifestis copied fromapp-code-sop:app/site.webmanifesttoapp-sop:site.webmanifestapp/app/.well-known/assetlinks.jsonis copied toapp-sop:.well-known/assetlinks.jsonapp/app/gfx/logo.svgis copied toapp-sop:gfx/logo.svg
Minimal IAM policy example for the test environment and both variants:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": [
"arn:aws:s3:::test-website-code-sop",
"arn:aws:s3:::test-app-code-sop"
]
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": [
"arn:aws:s3:::test-website-code-sop/*",
"arn:aws:s3:::test-app-code-sop/*"
]
}
]
}For different environments or additional variants, use the derived code bucket names from your config.
The scaffolded workflow looks like this:
# Required GitHub repository secrets:
# - AWS_ACCESS_KEY_ID
# - AWS_SECRET_ACCESS_KEY
# Required GitHub repository variable for push-based sync in multi-environment projects:
# - S3TE_ENVIRONMENT (for example dev, test, or prod)
# Optional GitHub repository variable:
# - S3TE_GIT_BRANCH (defaults to main)
# Notes:
# - workflow_dispatch can override the environment manually
# - if s3te.config.json contains exactly one environment, no S3TE_ENVIRONMENT variable is needed
# This workflow reads s3te.config.json at runtime and syncs all variants into their own code buckets.
name: S3TE Sync
on:
workflow_dispatch:
inputs:
environment:
description: Optional S3TE environment override from s3te.config.json
required: false
type: string
push:
paths:
- "app/**"
- "package.json"
- "package-lock.json"
- ".github/workflows/s3te-sync.yml"
jobs:
sync:
if: github.event_name == 'workflow_dispatch' || github.ref_name == (vars.S3TE_GIT_BRANCH || 'main')
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
shell: bash
run: |
if [ -f package-lock.json ]; then
npm ci
else
npm install
fi
- name: Resolve S3TE environment and AWS region from s3te.config.json
id: s3te-config
shell: bash
env:
WORKFLOW_INPUT_ENVIRONMENT: ${{ inputs.environment }}
REPOSITORY_S3TE_ENVIRONMENT: ${{ vars.S3TE_ENVIRONMENT }}
run: |
node -e "const fs=require('node:fs'); const fromInput=(process.env.WORKFLOW_INPUT_ENVIRONMENT || '').trim(); const fromVariable=(process.env.REPOSITORY_S3TE_ENVIRONMENT || '').trim(); const config=JSON.parse(fs.readFileSync('s3te.config.json','utf8')); const known=Object.keys(config.environments ?? {}); const requested=(fromInput || fromVariable || (known.length === 1 ? known[0] : '')).trim(); if(!requested){ console.error('Missing S3TE environment. Provide workflow_dispatch input \"environment\" or set GitHub repository variable S3TE_ENVIRONMENT. Known environments: ' + (known.length > 0 ? known.join(', ') : '(none)') + '.'); process.exit(1);} const environmentConfig=config.environments?.[requested]; if(!environmentConfig){ console.error('Unknown environment ' + requested + '. Known environments: ' + (known.length > 0 ? known.join(', ') : '(none)') + '.'); process.exit(1);} fs.appendFileSync(process.env.GITHUB_OUTPUT, 'environment=' + requested + '\n'); fs.appendFileSync(process.env.GITHUB_OUTPUT, 'aws_region=' + environmentConfig.awsRegion + '\n');"
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ steps.s3te-config.outputs.aws_region }}
- run: npx s3te validate --env ${{ steps.s3te-config.outputs.environment }}
- run: npx s3te sync --env ${{ steps.s3te-config.outputs.environment }}Usage
Once the project is installed, your everyday loop splits into two paths: deploy when infrastructure changes, sync when only project sources changed.
Daily Workflow
- Edit files in
app/part/andapp/website/. - If you use content-driven tags without Webiny, edit
offline/content/en.jsonoroffline/content/items.json. - If you use Webiny and want the current mirrored live content locally, download the content snapshot first.
- Validate and render locally.
- Run your tests.
- Use
deployfor the first installation or after infrastructure/config/runtime changes. - Use
syncfor day-to-day source publishing into the code buckets.
npx s3te download-content --env dev
npx s3te validate
npx s3te render --env dev
npx s3te test
npx s3te sync --env devIf you are not using Webiny yet, skip download-content and keep editing the local JSON files directly.
Use a full deploy only when needed:
npx s3te deploy --env devOnce Webiny is installed and the stack is deployed with Webiny enabled, CMS content changes are picked up in AWS through the DynamoDB stream integration. Those content changes do not require another sync or deploy.
CLI Commands
| Command | What it does |
| --- | --- |
| s3te init | Creates the starter project structure and base config. |
| s3te validate | Checks config and template syntax without rendering outputs. |
| s3te render --env <name> | Renders locally into offline/S3TELocal/preview/<env>/.... |
| s3te test | Runs the project tests from offline/tests/. |
| s3te download-content --env <name> | Downloads the mirrored S3TE content table into offline/content/items.json for local render and test runs. |
| s3te package --env <name> | Builds the AWS deployment artifacts without deploying them yet. |
| s3te sync --env <name> | Uploads current project sources into the configured code buckets. |
| s3te doctor --env <name> | Checks local machine and AWS access before deploy. |
| s3te deploy --env <name> | Deploys or updates the AWS environment and syncs source files. |
| s3te option <webiny|sitemap> | Writes or updates optional feature configuration such as Webiny or sitemap support in an existing S3TE project. |
Template Commands
S3TE uses literal HTML-like tags inside your .html and .part files. The tags are case-sensitive, always lowercase, and never use attributes. JSON-based commands must contain valid JSON and reject unknown properties.
The commands below are grouped by purpose:
Core Featureswork in every S3TE project.Webiny Featuresare the content-driven commands. Despite the name, they also work with local content files underoffline/content/when you are not using Webiny yet.
Core Features
Action
Loads another template fragment from the current variant's partDir, renders it recursively, and inserts the result at the current position.
Syntax
<part>head.part</part>The payload must be a relative path inside partDir. Leading / and .. are invalid.
Example
<head>
<part>head.part</part>
</head>Action
Evaluates one inline JSON rule and renders its template only when the rule matches the current render target.
Syntax
<if>{
"env": "prod",
"file": "index.html",
"not": false,
"template": "<meta name='robots' content='all'>"
}</if>Supported JSON properties:
envoptional: matches the current environment name case-insensitivelyfileoptional: matches the current output filename, for exampleindex.htmlnotoptional: inverts the final result whentruetemplaterequired: inline HTML to render when the rule matches
If both env and file are present, both must match.
If both conditions are omitted, the tag behaves like an inline template include and always renders its template.
Example
<if>{
"env": "prod",
"template": "<meta name='robots' content='all'>"
}</if>
<if>{
"env": "test",
"template": "<meta name='robots' content='noindex'>"
}</if>Action
Prints metadata of the file currently being rendered.
Syntax
<fileattribute>filename</fileattribute>Currently supported values:
filename: the output key relative to the target bucket, for examplenews/article-one.html
Example
<link rel="canonical" href="https://<lang>baseurl</lang>/<fileattribute>filename</fileattribute>">Action
Prints language-related metadata of the current render target.
Syntax
<lang>2</lang>
<lang>baseurl</lang>Currently supported values:
2: the current language code such asenordebaseurl: the resolved base hostname for the current language and environment
Example
<html lang="<lang>2</lang>">
<head>
<link rel="canonical" href="https://<lang>baseurl</lang>">
</head>
</html>Action
Selects the block whose tag name matches the current language and renders only that block.
Syntax
<switchlang>
<de>Willkommen</de>
<en>Welcome</en>
</switchlang>There is no fallback to defaultLanguage. If the current language block is missing, S3TE renders an empty string and records a warning.
Example
<p>
<switchlang>
<de>Dein ultimatives Website-Werkzeug</de>
<en>Your ultimate website tool</en>
</switchlang>
</p>Webiny Features
These commands read from the resolved content repository. With Webiny enabled, that means mirrored Webiny content. Without Webiny, the same commands can read from local JSON files under offline/content/.
Action
Loads a single content item by contentId and inserts its content fragment.
S3TE first tries the language-specific field content<lang>, for example contentde, and falls back to content if the language-specific field does not exist.
Syntax
<dbpart>impressum</dbpart>The payload is the content ID, not the internal database record ID.
Example
<body>
<dbpart>impressum</dbpart>
</body>Action
Queries matching content items and renders the given inline template once for each result.
Syntax
<dbmulti>{
"filter": [
{"forWebsite": {"BOOL": true}}
],
"filtertype": "equals",
"limit": 3,
"template": "<article><h2><dbitem>headline</dbitem></h2></article>"
}</dbmulti>Supported JSON properties:
filterrequired: array of legacy DynamoDB-style filter clausesfiltertypeoptional:equalsorcontains, default isequalslimitoptional: maximum number of items to rendertemplaterequired: inline template rendered once per match
Filter notes:
- every filter clause contains exactly one field
- multiple clauses are combined with logical
AND __typenamematches the content model, for examplearticle- supported legacy value wrappers are
S,N,BOOL,NULL, andL - results are sorted deterministically; items with a numeric field
orderare rendered first in ascending order, then items without a numericorder, thencontentId, thenid - this also works with mirrored Webiny content if the model contains a field with the exact field ID
orderand typenumber
Example
<dbmulti>{
"filter": [
{"__typename": {"S": "article"}},
{"forWebsite": {"BOOL": true}}
],
"limit": 3,
"template": "<a href='article-<dbitem>slug</dbitem>.html'><h2><dbitem>headline</dbitem></h2></a>"
}</dbmulti>Action
Turns one source template into multiple output files. The content items are selected by filter, and each item produces one rendered file.
Syntax
<dbmultifile>{
"filenamesuffix": "slug",
"filter": [
{"__typename": {"S": "article"}}
],
"limit": 10
}</dbmultifile>
<!doctype html>
<html>
<body>
<h1><dbmultifileitem>headline</dbmultifileitem></h1>
</body>
</html>Supported JSON properties:
filenamesuffixrequired: field whose value becomes the filename suffixfilterrequired: array of legacy DynamoDB-style filter clausesfiltertypeoptional:equalsorcontains, default isequalslimitoptional: maximum number of files to generate
Rules:
dbmultifilemust be the first non-whitespace construct in the file- the control block itself is not part of the output
- generated filenames follow the pattern
<basename>-<suffix>.<ext> - the suffix must not be empty and must not contain
/,\\, or: - suffixes must be unique within that template
Example
If the source file is article.html and the current item has "slug": "first-article", the generated output becomes article-first-article.html.
<dbmultifile>{
"filenamesuffix": "slug",
"filter": [
{"__typename": {"S": "article"}}
]
}</dbmultifile>
<article>
<h1><dbmultifileitem>headline</dbmultifileitem></h1>
</article>Action
Reads one field from the current content item. This works inside dbmulti templates and inside dbmultifile bodies.
Syntax
<dbitem>headline</dbitem>Special field names:
__typenamecontentIdidlocaletenant_version_lastChangedAt
For the field name content, S3TE again prefers content<lang> over content.
If the field value is a string array, S3TE serializes it as concatenated HTML links.
Example
<dbmulti>{
"filter": [{"__typename": {"S": "article"}}],
"template": "<article><h2><dbitem>headline</dbitem></h2><div><dbitem>content</dbitem></div></article>"
}</dbmulti>Action
Reads one field from the current content item and can apply one transformation mode. It is primarily meant for dbmultifile bodies, but works wherever a current content item exists.
Syntax
Simple field output:
<dbmultifileitem>headline</dbmultifileitem>JSON command mode:
<dbmultifileitem>{"field":"content","limit":160}</dbmultifileitem>Supported JSON properties:
fieldrequiredlimitoptional: truncate text to a maximum length and append...limitlowoptional: choose a random length betweenlimitlowandlimitformatoptional: currently onlydatelocaleoptional: used withformat: "date"divideattagoptional: cut a section out of the field valuestartnumberoptional: 1-based occurrence number for the divide startendnumberoptional: 1-based occurrence number for the divide end
Only one transform mode is allowed at a time:
- limit mode:
limitwith optionallimitlow - date mode:
format: "date" - divide mode:
divideattag
Date mode formats de as dd.mm.yyyy. All other locales currently format as mm/dd/yyyy.
Examples
Simple field output:
<dbmultifileitem>headline</dbmultifileitem>Truncated teaser text:
<dbmultifileitem>{"field":"content","limit":160}</dbmultifileitem>Date formatting:
<dbmultifileitem>{"field":"publishedAt","format":"date","locale":"de"}</dbmultifileitem>Extract one section from a larger HTML field:
<dbmultifileitem>{
"field":"content",
"divideattag":"<h2>",
"startnumber":2,
"endnumber":3
}</dbmultifileitem>Optional: Sitemap
You do not need sitemap.xml automation to use S3TE. If you want it, S3TE can maintain one sitemap.xml per published output bucket through a dedicated Lambda, just like the older 2.x generation.
Add this block to s3te.config.json:
"integrations": {
"sitemap": {
"enabled": true,
"environments": {
"dev": {
"enabled": false
}
}
}
}The top-level enabled acts as the default. integrations.sitemap.environments.<env>.enabled can override that for a single environment.
If you prefer the CLI path, this does the same option update:
npx s3te option sitemap --enable --write
npx s3te option sitemap --env test --enable --writeAfter enabling or disabling sitemap, redeploy the affected environment once:
npx s3te deploy --env prodWhen sitemap is enabled for an environment, S3TE adds one sitemap-updater Lambda to that environment stack and wires every output bucket to it for HTML create/delete events.
The Lambda maintains sitemap.xml directly inside the same output bucket:
- one sitemap per variant/language output bucket
- only published HTML files are tracked
404.htmlis ignoredindex.htmlbecomeshttps://example.com/- nested
news/index.htmlbecomeshttps://example.com/news/ - regular pages such as
about.htmlstayhttps://example.com/about.html
Because the trigger sits on the output bucket, the sitemap also stays correct when HTML is regenerated from Webiny content changes in AWS. Asset-only changes do not affect it.
Optional: Webiny CMS
You do not need Webiny to use S3TE. Start with plain HTML first. Add Webiny only when editors should maintain content in a CMS instead of editing local JSON files under offline/content/.
The supported target for this optional path is Webiny 6.x on its standard AWS deployment model.
Important for the Webiny path: S3TE does not turn on DynamoDB Streams on your Webiny table for you. You must enable the stream manually on the Webiny source table. S3TE uses that stream as the trigger source for CMS-driven rerendering.

This section assumes that S3TE is already installed and deployed. The S3TE-specific Webiny setup only starts after you already have a running Webiny 6.x installation in AWS.
- Install Webiny in AWS and finish the Webiny setup first.
- Find the Webiny DynamoDB table that contains the CMS entries you want S3TE to mirror.
- Manually enable DynamoDB Streams on that Webiny table before the first S3TE deploy with Webiny enabled.
Use
NEW_AND_OLD_IMAGES. Without that stream,s3te deploy --env <name>cannot wire the Webiny trigger and fails because the table has noLatestStreamArn. - Write the Webiny option into your existing S3TE config:
npx s3te option webiny --enable --source-table webiny-1234567 --tenant root --model article --writestaticContent and staticCodeContent are kept automatically. Add --model once per custom model you want S3TE to mirror.
--model article means:
articleis the technical Webiny model ID, not the human-readable label shown in the CMS UI.- S3TE adds that model ID to
integrations.webiny.relevantModelsins3te.config.json. - Only Webiny stream records whose model is listed in
relevantModelsare mirrored into the S3TE content table and can trigger rerendering. - If you omit
--model, only the built-in defaultsstaticContentandstaticCodeContentare mirrored. - You can pass the flag multiple times for multiple models, for example
--model article --model news --model event.
That makes the option example above equivalent to a config that contains:
"relevantModels": ["article", "staticContent", "staticCodeContent"]Use this for every Webiny model whose entries should be available to S3TE template commands like dbitem, dbmulti, dbmultifile, dbmultifileitem, or dbpart.
If a Webiny model should control the output order for dbmulti or dbmultifile, add a field with:
- field ID
order - field type
number
S3TE sorts matching items by that numeric order in ascending order. Items without a numeric order stay at the end.
If different environments should read from different Webiny installations or tenants, run the option command per environment:
npx s3te option webiny --env test --enable --source-table webiny-test-1234567 --tenant preview --write
npx s3te option webiny --env prod --enable --source-table webiny-live-1234567 --tenant root --write- Verify again that DynamoDB Streams are enabled on the Webiny source table with
NEW_AND_OLD_IMAGES. You enable this manually in the AWS console on the Webiny DynamoDB table underExports and streams. S3TE creates the Lambda event source mapping during deploy, but it does not create or enable the table stream itself. - If your S3TE language keys are not identical to your Webiny locales, add
webinyLocaleper language ins3te.config.json, for example"en": { "webinyLocale": "en-US" }. - If your Webiny installation hosts multiple tenants, keep
integrations.webiny.tenantset so S3TE only mirrors the intended tenant. - Check the project again:
npx s3te doctor --env prod- Redeploy the existing S3TE environment:
npx s3te deploy --env prodThat deploy updates the existing environment stack and, when Webiny is enabled, also deploys the separate Webiny option stack for content-mirror and its DynamoDB stream mapping. You do not need a fresh S3TE installation. After that, Webiny content changes flow through the deployed AWS resources automatically; only template or asset changes still need s3te sync --env <name>.
- To test current live CMS content locally, pull the mirrored S3TE content table into the local fixture folder before rendering or running tests:
npx s3te download-content --env prod
npx s3te render --env prod
npx s3te testdownload-content reads the mirrored S3TE content table, keeps only the newest mirrored item per contentId and locale, and writes the result to offline/content/items.json. The normal local dbpart, dbmulti, dbmultifile, dbitem, and dbmultifileitem flow then reads from that file.
Manual versus automatic responsibilities in this step:
- Manual: enable DynamoDB Streams on the Webiny source table
- Automatic during
s3te deploy: readLatestStreamArn, create the Lambda event source mapping, and deploy the S3TE Webiny mirror Lambda in the separate Webiny option stack
The option command writes or updates the integrations.webiny block in s3te.config.json. A typical result looks like this:
Example config block:
"integrations": {
"webiny": {
"enabled": true,
"sourceTableName": "webiny-1234567",
"mirrorTableName": "{stackPrefix}_s3te_content_{project}",
"tenant": "root",
"relevantModels": ["article", "staticContent", "staticCodeContent"],
"environments": {
"test": {
"sourceTableName": "webiny-test-1234567",
"tenant": "preview"
},
"prod": {
"sourceTableName": "webiny-live-1234567",
"tenant": "root"
}
}
}
}For localized Webiny projects, the language block can also carry the mapping explicitly:
"languages": {
"en": {
"baseUrl": "example.com",
"cloudFrontAliases": ["example.com"],
"webinyLocale": "en-US"
}
}The content-driven tags are documented in Template Commands, section Webiny Features.
