{
  "$type": "site.standard.document",
  "content": "---\ntitle: \"11ty and Vite for modern static websites\"\ndescription: \"How to set up 11ty with Vite and Tailwind v4 for static sites that need\n  proper asset bundling, HMR, and npm ecosystem access.\"\ntags:\n  - dev\n---\n\nI've been using [11ty](https://www.11ty.dev/) for static site generation for a\nwhile now (not this site---that's Jekyll---but for other stuff) and it's great.\nBut for a recent project I needed proper asset bundling, hot module replacement\n(HMR), and access to the npm ecosystem without building my own pipeline. Enter\n[Vite](https://vite.dev/).\n\nThe combination turns out to work really well, without adding _too_ much\ncomplexity over bare 11ty. Here's what I learned setting up 11ty with Vite (and\nTailwind v4 too) for the [LLMs Unplugged](https://www.llmsunplugged.org/)\nteaching resources site.\n\n## Why not just use 11ty alone?\n\nVanilla 11ty is deliberately minimal. You write markdown, it spits out HTML.\nWant to bundle JavaScript? Process CSS? Handle npm dependencies? You're on your\nown. For content-focused sites this is perfect---less magic means less to break.\n\nBut when your site needs interactive components, proper module resolution, and\nmodern CSS tooling, you end up reinventing the wheel. I tried the \"just manually\ncopy files and use browser ESM\" approach for a while, and it works until it\ndoesn't[^browser-esm].\n\n[^browser-esm]:\n    Mostly it stops working when you want to use npm packages that expect a\n    bundler, or when you need any kind of build-time transformation.\n\n## The integration pattern\n\nThe magic happens through\n[`@11ty/eleventy-plugin-vite`](https://www.npmjs.com/package/@11ty/eleventy-plugin-vite),\nwhich lets 11ty and Vite play nicely together. Here's the core setup in\n`eleventy.config.js`:\n\n```js\nimport EleventyVitePlugin from \"@11ty/eleventy-plugin-vite\";\nimport tailwindcss from \"@tailwindcss/vite\";\n\nexport default function (eleventyConfig) {\n  eleventyConfig.addPlugin(EleventyVitePlugin, {\n    viteOptions: {\n      plugins: [\n        tailwindcss(),\n        // ... other plugins\n      ],\n      build: {\n        rollupOptions: {\n          input: {\n            main: \"src/assets/main.js\",\n            slides: \"src/assets/slides.js\",\n          },\n        },\n      },\n    },\n  });\n\n  return {\n    dir: {\n      input: \"src\",\n      output: \"_site\",\n    },\n  };\n}\n```\n\nThe Vite config lives directly inside the 11ty config via `viteOptions`. When\nyou run `eleventy --serve`, it starts both the 11ty build and Vite's dev server.\nYou get HMR for your CSS and JavaScript while 11ty rebuilds HTML on file\nchanges. And I set up a small test suite using [Vitest](https://vitest.dev/) as\nwell... because honestly it's still nice to have some regression testing at\nleast.\n\nDuring build, 11ty generates HTML first, then Vite bundles your assets with\nproper hashing and minification. The plugin handles rewriting asset paths in\nyour HTML to point to the hashed files.\n\n## The key gotcha: passthrough files\n\nHere's where I wasted a bunch of time 🙃. 11ty has this concept of\n[passthrough file copy](https://www.11ty.dev/docs/copy/)---files that just get\ncopied directly to the output directory without processing. Useful for things\nlike `CNAME` files, `robots.txt`, PDFs, etc.\n\nThe problem is that Vite's build process empties the output directory before it\nruns. So 11ty copies your passthrough files, then Vite helpfully deletes them\nall.\n\nThe solution is a custom Vite plugin that runs _after_ Vite's build completes\nand copies those files back:\n\n```js\nfunction preservePassthroughOutputs() {\n  let rootDir;\n  let outDir;\n\n  return {\n    name: \"preserve-eleventy-passthrough\",\n    apply: \"build\",\n    configResolved(config) {\n      rootDir = config.root;\n      outDir = config.build.outDir;\n    },\n    async closeBundle() {\n      // Copy passthrough files after Vite is done\n      const passthroughFiles = [\"CNAME\", \"feed.xml\", \"favicon.svg\"];\n\n      for (const file of passthroughFiles) {\n        const sourcePath = path.join(rootDir, file);\n        if (await fileExists(sourcePath)) {\n          const destinationPath = path.join(outDir, file);\n          await fs.copyFile(sourcePath, destinationPath);\n        }\n      }\n    },\n  };\n}\n```\n\nYou could also use\n[`vite-plugin-static-copy`](https://github.com/sapphi-red/vite-plugin-static-copy)\nif you don't want to write your own plugin, though I found the custom one gave\nme more control over exactly what gets copied and when.\n\n## Tailwind v4's new approach\n\nThe above stack works great with whatever CSS approach you use, but I've also\nbeen moving more and more of my projects to Tailwind v4. So here's some advice\nfor that approach (disregard if you don't want to use Tailwind). In Tailwind v4\ninstead of a `tailwind.config.js` file you configure everything through CSS\nusing the `@theme` directive. Here's what `main.css` might look like:\n\n```css\n@import \"tailwindcss\";\n@plugin \"@tailwindcss/typography\";\n\n@theme {\n  --color-anu-gold: #be830e;\n  --color-anu-teal: #0085ad;\n  --font-body: \"Public Sans\", \"Inter\", system-ui, sans-serif;\n  --leading-body: 1.65;\n  --tracking-tight: -0.02em;\n}\n\n@layer base {\n  body {\n    font-family: var(--font-body);\n    line-height: var(--leading-body);\n  }\n}\n```\n\nNo config file, no JavaScript; just CSS with custom properties. The\n`@tailwindcss/vite` plugin handles everything[^tw-v4].\n\n[^tw-v4]:\n    Tailwind v4 is still in beta as of writing this, but it's been stable enough\n    for production use in my experience. Your mileage may vary.\n\nThis approach feels much more natural---design tokens as CSS custom properties\nmeans you can use them directly in your CSS without jumping through hoops. Want\nto reference your colour in some custom CSS? Just use `var(--color-anu-gold)`.\n\n## Dependencies and setup\n\nThe key packages you need for this stack are:\n\n```json\n{\n  \"dependencies\": {\n    \"@11ty/eleventy\": \"^3.1.2\",\n    \"@11ty/eleventy-plugin-vite\": \"^7.0.0\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/vite\": \"^4.1.16\",\n    \"vite\": \"^7.1.12\"\n  }\n}\n```\n\nYour npm scripts are nice and simple:\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"eleventy --serve\",\n    \"build\": \"eleventy\"\n  }\n}\n```\n\nThe Vite integration is completely invisible---the eleventy plugin handles\nstarting and stopping Vite as needed.\n\n## When this setup makes sense\n\nIf your site is purely content with minimal JavaScript, stick with vanilla 11ty.\nIt's faster to set up and has fewer moving parts.\n\nThis makes sense when you:\n\n- need proper JavaScript bundling and tree-shaking\n- want hot module replacement during development\n- have multiple entry points (main site JS, slide deck JS, admin panel, etc.)\n- use npm packages that expect a bundler\n- want to use Vitest for testing (it shares Vite's config understanding)\n- need modern CSS tooling like Tailwind with proper build-time processing\n\nFor the LLMs Unplugged site, we have interactive components, slide decks using\nreveal.js, and a bunch of build-time transformations. The extra complexity of\nVite pays for itself in developer experience.\n\n## The verdict\n\nSetting this up took me a bit of futzing about, but now that it's working the\ndevelopment experience is excellent. HMR makes CSS tweaking instant, and I\nstill get 11ty's flexibility for content with properly optimised build output.\n\nWould I do it again? Absolutely, but only for projects that actually need it.\nFor everything else, vanilla 11ty is the right amount of tooling[^right-tool].\n\n[^right-tool]:\n    The best tool is the one that solves your problem without creating new ones.\n    Boring technology and all that.\n\nYou can see the full implementation in the\n[LLMs Unplugged repository](https://github.com/ANUcybernetics/llms-unplugged/tree/main/website)\nif you want to dig into the details. The `eleventy.config.js` and\n`src/assets/main.css` files have all the configuration.\n",
  "createdAt": "2026-05-13T23:14:42.948Z",
  "description": "How to set up 11ty with Vite and Tailwind v4 for static sites that need proper asset bundling, HMR, and npm ecosystem access.",
  "path": "/blog/2025/11/24/11ty-and-vite-for-modern-static-websites",
  "publishedAt": "2025-11-24T00:00:00.000Z",
  "site": "at://did:plc:tevykrhi4kibtsipzci76d76/site.standard.publication/self",
  "tags": [
    "dev"
  ],
  "textContent": "How to set up 11ty with Vite and Tailwind v4 for static sites that need proper asset bundling, HMR, and npm ecosystem access.",
  "title": "11ty and Vite for modern static websites"
}