11ty and Bun
Brantley Harris

Brantley Harris @deadwisdom

About: Trying desperately to change the world for the better through *technology*... I know, it's probably a bad idea. he/him

Location:
Chicago, IL
Joined:
Jun 11, 2020

11ty and Bun

Publish Date: Jun 6
0 0

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
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 }}
Enter fullscreen mode Exit fullscreen mode

layout.html

<html>
<body>
  <main> {{ content }} </main>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

".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
Enter fullscreen mode Exit fullscreen mode

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()}`
    );
}
Enter fullscreen mode Exit fullscreen mode

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");
  }
Enter fullscreen mode Exit fullscreen mode

More Example Stuff

Entrypoint components/index.ts

import './menu-toggle.ts';
// Anything else you want to build
Enter fullscreen mode Exit fullscreen mode

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}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
        &copy; {{ year }} Extremely SaaS Solutions
    </footer>
  </div>
</body>
Enter fullscreen mode Exit fullscreen mode

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'});
Enter fullscreen mode Exit fullscreen mode

Credits

My name is Brantley Harris and I do consulting work, you can find me on LinkedIn for one.

Photo by Anete Lusina via Pexels

Comments 0 total

    Add comment