Move a folder of templates without touching a single reference inside it.
:::note[Why this tutorial exists]
Hard-coded, root-relative include paths are brittle: renameskills/ to library/skills/ and every {% include "skills/_status.html" %} breaks silently at authoring time and loudly at render time. Kida 0.8 ships two resolution modes that eliminate the problem — relative paths (./, ../) for co-located partials, and namespace aliases (@components/) for shared libraries. Pick the right tool for each reference and folder moves become zero-edit refactors.
:::
TL;DR
| Reference style | Use when | Survives folder move? |
|---|---|---|
{% include "./_card.html" %} |
Partial lives next to the caller | Yes — the pair moves together |
{% include "../shared/nav.html" %} |
Partial lives one folder up | Yes — as long as the subtree moves as a unit |
{% include "@components/card.html" %} |
Shared component library (cross-cutting) | Yes — alias root changes, call sites don't |
{% include "components/card.html" %} |
Only if the root path is truly stable | Only whilecomponents/stays put |
Rule of thumb: if a partial is owned by the caller's folder, use./ or ../. If it belongs to a shared library used from many folders, use an alias.
The Problem
Consider this layout:
templates/
├── skills/
│ ├── page.html
│ ├── _status.html
│ └── _trust.html
skills/page.htmlincludes its co-located partials:
{# skills/page.html #}
{% include "skills/_status.html" %}
{% include "skills/_trust.html" %}
Now move skills/ to library/skills/. Every include breaks:
TemplateNotFoundError: Template 'skills/_status.html' not found
You reach forripgrep, sweep every reference, and hope no dynamic {% include "skills/" + name %} slipped through. Meanwhile the partials still live right next to page.html— the relationship never changed, only the absolute prefix did.
Fix A: Relative Paths for Co-Located Partials
Use./ when the partial is a sibling, ../when it's one folder up. The caller references its neighbors the way it would in any filesystem:
{# skills/page.html — before #}
{% include "skills/_status.html" %}
{% include "skills/_trust.html" %}
{# skills/page.html — after #}
{% include "./_status.html" %}
{% include "./_trust.html" %}
Now move skills/ → library/skills/. Zero edits. The co-located partials move as a unit; their relative relationship is unchanged.
Relative paths resolve against the caller's directory at the time{% include %} / {% extends %} / {% embed %} / {% from ... import ... %}is evaluated — so they also work inside nested includes. A partial that includes its own neighbor keeps working no matter who included it.
{# pages/post.html #}
{% include "./partials/card.html" %}
{# pages/partials/card.html #}
{% include "./_tag.html" %} {# → pages/partials/_tag.html #}
Edge cases
- Path traversal still fires.
{% include "../../../etc/passwd" %}from a shallow caller raisesTemplateNotFoundError— the resolved path must still land inside a loader search root. - Python callers need absolute names.
env.get_template("./card.html")from Python raises a clear error. Relative resolution only makes sense from inside a template. - Interior
..is fine as long as the final resolved path stays within a search root:./foo/../card.htmlfrompages/about.htmlresolves topages/card.html.
Fix B: Namespace Aliases for Shared Libraries
Relative paths are perfect for partials owned by one folder. But a shared component library — a card, a nav, a button — is used from everywhere. Writing../../../ui/components/card.htmlfrom a deeply nested page is just as brittle as a hard-coded absolute path.
Configure an alias once on theEnvironment, then reference it by short name from anywhere:
from kida import Environment, FileSystemLoader
env = Environment(
loader=FileSystemLoader("templates/"),
template_aliases={
"components": "ui/components",
"layouts": "ui/layouts",
},
)
{# Any template, any depth: #}
{% extends "@layouts/base.html" %} {# → ui/layouts/base.html #}
{% include "@components/card.html" %} {# → ui/components/card.html #}
{% from "@components/nav.html" import nav %}
{% embed "@layouts/shell.html" %}{% end %}
Move ui/components/ to shared/ui/components/? Change one line of Python:
template_aliases={
"components": "shared/ui/components",
"layouts": "shared/ui/layouts",
}
No template edits. Every @components/reference keeps working.
Alias rules
- Prefix is
@name/.@was picked specifically because it's not a legal leading character in common template filenames, so it can't collide with a real path. - Aliases resolve before loader lookup.
{% include "@components/card.html" %}becomes{% include "ui/components/card.html" %}internally, then the normal loader pipeline runs. - Unknown aliases fail loudly with a list of configured aliases:
TemplateNotFoundError: Unknown template alias '@widgets/'. Configured aliases: @components/, @layouts/ - Aliases and relative paths don't compose.
@components/./fooresolves via the alias first (ui/components/./foo), then./is a no-op segment. Keep the two modes orthogonal — if you find yourself wanting composition, the component probably belongs in a different alias.
Which Fix for Which Reference
templates/
├── pages/
│ ├── about.html # caller
│ ├── _hero.html # → ./_hero.html (Fix A)
│ └── partials/
│ └── _card.html # → ./partials/_card.html (Fix A)
├── shared/
│ └── nav.html # → ../shared/nav.html (Fix A)
└── ui/
└── components/
└── button.html # → @components/button.html (Fix B)
Rules of thumb:
- Partial only makes sense inside this caller's folder? Use
./. - Partial is a couple folders away but moves as part of the same subtree? Use
../. - Partial is a cross-cutting primitive used from many unrelated pages? Make it an alias.
- Can't decide? Start with relative. Promote to an alias once three or more unrelated callers reference it.
Migration Recipe
Use this to sweep an existing project in one pass.
1. Inventory every cross-template reference
Let Kida find the easy wins for you first:
kida check templates/ --lint-fragile-paths
That flags every {% include %} / {% extends %} / {% embed %} / {% import %} / {% from %} whose target lives in the same folder as the caller and suggests the refactor-safe ./form. Fix those in one pass — no thinking required.
For a broader sweep, scan every cross-template reference by hand:
rg --pcre2 '\{% (include|extends|embed|from\s+") "[^"]+' templates/ -o -N
That lists every absolute reference. You're looking for two patterns:
- Same-folder or near-sibling paths — candidates for
.//../. - Paths rooted at a shared library prefix (e.g.
ui/components/...,layouts/...,partials/...) — candidates for aliases.
2. Convert same-folder references to./
For each page file, find references whose target lives in the same folder. Example sweep forskills/:
rg '\{% include "skills/' templates/skills/
Rewrite:
- {% include "skills/_status.html" %}
+ {% include "./_status.html" %}
3. Promote shared libraries to aliases
If you see three or more folders referencingui/components/*, alias it:
env = Environment(
loader=FileSystemLoader("templates/"),
template_aliases={"components": "ui/components"},
)
Then sweep:
rg '\{% include "ui/components/' templates/ -l
Rewrite:
- {% include "ui/components/card.html" %}
+ {% include "@components/card.html" %}
4. Test that nothing moved
Existing absolute paths are guaranteed byte-identical after the upgrade — no template that keeps hard-coded paths needs a change. Run your test suite; nothing should shift.
5. Actually move a folder
The whole point. Pick a folder that only uses./ / ../internally and rename it:
git mv templates/skills templates/library/skills
Render a page. It should just work.
When Not to Refactor
- Dynamic includes —
{% include "components/" + widget_type + ".html" %}can't be converted to./without losing the dynamic part. Leave these absolute, or alias the prefix. - One-off paths that are unlikely to move —
{% include "templates/404.html" %}in a single error handler is fine as-is. The migration is opt-in per reference. - Generated templates — if another tool writes include paths, upgrade that tool, not the output.
See Also
- Includes — Full include syntax including
Relative PathsandNamespace Aliasessections. - Inheritance —
{% extends %}accepts the same relative and alias forms. plan/rfc-relative-template-resolution.md— Design document for the resolution system.