Custom Pages
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
In Admin Panel:
- Go to Pages β Add New Page
- Enter page title and content
- Set Page Type to "Custom Template"
- Set status to "Published"
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 paginationarchive
- Archive listing with paginationarticles
- Article listing with paginationnews
- News listing with paginationsearch
- 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 countstags
- Lists all tags with post countstopics
- 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:
- Create parent page (
documentation
) - Create child page (
getting-started
) - In child page settings, select
documentation
as Parent Page - Create grandchild (
installation
) withgetting-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
to11:59
= AM12:00
to23: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:
- Exact match:
custom/[full-path].html
- Parent template:
custom/[parent-path].html
- Grandparent template:
custom/[grandparent].html
- Fallback:
templates/content.html
ortemplates/layout.html
Example
For /documentation/getting-started/installation
:
- First tries:
custom/documentation-getting-started-installation.html
- Then tries:
custom/documentation-getting-started.html
- Then tries:
custom/documentation.html
- Finally:
templates/content.html
ortemplates/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:
- Page not published (
status: "draft"
) - Missing parent page
- Invalid parent relationship
- Slug mismatch
- 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:
- Template file doesn't exist
- Wrong naming convention
- 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:
- Full path template (
custom/parent-child-grandchild.html
) - Parent template (
custom/parent-child.html
) - Grandparent template (
custom/parent.html
) - 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.