Developer Guide — Templating, Layouts, and Partials
Complete developer reference for Dax Pro templates: the DAX template syntax, layout inheritance, partial composition, variable scope, conditionals, loops, and the file structure of templates/. Customize layouts, build new partials, extend the theme, and modify the engine-facing behavior.
Who This Chapter Is For
Everything up to this chapter has been for site owners: people who edit content, configure the site, and deploy. This chapter is for developers: people who want to change how the site looks, add new sections, build new layouts, or extend the theme beyond what Dax Pro ships with.
If you have never edited a .dax file before, this chapter is your starting point. If you have edited templates in Jekyll, Hugo, Eleventy, or Liquid, most of this will feel familiar — the syntax is similar, the concepts are the same.
By the end of this chapter, you will:
- Understand the full template syntax Dax Pro supports
- Know the difference between layouts and partials
- Be able to add a new section to the homepage
- Be able to create a new layout for a new page type
- Understand variable scope and the
{{ }}/{% %}delimiters - Know what is possible and what is not (Dax Pro is deliberately simple)
- Include —
{% include file.dax %} - For —
{% for item in list %}...{% endfor %} - If —
{% if condition %}...{% endif %} - Else —
{% if condition %}...{% else %}...{% endif %} - Variable substitution —
{{ variable }} - Nested variable access —
{{ obj.key.subkey }} - Fallback —
{{ a or b }} {% elif %}— use nested{% if %}inside{% else %}instead{% set %}or variable assignment- Filters or pipes (
{{ name | uppercase }}) containsin{% if %}conditions{% for %}...{% else %}(empty loops have no else branch)- Arithmetic in templates
- String concatenation
The Template System at a Glance
Dax Pro uses a custom template engine built into the DAX C# engine. It is not Liquid, Handlebars, Mustache, or Blade. It is a small, purpose-built engine that supports exactly the features Dax Pro needs — and no more.
The engine supports:
The engine does not support:
If you need something the engine does not support, the correct fix is to prepare the data in the content layer (frontmatter or _data/), not to add engine features. The engine is intentionally minimal because minimal engines are easier to debug.
The Template Directory Structure
All templates live under templates/:
templates/
├── layouts/
│ ├── base.dax
│ ├── home.dax
│ ├── post.dax
│ ├── posts-list.dax
│ ├── page.dax
│ ├── contact.dax
│ ├── service.dax
│ ├── services-list.dax
│ ├── tag.dax
│ └── tags.dax
└── partials/
├── shared/
│ ├── nav.dax
│ ├── mobile-drawer.dax
│ ├── footer.dax
│ └── seo.dax
├── home/
│ ├── hero.dax
│ ├── trust.dax
│ ├── solutions.dax
│ ├── performance.dax
│ ├── showcase.dax
│ ├── pricing.dax
│ ├── insights.dax
│ └── contact.dax
├── blog/
│ ├── header.dax
│ ├── card.dax
│ └── pagination.dax
├── post/
│ ├── header.dax
│ ├── cover.dax
│ ├── tags.dax
│ └── nav.dax
├── page/
│ ├── header.dax
│ └── cta-box.dax
├── contact/
│ ├── header.dax
│ └── box.dax
├── service/
│ ├── header.dax
│ ├── cta.dax
│ └── nav.dax
├── services/
│ ├── header.dax
│ └── pagination.dax
├── tag/
│ └── header.dax
└── tags/
└── header.dax
Two folders, two purposes:
layouts/ — one file per page type. A layout defines the outer shell of a page. base.dax is the root layout (the document). Every other layout includes base.dax through its own frontmatter.partials/ — reusable snippets. Partials are included by layouts or by other partials. They are grouped by context: shared/ for things used everywhere, home/ for homepage sections, blog/ for blog-related snippets, and so on.The naming convention:
base.dax — the HTML skeleton{type}.dax — a layout for one page type (home.dax, post.dax, page.dax){type}-list.dax — a layout for a listing (posts-list.dax, services-list.dax){context}/{name}.dax — a partial specific to a context (home/hero.dax, blog/card.dax)Layout Inheritance
Dax Pro uses layout chaining. A page's frontmatter declares its layout. That layout declares its own parent layout. The chain continues until base.dax, which has no parent.
Example — a blog post:
1. content/posts/my-post.md has layout: "post.dax"
2. post.dax has frontmatter layout: base.dax
3. base.dax has no layout
The engine renders in reverse order:
1. Start with the Markdown body of my-post.md, converted to HTML
2. Wrap that HTML in post.dax, replacing {{ content }} with the rendered post
3. Wrap the result in base.dax, replacing {{ content }} with the rendered post
The final output is a complete HTML document.
Example — a service page:
1. content/services/my-service.md has layout: "service.dax"
2. service.dax has frontmatter layout: base.dax
3. base.dax has no layout
Same pattern. Two levels.
Example — the homepage:
1. content/index.md has layout: "home.dax"
2. home.dax has frontmatter layout: base.dax
3. base.dax has no layout
Same pattern.
The pattern is consistent: every layout except base.dax has exactly one parent. Layout chains never exceed three levels. The engine enforces a maximum of five levels, but Dax Pro uses two.
To add a new layout — for example, a "thanks" page after contact form submission:
1. Create templates/layouts/thanks.dax with layout: base.dax in its frontmatter
2. Create content/thanks.md with layout: "thanks.dax" in its frontmatter
That is the entire setup. No registration, no configuration.
To remove a layout chain level — flatten post.dax into base.dax — you would merge the content of both layouts and update all post frontmatter from post.dax to base.dax. Rarely necessary; the current structure is clean.
The base.dax Layout
base.dax is the root of every page. It renders the element, the with SEO tags, the navigation bar, the mobile drawer, the main content area, and the footer.
A minimal base.dax looks like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% include shared/seo.dax %}
<link rel="stylesheet" href="/css/style.css">
<link rel="alternate" type="application/rss+xml" title="{{ metadata.title }}" href="/rss.xml">
</head>
<body>
{% include shared/nav.dax %}
{% include shared/mobile-drawer.dax %}
{{ content }}
{% include shared/footer.dax %}
<script src="/js/main.js" defer></script>
</body>
</html>
Key concepts:
{% include shared/seo.dax %} — inserts the SEO partial (title, description, OpenGraph, JSON-LD){% include shared/nav.dax %} — inserts the navigation bar{% include shared/mobile-drawer.dax %} — inserts the mobile menu drawer{{ content }} — the placeholder where the child layout's content is inserted{% include shared/footer.dax %} — inserts the footer — loads the vanilla JS file (mobile menu, form handling) — loads the CSSDo not change the structure of base.dax without understanding the consequences. Every page depends on it. Changes to base.dax affect every page on the site.
To add a global script (e.g., analytics):
<script async src="https://plausible.io/js/script.js" data-domain="yourdomain.com"></script>
Place it just before in base.dax. Rebuild. Every page includes it.
To add a global meta tag:
<meta name="theme-color" content="#512BD4">
Place it inside in base.dax.
The home.dax Layout
home.dax renders the homepage. It is a thin layout — mostly a container with partial includes for each section.
---
layout: base.dax
<main>
{% include home/hero.dax %}
{% include home/trust.dax %}
{% include home/solutions.dax %}
{% include home/performance.dax %}
{% include home/showcase.dax %}
{% include home/pricing.dax %}
{% include home/insights.dax %}
{% include home/contact.dax %}
</main>
Each include is one section. The section itself is in templates/partials/home/{section}.dax. The data comes from content/index.md's frontmatter.
The order of includes determines the visual order. To move the pricing section above the showcase, reorder the include lines:
<main>
{% include home/hero.dax %}
{% include home/trust.dax %}
{% include home/solutions.dax %}
{% include home/performance.dax %}
{% include home/pricing.dax %}
{% include home/showcase.dax %}
{% include home/insights.dax %}
{% include home/contact.dax %}
</main>
Rebuild and the new order is live.
To add a new section — for example, a testimonials section:
1. Create content/index.md frontmatter with a new testimonials: block
2. Create templates/partials/home/testimonials.dax that renders the block
3. Add {% include home/testimonials.dax %} to home.dax at the desired position
4. Rebuild
To remove a section — for example, hide the showcase:
1. Comment out or delete {% include home/showcase.dax %} from home.dax
2. Rebuild
The section is hidden. If you also want to remove the data from index.md, do so — but leaving the data in place is fine. It just does not render.
Partials in Detail
Partials are the building blocks. Each partial is one visual section or one shared component. Partials are small — usually 10–40 lines of template code — and self-contained.
Anatomy of a partial:
<section class="dax-section-trust" id="capabilities">
<div class="dax-container">
<div class="dax-section-header">
<div class="dax-kicker">{{ trust.kicker }}</div>
<h2 class="dax-section-title">{{ trust.title }}</h2>
<p class="dax-section-description">{{ trust.description }}</p>
</div>
<div class="dax-trust-grid">
{% for card in trust.cards %}
<div class="dax-trust-card">
<div class="dax-trust-index">{{ card.index }}</div>
<h3 class="dax-trust-title">{{ card.title }}</h3>
<p class="dax-trust-desc">{{ card.desc }}</p>
</div>
{% endfor %}
</div>
</div>
</section>
What it demonstrates:
, , elements
Variable substitution — {{ trust.kicker }} pulls from the frontmatter
Loop — {% for card in trust.cards %} iterates over a list
Nested variable access — {{ card.title }} accesses a field within the loop's current item
Partial naming conventions:
One partial per section — hero.dax, trust.dax, pricing.dax are each one visual block
Prefix with context — home/hero.dax, not hero.dax. If a new layout needs a hero, it uses home/hero.dax or creates its own
Shared partials in shared/ — nav.dax, footer.dax, seo.dax
To modify an existing partial — for example, add a subtitle to the trust cards:
1. Open templates/partials/home/trust.dax
2. Add {{ card.subtitle }}
after the title
3. Add subtitle to each card in content/index.md
4. Rebuild
To create a new partial:
1. Create templates/partials/{context}/{name}.dax
2. Write the HTML with {{ variable }} placeholders
3. Add the data to the relevant content frontmatter or _data/
4. Include the partial from a layout
To reuse a partial across layouts:
If both home.dax and a new landing.dax need the same hero, they both include {% include home/hero.dax %}. The data comes from the frontmatter of each page. The template does not care which page includes it, as long as the frontmatter has the expected hero: block.
To version a partial without breaking existing pages:
Create templates/partials/home/hero-v2.dax, switch one page's layout to use it, verify it works, then switch others. Do not edit hero.dax in place if you cannot verify all pages at once.
Variable Scope
Variables come from three sources, in order of priority:
1. Page frontmatter — the frontmatter of the current Markdown file
2. _data/ files — metadata, nav, footer
3. Special variables — {{ content }}, {{ url }}, {{ collections }}, {{ pagination }}
Page frontmatter overrides _data/. For example, if _data/metadata.yaml has title: "Dax Pro" but content/about.md has title: "About Us", the about page uses "About Us".
Inside {% for %} loops, the loop variable is scoped to the loop:
{% for card in trust.cards %}
{{ card.title }} {# access the current card #}
{% endfor %}
Outside the loop, card is undefined. This is correct — the loop variable does not leak.
Inside {% include %}, the included partial inherits the entire context of the includer. If home.dax includes home/hero.dax, the hero partial has access to hero.* from the current page's frontmatter. It also has access to metadata, nav, footer from _data/.
This means partials can access any variable the layout can access. It also means that if two partials both use {{ title }}, they share the same value — the current page's title. Partial namespaces are not isolated; they share the caller's context.
Common scope mistakes:
Using a variable that does not exist — Dax Pro renders empty string, not an error. If a section is empty, the variable is probably misspelled or the frontmatter does not have that field
Accessing loop variables outside the loop — renders empty. Move the reference inside
Shadowing _data/ variables — if content/index.md has title: "Home", the homepage title is "Home", overriding _data/metadata.yaml. That is intentional, but be aware of it
Conditionals
The {% if %} construct supports:
Truthy checks — {% if variable %}
Equality — {% if variable == "value" %}
Inequality — {% if variable != "value" %}
OR — {% if a or b %}
AND — {% if a and b %}
Nested if — {% if a %}{% if b %}...{% endif %}{% endif %}
Not supported:
contains
elif (use nested if)
not (use != or restructure)
in (check the list contents differently)
Comparison operators other than == and !=
Example — show a feature list only if it exists:
{% if item.features %}
<div class="dax-feature-list">
{% for feature in item.features %}
<div class="dax-feature-item">{{ feature }}</div>
{% endfor %}
</div>
{% endif %}
Example — show different content for premium vs. free:
The engine cannot do this directly. The correct fix: put both content variants in the frontmatter with distinct keys, and use two {% if %} blocks driven by a flag:
# frontmatter
is_premium: true
premium_note: "This feature requires a license."
free_note: "This feature is available in the free tier."
{% if is_premium %}
<p>{{ premium_note }}</p>
{% else %}
<p>{{ free_note }}</p>
{% endif %}
The engine checks is_premium. If true, it renders premium_note. Else, free_note.
Example — fallback to a default:
{{ page.description or metadata.description }}
If page.description is defined and non-empty, use it. Else, use metadata.description. The or operator in {{ }} is a fallback, not a logical OR.
Example — show the featured flag only when true:
<div class="dax-price-card{% if pricing.tier2.featured %} featured{% endif %}">
If pricing.tier2.featured is truthy (true boolean), the class featured is added. If false or missing, the class is omitted. This is how Dax Pro's pricing section highlights the middle tier.
Loops
The {% for %} construct supports:
Basic iteration — {% for item in list %}
Limit — {% for item in list limit:3 %}
Nested — {% for a in list1 %}{% for b in a.list %}{% endfor %}{% endfor %}
Loop variable access — {{ item.field }}, {{ item.subfield.subsubfield }}
Not supported:
Loop index — {{ loop.index }}, {{ loop.first }}, {{ loop.last }} do not exist. Prepare the index in the frontmatter if needed
Loop else — {% for %}...{% else %}...{% endfor %} is not supported. Check if the list is empty with {% if list %} before the loop
Filters — no reverse, sort, slice, filter
Break — no way to exit a loop early
Example — limit to three items:
{% for post in collections.posts limit:3 %}
The limit:3 syntax requires no space around the colon: limit:3, not limit: 3.
Example — check if a list has content before looping:
{% if trust.cards %}
<div class="dax-trust-grid">
{% for card in trust.cards %}
<div class="dax-trust-card">{{ card.title }}</div>
{% endfor %}
</div>
{% endif %}
If trust.cards is empty or missing, the whole block is skipped. Without the outer {% if %}, an empty list would render an empty , which is usually fine but sometimes shows a gap in the layout.
Example — access a nested list:
# frontmatter
solutions:
items:
- title: "First"
features: ["a", "b", "c"]
- title: "Second"
features: ["d", "e"]
{% for item in solutions.items %}
<h3>{{ item.title }}</h3>
<ul>
{% for feature in item.features %}
<li>{{ feature }}</li>
{% endfor %}
</ul>
{% endfor %}
Nested loops work. The outer loop iterates over solutions.items. The inner loop iterates over item.features for each item.
Limitation: the engine does not support loop.last, so you cannot add a separator that omits the last item. If you need a separator (like · between items), prepare it in the frontmatter as a list of strings that includes the separator, or accept the trailing separator.
Special Variables
Dax Pro provides several variables that are not from frontmatter:
Variable
Type
Purpose
{{ content }}
string
The rendered Markdown body of the current page
{{ url }}
string
The current page's URL path (e.g., /posts/my-post/)
{{ collections }}
object
All collections as { collectionName: [page, page, ...] }
{{ pagination }}
object
Current page's pagination info (only in listing layouts)
{{ tags }}
list
All tags across all pages (in tags.dax)
{{ posts }}
list
Posts in the current tag (in tag.dax)
{{ tag }}
string
Current tag name (in tag.dax)
{{ content }} — where the child layout's rendered HTML is inserted. Only used in base.dax and any intermediate layouts. Leaf partials do not use it.
{{ url }} — useful for active-link highlighting:
<a href="/about/" class="dax-nav-link{% if url == '/about/' %} active{% endif %}">About</a>
{{ collections }} — access all pages of any collection:
{% for post in collections.posts limit:3 %}
collections.posts is the list of all posts. limit:3 restricts to the three most recent.
{{ pagination }} — only available in posts-list.dax, services-list.dax:
Field
Type
Purpose
pagination.items
list
Items on this page
pagination.current_page
number
Current page number (1-based)
pagination.total_pages
number
Total number of pages
pagination.total_items
number
Total items across all pages
pagination.prev_url
string or null
URL of the previous page, or null
pagination.next_url
string or null
URL of the next page, or null
pagination.has_prev
boolean
Whether there is a previous page
pagination.has_next
boolean
Whether there is a next page
{{ tag }}, {{ posts }} — available in tag.dax, where a single tag page is rendered:
<h1>Topic: #{{ tag }}</h1>
{% for post in posts %}
<a href="{{ post.url }}">{{ post.title }}</a>
{% endfor %}
Common Template Patterns
Adding a New Section to the Homepage
1. Add the section data to content/index.md frontmatter:
testimonials:
kicker: "What Clients Say"
title: "Trusted by Teams Worldwide"
items:
- quote: "Dax Pro cut our build time from 8s to 0.1s."
author: "Jane Doe, CTO at Acme"
- quote: "The Decap CMS integration saved our editors hours per week."
author: "John Smith, Marketing Lead"
2. Create templates/partials/home/testimonials.dax:
<section class="dax-section-testimonials">
<div class="dax-container">
<div class="dax-section-header">
<div class="dax-kicker">{{ testimonials.kicker }}</div>
<h2 class="dax-section-title">{{ testimonials.title }}</h2>
</div>
<div class="dax-testimonial-grid">
{% for item in testimonials.items %}
<div class="dax-testimonial-card">
<p class="dax-testimonial-quote">"{{ item.quote }}"</p>
<p class="dax-testimonial-author">— {{ item.author }}</p>
</div>
{% endfor %}
</div>
</div>
</section>
3. Add {% include home/testimonials.dax %} to home.dax at the desired position
4. Add CSS for .dax-section-testimonials, .dax-testimonial-grid, .dax-testimonial-card, .dax-testimonial-quote, .dax-testimonial-author in style.css
5. Rebuild
Creating a New Layout
1. Create templates/layouts/landing.dax:
---
layout: base.dax
<main>
{% include home/hero.dax %}
{% include home/pricing.dax %}
{% include home/contact.dax %}
</main>
2. Create content/landing.md:
---
layout: "landing.dax"
title: "Landing Page"
description: "A focused page for a specific campaign."
hero:
kicker: "..."
# ... full hero data
pricing:
# ... full pricing data
contact:
# ... full contact data
3. The page is live at /landing/.
Overriding a Partial for One Layout
If you want the homepage to use a different hero partial, without breaking other pages that might use home/hero.dax:
1. Create templates/partials/home/hero-alt.dax (copy of hero.dax with your changes)
2. In home.dax, change {% include home/hero.dax %} to {% include home/hero-alt.dax %}
3. The other layouts that include home/hero.dax continue to work
Adding a New Collection
1. Create content/newcollection/ folder
2. Add content/newcollection/item1.md, content/newcollection/item2.md with frontmatter including layout: "newcollection-item.dax"
3. Create templates/layouts/newcollection-item.dax with the item template
4. Create content/newcollection.md (controller) with:
---
layout: "newcollection-list.dax"
title: "New Collection"
description: "Listing of items in the new collection."
collection: "newcollection"
pagination: 9
5. Create templates/layouts/newcollection-list.dax with the listing template
6. Rebuild
The new collection is live at /newcollection/.
What Is Not Possible
Dax Pro's engine is intentionally minimal. These things are not possible without modifying the C# engine:
Custom filters — no {{ text | uppercase }}
Conditional logic inside attributes — {% if %} works between tags but not inside class="..." (you must build the class string in the frontmatter or use two separate elements)
Loop metadata — no loop.index, loop.first, loop.last
Sorting or filtering — data must be pre-sorted in the frontmatter or in _data/
Function calls — no {{ now() }} or {{ date.format() }}
Partial with parameters — {% include partial.dax with var=value %} is not supported
Inheritance beyond layout chain — no block/extends system
Dynamic routes — routes are determined by the file system, not by frontmatter
If you need any of these, the correct fixes are:
1. Prepare data in the content layer — add computed values to frontmatter or _data/
2. Modify the C# engine — a significant undertaking, only for developers comfortable with C#
3. Use a different tool — Dax Pro is for static content sites. Complex templating needs a different engine
The engine's simplicity is a feature, not a bug. Every feature the engine does not have is a feature that cannot break silently.
The seo.dax Partial
The SEO partial is central to every page. It renders the , description, OpenGraph, Twitter Card, favicon, canonical URL, and JSON-LD structured data.
Key variables:
{{ title }} or {{ metadata.title }} — page title
{{ description }} or {{ metadata.description }} — page description
{{ image }} or {{ metadata.image }} — OpenGraph image
{{ metadata.url }}{{ url }} — canonical URL
What it renders:
<meta name="generator" content="DAX dotnet - Pure C# Slim SSG">
<title>{{ title or metadata.title }}</title>
<meta name="description" content="{{ description or metadata.description }}">
<link rel="canonical" href="{{ metadata.url }}{{ url }}">
<meta property="og:type" content="{% if layout contains 'post' %}article{% else %}website{% endif %}">
<!-- ... OpenGraph and Twitter Card tags ... -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "{% if layout contains 'post' %}Article{% else %}WebSite{% endif %}",
...
}
</script>
Do not remove the SEO partial from base.dax. Every page depends on it for correct SEO metadata.
To customize the JSON-LD schema — for example, to add a Review type for a review page — edit shared/seo.dax. This is a template change, not a data change.
Debugging Template Issues
Issue: A variable renders as empty.
The variable is misspelled
The frontmatter does not have that field
The field is in the wrong scope (e.g., inside a loop but referenced outside)
Fix: add {{ page | json }} somewhere in the layout. Or add as an HTML comment. Rebuild. Inspect the source.
Issue: A section does not appear at all.
The {% if %} condition evaluates to false
The include statement is missing or misspelled
The partial file does not exist
Fix: check home.dax for the include line. Confirm the partial file exists. Add a temporary {% if true %}DEBUG{% endif %} at the top of the partial to verify it is being included.
Issue: A loop iterates zero times.
The list is empty or missing
The variable name is wrong
The list is a string, not an array
Fix: add {% if list %} before the loop. If the loop is skipped, the list is empty. Inspect the frontmatter to see if the list is populated correctly.
Issue: The layout chain is not applied.
The frontmatter layout field is misspelled
The layout file does not exist at the expected path
The layout's own frontmatter is missing
Fix: check content/{file}.md for layout: "post.dax". Check templates/layouts/post.dax exists. Check that post.dax starts with --- and has layout: base.dax.
Issue: {% include %} does not resolve.
The partial path is wrong
The partial file does not exist
The LoadPartial fallback tried templates/layouts/{name} and got confused
Fix: verify templates/partials/{context}/{name}.dax exists. Use the exact path relative to templates/partials/.
Customizing CSS
CSS lives in public/css/style.css. It is one file — no preprocessing, no build step. All styles for all pages live in one place.
CSS variables are defined at the top of style.css:
:root {
--dotnet-purple: #512BD4;
--dotnet-purple-dark: #3F1FA8;
--azure-blue: #0078D4;
--text-main: #0F172A;
--text-secondary: #475569;
--surface-canvas: #FAFAFC;
--surface-card: #FFFFFF;
--border-subtle: #E2E8F0;
--radius-md: 10px;
--radius-lg: 16px;
--max-width: 1240px;
/ ... more variables ... /
}
To change the brand color — change --dotnet-purple:
:root {
--dotnet-purple: #FF6B35; / example: orange instead of purple /
}
Rebuild. Every element using --dotnet-purple updates.
To add new styles for a new section — append to the end of style.css:
/ Testimonials section /
.dax-section-testimonials {
padding: 5.5rem 0;
background: var(--surface-canvas);
border-bottom: 1px solid var(--border-subtle);
}
.dax-testimonial-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.75rem;
}
.dax-testimonial-card {
padding: 2rem;
background: var(--surface-card);
border-radius: var(--radius-lg);
border: 1px solid var(--border-card);
}
.dax-testimonial-quote {
font-size: 1.05rem;
color: var(--text-main);
font-style: italic;
margin-bottom: 1rem;
}
.dax-testimonial-author {
font-size: 0.9rem;
color: var(--text-muted);
font-weight: 600;
}
@media (max-width: 768px) {
.dax-testimonial-grid {
grid-template-columns: 1fr;
}
}
Common CSS mistakes:
Editing style.css but not rebuilding — CSS is copied from public/css/ to site/css/ during build. If you edit the source and refresh without rebuilding, the change is not reflected
Hardcoding colors — use CSS variables. When you want to change the brand color later, you change one line
Not testing responsive — resize the browser after every change. The theme is responsive; new sections must be responsive too
Forgetting the mobile breakpoint — the theme uses @media (max-width: 768px) for mobile. New sections should include a mobile rule
Customizing JavaScript
public/js/main.js contains the vanilla JS for two features:
1. Mobile navigation — the drawer menu toggle
2. Form handling — the contact form's validation and submission
The JS is small — 100–150 lines total. It does not depend on any library. It is loaded with defer so it does not block rendering.
To add a new JS feature — for example, a copy-to-clipboard button:
1. Add the button markup to a partial
2. Add a data-copy-target attribute
3. Add the event handler to main.js
4. Rebuild
Common JS mistakes:
Removing the defer attribute — the script blocks rendering. Pages load slower. The Lighthouse score drops
Using document.write() — breaks HTML parsing. Never use it
Loading external JS from CDN — adds a third-party dependency, privacy concern, and potential failure point. Self-host all JS
Not testing on mobile — mobile navigation and touch interactions must work on actual devices, not just browser DevTools
Adding a New Page Type
"Page type" means a new category of content with its own layout, controller, and URL structure. Blog posts, services, and pages are three existing page types. Adding a fourth:
Step 1: Create the item layout.
templates/layouts/faq.dax:
---
layout: base.dax
<main>
{% include faq/header.dax %}
<article class="dax-article-container">
<div class="dax-prose">
{{ content }}
</div>
</article>
</main>
Step 2: Create the listing layout.
templates/layouts/faqs-list.dax:
---
layout: base.dax
<main>
{% include faqs/header.dax %}
<section class="dax-faqs-section">
<div class="dax-container">
<div class="dax-faqs-grid">
{% for faq in pagination.items %}
<div class="dax-faq-card">
<h3>{{ faq.title }}</h3>
<p>{{ faq.description }}</p>
<a href="{{ faq.url }}">Read more</a>
</div>
{% endfor %}
</div>
</div>
</section>
</main>
Step 3: Create the item partials.
templates/partials/faq/header.dax — the page header for one FAQ
templates/partials/faqs/header.dax — the page header for the listing
Step 4: Create the content folder.
content/faqs/ with items like content/faqs/how-to-install.md:
---
layout: "faq.dax"
title: "How to Install Dax Pro"
description: "Step-by-step installation guide."
Step 5: Create the controller.
content/faqs.md:
---
layout: "faqs-list.dax"
title: "Frequently Asked Questions"
breadcrumb: "FAQ"
description: "Common questions about Dax Pro."
collection: "faqs"
pagination: 10
Step 6: Add CSS.
Add .dax-faqs-section, .dax-faqs-grid, .dax-faq-card styles.
Step 7: Rebuild.
dotnet run -- build
The FAQ listing appears at /faqs/. Individual FAQs at /faqs/{slug}/.
Step 8 (optional): Add to navigation.
Edit _data/nav.yaml:
items:
- title: "Home"
url: "/"
- title: "FAQ"
mobile_title: "FAQ"
url: "/faqs/"
# ... other items
What You Have Now
After this chapter, you can:
✅ Understand the difference between layouts and partials
✅ Read and edit any .dax file in Dax Pro
✅ Add a new section to the homepage
✅ Create a new layout
✅ Create a new page type with its own collection
✅ Understand variable scope, conditionals, and loops
✅ Debug template issues
✅ Customize CSS and JavaScript
✅ Know the limits of the engine and the correct fixes
The Developer Guide is complete. The site is deployed, editable, and extensible. The next chapter is the Conclusion — a summary of everything built, a quick-reference index, and next steps for continued development.
→ Continue to Conclusion