Declared components
Accent used to expand [shortcode] syntax, which embedded a reusable component
but declared nothing about it. Nothing knew which attributes [alert] took, so
a misspelled attribute rendered as nothing and a typo in a variant name reached
the page.
A declared component fixes that. You write a small YAML manifest beside the template, author the component with tag syntax, and Accent validates every use against the declaration.
This is the only component syntax. The square-bracket form has been
removed, and
accent migrate shortcodes rewrites a
site that still uses it.
This page is the reference. For a hands-on walk-through that builds a hero, a call-to-action band, and a teaser grid and composes a landing page from them, see the landing page components tutorial.
Declare the component
A manifest lives beside its template, in your theme’s shortcodes/ directory:
themes/default/shortcodes/
├── callout.html.jinja the markup
└── callout.yaml the declaration
# themes/default/shortcodes/callout.yaml name: callout description: A titled box that sets a passage apart. params: - name: type type: enum values: [info, success, warning, danger] required: false description: The kind of notice. body: markdown
name must match the filename. params are the attributes, in the order you
declare them. body says whether the component wraps content and how that
content meets your template:
body: markdownis block content: paragraphs, lists, headings, nested components. Accent splices it between blank lines, so the block element your template puts it in closes first and the body renders as markdown.body: inlineis a label or a phrase that stays on its line, such as a button’s text. Accent splices it flush; inline markdown in it still renders.- Omit
bodyand the component is self-closing.
Attribute types come from the same vocabulary document models use: string,
number, boolean, enum, list, object, reference, and media.
A list declares what each item is under items:: a type, and the values,
items or fields that type needs. An object declares its fields under
fields:, each written like a parameter, with a name and optionally
required and description. They nest, so a list of links is:
# themes/default/shortcodes/menu.yaml name: menu params: - name: links type: list required: true items: type: object fields: - name: label type: string required: true - name: href type: reference required: true
Content writes the value as an array of objects, and the template reads it like any other attribute:
{% menu links=[{label: "Home", href: "/"}, {label: "Blog", href: "/blog"}] /%}
<ul>{% for link in attrs.links %}<li><a href="{{ url(link.href) }}">{{ link.label }}</a></li>{% endfor %}</ul>
accent validate checks every item and field, and names where a value does not
match, such as links[1].label. A list without items: or an object
without fields: is a manifest error, because nothing would say what the value
holds.
This theme’s related component takes its links that way, and the list below
is one:
Inline components
By default a component renders as a block of its own: Accent puts blank lines
around its markup so the surrounding markdown reads it as a block. A component
meant to sit inside a line – a badge in the middle of a sentence, or in a
heading – declares inline: true:
name: edition description: An inline badge naming the license tier a feature needs. inline: true params: - name: edition type: enum values: [free, core_plus, standard, pro]
## Integrate {% edition edition="pro" /%}
Without inline: true that heading would be split into three blocks.
button declares it too, for a different reason: a button is written in a
sentence as often as on a line of its own, and
{% button href="/x" %}Go{% /button %} on one line is an inline placement.
An inline component is valid in both positions; a block component written
inside a line is reported as misplaced by accent validate.
Components that read their children
Most components wrap a body. One kind composes: a tabbed switcher has to emit a
button per panel, carrying that panel’s label, before any panel. By the time
the parent renders, its body is one string of markup and a label cannot be
recovered from it – so a component template can read children, the
component’s immediate child tags, each with its own attributes and body.
{# themes/default/shortcodes/tabs.html.jinja #}
<div class="tabs" data-tab-group="{{ attrs.group | default("default") }}">
<div class="tabs-nav" role="tablist">
{%- for c in children if c.name == "tab" %}
<button role="tab" data-tab="{{ c.attrs.label | slugify }}">{{ c.attrs.label }}</button>
{%- endfor %}
</div>
{%- for c in children if c.name == "tab" %}
<div class="tabs-panel" role="tabpanel">{{ c.body }}</div>
{%- endfor %}
</div>
{% tabs group="os" %} {% tab label="macOS" %}Install with Homebrew.{% /tab %} {% tab label="Linux" %}Install with the script.{% /tab %} {% /tabs %}
Each child’s attrs are the same resolved, escaped values the parent’s own
attrs are, so reading a child is no more privileged than reading an
attribute. A component that reads children normally ignores body, because
it is rendering the children itself.
Author with it
{% callout type="warning" %} Back up your content directory before upgrading. {% /callout %}
A self-closing component takes no body. The shipped edition badge is one:
{% edition edition="pro" /%}
The shipped button is the worked example of a label body. Its manifest
declares both keys:
name: button description: A call-to-action link styled as a button. params: - name: variant type: enum values: [default, primary, secondary] required: false - name: href type: reference required: true body: inline inline: true
and its template keeps the label on the line, inside the link:
{%- set variant = attrs.variant | default("default") -%}
<a href="{{ url(attrs.href) }}" class="btn btn-{{ variant }}">{{ body }}</a>
Passing href through url() is what keeps the link correct on a sub-path
deployment, and keeps the slash literal rather than HTML-escaped.
It is written on one line, in a sentence or on a line of its own:
Ready? {% button variant="primary" href="/download" %}Download{% /button %}
Values you interpolate are escaped, so an attribute containing <script>
reaches the page as text.
A component body renders as markdown. The body is spliced into the page as
markdown source at the spot where the template interpolates {{ body }}, and
the manifest’s body value decides how. A body: markdown body arrives
between blank lines, so a template can put it straight inside a <div> and it
renders: paragraphs, lists, headings, fenced code, and nested components all
work, through the same pipeline as the rest of the page. A body: inline body
arrives flush, which is what a label inside an <a> or <span> needs. Two
things to keep in the template: write {{ body }} and the tag that closes
after it in column 0, because an indented line after a blank line is an
indented code block; and declare body: inline for a label, because blank
lines inside an inline element turn the label into a paragraph. accent validate --templates reports both mistakes.
Variables
A component attribute can reference a variable with $:
{% callout type="info" %} Welcome to {% $site.name %}. {% /callout %}
Four namespaces are available:
| Variable | What it holds |
|---|---|
$site | name, description, language, url, base_path |
$frontmatter | the page’s own frontmatter, including your custom fields |
$template | the template rendering the page |
$edition | the licensed edition, when the site supplies one |
Nothing else is available, deliberately. The clock, environment variables, data files, remote URLs, and anything that varies per request are all unavailable, because Accent’s page cache cannot tell when they change. A page built from one of them would serve stale output until an unrelated edit happened to clear the cache. The three sources above are exactly the three the cache knows how to invalidate.
Conditionals
{% if $frontmatter.draft %} This page is a draft. {% else /%} This page is published. {% /if %}
Conditions are evaluated by Accent’s own expression engine, not by the template language. Content stays declarative: it can select between branches, but it cannot run arbitrary template code.
Partials
A partial splices another markdown file into the page:
{% partial file="shared/upgrade-notice.md" /%}
The name is resolved against your content directory first, then your theme’s
partials/ directory. A partial may include another; a cycle is reported
rather than followed.
Partials are tracked. Editing a partial re-renders every page that includes
it – in the dev server and in an incremental accent build alike – because
Accent records which files each page was built from. Deleting a partial
re-renders its former includers too.
Canonical components
Components are theme-scoped, so a page that uses a component only one theme declares breaks when the site switches to another theme. The canonical components are the part of the vocabulary you can rely on: every conforming theme implements each of them with the same attribute names, so content that uses only these renders under any conforming theme.
| Component | Attributes | What it is |
|---|---|---|
callout | type, title, collapsible | a titled box for a note, a warning, or another notice |
accordion | summary, open | a section collapsed behind a summary line |
button | href, variant | a link styled as a button |
card | title, href, image | a block with an optional image, title, and link |
table | none | the built-in table; the theme styles it |
tag | href | a small inline label |
image | src, alt, caption | an image with alt text and an optional caption |
breadcrumb | none | a trail of links written in Markdown |
tabs / tab | group / label, id | tabbed panels |
Names are fixed and values are a floor. A conforming theme declares every
attribute above, and where an attribute has fixed values, the theme accepts at
least those: callout takes info, success, warning, and danger, and
button takes default, primary, and secondary. A theme can add
attributes, values, and components of its own; content that uses them depends
on that theme.
Markup landmarks. Each theme writes its own markup and classes, but every
conforming theme marks the same landmarks, so a script or a stylesheet that
finds a callout’s title or a tab’s panel works under any conforming theme. The
component’s root element carries data-canonical with the component’s name,
and each part carries data-part:
| Component | Parts |
|---|---|
callout | title |
accordion | summary; the root is a <details> element |
tabs | tablist, and a tab and a panel for each tab |
card | title and image, when those attributes are set |
image | image, and caption when it is set |
button, tag, breadcrumb | the root only; a button is an <a> or a <button>, and a breadcrumb is a <nav> |
<div class="callout callout-warning" role="complementary" aria-label="Heads up" data-canonical="callout"><p class="callout-title" data-part="title">Heads up</p> ... </div>
A theme you write for the canonical components carries the same attributes.
The set was chosen by measurement. A component is canonical when at least three
of four CSS frameworks – Bulma, Pico CSS, Foundation, and USWDS – style it
and content writes it, rather than page chrome such as a navigation bar. tabs
is the exception: two of the four have it, and it is canonical because the
documentation Accent ships uses tabs throughout.
accent validate checks the active theme against the set, with or without a
page that uses a missing component, and reports each gap as an error. When a
page uses a component the theme does not declare, the finding names the theme
and says whether the component is canonical:
error /docs/example
line 8: theme `minimal` does not implement `card`, one of the canonical
components every theme is expected to provide; add it to the theme, or
use a theme that implements the canonical components
[accent::component-not-in-theme]
A theme that provides no components, such as a marketing site’s theme,
declares that in theme.yaml, and accent validate does not check it:
components: none
For a site part way through a theme change, accent validate --lax reports a
theme’s gaps, and pages using a canonical component the theme lacks, as
warnings that do not fail the run. A name that no theme, builtin, or plugin
declares stays an error under --lax, because it may be a misspelling.
Builtin components
Four components need something a theme template is never given, so Accent renders them itself. Every theme has them, and neither a theme nor a plugin can redefine them.
| Component | What it renders | Attributes |
|---|---|---|
download | a file beside the page, as a card with its name, type, and size | src (required), title, show_size, class |
video | a video beside the page, with its captions | src (required), captions, poster, width, controls, autoplay, loop, muted |
diagram | a diagram, the way a fenced diagram block renders it, with a caption and alt text | type (required), src, theme, width, height, caption, alt, class |
search | the site’s search box, for the active search backend | placeholder, limit |
{% download src="report.pdf" /%} {% video src="talk.mp4" captions="talk.vtt" poster="talk.jpg" /%} {% diagram type="mermaid" caption="Publishing flow" alt="A draft goes to review, then to publishing" %} flowchart LR A[Draft] --> B{Review} --> C[Publish] {% /diagram %} {% search placeholder="Search the docs" limit=5 /%}
download and video serve files the page owns: put the file in the page’s
directory and name it. Both refuse a URL, because Accent can neither read a
remote file’s size and type nor serve it. For a PDF, download renders the
same card as the pdf_card() template function.
The file must also be a type Accent serves. Images, video, audio, PDFs, text
and data files such as .csv and .json, and .vtt captions are served as
they are. Any other type, such as .zip, needs its extension in
media.attachments.extensions, which serves it as a download:
media: attachments: extensions: [zip]
diagram takes its source from the tag’s body, or from a file beside the page
named by src, not both. It renders through the same pipeline, cache, and
diagrams.on_error policy as a fenced block. search renders what
search_form() renders, and nothing while search is turned off.
Captions are part of video. Name a WebVTT (.vtt) caption file with
captions, or write captions="none" when the video has no speech or no
captions exist. accent validate reports a video with neither, and
accent validate --lax relaxes that finding. A SubRip (.srt) file is refused
with a message to convert it, because browsers load WebVTT captions only.
accent validate also reports a src that names a file the page does not
have, listing the files it does have, and a builtin given a URL. Both stay
errors under --lax.
Where a component can come from
Every declared component records where it was declared, and three sources are possible: Accent’s own built-in tags, your theme, and an installed plugin. When two sources declare the same name, the higher-precedence one wins:
built-in > theme > plugin
A plugin cannot redefine a tag your theme declares, and neither can redefine a built-in. This matters because a schema does not control one tag’s markup – it controls what counts as valid content across your whole site, so installing a plugin must not silently change what your existing pages mean. When a declaration is shadowed, Accent logs which source won and which lost.
Two declarations at the same level are an error naming both files, never a silent overwrite.
{% for %} is not content
Components share {% %} with the theme’s template language, and that is the
one thing worth knowing before you start. Template control flow belongs in
the theme. A page cannot loop, include, or extend, and writing
{% for item in $items %}
in a page is reported as its own kind of problem rather than as a misspelled component:
error /docs/example
line 12: `{% for %}` is template syntax, not content. Template control
flow belongs in the theme's `.html.jinja` files, which content cannot
reach. If you meant a component, declare one in the theme's
`shortcodes/` directory and use its name here.
The reserved words are for, endfor, include, extends, block, set,
macro, call, filter, raw, with, and import. A component whose name
merely starts with one – setting, for-each – is unaffected, and a
reserved word inside a code fence or backticks is prose, not a tag, so pages
that document template syntax are fine.
What gets reported
accent validate reports every component problem, so a broken tag fails CI
instead of reaching a reader. The same findings appear in the admin panel and
in dev-server logs.
| Problem | What you see |
|---|---|
| A component the theme does not declare | the tag is left in the page, and the error names the theme and whether the component is canonical |
| A theme missing a canonical component | reported against the theme, whether or not a page uses the component |
A builtin given a URL, a file the page lacks, a file type Accent does not serve, or .srt captions | the tag is left in the page, and the error names the file |
A video with no captions | reported; --lax relaxes it |
| Template syntax in content | the directed message above, under its own check |
| Undeclared attribute | reported with the attribute name |
| Wrong type, or a value outside an enum | reported with what was expected |
| Missing required attribute | reported with the attribute name |
| Manifest that will not parse | reported with the file path; other components still load |
| Manifest with no template | reported at load, not on the first page that uses it |
Migrate your existing shortcodes
accent migrate shortcodes rewrites [shortcode] content into tag syntax. It
reports what would change and writes nothing until you pass --write:
accent migrate shortcodes --config config.yaml accent migrate shortcodes --config config.yaml --write
It rewrites a shortcode only when it can verify the result, and leaves everything else exactly as you wrote it, with the file, the line, and the reason. Shortcodes inside fenced code blocks are never touched, and neither is frontmatter.
Tell it what your positional arguments meant. Shortcodes take bare words;
tags take named attributes. Add a migrate block to the manifest so the
command does not have to guess:
name: callout params: - name: type type: enum values: [info, success, warning, danger] body: markdown migrate: positional: [type] # [callout warning] -> {% callout type="warning" %}
Without it, [callout warning] is reported rather than rewritten.
Tell it what a renamed component used to be called. When a theme renames a
component, list the old name under migrate.from, with any attribute it
renamed. The default theme renamed alert to callout and details to
accordion, so its callout manifest says:
migrate: positional: [type] from: - name: alert rename: variant: type # [alert variant="warning"] -> {% callout type="warning" %}
A component declared under the shortcode’s own name still wins, so a theme that
keeps an alert component migrates [alert] to it.
Useful flags:
| Flag | What it does |
|---|---|
--write | apply the rewrite; without it nothing is written |
--only NAME | migrate only this component; repeatable |
--include-frozen | also rewrite pages in frozen documentation versions |
Pages in a frozen documentation version are reported but not rewritten, because
a frozen version is errata-only and its content hashes are recorded. Passing
--include-frozen takes them, and records the new hashes as it goes, so
accent validate stays clean. The report names every page it rebaselined –
those are released documents that changed after release, and you should be able
to see which.
Check the result with a build diff. Build before and after, and compare:
accent build --output /tmp/before --clean accent migrate shortcodes --write accent build --output /tmp/after --clean diff -rq /tmp/before /tmp/after
Rendered pages should be identical. The word count in each page’s structured
data, and llms-full.txt, read your markdown source rather than the rendered
page, so those do change – everything else changing is worth investigating.