Skip to main content

Organizing Templates

A template tree is only half the story — how you arrange your templates determines how flexible your archetype is. This page covers the patterns that show up in well-structured archetypes: multiple content roots, shared fragments, library partials, dynamic names, and binary assets.

Content roots

Nothing about the directory name contents/ is magic. Your script renders any directory in the archetype by path:

directory.render("contents", context)

That means you can keep several content roots and render each one conditionally. This is far cleaner than threading {% if %} guards through dozens of files:

my-archetype/
├── contents/ # always rendered
│ └── {{ project_name }}/...
├── docker/ # only when requested
│ └── {{ project_name }}/
│ ├── Dockerfile
│ └── .dockerignore
└── ci/ # only when requested
└── {{ project_name }}/.github/workflows/ci.yml
directory.render("contents", context)

if archetype.switches.is_enabled("docker") or context:get("use_docker") then
directory.render("docker", context)
end

if context:get("use_ci") then
directory.render("ci", context)
end

Gating on a switch lets power users opt in from the command line; gating on a prompt answer keeps the interactive flow friendly. Many archetypes do both, as above.

directory.render also accepts a destination subdirectory, useful when a root shouldn't wrap itself in the project directory:

directory.render("extras/monitoring", context, {
destination = context:get("project_name"),
})

Shared fragments: includes/

Anything you'd otherwise copy-paste across templates — license headers, config sections, boilerplate blocks — belongs in your archetype's includes/ directory, spliced in with {% include %}:

my-archetype/
├── includes/
│ └── license-header.atl
└── contents/
└── {{ project_name }}/src/main.rs
{% include "license-header.atl" %}

fn main() {
println!("{{ project_name }}");
}

The includes/ location is fixed by convention — it's automatically on the include search path, no configuration needed. Included fragments share the including template's flat context, so a fragment can freely use {{ project_name }}, loops, conditions, and even further includes.

The .atl extension is conventional for fragments (it signals "this is a partial, not a file to be emitted"), but any text file works.

Library includes

When your archetype composes a library — a catalog entry marked library: true — that library's includes/ directory joins the include search path too, namespaced under the catalog key you mounted it with:

catalog:
editor-config:
source: "https://github.com/archetect-common/editor-config-library.git"
library: true
show: false

Your templates can now reference the library's partials with the mount key as a prefix:

{% include "editor-config/editorconfig-root.atl" %}
{% include "editor-config/editorconfig-rust.atl" %}

Your own includes/ is searched first, so you can shadow a library partial by shipping a file at the same relative path (e.g. includes/editor-config/editorconfig-rust.atl) — handy for overriding one fragment while keeping the rest of the library.

Publishing include paths from a library

If you author libraries, there's a wrinkle: the library doesn't know what mount key consumers will pick. archetype.include_path(rel) solves this — in library mode it returns "<mount_key>/<rel>", and standalone it returns rel unchanged. Libraries use it to publish ready-to-use include paths into the consumer's context:

-- In the library's module, during its prompt phase:
if archetype.is_library() then
local components = context:get("components") or {}
components.editor_config = {
section_includes = {
rust = archetype.include_path("editorconfig-rust.atl"),
},
}
context:set("components", components)
end

Consumers (and the library itself) can then use those published paths from Lua — for example with file.render, which resolves sources against the include search path:

local sections = context:get("components").editor_config.section_includes
file.render(sections.rust, context, { destination = ".editorconfig" })

Note that {% include %} inside a template takes a quoted literal path only — it is resolved when the template is compiled, so it cannot accept a context expression. When a template needs a library partial, spell out the mount-key-prefixed path as shown above.

Dynamic file and directory names

File and directory names are templates, subject to the same rendering as contents. The canonical use is wrapping the whole output in the project directory:

contents/
└── {{ project_name }}/
├── Cargo.toml
└── src/main.rs

Names can use filters and any context key — including the kebab-cased and other case-expanded spellings:

contents/
└── {{ project-name }}/
└── src/main/java/
└── {{ package | directory_case }}/
└── {{ ProjectName }}Application.java

Because the flat context carries every case spelling, picking the right key ({{ project_name }} vs {{ ProjectName }}) is usually all the "logic" a name needs.

Binary and asset files

You don't need to do anything special for images, fonts, archives, or other non-text assets dropped into a content root. Before rendering each file, Archetect inspects its content: files detected as binary are copied through byte-for-byte, never parsed as templates. Text files are rendered.

Two things to note:

  • The binary file's name is still rendered — {{ project_name }}-logo.png works as expected.
  • Detection is content-based, not extension-based. A text file full of {{ ... }}-looking sequences that you don't want rendered isn't a binary — protect it with {% raw %} blocks instead (see Template Syntax).