Custom Pages

On this page

Introduction

Custom pages in Aether CMS allow you to create unique page layouts that differ from the standard blog post or static page templates. They use custom templates from your theme's custom directory and can be organized in hierarchical structures.

Key Features

  • Custom Templates: Use theme-specific templates instead of default layouts
  • Nested Structure: Create multi-level page hierarchies (up to 3 levels)
  • Template Inheritance: Child pages inherit parent templates when specific templates don't exist
  • Sibling Navigation: Automatic prev/next navigation between related pages
  • Breadcrumbs: Automatic breadcrumb generation for nested pages
  • Flexible URLs: Clean URLs that reflect the page hierarchy
  • Special Template Types: Automatic pagination and taxonomy features for specific templates

Custom Pages Basics

What Are Custom Pages?

Custom pages are special pages that:

  • Use templates from your theme's custom directory
  • Have direct URLs (e.g., /about instead of /page/about)
  • Can be nested to create hierarchical structures
  • Support automatic features like pagination and taxonomy listings

Custom vs. Normal Pages

Aspect Normal Pages Custom Pages
URL /page/[slug] /[slug]
Template Location templates/page.html custom/[slug].html
Nesting Not supported Up to 3 levels
Inheritance Fixed template Parent template fallback
Special Features Basic content only Pagination, taxonomies, etc.

Creating Custom Pages

Step 1: Create the Page

  1. In Admin Panel:

    • Go to Pages β†’ Add New Page
    • Enter page title and content
    • Set Page Type to "Custom Template"
    • Set status to "Published"
  2. Via API with Authentication: Refer to the Pages API

Step 2: Create the Template

Create a new file in your theme's custom directory:

<!-- themes/[theme-name]/custom/about.html -->
<article class="custom-page">
    <header class="page-header">
        <h1>{{metadata.title}}</h1>
        {{#if metadata.subtitle}}
        <p class="subtitle">{{metadata.subtitle}}</p>
        {{/if}}
    </header>

    <main class="page-content">{{content}}</main>

    {{#if metadata.featuredImage}}
    <figure class="featured-image">
        <img
            src="/content/uploads{{metadata.featuredImage.url}}"
            alt="{{metadata.featuredImage.alt |
        defaults(metadata.title)}}"
            width="{{metadata.featuredImage.width}}"
            height="{{metadata.featuredImage.height}}"
        />
        {{#if metadata.featuredImage.caption}}
        <figcaption>{{metadata.featuredImage.caption}}</figcaption>
        {{/if}}
    </figure>
    {{/if}}
    <!---->
    {{#if metadata.updatedAt}}
    <div class="page-meta">
        <p class="last-updated">Last updated: {{metadata.updatedAt | dateFormat("MMMM D, YYYY")}}</p>
    </div>
    {{/if}}
</article>

Step 3: Access Your Page

Your custom page will be available at: https://yoursite.com/about

Special Template Types

Pagination Templates

Templates with these slugs automatically receive pagination and post listings:

  • blog - Blog listing with pagination
  • archive - Archive listing with pagination
  • articles - Article listing with pagination
  • news - News listing with pagination
  • search - Search results with pagination

Example blog template (custom/blog.html):

<div class="blog-page">
    <header class="page-header">
        <h1>{{metadata.title | defaults("Blog")}}</h1>
        {{#if metadata.subtitle}}
        <p class="subtitle">{{metadata.subtitle}}</p>
        {{/if}}
    </header>

    {{#if content}}
    <section class="page-intro">{{content}}</section>
    {{/if}}<
    <!---->
    {{#if posts}}
    <section class="blog-posts">
        <div class="posts-grid">
            {{#each posts}}
            <article class="post-card">
                <h2><a href="/post/{{metadata.slug}}">{{metadata.title}}</a></h2>
                {{#if metadata.featuredImage}}
                <img
                    src="/content/uploads{{metadata.featuredImage.url}}"
                    alt="{{metadata.featuredImage.alt | defaults(metadata.title)}}"
                />
                {{/if}}
                <p class="excerpt">
                    {{#if metadata.excerpt}} {{metadata.excerpt | truncate(150)}} {{#else}} {{content |
                    truncateWords(25)}} {{/if}}
                </p>
                <div class="post-meta">
                    <time datetime="{{metadata.createdAt}}">{{metadata.createdAt | dateFormat("MMM D, YYYY")}}</time>
                </div>
            </article>
            {{/each}}
        </div>

        {{#include("partials/pagination.html")}}
    </section>
    {{/if}}
</div>

Taxonomy Count Templates

Templates with these slugs automatically receive taxonomy statistics:

  • categories - Lists all categories with post counts
  • tags - Lists all tags with post counts
  • topics - Lists all topics with post counts

Example categories template (custom/categories.html):

<div class="categories-page">
    <header class="page-header">
        <h1>{{metadata.title | defaults("Categories")}}</h1>
        {{#if metadata.subtitle}}
        <p class="subtitle">{{metadata.subtitle}}</p>
        {{/if}}
    </header>

    {{#if content}}
    <section class="page-intro">{{content}}</section>
    {{/if}}
    <!---->
    {{#if hasTaxonomyData}}
    <section class="categories-grid">
        {{#each categories}}
        <div class="category-card">
            <h3><a href="/category/{{slug}}">{{name}}</a></h3>
            <p class="count">{{count}} {{count == 1 ? "post" : "posts"}}</p>
        </div>
        {{/each}}
    </section>
    {{#else}}
    <p>No categories found.</p>
    {{/if}}
</div>

Nested Custom Pages

Creating a Nested Structure

You can create hierarchical structures like:

/documentation
β”œβ”€β”€ /documentation/getting-started
β”‚   β”œβ”€β”€ /documentation/getting-started/installation
β”‚   └── /documentation/getting-started/configuration
└── /documentation/api
    β”œβ”€β”€ /documentation/api/authentication
    └── /documentation/api/endpoints

Setting Up Parent-Child Relationships

Via Admin Panel:

  1. Create parent page (documentation)
  2. Create child page (getting-started)
  3. In child page settings, select documentation as Parent Page
  4. Create grandchild (installation) with getting-started as parent

Via API:

POST /api/pages with Authentication

Parent Page Request Body:

{
    "metadata": {
        "title": "Documentation",
        "slug": "documentation",
        "pageType": "custom",
        "status": "published"
    },
    "content": "# Documentation\n\nWelcome to our documentation."
}

Child Page Request Body:

{
    "metadata": {
        "title": "Getting Started",
        "slug": "getting-started",
        "pageType": "custom",
        "parentPage": "documentation",
        "publishDate": "2025-05-27T09:00", // Controls sibling order
        "status": "published"
    },
    "content": "# Getting Started\n\nLet's get you started."
}

Grandchild Page Request Body:

{
    "metadata": {
        "title": "Installation",
        "slug": "installation",
        "pageType": "custom",
        "parentPage": "getting-started",
        "publishDate": "2025-05-27T09:17", // Controls sibling order
        "status": "published"
    },
    "content": "# Installation\n\nInstallation instructions here."
}

Understanding publishDate and Sibling Order

The publishDate can be set via the API as shown above, or or through the Publish Date field in the Editor, which allows you to manually choose the publish date and time for a page.
This timestamp not only determines when the page is considered published, but it also controls the display order among sibling pagesβ€”pages that share the same parent.

Key Behavior:

  • Pages with earlier publishDate values appear first among siblings.

  • If two or more pages share the same publishDate (including time), their order is determined alphabetically by title.

  • The system uses a 24-hour clock (ISO 8601 format) for publishDate, where:

    • 00:00 to 11:59 = AM
    • 12:00 to 23:59 = PM

Children Sibling Order

If a new child page titled User Interface with slug user-interface, is created with documentation as a parent, and it has this publishDate:

"publishDate": "2025-05-27T09:05"

And the existing getting-started page (same parent: documentation) has:

"publishDate": "2025-05-27T09:00"

Then User Interface will appear after Getting Started in the sibling list under documentation.

Grandchildren Sibling Order

If a new grandchild page titled Update with slug update, is created with getting-started as a parent, and it has this publishDate:

"publishDate": "2025-05-27T09:15"

And the existing installation page (same parent: getting-started) has:

"publishDate": "2025-05-27T09:17"

Then Update will appear before Installation in the list of children under getting-started.

This makes it easy to manually control the order of pages using time-based logic.

Tip: Use the Editor’s date picker to fine-tune the time with AM/PM, or switch to a 24-hour format to ensure consistent, predictable ordering.

Template Inheritance

How It Works

When a page is accessed, the system looks for templates in this order:

  1. Exact match: custom/[full-path].html
  2. Parent template: custom/[parent-path].html
  3. Grandparent template: custom/[grandparent].html
  4. Fallback: templates/content.html or templates/layout.html

Example

For /documentation/getting-started/installation:

  1. First tries: custom/documentation-getting-started-installation.html
  2. Then tries: custom/documentation-getting-started.html
  3. Then tries: custom/documentation.html
  4. Finally: templates/content.html or templates/layout.html

Creating Template Hierarchy

themes/[theme]/custom/
β”œβ”€β”€ documentation.html                         # Base docs template
β”œβ”€β”€ documentation-getting-started.html         # Getting started section
β”œβ”€β”€ documentation-api.html                     # API section
└── documentation-tutorials.html               # Tutorials section

Template Example with Inheritance

<!-- custom/documentation.html -->
<div class="documentation-layout">
    <aside class="docs-sidebar">{{#include("partials/docs-navigation.html")}}</aside>

    <main class="docs-content">
        {{#if breadcrumbs}}
        <nav class="breadcrumbs" aria-label="Breadcrumb">
            <ol>
                <li><a href="/">Home</a></li>
                {{#each breadcrumbs}}
                <li aria-current="{{active ? `page` : ``}}">
                    {{#if active}}
                    <span>{{title}}</span>
                    {{#else}}
                    <a href="{{slug}}">{{title}}</a>
                    {{/if}}
                </li>
                {{/each}}
            </ol>
        </nav>
        {{/if}}

        <article class="page-content">
            <header>
                <h1>{{metadata.title}}</h1>
                {{#if metadata.subtitle}}
                <p class="subtitle">{{metadata.subtitle}}</p>
                {{/if}}
            </header>

            <div class="content">{{content}}</div>

            {{#if metadata.updatedAt}}
            <div class="page-meta">
                <p class="last-updated">Last updated: {{metadata.updatedAt | dateFormat("MMMM D, YYYY")}}</p>
            </div>
            {{/if}}
        </article>

        {{#if siblingNavigation}}
        <nav class="page-navigation">{{#include("partials/sibling-navigation.html")}}</nav>
        {{/if}}
    </main>
</div>

Sibling Navigation

Automatic Sibling Navigation

The system automatically provides navigation between pages at the same level:

<!-- partials/sibling-navigation.html -->
{{#if siblingNavigation}}
<nav class="sibling-navigation">
    <div class="nav-links">
        {{#if siblingNavigation.prev}}
        <div class="nav-prev">
            <span class="nav-label">Previous</span>
            <a href="{{siblingNavigation.prev.url}}" class="nav-link">← {{siblingNavigation.prev.title}}</a>
        </div>
        {{/if}}
        <!---->
        {{#if siblingNavigation.next}}
        <div class="nav-next">
            <span class="nav-label">Next</span>
            <a href="{{siblingNavigation.next.url}}" class="nav-link">{{siblingNavigation.next.title}} β†’</a>
        </div>
        {{/if}}
    </div>
</nav>
{{/if}}

Full Sibling List

{{#if siblingNavigation}}
<nav class="siblings-list">
    <h4>In this section{{#if siblingNavigation.parentTitle}} ({{siblingNavigation.parentTitle}}){{/if}}:</h4>
    <ul>
        {{#each siblingNavigation.siblings}}
        <li class="{{active ? `active` : ``}}">
            <a href="{{url}}" aria-current="{{active ? `page` : ``}}">{{title}}</a>
        </li>
        {{/each}}
    </ul>
</nav>
{{/if}}

Available Navigation Data

The following siblings structure shows the output for the direct children of the Documentation parent section, as seen on the /documentation/getting-started page of this site using {{ siblingNavigation | dump }}:

{
    "siblings": [
        {
            "title": "Getting Started",
            "slug": "getting-started",
            "url": "/documentation/getting-started",
            "active": true,
            "order": 0
        },
        {
            "title": "User Interface",
            "slug": "user-interface",
            "url": "/documentation/user-interface",
            "active": false,
            "order": 1
        },
        {
            "title": "Core Concepts",
            "slug": "core-concepts",
            "url": "/documentation/core-concepts",
            "active": false,
            "order": 2
        },
        {
            "title": "Theming",
            "slug": "theming",
            "url": "/documentation/theming",
            "active": false,
            "order": 3
        },
        {
            "title": "API Reference",
            "slug": "api-reference",
            "url": "/documentation/api-reference",
            "active": false,
            "order": 4
        },
        {
            "title": "Resources",
            "slug": "resources",
            "url": "/documentation/resources",
            "active": false,
            "order": 5
        }
    ],
    "prev": null,
    "next": {
        "title": "User Interface",
        "slug": "user-interface",
        "url": "/documentation/user-interface",
        "order": 1
    },
    "parentTitle": "Documentation"
}

The next siblings structure shows the output for the direct children of User Interface section, which is itself a child of Documentation. These entries are therefore grandchildren of Documentation, and the data is shown as it appears on the /documentation/user-interface/theme-management page of this site using {{ siblingNavigation | dump }}:

{
    "siblings": [
        {
            "title": "Dashboard",
            "slug": "dashboard",
            "url": "/documentation/user-interface/dashboard",
            "active": false,
            "order": 0
        },
        {
            "title": "Media Library",
            "slug": "media-library",
            "url": "/documentation/user-interface/media-library",
            "active": false,
            "order": 1
        },
        {
            "title": "Theme Management",
            "slug": "theme-management",
            "url": "/documentation/user-interface/theme-management",
            "active": true,
            "order": 2
        },
        {
            "title": "User Management",
            "slug": "user-management",
            "url": "/documentation/user-interface/user-management",
            "active": false,
            "order": 3
        },
        {
            "title": "Site Settings",
            "slug": "site-settings",
            "url": "/documentation/user-interface/site-settings",
            "active": false,
            "order": 4
        },
        {
            "title": "Content Table Management",
            "slug": "content-table-management",
            "url": "/documentation/user-interface/content-table-management",
            "active": false,
            "order": 5
        },
        {
            "title": "Editor",
            "slug": "editor",
            "url": "/documentation/user-interface/editor",
            "active": false,
            "order": 6
        }
    ],
    "prev": {
        "title": "Media Library",
        "slug": "media-library",
        "url": "/documentation/user-interface/media-library",
        "order": 1
    },
    "next": {
        "title": "User Management",
        "slug": "user-management",
        "url": "/documentation/user-interface/user-management",
        "order": 3
    },
    "parentTitle": "User Interface"
}

Breadcrumbs

Automatic Breadcrumb Generation

Breadcrumbs are automatically generated for nested pages:

{{#if breadcrumbs}}
<nav class="breadcrumbs" aria-label="Breadcrumb">
    <ol>
        <li><a href="/">Home</a></li>
        {{#each breadcrumbs}}
        <li class="{{active ? `active` : `inactive`}}" aria-current="{{active ? `page` : ``}}">
            {{#not active}}
            <a href="{{slug}}">{{title}}</a>
            {{#else}}
            <span>{{title}}</span>
            {{/not}}
        </li>
        {{/each}}
    </ol>
</nav>
{{/if}}

Breadcrumb Data Structure

{
    "breadcrumbs": [
        {
            "title": "Documentation",
            "slug": "/documentation",
            "order": 0,
            "active": false
        },
        {
            "title": "Getting Started",
            "slug": "/documentation/getting-started",
            "order": 1,
            "active": false
        },
        {
            "title": "Installation",
            "slug": "/documentation/getting-started/installation",
            "order": 2,
            "active": true
        }
    ]
}

Static Site Generation

File Structure

When generating a static site, nested custom pages create proper directory structures:

_site/
β”œβ”€β”€ documentation/
β”‚   β”œβ”€β”€ index.html                    # /documentation
β”‚   β”œβ”€β”€ getting-started/
β”‚   β”‚   β”œβ”€β”€ index.html                # /documentation/getting-started
β”‚   β”‚   β”œβ”€β”€ installation/
β”‚   β”‚   β”‚   └── index.html            # /documentation/getting-started/installation
β”‚   β”‚   └── configuration/
β”‚   β”‚       └── index.html            # /documentation/getting-started/configuration
β”‚   └── api/
β”‚       β”œβ”€β”€ index.html                # /documentation/api
β”‚       β”œβ”€β”€ authentication/
β”‚       β”‚   └── index.html            # /documentation/api/authentication
β”‚       └── endpoints/
β”‚           └── index.html            # /documentation/api/endpoints

Generation Behavior

  • Template inheritance works the same in SSG as in CMS
  • Sibling navigation is pre-built during generation
  • Breadcrumbs are included in each generated page
  • Clean URLs are supported with proper directory structure
  • Special templates (pagination, taxonomies) are fully supported

Best Practices

1. Organize Templates Hierarchically

custom/
β”œβ”€β”€ docs.html                   # Base template for all docs
β”œβ”€β”€ docs-api.html               # Template for API section
β”œβ”€β”€ docs-tutorials.html         # Template for tutorials section
β”œβ”€β”€ blog.html                   # Paginated blog template
└── categories.html             # Taxonomy template

2. Use Consistent Naming

  • Slugs: Use lowercase, hyphens for spaces (getting-started)
  • Templates: Match the full path (docs-getting-started.html)
  • Order: Set explicit publishDate or custom alphabetical order for controlled navigation

3. Set Proper Order Values

{
    "metadata": {
        "title": "Introduction",
        "slug": "introduction",
        "parentPage": "getting-started",
        "publishDate": "2025-05-27T09:17", // Controls sibling order
        "status": "published"
    }
}

4. Validate Parent Relationships

The system prevents circular references, but plan your hierarchy:

βœ… Valid:
docs β†’ getting-started β†’ installation

❌ Invalid:
docs β†’ getting-started β†’ docs (circular)

5. Use Semantic HTML

<article class="custom-page" role="main">
    <header>
        <h1>{{metadata.title}}</h1>
    </header>

    <main class="page-content">{{content}}</main>

    <nav aria-label="Page navigation">{{#include("partials/sibling-navigation.html")}}</nav>
</article>

Troubleshooting

Page Shows 404

Possible causes:

  1. Page not published (status: "draft")
  2. Missing parent page
  3. Invalid parent relationship
  4. Slug mismatch
  5. Reserved path conflict

Solution:

// Check page status
const page = await contentManager.getContentByProperty("page", "slug", "your-slug")
console.log("Page status:", page?.frontmatter?.status)

// Verify parent
if (page?.frontmatter?.parentPage) {
    const parent = await contentManager.getContentByProperty("page", "slug", page.frontmatter.parentPage)
    console.log("Parent exists:", !!parent)
}

// Check for reserved paths
const reservedPaths = ["aether", "api", "post", "page", "rss", "sitemap"]
console.log("Path reserved:", reservedPaths.includes("your-slug"))

Template Not Found

Possible causes:

  1. Template file doesn't exist
  2. Wrong naming convention
  3. Theme not properly loaded

Solution:

// Check template path
const templatePath = await resolveTemplatePath({
    themeManager,
    contentType: "custom",
    slug: "your-slug",
    isCustomPage: true,
})
console.log("Template path:", templatePath)
console.log("Template exists:", existsSync(templatePath))

Missing Sibling Navigation

Cause: Only pages with parents get sibling navigation.

Solution: Root-level pages don't have siblings by design. Create a parent page if needed.

Wrong Template Used

Cause: Template inheritance following wrong path.

Solution: Check template existence in order:

  1. Full path template (custom/parent-child-grandchild.html)
  2. Parent template (custom/parent-child.html)
  3. Grandparent template (custom/parent.html)
  4. Fallback template (templates/content.html)

Template Functions

getSiblingCustomPagesNavigation (CMS)

const siblingNavigation = await getSiblingCustomPagesNavigation(contentPage, contentManager, req)

buildSiblingCustomPagesNavigation (SSG)

const navigationMap = buildSiblingCustomPagesNavigation(customPages)
const navigation = navigationMap.get(pageSlug)

resolveTemplatePath

const templatePath = await resolveTemplatePath({
    themeManager,
    contentType: "custom",
    slug: "page-slug",
    isCustomPage: true,
})

Advanced Examples

Documentation Site Template

<!-- custom/docs.html -->
<div class="docs-layout">
    {{#if breadcrumbs}}
    <nav class="breadcrumbs">
        <ol>
            <li><a href="/">Home</a></li>
            {{#each breadcrumbs}}
            <li class="{{active ? `active` : ``}}">
                {{#if !active}}
                <a href="{{slug}}">{{title}}</a>
                {{#else}}
                <span>{{title}}</span>
                {{/if}}
            </li>
            {{/each}}
        </ol>
    </nav>
    {{/if}}

    <div class="docs-container">
        <aside class="docs-sidebar">
            {{#if siblingNavigation}}
            <nav class="docs-nav">
                <h3>{{siblingNavigation.parentTitle | defaults("Documentation")}}</h3>
                <ul>
                    {{#each siblingNavigation.siblings}}
                    <li class="{{active ? `active` : ``}}">
                        <a href="{{url}}" aria-current="{{active ? `page` : ``}}">{{title}}</a>
                    </li>
                    {{/each}}
                </ul>
            </nav>
            {{/if}}
        </aside>

        <main class="docs-content">
            <article>
                <header>
                    <h1>{{metadata.title}}</h1>
                    {{#if metadata.subtitle}}
                    <p class="subtitle">{{metadata.subtitle}}</p>
                    {{/if}}
                </header>

                <div class="content">{{content}}</div>

                {{#if metadata.updatedAt}}
                <div class="page-meta">
                    <p>Last updated: {{metadata.updatedAt | dateFormat("MMMM D, YYYY")}}</p>
                </div>
                {{/if}}
            </article>

            {{#if siblingNavigation && (siblingNavigation.prev || siblingNavigation.next)}}
            <nav class="page-nav">
                {{#if siblingNavigation.prev}}
                <a href="{{siblingNavigation.prev.url}}" class="nav-prev">← {{siblingNavigation.prev.title}}</a>
                {{/if}} {{#if siblingNavigation.next}}
                <a href="{{siblingNavigation.next.url}}" class="nav-next">{{siblingNavigation.next.title}} β†’</a>
                {{/if}}
            </nav>
            {{/if}}
        </main>
    </div>
</div>

Multi-purpose Landing Page

<!-- custom/landing.html -->
<div class="landing-page">
    <section class="hero">
        <div class="container">
            <h1>{{metadata.title}}</h1>
            {{#if metadata.subtitle}}
            <p class="hero-subtitle">{{metadata.subtitle}}</p>
            {{/if}}
            <!---->
            {{#if metadata.featuredImage}}
            <div class="hero-image">
                <img
                    src="/content/uploads{{metadata.featuredImage.url}}"
                    alt="{{metadata.featuredImage.alt | defaults(metadata.title)}}"
                />
            </div>
            {{/if}}
        </div>
    </section>

    <section class="content">
        <div class="container">{{content}}</div>
    </section>

    {{#if recentPosts}}
    <section class="featured-posts">
        <div class="container">
            <h2>Latest Posts</h2>
            <div class="posts-grid">
                {{#each recentPosts}}
                <article class="post-card">
                    <h3><a href="/post/{{metadata.slug}}">{{metadata.title}}</a></h3>
                    <p class="date">{{metadata.createdAt | dateFormat("MMM D, YYYY")}}</p>
                </article>
                {{/each}}
            </div>
            <a href="/blog" class="view-all">View All Posts</a>
        </div>
    </section>
    {{/if}}
</div>

Conclusion

Custom pages with nested structures provide a powerful way to organize and present content in Aether CMS. With template inheritance, automatic navigation, proper SSG support, and special template features like pagination and taxonomy listings, you can create sophisticated documentation sites, knowledge bases, blogs, or any hierarchical content structure your project needs.

The system's automatic features (sibling navigation, breadcrumbs, template inheritance) reduce maintenance overhead while providing a rich user experience, and the special template types (pagination, taxonomies) add powerful content management capabilities without additional configuration.