Eleventy builds to a folder of static HTML, so deploying it is two settings: build command npx @11ty/eleventy, publish directory _site. Everything else is refinement.
That simplicity is the whole reason to use Eleventy, and it is also why the things that do go wrong are always the same four. This covers the working config, the Node version mismatch that produces a build failing only in CI, and the parts most tutorials leave out because they are boring and matter.
Table of contents
- The minimum that works
- The Node version trap
- Configuration worth adding
- Headers, redirects and the things people skip
- Builds, previews and what happens on a push
- Where this setup runs out
- How this fits the rest of the stack
- FAQ
The minimum that works
Create a netlify.toml in the repository root. You can configure this in the web interface instead, but committing it means the configuration travels with the code and gets reviewed with it.
[build]
command = "npx @11ty/eleventy"
publish = "_site"
[build.environment]
NODE_VERSION = "20"
Connect the repository, and the first deploy runs. Netlify installs dependencies, runs the command, and serves whatever ended up in _site.
If your package.json already has a build script, use that instead so local and CI runs are identical:
{
"scripts": {
"build": "eleventy",
"dev": "eleventy --serve"
}
}
Then the command becomes npm run build. The advantage is that there is one definition of what building means, and it is the one you use every day.
The Node version trap
This is the failure that wastes the most time, and it looks like a code problem while being an environment problem.
Netlify picks a default Node version that is not necessarily yours. Eleventy 3 is ESM-only and needs a modern Node; a build environment on an older default will fail with a module resolution error that reads like a broken import. You will check the import. The import is fine.
Pin it explicitly, in both places:
# .nvmrc
20
And in netlify.toml, as shown above. Belt and braces, because the two are read at different points and having them agree removes the question entirely.
The related trap is the lockfile. If your repository has package-lock.json and you developed with a different package manager, the build installs a different dependency tree than you tested against. Commit one lockfile, from the manager you actually use, and delete the others.
Configuration worth adding
A working eleventy.config.js for a typical site:
export default function (eleventyConfig) {
eleventyConfig.addPassthroughCopy('src/assets');
eleventyConfig.addPassthroughCopy({ 'src/static': '/' });
eleventyConfig.addFilter('readableDate', (d) =>
new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).format(d)
);
return {
dir: { input: 'src', output: '_site', includes: '_includes' },
markdownTemplateEngine: 'njk',
};
}
Passthrough copy is the one people miss on their first deploy. Eleventy only outputs files it processes as templates, so CSS, images and fonts are silently absent from the build unless you tell it to copy them. The symptom is a site that renders as unstyled HTML, which looks alarming and is a one-line fix.
If your output directory is not _site, the publish setting in netlify.toml has to match. Two places, one value, and they drift the moment someone changes one of them.
Headers, redirects and the things people skip
Static hosting defaults are conservative, and a few lines meaningfully improve both speed and security.
[[headers]]
for = "/assets/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[headers]]
for = "/*"
[headers.values]
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
The long cache on assets is only safe if those filenames change when the content does. If you are not fingerprinting, use a short cache or you will serve stale CSS to returning visitors for a year, which is a genuinely hard bug to diagnose because it works perfectly for you.
Redirects belong in the same file, and matter most when you are migrating from an existing site with a different URL structure:
[[redirects]]
from = "/old-blog/:slug"
to = "/posts/:slug"
status = 301
Use 301 for permanent moves so search engines transfer the ranking, and 302 only when the change is genuinely temporary. Getting this backwards on a site migration throws away every link anyone has ever made to you.
Builds, previews and what happens on a push
Every push to the production branch triggers a build and, if it succeeds, a deploy. Pull requests get their own preview URL, which is the feature most worth using and most often ignored.
Preview deploys mean content changes get reviewed on a real rendered page rather than as a diff in markdown. For a site where non-developers contribute, that single workflow change removes most of the back and forth.
Keep the build fast and it stays pleasant. Eleventy is quick by default, so if a build is slow the cause is almost always image processing or an unbounded data fetch in a global data file. A build that hits an external API without caching is also a build that fails whenever that API does.
One habit worth adopting: run the production build locally before pushing something structural.
npx @11ty/eleventy
npx serve _site
It catches passthrough and path problems in seconds rather than in a CI log.
Where this setup runs out
Eleventy plus static hosting is close to the ideal setup for content sites, and it stops where dynamic behaviour starts.
A contact form, a search endpoint, a login, a comment system, anything that writes to a database: none of those are static files, and each one is a decision about where the dynamic half runs. Serverless functions cover small cases. Anything with a real database, sessions, or background work needs a process that stays alive.
The shape that holds up well is keeping the site static and putting the dynamic pieces behind an API on a subdomain. The content stays fast and cheap, and the stateful part lives somewhere built for state, with its own logs and its own scaling.
For reference on the hosting side: an Eleventy site on RunxBuild is a static deploy from GitHub with custom domains and certificates handled, headers and redirects configurable per path, and 120GB of bandwidth included before it bills at ten cents a gigabyte. If the dynamic half arrives later, it deploys as a service beside it on the same platform rather than as a second provider to manage.
How this fits the rest of the stack
Static sites are the cheapest thing to run and the easiest to underestimate later, because the first dynamic feature changes the shape of the bill rather than nudging it. Pricing the static half and the eventual service half together takes a couple of minutes, and the RunxBuild hosting calculator shows bandwidth, service, database and storage as separate lines so it is obvious which of them a new feature actually adds.
Useful related references:
- Django on Netlify: Why It Does Not Work, and What To Do Instead
- Branch Deploy on Netlify: How It Works and When to Use One
- Netlify Templates: What Starter Templates Give You and What They Do Not
- Builds on RunxBuild
FAQ
What build command and publish directory does Eleventy need on Netlify?
Build command npx @11ty/eleventy, or npm run build if you have defined a build script, and publish directory _site unless you changed the output directory in your Eleventy config. If you did change it, the publish setting has to match.
Why does my Eleventy build work locally but fail on Netlify?
Almost always the Node version. The build environment defaults to a version that may be older than yours, and Eleventy 3 is ESM-only, so the failure surfaces as a module resolution error that looks like a broken import. Pin the version in both .nvmrc and netlify.toml.
Why is my deployed Eleventy site missing its CSS and images?
Eleventy only outputs files it processes as templates, so static assets are skipped unless you add addPassthroughCopy for their directory. The site renders as unstyled HTML, which looks dramatic and is a one-line fix in the config.
Should I configure Netlify in the dashboard or in netlify.toml?
In netlify.toml, committed to the repository. The configuration then travels with the code, gets reviewed alongside it, and is identical across branches and preview deploys. Dashboard settings are invisible to everyone who was not looking at the dashboard.
Can Eleventy handle forms, search or a login?
Not on its own, since it produces static files. Small dynamic pieces can go in serverless functions. Anything with a database, sessions or background jobs needs a running service, and the usual arrangement is to keep the site static and put the dynamic half behind an API subdomain.