In case you don't know eleventy is pretty much the best way to make a static web site. And Bun is the fastest way I've found to compile typescript. Bun does a lot of things, actually, and one of those things is to replace all your webpacky rabbit holes with a super fast and focused bundler Bun.build.
But bun is a bit raw. So here's a system that works ace for me.
This setup is made to focus on very healthy web development practices where we build, gasp, HTML and very targeted web-components where we need extra functionality. It can build apps single-page, multi-page, or anything in-between without becoming a sticky ball of mud because we start simple and stay there.
Libs
First ask Claude to install Bun. Ask really nicely, and tip him well, you know he's dealing with your shit all day. If he goes rogue, spend a long time crafting the perfect prompt to tell him to just go to https://bun.sh/ and follow the instructions.
Next, install eleventy with bun. Let's go over Claude's head this time. You know, if you want that LLM money, you need to think like an LLM, and they would just spew things into your command line, so we can do that too:
bun add @11ty/eleventy
Running 11ty
Okay, so 11ty's command line specifies "node" at the top of it, so bun will dutifully execute in node, which isn't AT ALL THE POINT.
So to execute bun correctly, we need to give bun the '-b' flag, which is '--bun' (I love it when cities have streets that are the name of the city) and that will intercept any calls to node and run them in bun:
bun -b run eleventy --serve
This will serve at https://0.0.0.0:8080, but unless you are an overachiever, should give simply a 404 since we have no files.
Eleventy Basics
Okay, so by default 11ty will do it's 11ty thing, you can create pages like index.md
, and save it. That will automatigically become your index.html. You can also give it a "layout", which will become the wrapper around your page.
index.md
---
title: "Yet Another SaaS"
layout: layout.html
---
# {{ title }}
layout.html
<html>
<body>
<main> {{ content }} </main>
</body>
</html>
".html" pages are actually, by default, liquid templates, which works but also you can change that.
Project Layout
Okay, so you do you, obviously, but here's a project layout that works for me:
_site -- final built static content directory
pages -- where we put our content
_media -- where we put our images/styles/etc
_layouts -- where we put our layouts
pages.html -- our default layout
index.md -- our index page
components -- where we will put our typescript / web-components
index.ts -- the entrypoint for building
.eleventy.js -- our 11ty settings
build.js -- our typescript / web-components build script
package.json -- our package settings
tsconfig.json -- our typescript settings
Eleventy Config
Now, 11ty has an evolving system for configs, but the latest and greatest is to put a .eleventy.js
in your project root. You can have multiple, and use --config=<filename>
to chose, but that's the default one.
Within it, we export a default function that will be called when eleventy starts up:
// Build script for our components
import { build } from "./build.js";
// 11ty runs this function to configure itself
export default function(eleventyConfig) {
// Before 11ty runs, we will run our build script
eleventyConfig.on("eleventy.before", async ({ directories, runMode, outputMode }) => {
// Build our components and put them in the js directory
build('_site/js').catch((error) => {
console.error("Error during build:", error);
});
});
// Our content is in the `pages` folder, this is our base
eleventyConfig.setInputDirectory("pages");
// Copy files from the `pages/_media` folder and put them in
// `_site/media` in our final build
eleventyConfig.addPassthroughCopy({"_media": "media");
// Our layouts are in "pages/_layouts"
eleventyConfig.setIncludesDirectory('_layouts');
// Make a "layout.html" the default layout
eleventyConfig.addGlobalData("layout", "layout.html");
// If we make a change to our components, reload
eleventyConfig.addWatchTarget("./components/");
// This is using the default, but you can change this here
eleventyConfig.setOutputDirectory("_site");
// Short codes are little scripts you can embed, this is for
// the copyright in your footer, which legally has to be
// current, fyi
eleventyConfig.addShortcode("year", () => `${new
Date().getFullYear()}`
);
}
Build
Alright, so now eleventy will run our build script every time it runs, which includes when you make a file change and it automatically recompiles.
Here is the build script I use, it makes a nice little table to explain what was done. We also set it to import css/html as files, split our files into shared chunks, minify the output, and add external source maps. We also set it to name our files basically like how they are in our components
directory with the same relative path.
import { filesize } from "filesize";
export const buildConfig = {
watchDirs: ['./components'],
entrypoints: ['./components/index.ts'],
naming: {
asset: "[dir]/[name].[ext]"
},
publicPath: "/",
}
/// Building
export async function build(outdir) {
try {
Bun.build;
} catch (e) {
console.error("Bun is not available. Please install Bun to use this feature. You might need to add -b after bun to make it intercept node.");
return;
}
let result = await Bun.build({
entrypoints: buildConfig.entrypoints,
outdir: outdir,
publicPath: buildConfig.publicPath,
naming: buildConfig.naming,
loader: {
".css": "file",
".html": "file",
},
splitting: true,
minify: true,
sourcemap: 'external'
})
if (!result.success) {
result.logs.forEach(log => console.error(log, '\n\n----\n'));
console.log("\n❌ ", new Date(), "\n");
return;
}
let items = result.outputs.filter(art => !art.path.endsWith('.map')).map(art => ({
'name': '📗 .' + art.path,
'loader': art.loader,
'size': "\x1b[33m" + filesize(art.size) + "\x1b[0m"
}));
console.table(items);
console.log("\n✔️ ", new Date(), extraStatus, "\n");
}
More Example Stuff
Entrypoint components/index.ts
import './menu-toggle.ts';
// Anything else you want to build
Menu Toggle components/menu-toggle.ts
import {LitElement, css, html, svg} from 'lit';
import {customElement, property} from 'lit/decorators.js';
let hamburgerIcon = svg`
<svg width="19" height="13" viewBox="0 0 19 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.342712 0.743286H18.3427V2.74329H0.342712V0.743286ZM0.342712 5.74329H18.3427V7.74329H0.342712V5.74329ZM0.342712 10.7433H18.3427V12.7433H0.342712V10.7433Z" fill="black"/>
</svg>
`
@customElement('menu-toggle')
export class MenuToggle extends LitElement {
static styles = css`
:host {
border: none;
background: transparent;
padding: var(--site-menu-toggle-padding, 0);
cursor: pointer;
}
`;
connectedCallback(): void {
super.connectedCallback();
this.addEventListener('click', () => {
document.body.classList.toggle('menu-open');
});
}
render() {
return html`${hamburgerIcon}`;
}
}
Layout pages/_layouts/layout.html
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }}</title>
<script type="module" src="/js/index.js"></script>
<link rel="stylesheet" href="/media/base.css">
</head>
<body>
<nav>
<menu-toggle></menu-toggle>
<ul class="title">
<li class="logo">Yet Another SaaS</li>
<li><a href="/about">About Us</a></li>
</ul>
</nav>
<main>
{{ content }}
</main>
<footer>
© {{ year }} Extremely SaaS Solutions
</footer>
</div>
</body>
Entrypoint components/app.ts
You can create many entrypoints (see buildConfig in build.js),
so keep your index/page bundle small. For complex apps make more entry points for the functionality you need.
import './app-harness.ts';
import './data-sources.ts';
import './data-complex-things.ts';
import {thing } from 'weirdLibrary';
thing.configure({'name': 'Project Orpheus'});
Credits
My name is Brantley Harris and I do consulting work, you can find me on LinkedIn for one.