Build a static website with Eleventy and Tailwind CSS on Fedora Silverblue 43
Let's build a static website using Podman, Eleventy, and Tailwind CSS on Fedora Silverblue.
I recently had to build an informational website, so I thought a static site would be the way to go. I created a HTML page, embedded the Tailwind CSS Play CDN script, put together a few Tailwind blocks, and filled them with copy. Then I copied the page for other pages, swapped out the content, and voilà, my static site was done.
Now, if I want to add something to the menu or footer, it's not hard to manually edit a handful of pages, but it's prone to errors. If I had PHP at my disposal, I would create a couple template parts and include them on each page, so naturally, I wondered if there was a static site generator that could, more or less, do just that.
I looked at a number of static site generators with the hope of finding one that made it easy to convert my existing design into a corresponding theme, but when I discovered Eleventy, I didn't see anything about themes at all. As it turns out, that's a good thing, because it means I don't have to learn how to build a custom theme.
The beauty of Eleventy's simplicity (and power) didn't hit me until I started working with it. After a bit of tinkering to get it all to work on Fedora Silverblue, it was a breeze to convert my website. To save you the trouble, here's a guide on how I did that.
Install Podman Compose
Fedora Silverblue already has Podman installed to run containers, but with Podman Compose, we can spin up the environment with just a single command.
Let's install Podman Compose:
rpm-ostree install podman-composeWe'll have to restart Fedora to apply this package.
Create project directory
I like to keep projects in a Projects directory within my home folder. In this guide, we'll use eleventy as the project directory within Projects.
Let's create a Projects and eleventy directory:
mkdir -p ~/Projects/eleventyCreate package.json file
With the help of NPM, we'll install everything we need to run Eleventy with Tailwind CSS inside a Podman container.
Let's create the package.json file:
{
"name": "eleventy",
"version": "1.0.0",
"type": "module",
"devDependencies": {
"@11ty/eleventy": "^3.1.2",
"@tailwindcss/cli":"^4.1.18",
"tailwindcss": "^4.1.18",
"npm-run-all": "^4.1.5"
},
"scripts": {
"dev": "npm-run-all --parallel dev:css dev:site",
"dev:css": "npx @tailwindcss/cli -i ./styles/styles.css -o ./public/styles/styles.css --watch",
"dev:site": "npx @11ty/eleventy --serve"
}
}Our development dependencies are:
- @11ty/eleventy: To generate our static website.
- @tailwindcss/cli: To generate our Tailwind styles.
- tailwindcss: To style our website with Tailwind.
- npm-run-all: To run two watchers in parallel (see below).
And then we create a few scripts to:
- dev: Watch for both CSS and site changes.
- dev:css: Watch for CSS changes and output
styles.css. - dev:site: Watch for site changes and serve the site.
Create docker-compose.yml file
Docker and Podman composition files are interchangeable, hence the name docker-compose.yml. This will build a simple container to generate and serve our site locally.
Let's create the docker-compose.yml file:
version: "3.8"
services:
web:
image: node:24-alpine
working_dir: /app
volumes:
- ./:/app:Z
ports:
- "8011:8080"
command: sh -c "npm install && npm run dev"Here's what it does:
- web: Creates a container called
web. - image: Uses the Node (version 24) Alpine image for the container.
- working_dir: Runs commands from that directory.
- volumes: Mounts the
eleventydirectory to theappdirectory inside the container. - ports: Exposes port
8080within the container to us locally on port8011. - command: Runs
npm installandnpm run devwhen the container starts.
Define project structure
We can set up the project in many different ways, but here's one way to do it. Once you see how it all comes together, you can change this structure (and update necessary configuration files) as needed.
eleventy/
|– content/
|– favicon/
|– images/
|– public/
|– scripts/
|– styles/
|– .gitignore
|– docker-compose.yml
|– eleventy.config.js
|– package.jsonLet's walk through each directory/file:
eleventyis the project directory.contentis for pages, layouts, etc.faviconis for favicon files (if you have them).imagesis for website images (if you have some).publicis automatically created by Eleventy when the site is generated.scriptsis for custom JavaScript (if you have any).stylesis for us to import Tailwind into the project..gitignorecontainsnode_modulesso that this directory doesn't get committed.docker-compose.ymlcontains instructions on how to build the container.eleventy.config.jstells Eleventy how the project is structured (more on that below).package.jsoncontains the packages required to build and run the website.
Create eleventy.config.js file
With the project structure in hand, you can now cross-reference it with the Eleventy configuration file below and make changes as you see fit.
Let's create the eleventy.config.js configuration file:
export default async function(eleventyConfig) {
eleventyConfig.addPassthroughCopy("favicon");
eleventyConfig.addPassthroughCopy("images");
eleventyConfig.addPassthroughCopy("scripts");
eleventyConfig.setInputDirectory("content");
eleventyConfig.setOutputDirectory("public");
};- addPassthroughCopy: Means that the
favicon,images, andscriptdirectories are copied as-is from theeleventydirectory into thepublicdirectory, which will contain the statically generated website. If you have none of those resources, you don't have to create or copy those directories. - setInputDirectory: Configures the directory Eleventy will watch for page and layout changes to generate the website. If not configured, the default would be the project root.
- setOutputDirectory: Configures the directory Eleventy will output the generated website in and serve it from there. If not configured, the default would be
_site.
Create styles.css for Tailwind
In order to use Tailwind CSS, we need to import the package into our project.
Let's create a styles.css file within the styles directory:
@import "tailwindcss";If you need to set any theme variables, you can do so right after @import.
Create a simple website
With all of the infrastructure in place, we can build a simple website with a single layout and a couple pages. You can use this setup to then add additional pages and/or layouts as required.
Let's create a default.liquid layout file in the content/_includes/layouts directory:
---
title: Example Industries
description: A brief description on what we do.
image: https://example.org/images/background.jpg
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% if title != 'Example Industries' %}{{ title }} | Example Industries{% else %}{{ title }}{% endif %}</title>
<meta name="description" content="{{ description }}" />
<meta name="generator" content="{{ eleventy.generator }}">
<meta property="og:url" content="https://example.org/" />
<meta property="og:type" content="website" />
<meta property="og:title" content="{{ title }}" />
<meta property="og:description" content="{{ description }}" />
<meta property="og:image" content="{{ image }}" />
<meta name="robots" content="index, follow">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="language" content="English">
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
<link rel="manifest" href="/favicon/manifest.json">
<link rel="stylesheet" href="/styles/styles.css" />
</head>
<body>
{{ content }}
<script src="/scripts/scripts.js"></script>
</body>
</html>Using front matter (the top part of the file), we can set default data to be used by our layout file for title, description, and image. That is to say, if a specific page using the default layout doesn't provide this data, it will default to what's set in the layout.
We then output this data via {{ title }}, {{ description }}, and {{ image }}. Note that we also have access to what generated the site via {{ eleventy.generator }}. It's not required, but a good way to support the Eleventy project.
The {{ content }} gets replaced with whatever the page content is in a file that implements the default layout.
Let's create a index.liquid file in the content directory to be our Homepage:
---
layout: layouts/default.liquid
---
<h1 class="text-3xl font-bold underline">
{{ title }}
</h1>
<p>Welcome to the homepage.</p>Using layout, we can tell Eleventy which layout file to use. It's also important to note how front matter data cascades, because it allows us to both reuse the value of title as defined in the layout file with {{ title }}, or we could override it by adding a title below layout.
In the example above, {{ title }} will render "Example Industries."
Let's also create a about.liquid file in the content directory to be our About page:
---
layout: layouts/default.liquid
title: About
description: Welcome to the about page.
---
<h1 class="text-3xl font-bold underline">
{{ title }}
</h1>
<p>Welcome to the about page.</p>In the example above, {{ title }} will render "About | Example Industries" due to the conditional in the layout file.
While this guide uses Liquid templates, you can choose from a number of templating languages– or mix and match them.
Generate static site files
Start the container to generate the site, watch for future CSS and site changes, and serve the site for us to see:
podman-compose upYou can visit your website here: http://localhost:8011.
It's serving everything contained in the public directory, and if you look closely, you'll see something amazing. Your index.liquid has become the root of your site at http://localhost:8011/, but your about.liquid isn't at http://localhost:8011/about.html, it's at http://localhost:8011/about/.
Eleventy is quite intuitive and powerful on how it generates its permalinks.
This means that if you just have a handful of pages, you can keep them all in the root of content, but if you need something more segregated or complicated, you can also opt to create sub directories. I really love how they solved this.
As you add pages or change styles, the site will automatically regenerate and live reload in your browser. When you're done, you can hit CTRL + C to stop the container. When you're ready to publish your site, take everything within the public directory and upload it to any static website host.
In closing, Eleventy surprised me and made it quite simple to convert a truly static site to something more dynamic. I recommend you take a look at the Eleventy docs to see what else it can do. Good luck with your project!
Featured image by Marcel Eberle.