How to create a theme for Aether CMS

LebCitLebCit
18 min read

Intro

I've released today (2025-06-06) Aether CMS which is a modern and flexible Node.js content management system with an integrated - ultra fast - static site generator.
While I've created a detailed documentation for Aether - covering every aspect of its functionalities (including theming) - my decade of experience in theming tells me that even if the system is easy with an in-depth documentation, it's not directly obvious (even for developers) how things work together.
The purpose of this tutorial is to simplify Aether theming's documentation by offering a clearer, step-by-step approach to creating a theme using a real-world example.


Theme Structure

A theme in Aether consists of 3 required files:

  • theme.json β†’ The configuration file for your Aether theme

  • templates/layout.html β†’ The main template for your site

  • assets/css/style.css β†’ The main stylesheet for your theme

Assuming you're building a landing page for a new product, a registration page for an event, a long-form sales page, a mobile app promo with store links, or even a simple 'coming soon' page, the basic structure of your Aether theme would look like this:

πŸ“ my-aether-theme/
β”œβ”€β”€ πŸ“ assets/
β”‚   └── πŸ“ css/
β”‚       └── 🎨 style.css
β”œβ”€β”€ πŸ“ templates/
β”‚   └── 🧩 layout.html
└── πŸ› οΈ theme.json

Theme JSON File

The theme.json file of your Aether theme is where you define the configuration fields for it. For brevity I'll only use the required fields for an Aether theme. Create a theme.json file at the root of my-aether-theme and populate it with the following data:

{
    "title": "Clean Blog",
    "description": "Clean and responsive, with a clear typography, perfect for personal blogs",
    "version": "1.0.0",
    "author": "YOUR-NAME-HERE",
    "authorUrl": "",
    "tags": ["blog", "modern", "responsive", "simple", "clean"],
    "license": "GPL-3.0-or-later",
    "features": [
        "Responsive design",
        "One Column",
        "SEO optimized",
        "Social media integration",
        "Featured Images",
        "Custom Pages"
    ],
    "screenshot": ""
}

These fields are required for the system to allow the theme upload. As you've noticed, authorUrl and screenshot are provided with an empty-string (""); you can do the same for tags and features with an empty-array ([]).
The only field you cannot change is license, its value must be exactly "GPL-3.0-or-later".


Main Template

The layout.html file is the main template for your site, it must be located inside a templates folder at the root of your theme directory (e.g., my-aether-theme/templates/layout.html).


Main Stylesheet

The style.css file is the main stylesheet for your site. This file must be located inside a css folder, which is within an assets folder at the root of your theme directory (e.g., my-aether-theme/assets/css/style.css).


Templating

Templating is the process of getting data from the backend and displaying it in a template the way we want.
In Aether, a template is just an HTML file β€” the backbone of the web β€” with a .html extension (e.g., layout.html).
Aether provides a variety of variables you can use in a theme template, depending on the context.
In this tutorial, we're going to recreate the Clean Blog theme by Start Bootstrap as an Aether theme.
The most important aspect of templating is understanding the structure of the theme you want to create. This makes it easier to break the theme into reusable parts and components, following the DRY principle (Don't Repeat Yourself).


Clean Blog Structure

By looking at the Clean Blog preview, we can see that the homepage consists of the following parts:

  1. A logo with a navigation menu at the top

  2. A hero section with a background image and some text

  3. A content section listing posts with navigation

  4. A footer with social media links and copyright information

After exploring the available links in the theme’s menu, we can see that the post sample features a background image that fills the height of the screen followed by its content. In contrast, the about and contact pages display their own content.
To summarize this quick overview: the post sample page stands out with a slightly different structure, mainly due to its taller background image and unique layout compared to the other pages.


Install Aether

To follow along with creating Clean Blog as an Aether theme, I recommend installing Aether and working alongside me. This way, you can better grasp the concepts and see how everything comes together visually in your browser.
I'll assume you have Node.js and npm installed on your machine. Make sure your Node.js version is at least 18; if not, you can update by installing the latest LTS (Long Term Support) version from the official Node.js download page.
To install Aether, choose a location on your machine where you prefer to work (e.g., in a tests folder) and open it in your code editor (I use VS Code). Then, in the terminal, run the following command:

npx create-aether-cms my-cms-site

This will create a ready-to-use version of Aether in a folder named my-cms-site.
During the installation process, the terminal will output the following logs until it finishes:

$ npx create-aether-cms my-cms-site
🌳 Creating a new Aether CMS project in my-cms-site...
Cloning into 'D:\tests\my-cms-site'...
remote: Enumerating objects: 240, done.
remote: Counting objects: 100% (240/240), done.
remote: Compressing objects: 100% (212/212), done.
remote: Total 240 (delta 19), reused 240 (delta 19), pack-reused 0 (from 0)
Receiving objects: 100% (240/240), 467.82 KiB | 680.00 KiB/s, done.
Resolving deltas: 100% (19/19), done.
πŸ“„ Created .env file with default settings
πŸ“¦ Updated package.json with your project details
πŸ“ Created default content structure
πŸ“¦ Installing dependencies (this might take a few minutes)...

added 7 packages, and audited 8 packages in 2s

found 0 vulnerabilities
πŸ”„ Setting up git repository...
βœ… Renamed origin remote to upstream
Do you want to set up a new git origin for your project? (y/n): n
πŸ”„ Git repository configured with update capabilities

πŸŽ‰ Success! Created my-cms-site at D:\tests\my-cms-site
πŸš€ Get started with the following commands:

    cd my-cms-site
    npm start

πŸ”‘ Default admin credentials:
    Username: admin
    Password: admin

πŸ“š For documentation, visit: https://aether-cms.pages.dev/

During the installation process, you'll be asked whether you want to connect your Aether installation to a remote Git repository. If you choose not to, the installation will still work locally, and you can always set the Git origin later using:

git remote add origin <your-repo-url>

With Aether set up, let’s get to work and start recreating Clean Blog for Aether!


Clean Blog Creation

If you've already created your Aether theme structure as described above, just rename the root folder from my-aether-theme to clean-blog. This helps keep things tidy and follows a logical convention, since the title specified in the theme.json file is "title": "Clean Blog".

Copy the clean-blog folder into my-cms-site/content/themes. Your content folder structure should now look like this:

πŸ“ content/
β”œβ”€β”€ πŸ“ cache/
β”œβ”€β”€ πŸ“ data/
β”œβ”€β”€ πŸ“ themes/
β”‚   β”œβ”€β”€ πŸ“ clean-blog/
β”‚   β”‚   β”œβ”€β”€ πŸ“ assets/
β”‚   β”‚   β”‚   └── πŸ“ css/
β”‚   β”‚   β”‚       └── 🎨 style.css
β”‚   β”‚   β”œβ”€β”€ πŸ“ templates/
β”‚   β”‚   β”‚   └── 🧩 layout.html
β”‚   β”‚   └── πŸ› οΈ theme.json
β”‚   └── πŸ“ default/
β”œβ”€β”€ πŸ“ uploads/

Starting Aether

If you're comfortable using the terminal, you can navigate into the folder and start the project by running:

cd my-cms-site
npm start

Otherwise, open my-cms-site with your code editor, open a terminal from there, and run:

npm start

The terminal will output something like this:

$ npm start

> aether-cms@1.0.0 start
> node index.js

Creating default admin user...
Default admin user created. Username: admin, Password: admin
You have the latest version of litenode
App @ http://localhost:8080

You can now open http://localhost:8080 in your browser, or simply click the link in the terminal output. You’ll be presented with the default Aether theme.

Note: localhost refers to your own computer, it's a way to access the site you're running locally, just for you. The number 8080 is the port it's running on.


The Administration

To access the Aether administration dashboard, navigate to http://localhost:8080/aether and log in using the default admin credentials:

  • Username: admin

  • Password: admin

In the Aether dashboard, click the Themes link in the left sidebar. You will be redirected to the Themes Management page. Click on the Clean Blog theme card, and the Theme Details sidebar will appear on the right side of your screen, displaying its information.

Next, click the Activate Theme button. A toast notification will confirm that the theme has been successfully activated, and the page will refresh. Finally, click the View Site button at the top of the Aether dashboard. A new tab will open with a blank page.

Congratulations! You’ve just created the base for an Aether theme.


Clean Blog Index

As seen previously, the Clean Blog homepage displays a list of posts. We have access to Clean Blog’s source code in its GitHub repository. Navigate to the dist folder and copy the contents of index.html into the content/themes/clean-blog/templates/layout.html file. Now, refresh the blank page tab, and the HTML from layout.html will be rendered.

Next, do the same with styles.css by copying its contents into content/themes/clean-blog/assets/css/style.css. If you refresh the browser, you won’t see any changes yet, because the stylesheet link doesn’t point to the correct path. To fix this, update the link in layout.html as follows:

<!-- Find this line -->
<link href="css/styles.css" rel="stylesheet" />

<!-- Replace it with -->
<link href="/content/themes/clean-blog/assets/css/style.css" rel="stylesheet" />

Finally, to get the scripts working locally, create a js folder inside the assets directory. Then create a new file named main.js and copy the script’s contents into it. Update the script path in layout.html like this:

<!-- Find this line -->
<script src="js/scripts.js"></script>

<!-- Replace it with -->
<script src="/content/themes/clean-blog/assets/js/main.js"></script>

Reload the browser. Abracadabra! You’ve successfully replicated the Clean Blog homepage.
Your clean-blog folder should now look like this:

πŸ“ clean-blog/
β”œβ”€β”€ πŸ“ assets/
β”‚   β”œβ”€β”€ πŸ“ css/
β”‚   β”‚   └── 🎨 style.css
β”‚   └── πŸ“ js/
β”‚       └── πŸ“œ main.js
β”œβ”€β”€ πŸ“ templates/
β”‚   └── 🧩 layout.html
└── πŸ› οΈ theme.json

In Aether, static assets like CSS and JavaScript are loaded using absolute paths (e.g. /content/themes/clean-blog/assets/...) from the theme's assets folder. You can also organize third-party libraries by placing them in a vendors folder within assets for easy access and cleaner structure.

Note: For images that you don’t want to upload through the media library, you can create an images folder inside the assets directory and reference them directly in your templates, always using absolute paths.


Dynamic Rendering

Replicating the Clean Blog homepage was easy and straightforward, just as Aether is designed to be.

Now comes the fun part, where you'll step into the role of an architect: breaking down the structure into reusable parts and components, then dynamically combining them using modern tools like LiteNode, the core of Aether. It provides everything you need without relying on any third-party libraries whatsoever!

Create 3 new folders within clean-blog:

  • partials - contains the main building blocks of the theme (e.g. head, footer, etc.)

  • contents – holds the rendering logic for page content based on its context

  • custom - holds the rendering logic for custom pages (e.g. blog, search, etc.)

As discussed previously, understanding the structure is a crucial aspect of templating. While we already understood the structure of Clean Blog, we now need to scan its HTML and identify the differences we previously noticed visually. I’ll simplify this task and point out the key differences.

1. The Header of Posts

<!-- The header for the homepage has this structure -->
<header class="masthead" style="background-image: url('assets/img/home-bg.jpg')">
    ...
    <div class="site-heading">
        <h1>Clean Blog</h1>
        <span class="subheading">A Blog Theme by Start Bootstrap</span>
    </div>
    ...
</header>

<!-- The header for all pages has this structure -->
<header class="masthead" style="background-image: url('assets/img/home-bg.jpg')">
    ...
    <div class="page-heading">
        <h1>About Me</h1>
        <span class="subheading">This is what I do.</span>
    </div>
    ...
</header>

<!-- The header for individual posts has this structure -->
<header class="masthead" style="background-image: url('assets/img/post-bg.jpg')">
    ...
    <div class="post-heading">
        <h1>Man must explore, and this is exploration at its greatest</h1>
        <h2 class="subheading">Problems look mighty small from 150 miles up</h2>
        <span class="meta">
            Posted by
            <a href="#!">Start Bootstrap</a>
            on August 24, 2023
        </span>
    </div>
    ...
</header>

The differences here are:

  • The homepage header uses a <div> with the class site-heading.

  • The page header uses a <div> with the class page-heading.

  • The post header uses a <div> with the class post-heading.

  • The post subtitle uses an <h2> tag instead of a <span>.

  • Post metadata such as author and date is included in a <span class="meta">.

2. Main Content Wrappers by Content Type

<!-- 1. Index content -->
<div class="container px-4 px-lg-5">
    ...
    <!-- Posts listing -->
</div>

<!-- 2. Page content -->
<main class="mb-4">
    <div class="container px-4 px-lg-5">
        ...
        <!-- Content of the page -->
    </div>
</main>

<!-- 3. Post content -->
<article class="mb-4">
    <div class="container px-4 px-lg-5">
        ...
        <!-- Content of the post -->
    </div>
</article>

Here, the differences are:

  • Index content does not include the mb-4 bottom margin class.

  • Page content is wrapped in a <main> element.

  • Post content is wrapped in an <article> element.

⚠️

Remember: You're now an architect and need to make practical decisions. Modifying, replacing, or even removing parts or components is part of your job, as long as you understand what you're doing and avoid breaking the theme!


Practical Decisions

After reviewing the Clean Blog stylesheet and scripts, I found the following:

  1. The .site-heading and .page-heading classes share the same rules.

  2. The <article> and <main> tags do not have any specific styles or scripts associated with them.

Based on this, my decisions for the theme are straightforward:

  1. Use .page-heading as the class for homepage headings.

  2. Wrap the entire site content in a <main class="mb-4"> tag.

What does this mean?

This means I can safely remove all CSS rules related to .site-heading and unify the site's content wrapper. Adding some margin to the list of posts on the homepage and changing the <article> wrapper for single posts to <main> will not affect the structure, responsiveness, or functionality of the theme.

I'm also going to remove Font Awesome and replace the icons used in the theme with inline SVGs from Feather. Loading an entire icon library just for a few icons isn't a good idea, especially when it comes to performance.

In most cases, there's always a lighter alternative, so don't be lazy and take the time to look!
Remember: You don't need a cannon to kill a fly!

Note: Even if a decision might affect the theme, if you think it’s the right call, you can simply scan the structure and update the relevant IDs, classes, and tags to match your needs.


Clean Blog Templates

Now that we know what changes we're going to make, it's finally time to break the theme into reusable parts and components. To do this, you'll need to create the following files:

  1. In the partials folder:

    • head.html

    • menu.html

    • header.html

    • content.html

    • pagination.html

    • footer.html

  2. In the contents folder:

    • 404.html

    • posts-listing.html

  3. In the custom folder:

    • blog.html

    • homepage.html

The structure of your clean-blog folder should now look like this:

πŸ“ clean-blog
β”œβ”€β”€ πŸ“ assets
β”‚   β”œβ”€β”€ πŸ“ css
β”‚   β”‚   └── 🎨 style.css
β”‚   β”œβ”€β”€ πŸ“ js
β”‚   β”‚   └── πŸ“œ main.js
β”‚   └── πŸ“¦ vendors
β”‚       └── πŸ“œ bootstrap.bundle.min.js
β”œβ”€β”€ πŸ“ contents
β”‚   β”œβ”€β”€ ❌ 404.html
β”‚   └── πŸ“ posts-listing.html
β”œβ”€β”€ πŸ“ custom
β”‚   β”œβ”€β”€ πŸ“° blog.html
β”‚   └── 🏠 homepage.html
β”œβ”€β”€ πŸ“ partials
β”‚   β”œβ”€β”€ 🧩 content.html
β”‚   β”œβ”€β”€ 🧩 footer.html
β”‚   β”œβ”€β”€ 🧩 head.html
β”‚   β”œβ”€β”€ 🧩 header.html
β”‚   └── 🧩 menu.html
β”œβ”€β”€ πŸ“ templates
β”‚   └── 🧱 layout.html
└── πŸ› οΈ theme.json

Look closely: I’ve added a vendors folder inside the assets directory and included the Bootstrap scripts in a file named bootstrap.bundle.min.js.

You can create the folder and file yourself, then copy and paste the content from the link above into that file.

Now, for the files in the contents, custom, and partials directories, copy and paste the corresponding content provided below into each file based on its name.

head.html

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <meta name="description" content="{{metadata.seoDescription || metadata.excerpt || site.siteDescription}}" />
    <meta name="author" content="{{metadata.author}}" />
    <title>{{homeRoute ? "Home" : metadata.title}} - {{site.siteTitle}}</title>
    <link rel="icon" href="/content/uploads{{site.siteIcon}}" />

    <!-- Google fonts-->
    <link
        href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic"
        rel="stylesheet"
        type="text/css"
    />
    <link
        href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800"
        rel="stylesheet"
        type="text/css"
    />

    <!-- Core theme CSS (includes Bootstrap)-->
    <link href="/content/themes/clean-blog/assets/css/style.css" rel="stylesheet" />
</head>

menu.html

<nav class="navbar navbar-expand-lg navbar-light" id="mainNav">
    <div class="container px-4 px-lg-5">
        <a class="navbar-brand" href="/">
            {{site.siteTitle}}
        </a>
        <button
            class="navbar-toggler"
            type="button"
            data-bs-toggle="collapse"
            data-bs-target="#navbarResponsive"
            aria-controls="navbarResponsive"
            aria-expanded="false"
            aria-label="Toggle navigation"
        >
            Menu
            <!-- Home Feather Icon -->
            <svg
                xmlns="http://www.w3.org/2000/svg"
                width="12"
                height="12"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
                stroke-linecap="round"
                stroke-linejoin="round"
                class="feather feather-menu"
            >
                <line x1="3" y1="12" x2="21" y2="12" />
                <line x1="3" y1="6" x2="21" y2="6" />
                <line x1="3" y1="18" x2="21" y2="18" />
            </svg>
        </button>
        <div class="collapse navbar-collapse" id="navbarResponsive">
            <ul class="navbar-nav ms-auto py-4 py-lg-0">
                <!-- Clean Blog menu is a top level menu only. -->
                <!-- It doesn't support nested children under a parent. -->
                <!-- That's why we retrieve top level items only -->
                <!-- Learn more about the Aether Menu System at:
                https://aether-cms.pages.dev/documentation/theming/menu-system/ -->
                {{#each menuItems}}
                    <!---->
                    {{#if depth=0}}
                        <li class="nav-item">
                            <a class="nav-link px-lg-3 py-3 py-lg-4" href="{{url}}">
                                {{title}}
                            </a>
                        </li>
                    {{/if}}
                    <!---->
                {{/each}}
            </ul>
        </div>
    </div>
</nav>

header.html

<header class="masthead" style="background-image: url(/content/uploads{{metadata.featuredImage.url}})">
    <div class="container position-relative px-4 px-lg-5">
        <div class="row gx-4 gx-lg-5 justify-content-center">
            <div class="col-md-10 col-lg-8 col-xl-7">
                <div class="{{fileType === `post` ? `post-heading` : `page-heading`}}">
                    {{#if fileType === "post"}}
                    <h1>{{metadata.title}}</h1>
                    {{#if metadata.subtitle}}
                    <h2 class="subheading">{{metadata.subtitle}}</h2>
                    {{/if}}
                    <span class="meta">
                        Posted by {{metadata.author}} on {{metadata.publishDate | dateFormat("MMMM D, YYYY")}}
                    </span>
                    {{#else}}
                    <h1>{{homeRoute ? site.siteTitle : metadata.title}}</h1>

                    {{#if homeRoute && site.siteDescription}}
                    <h2 class="subheading">{{site.siteDescription}}</h2>
                    {{#elseif metadata.subtitle}}
                    <h2 class="subheading">{{metadata.subtitle}}</h2>
                    {{/if}}
                    <!---->
                    {{/if}}
                </div>
            </div>
        </div>
    </div>
</header>

content.html

<main class="mb-4">
    <div class="container px-4 px-lg-5">
        <div class="row gx-4 gx-lg-5 justify-content-center">
            <div class="col-md-10 col-lg-8 col-xl-7">
                <!-- Content by context  -->
                <!-- Learn more about variables in Aether by visiting:
                https://aether-cms.pages.dev/documentation/theming/template-variables-by-context/  -->
                {{#if notFoundRoute}}
                <!-- If no content has been found, display the 404.html template content -->
                {{#include("contents/404.html")}}
                <!---->
                {{#elseif homeRoute || metadata.slug === "blog"}}
                <!-- On the homepage and the blog, display the posts-listing.html template -->
                {{#include("contents/posts-listing.html")}}
                <!---->
                {{#elseif fileType === "post"}}
                <!-- For posts, display the post content and pagination -->
                {{content}}
                <!---->
                {{#include("partials/pagination.html")}}
                <!---->
                {{#else}}
                <!-- For normal pages, display the page content -->
                {{content}}
                <!---->
                {{/if}}
            </div>
        </div>
    </div>
</main>

pagination.html

<!-- Creating pagination in Aether is super easy, read more about it at:
https://aether-cms.pages.dev/documentation/theming/working-with-pagination/ -->
{{#if pagination || prevPost || nextPost}}
<!-- Posts pagination -->
<div class="d-flex justify-content-between mb-4">
    {{#if pagination.nextPage}}
    <!-- Older posts -->
    <a href="{{pagination.urls.next}}" class="btn btn-primary text-uppercase" rel="next">← Older Posts</a>
    {{/if}}

    <!---->

    {{#if pagination.prevPage}}
    <!-- Newer posts -->
    <a href="{{pagination.urls.prev}}" class="btn btn-primary text-uppercase ms-auto" rel="prev">Newer Posts β†’</a>
    {{/if}}

    <!---->

    {{#if prevPost}}
    <!-- Previous Post Link (only if there's a previous post) -->
    <a
        href="/post/{{prevPost.slug}}"
        class="prev-post btn btn-primary btn-sm m-3"
        rel="prev"
        aria-label="Go to the previous post"
    >
        Previous: {{prevPost.title}}
    </a>
    {{/if}}

    <!---->

    {{#if nextPost}}
    <!-- Next Post Link (only if there's a next post) -->
    <a
        href="/post/{{nextPost.slug}}"
        class="next-post btn btn-primary btn-sm m-3 ms-auto"
        rel="next"
        aria-label="Go to the next post"
    >
        Next: {{nextPost.title}}
    </a>
    {{/if}}
</div>
{{/if}}

footer.html

<footer class="border-top">
    <div class="container px-4 px-lg-5">
        <div class="row gx-4 gx-lg-5 justify-content-center">
            <div class="col-md-10 col-lg-8 col-xl-7">
                <ul class="list-inline text-center">
                    <li class="list-inline-item">
                        <a href="javascript:void(0)" aria-label="Twitter">
                            <span>
                                <!-- Twitter Feather Icon -->
                                <svg
                                    xmlns="http://www.w3.org/2000/svg"
                                    width="24"
                                    height="24"
                                    viewBox="0 0 24 24"
                                    fill="none"
                                    stroke="currentColor"
                                    stroke-width="2"
                                    stroke-linecap="round"
                                    stroke-linejoin="round"
                                    class="feather feather-twitter"
                                    aria-hidden="true"
                                >
                                    <path
                                        d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"
                                    ></path>
                                </svg>
                            </span>
                        </a>
                    </li>

                    <li class="list-inline-item">
                        <a href="javascript:void(0)" aria-label="Facebook">
                            <span>
                                <!-- Facebook Feather Icon -->
                                <svg
                                    xmlns="http://www.w3.org/2000/svg"
                                    width="24"
                                    height="24"
                                    viewBox="0 0 24 24"
                                    fill="none"
                                    stroke="currentColor"
                                    stroke-width="2"
                                    stroke-linecap="round"
                                    stroke-linejoin="round"
                                    class="feather feather-facebook"
                                    aria-hidden="true"
                                >
                                    <path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path>
                                </svg>
                            </span>
                        </a>
                    </li>

                    <li class="list-inline-item">
                        <a href="javascript:void(0)" aria-label="GitHub">
                            <span>
                                <!-- GitHub Feather Icon -->
                                <svg
                                    xmlns="http://www.w3.org/2000/svg"
                                    width="24"
                                    height="24"
                                    viewBox="0 0 24 24"
                                    fill="none"
                                    stroke="currentColor"
                                    stroke-width="2"
                                    stroke-linecap="round"
                                    stroke-linejoin="round"
                                    class="feather feather-github"
                                    aria-hidden="true"
                                >
                                    <path
                                        d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
                                    />
                                </svg>
                            </span>
                        </a>
                    </li>
                </ul>
                <div class="small text-center text-muted fst-italic">
                    {{site.footerCode}}
                </div>
            </div>
        </div>
    </div>
</footer>

404.html

<p>
    We couldn't find the page you were looking for. It might have been moved, deleted, or you may have typed the URL
    incorrectly. You can return to the homepage to keep exploring.
</p>

<a href="/" class="btn btn-success text-uppercase" rel="next">Okay, Take Me Home!</a>

posts-listing.html

{{#each posts}}
<!-- Post preview-->
<div class="post-preview">
    <a href="/post/{{metadata.slug}}">
        <h2 class="post-title">{{metadata.title}}</h2>
        {{#if metadata.subtitle}}
        <h3 class="post-subtitle">{{metadata.subtitle}}</h3>
        {{/if}}
    </a>

    <p class="post-meta">Posted by {{metadata.author}} on {{metadata.publishDate | dateFormat("MMMM D, YYYY")}}</p>
</div>

<!-- Divider-->
<hr class="my-4" />
{{/each}}

<!---->

{{#include("partials/pagination.html")}}

For blog.html and homepage.html, you only need to add a single line to each:

<!-- A simple and effective trick to avoid building a custom page from scratch.
If the design is the same, define its logic as we did in content.html.
Aether's intelligent template inheritance and resolution will handle the rest.
Learn more here: https://aether-cms.pages.dev/documentation/theming/template-inheritance-and-resolution/ -->
{{#include("templates/layout.html")}}

All you have to do after making the necessary replacements is:

  1. Add some new posts.

  2. Add 2 custom pages named blog and homepage.

  3. Create a menu under the Menu tab in the Site Settings.

  4. Visit your site.

  5. Abracadabra! You’ve got a fully working site with a custom Clean Blog theme for Aether.


Wrapping Up

Aether is so flexible that you could recreate Clean Blog in many different ways.
What I’ve shared here is the simplest version, without making full use of Aether’s powerful features.
You can now take this as a foundation, build on it, and create something entirely new!

I’ve added comments in the templates to help explain each step.
If you have any questions about this tutorial or Aether in general, feel free to reach out in the Q&A section on GitHub.

Soon, I’ll be adding the Clean Blog theme to Aether Marketplace, where you’ll be able to download and use it with just one click.
If you plan to use the custom version created in this tutorial, you can name the folder custom-clean-blog and update the theme.json title to Custom Clean Blog, just to keep things consistent.

You may have noticed (or maybe not!), but I never asked you to restart the server at any point in this tutorial. That’s one of LiteNode’s superpowers, the backbone of Aether, which applies changes instantly without needing a reboot.

You can learn more about Aether’s powerful features in the Aether documentation.

If you find value in Aether or want to help it grow, your support β€” whether it's feedback, sharing, contributing, or funding β€” can make a real difference. The long-term goal is to build a sustainable open-source project that stays fast, independent, and truly community-driven. If you'd like to support Aether, giving it a star on GitHub would be greatly appreciated and genuinely helpful.

0
Subscribe to my newsletter

Read articles from LebCit directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

LebCit
LebCit

I'm LebCit, a Citizen of a small country called Lebanon in the Middle East. I love to read a lot, learn as much as I can, and of course apply and share with others.