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).