Introduction
✏️ What is Nibula #
Nibula is an open source static site generator built on top of Eleventy (11ty)
It has one clear mission: make the jump from small hand-written practice sites to a real project setup as gentle as possible.
🎯 Why choose Nibula #
Beginner friendly #
If you've only ever written HTML, CSS and a bit of JavaScript, moving to a framework usually feels like starting over. A new syntax, new rules, new folder structures, and a pile of documentation to get through before you can even see a page on screen.
Nibula is designed to avoid exactly that. You stay close to the three languages you already know, and the folder structure stays small enough to hold in your head — a handful of folders whose names say what's inside, instead of a convention you have to study before it makes sense.
Everything else is optional. Nunjucks adds loops and includes to your HTML, Markdown files can carry the same logic, data files can feed a whole page — but none of it is needed on day one. A page written in plain HTML works just as well, and you pick the rest up when you have a reason to.
Ready to publish #
The hard part of a first site usually isn't building it — it's everything that comes after. Meta tags, a sitemap, a robots.txt, the server config your host expects: four things nobody taught you, all at once, right when you thought you were done.
Nibula writes them while you work. Fill in two data files and the SEO
tags, the sitemap and llms.txt build themselves from your pages.
.htaccess and web.config are already in the output folder, ready to upload,
and an nginx.conf is waiting if you have your own server.
When your pages are done, you're done — there's no second phase where you have to learn how to put a site online.
Nothing is hidden #
Every one of those files lives in the project, in the folder you'd expect, in the form you'd have written by hand. When you need to change something, you open it and change it — there's no configuration layer to learn first, and no generated code you're not supposed to touch.
Even the output folder is yours to move: one command points the whole build somewhere else — your local server's document root, for instance — and both the dev server and your own keep serving the same files as you save. See Nibula CLI.
A good fit for #
- Showcase sites and portfolios — a handful of pages, stable content, built to be found
- Small business sites — where a contact form on cheap shared hosting is the whole requirement, and the PHP backend is exactly that
- Landing pages — one page, fast, with the meta tags already in place
- Project documentation — like the page you're reading
- The project you build to learn — the one after the tutorials, when you want something real without picking up React first
- A small blog — up to a point: the assistant creates pages one at a time, which is fine for twenty posts and wrong for two hundred
🤔 When not to use it #
Nibula builds static pages. If your site needs users to log in, save something, or see different content depending on who they are, you want a framework with a server behind it — Next.js, Nuxt, Laravel.
It doesn't scale to hundreds of pages coming from a database or a CMS. The assistant creates pages one at a time, by hand, which is right for a site with ten or twenty of them and wrong for a catalogue.
If you already work with React, Vue or Angular, stay there. Nibula's whole point is being a first step for people who don't — for you it would be a step sideways.
⚙️ About Eleventy (11ty) #
Eleventy is the engine underneath. It takes your templates and your data and turns them into plain HTML files, once, when you build — which is why the result is just a folder you can upload anywhere.
Nibula is a layer on top, not a replacement. Eleventy gives you the build; Nibula adds the folder structure, the SCSS and JavaScript pipeline, the page assistant, the SEO files and the server configs. Everything Eleventy can do, your project can do — including the parts Nibula doesn't set up for you.
.eleventy.js sits in your project root, and it's the same configuration file
you'd find in any Eleventy project. When you need something beyond what Nibula
wires up — a new filter, a collection, a different output folder — the
Eleventy documentation applies directly, with no
translation needed.
Quick start
📋 Prerequisites #
Required #
- Node.js — v18.0.0 or higher
- Composer — latest version, only if you pick the PHP backend
- Better Nunjucks — VS Code extension by Ed Heltzel, for syntax highlighting in .njk files
Recommended #
- Material Icon Theme — VS Code extension by Philipp Kief
📦 Installation #
Install Nibula once, globally:
npm install -g nibula
This gives you the nib command, with nbl and nibula as aliases.
Global means you install it once on your machine, not once per project — from then on nib is available from any folder.
Once installed, these are the commands you'll use:
| Command | What it does |
|---|---|
| nib new | Creates a new project |
| nib run | Starts the dev server and rebuilds as you save |
| nib build | Builds the site for publishing |
| nib clean | Empties the output folder |
| nib cli | Opens the assistant to add, rename or remove pages |
| nib update | Updates Nibula itself |
| nib ver | Prints the version in use |
See Nibula CLI for what the assistant can do.
🚀 Project creation #
From the folder where you keep your websites, run:
nib new your-project
The scaffolder asks you three things:
| Question | Options |
|---|---|
| Language | JavaScript or TypeScript |
| CSS framework | Bootstrap, Bulma, Foundation, UIkit, or none |
| Backend | Node.js or PHP |
TypeScript adds types to JavaScript — pick it if you already know what that means, otherwise JavaScript is the safer start.
The CSS framework can be switched later by commenting a few lines.
The Backend is the choice worth getting right: Node and PHP offer the same API, but they run in very different places — see Deploy.
Dependencies are installed for you. If you pick Node, the Composer step is skipped entirely, so you don't need Composer on your machine at all.
Then move into the project and start the dev server:
cd your-project
nib run
Your site is now at localhost:8080, and it rebuilds every time you save a file.
Page structure
🧩 Composition #
The page settings #
Each page lives in routes/ as a .njk file.
---
title: "my-page"
permalink: "/"
layout: base.njk
---
| Key | Description |
|---|---|
| title | The internal name of the page. Changing it breaks its styles and scripts, so leave it as it is |
| permalink | The address the page will have on your site |
| layout | The look the page is built into (see layouts) |
The permalink needs a slash at both ends: /my-page/, not my-page.
Without the trailing one you get my-page.html instead of a clean /my-page/ address
What a page is made of #
A page is made of three files, plus one entry in a shared data file.
| File | What it holds |
|---|---|
routes/my-page.njk |
The page itself: its settings and its content |
scss/pages/myPage.scss |
The styles that apply only to this page |
js/pages/myPage.js |
The code that runs only on this page |
The route uses the name as you typed it; the stylesheet and script use its camelCase form. The assistant handles the conversion for you.
That entry lives in data/pages.json (see Each page SEO):
"myPage": {
"seo": {
"title": "My Page",
"description": "Description",
"keywords": "",
"noindex": false,
"canonical": ""
},
"cdn": {
"css": [],
"js": []
}
}
You can create, delete or rename them by hand, or let the Nibula CLI do it for you — including that record, the easiest one to forget.
Inside your page file (routes/my-page.njk) you write only the HTML content of that page — it goes automatically inside the main tag. Everything around it comes from the layout.
You can write that content directly in the page, or split it into components — separate files you write once and pull into any page with a single line.
🧱 Layouts #
A layout is the shell a page is rendered into. Your page provides the content; the layout provides everything around it — the head, the header, the footer, the stylesheets and the scripts.
Layouts live in src/frontend/layouts/, and every page picks one in its
settings:
---
title: "homepage"
permalink: "/"
layout: base.njk
---
What base.njk gives you #
base.njk is the default, and it's the one every new page starts with. It writes for you:
You can pick a different one with the Nibula CLI when you create the page — if you have more than one
| Part | What it does |
|---|---|
| head | The page title, description, canonical link and social preview tags, taken from your data files |
| favicon | The three icon tags, pointing at assets/brand/ |
| CSS | The stylesheet of the current page, plus any CDN links you listed for it |
| header and footer | Included from components/global/, so they're identical on every page |
| JS | The script of the current page, the framework bundle, and any CDN scripts |
The page's own stylesheet and script are found by title: a page whose title is myPage loads css/pages/myPage.css and js/pages/myPage.js. The same name is the key of its record in data/pages.json — that's why the title shouldn't be changed by hand.
Loading something from a CDN #
If a page needs a library that isn't in your project — a chart library, a font,
an icon set — list its URL in that page's record in data/pages.json:
"myPage": {
"seo": { ... },
"cdn": {
"css": [
"https://cdn.example.com/lib.min.css",
"https://cdn.another.com/lib.min.css"
],
"js": [
"https://cdn.example.com/lib.min.js",
"https://cdn.another.com/lib.min.js"
]
}
}
The layout adds them to that page only, so a library used on one page isn't downloaded on all the others.
If you want to add a CDN to all pages, you should add a link into the layout
Making your own #
You may need a full-screen login that starts straight with the content, a different header on a group of pages, or a CDN loaded on all of them instead of one at a time.
A custom layout is worth it when something changes for some pages. If it
changes for all of them, edit base.njk directly.
Copy layouts/base.njk, give the copy a name of your own, and change only what
those pages need.
Custom layouts examples #
No header and no footer. For a page that fills the screen on its own, create
layouts/straight-content.njk and remove the two global includes:
<body>
<!-- Drop this -->
{% include "global/header.njk" ignore missing %}
<main>
{{ content | safe }}
</main>
<!-- Drop this -->
{% include "global/footer.njk" ignore missing %}
</body>
Those two come from
components/global/— see components
A different header. Point the include at another component instead:
{% include "global/landing-header.njk" ignore missing %}
A library on some pages only. Add the link in the head, next to the others — no need to repeat it in each page's CDN list:
<link rel="stylesheet" href="https://cdn.example.com/lib.min.css">
Everything you leave alone — the SEO tags, the stylesheet and script links — keeps working without you touching it.
Then point a page at your layout:
---
title: "login"
permalink: "/login/"
layout: straight-content.njk
---
🖱️ Nibula CLI #
Options #
The assistant writes the three files and the pages.json record for you, keeping the names in sync. Run it from anywhere inside a project:
nib cli
| Option | What it does |
|---|---|
| Create page | Writes the three files and the record, asking which layout to use if you have more than one |
| Remove page | Deletes them all, after asking for confirmation |
| Rename page | Renames the files, updates the front matter, and moves the record |
| Configure output path | Changes where the site is built, across every file that references it |
homepage
(index.njk)and 404 are protected — the assistant won't touch them
Renaming leaves the contents of the record alone: your SEO title, description and CDN links stay as you wrote them, and so do the components you included.
A new page arrives empty apart from its settings and two commented examples — one for a component, one for a markdown file.
Where the site is built #
By default the build goes to out/. Change it when the folder you build into isn't the folder you work in — a local server's document root, a shared drive, a path your host expects. The dev server follows, rebuilding there as you save, with nothing to copy by hand.
Any absolute or relative path works:
C:/laragon/www
../any-folder
If you want to reset the output path to the project root, just type . as path
If you don't have a reason to change it, leave
out/— it's already ignored by git and cleaned by nib clean
Components
What is a component #
A component is a piece of a page you write once and use wherever you need it — a
header, a card, a section of text. It's just a file in
src/frontend/components/, and nothing has to be registered anywhere.
There are two kinds, depending on what's inside.
Create a component #
To create a component (.njk or .md), just create a new file under
src/frontend/components/ and fill it with your content.
You can create your custom subfolders and organize the components path as you prefer.
src/frontend/components/
├── global/
│ ├── header.njk
│ └── footer.njk
├── legal/
│ ├── privacy-policy.md
│ └── . . .
├── cards/
│ ├── feature-card.njk
│ └── . . .
└── . . .
What is Nunjucks #
Nunjucks (.njk) is basically an HTML file that can do a few more things like if statements, for loops, includes of other components and access to your data files.
That's what lets you write a header once and have it on every page, or build a list of cards from a JSON file instead of copying the same markup ten times.
Nunjucks component examples #
components/feature-card.njk
<div class="pricing-card">
<h3>Professional</h3>
<p>€19 / month</p>
<ul>
<li>Unlimited projects</li>
<li>Priority support</li>
<li>Custom domain</li>
</ul>
<a class="btn" href="/signup/">Subscribe</a>
</div>
Writing every item by hand is fine here, but it stops being fine when the list grows or when the same values appear on more than one page. This is where .njk files help: you list your values once and loop over them.
components/feature-card.njk
{% set features = ["Unlimited projects", "Priority support", "Custom domain"] %}
<div class="pricing-card">
<h3>Professional</h3>
<p>€19 / month</p>
<ul>
{% for feature in features %}
<li>{{ feature }}</li>
{% endfor %}
</ul>
<a class="btn" href="/signup/">Subscribe</a>
</div>
With three items, writing them out by hand is shorter. Loops earn their keep when the list grows, or when the same values feed more than one page
You don't have to use this. Writing the content out by hand works just as well, and you can come back to loops when you need them
What is Markdown #
Markdown (.md) is your best choice of component if you just need to write text like documentation or an article.
They are easy to write and read even in raw form. You can create headings, lists, links, tables etc with just 1 symbol instead of HTML tags
Markdown component examples #
components/example.md
## h2 title
A paragraph of plain text. You can make a word **bold** or _italic_.
- List element
- Another one
### h3 title
One more paragraph, with a [link](https://github.com/rhaastrake/nibula) in it.
A table
| Column | Another |
| ------ | ------- |
| Value | Value |
| Value | Value |
Result
h2 title #
A paragraph of plain text. You can make a word bold or italic.
- List element
- Another one
h3 title #
One more paragraph, with a link in it.
A table
Column Another Value Value Value Value
From your SCSS, target the tags inside .markdown-body to reach every Markdown file at once, and it's how you set things like heading colours or table borders across the whole site:
.markdown-body {
.red-color {
color: red;
. . .
}
. . .
}
Plain Markdown gives you no way to set an id or a class, and no logic. Nibula preconfigures both for you: Ids and classes in curly braces at the end of a line, so your SCSS can reach a specific piece of text:
## Title {id="title-id" class="highlight"}
Go to [the title](#title-id)
Headings already get an id from their text. Writing one yourself replaces it with a custom one
Nibula also preconfigures Nunjucks inside Markdown, so the same if, for and variables you use in a .njk file work in a .md file too:
components/plans.md
## Our plans
{% set plans = ["Free", "Pro", "Team"] %}
{% for plan in plans %}
- {{ plan }}
{% endfor %}
Result
Our plans #
- Free
- Pro
- Team
Values from a data file work too, but only if you pass them when you include the file — a
.mddoesn't see your data on its own
Include a component #
A component does nothing until a page asks for it. You write that line in a page, in a layout, or in another component — a card can pull in a button, a section can pull in three cards.
The two kinds are included differently, because .njk is already a template while .md has to be turned into HTML first.
Include njk #
{% include "feature-card.njk" ignore missing %}
Paths start from src/frontend/components/, so you never write that part.
If the component is in a subfolder, include the path too — for example
cards/feature-card.njkignore missing tells the build to skip the include instead of stopping when the file isn't there
The component sees the same values the page sees, so `` works without passing anything
Include markdown #
A .md file needs renderFile, which converts it and drops the result in place. This very page is built that way, mixing .njk components and .md files.
<div class="markdown-body">
{% renderFile "your-file.md" | componentsPath, { site: site }, "njk,md" %}
</div>
Five parts to that line:
| Part | What it does |
|---|---|
| markdown-body | The wrapper that applies the Markdown styling |
| renderFile | Turns the file into HTML and puts it in the page |
| componentsPath | Fills in the src/frontend/components/ part of the path |
| { site: site } | Hands the file the data it needs |
| "njk,md" | Tells Eleventy the file is Nunjucks + Markdown |
The curly braces list what the Markdown is allowed to see: on the left the name the file will use, on the right the data you're passing. So { site: site } lets you write `` inside the file — see data files.
Pass as many as the file needs: { site: site, pages: pages }. Without them, a `` written inside the Markdown comes out empty
Subfolders work the same way as
.njkfiles —legal/privacy-policy.md
Styling
What is SCSS #
.
.
.
.
.
.
Structure #
.
.
.
.
.
.
Create a module #
.
.
.
.
.
.
Import a module #
.
.
.
.
.
.
CSS frameworks #
.
.
.
.
.
.
Javascript
Javascript or TypeScript #
.
.
.
.
.
.
Structure #
.
.
.
.
.
.
Create a module #
.
.
.
.
.
.
Import a module #
.
.
.
.
.
.
SEO
Each page SEO #
.
.
.
.
.
.
What is SEO #
.
.
.
.
.
.
Best practices #
.
.
.
.
.
.
Sitemap generation #
.
.
.
.
.
.
Robots & Crawlers #
.
.
.
.
.
.
Backend
Node.js or PHP #
.
.
.
.
.
.
Structure #
.
.
.
.
.
.
Endpoints logic #
.
.
.
.
.
.
Database #
.
.
.
.
.
.
Additional packages #
.
.
.
.
.
.
Node service #
.
.
.
.
.
.
Deploy
How to deploy #
.
.
.
.
.
.
Hosting options #
.
.
.
.
.
.
Apache / IIS #
.
.
.
.
.
.
Nginx #
.
.
.
.
.
.
About
Changelog #
.
.
.
.
.
.
Roadmap #
.
.
.
.
.
.
Dependecies #
.
.
.
.
.
.
⭐ Support Nibula #
Thanks for giving Nibula a try.
It's a one-person project, so every bit of feedback counts more than you'd think: if something breaks, if the documentation isn't clear, or if a feature is missing, open an issue.
If Nibula was useful to you, leaving a ⭐ on the repository is the easiest way to help other people find it.
You can also support the project by buying me a coffee on PayPal ☕🫡