Object Fields

When defining objects in your Object Definition File, each object’s schema is defined by key value pairs, where the keys are field names and the values are types. When rendering, each type is represented by a distinct liquid value or an object hash.

The available types are:

string

Values will be interpreted as a liquid string.

Note that string values are themselves rendered as liquid — see liquid in field values.

secret

A secret holds a string that your site needs but your built site must never contain — an API key, a webhook signing secret, a third-party token.

[settings]
title = "string"
api_key = "secret"

In an object file it's an ordinary string:

title = "My Site"
api_key = "sk_live_abc123"

Secrets are removed from templates. A secret field is never added to a page or template context at all, so referencing one isn't an empty value — it's an unknown variable, and the build fails:

<!-- this fails the build, it does not render blank -->
{{ settings.api_key }}

That's deliberate: a typo can't quietly ship a key into your HTML, and the build error never contains the value.

Secrets are readable where the code runs on your side of the wall:

  • From carriers, on the third argument — objects.settings.api_key. Carriers are exactly the place to use one.
  • From the rust library, via FieldValue::as_secret. Display and the liquid ValueView both redact, so as_secret is the only way to read the value.
  • From archival types, which includes secret fields — anything consuming those types is a server-side consumer.

In the archival editor, a secret field is masked until you reveal it, and list views never show it.

A secret is not encrypted. It's written to your object's toml file as plain text, which means it's committed to your repo and lives in your git history like any other value. secret keeps a value out of your built site, not out of your repo. If a value must not exist in your git history at all, don't put it in an object — keep it in your carrier's own deployment configuration.

number

Values will be displayed as a liquid number.

Note that while it’s perfectly valid to specify integers in files, these numbers are always interpreted as floats - specifically as signed 64 bit floats.

Rendering

When rendering numbers, it’s typical to use the round filter to obtain the desired precision. See https://shopify.github.io/liquid/filters/round/

{{ object.number | round: 2 }}

boolean

Values will be displayed as a liquid boolean.

There are only two values, which should be defined unquoted - either true or false. You can use these values for reliable control flow, usually inside if statements.

date

Values will be interpreted as a liquid datetime, but represented as a string in toml files.

The following date formats are supported in toml files:

  • YYYY-MM-DD HH:MM:SS
  • YYYY-MM-DD
  • DD Month YYYY HH:MM:SS
  • DD Mon YYYY HH:MM:SS
  • MM/DD/YYYY HH:MM:SS
  • M/D/YY HH:MM:SS (short form — a two-digit year is interpreted in the current century)
  • Dow Mon DD HH:MM:SS YYYY

In all formats, you may specify offsets by appending either +HHMM or -HHMM. If you don’t provide an offset, your machine’s local offset is used.

A few conveniences:

  • You may use the keywords now or today (case-insensitive) in place of a date, and archival will fill in the current date and time when the site builds.
  • If you omit the time portion, it defaults to 00:00:00.

Rendering dates

When rendering a date, you’ll typically want to use the date filter to display human-readable values.

For instance:

This page was last updated at {{ object.date | date: "%Y-%m-%d %H:%M" }}.

This filter uses the same formatting strings as strftime.

markdown

Values will be parsed as markdown before rendering, and output as HTML. Only fields declared as markdown are parsed this way — markdown written into a string field is emitted as-is, without being converted.

Markdown is a simple formatting language that is intended to allow you to write formatting into a human-readable text that can then be translated into actual formatting code later. For more info on markdown, check out the Markdown Guide or the Syntax page.

archival uses comrak to parse markdown, which supports standard markdown plus Github Flavored Markdown extensions. This means that the following additional formatting is available:

| foo | bar |
| --- | --- |
| baz | bim |
- [ ] foo
- [x] bar
~~Hi~~ Hello, ~there~ world!
  • Autolinks (links will automatically be clickable)

In addition to the GFM extensions above, archival enables a few more comrak extensions:

  • Footnotes — reference a footnote with [^1] and define it later with [^1]: ….
  • Description lists — a term on one line followed by : definition lines.
  • Superscript — wrap text in ^carets^ to raise it.
  • Header IDs — headings are given id anchors automatically, so you can link directly to a section.

A note on HTML and security

archival renders markdown with raw/unsafe HTML enabled — any HTML you write inside a markdown field is passed through to the output as-is, and no tag filtering is applied. This is convenient when you trust the content (you’re the author), but it means markdown is not sanitized for you. If you ever render markdown that came from an untrusted source, sanitize it yourself before trusting the output.

A note on liquid

Liquid you write inside a markdown field is rendered, not printed — as it is in every other field type. See liquid in field values below for how that works, how to escape it, and the one markdown-specific gotcha (use single quotes for liquid string literals here).

image, video, upload, audio

These types are special object types that have the following keys:

[fieldname]
filename = "asparagus-bacon-sambal-lime-vinaigrette.png"
sha = "31f4725e732340541f83065d7bde1eba22a3aa2dd507f2811b8f69a07250379b"
name = "asparagus-bacon-sambal-lime-vinaigrette.png"
display_type = "image"
mime = "image/png"

The display_type depends on the type definition, and the others are populated by either the archival editor or the archival upload command.

These files are uploaded to a cdn, and will be retrieved at the url <cdn_url>/<sha>. When rendering, a url field is automatically generated for these file types.

To use one of these types in an object, you’ll generally access it via the URL. For instance, showing a simple image might look like this:

<img src="{{object.fieldname.url}}" alt="{{object.name}}"/>

Other metadata is useful depending on the file type. For instance, you could display a video using:

<video title="{{object.video.name}}" controls>
    <source
      src="{{object.video.url}}"
      name="{{object.video.filename}}"
      type="{{object.video.mime}}">
</video>

enum

An enum constrains a field to a fixed set of string values. Instead of a type name, you define an enum directly as an array of strings in your object definition:

[post]
title = "string"
status = ["draft", "published", "archived"]
template = "post"

In an object file, the value must be one of the listed strings. archival validates this when it builds, and the build will fail if the value isn’t in the list:

title = "My Post"
status = "published"

Enums render as a plain string, so you can print them or use them for control flow:

{% if post.status == "published" %}
  <article>{{ post.content }}</article>
{% endif %}

This is different from the grouping pattern, which uses an open-ended string field — an enum is the right choice when you want to guarantee the value is one of a known set.

oneof

A oneof (a union, or “tagged” type) lets a single field hold a value that may be one of several different types. You define it as an array of tables, where each table gives a name and a type:

[post]
title = "string"
template = "post"
[[post.media]]
name = "photo"
type = "image"
[[post.media]]
name = "embed"
type = "string"

The type of each option may be any scalar or file type (string, number, image, video, etc.).

In an object file, you select one option by setting the type (to one of the option names) and providing a value of that option’s type:

title = "My Post"
# a string option:
[media]
type = "embed"
value = "https://youtube.com/watch?v=…"
title = "My Post"
# an image option — value is a file table:
[media]
type = "photo"
[media.value]
sha = "9d18d8319c33ea2b10aeb6c833c1fe66304135e0e40aa6e509df5b6733fd3029"
mime = "image/png"
display_type = "image"
filename = "cover.png"

When rendering, a oneof exposes two properties: type (the selected option name) and value (the underlying value). Branch on type to render the right thing:

{% if post.media.type == "photo" %}
  <img src="{{ post.media.value.url }}" alt="{{ post.title }}"/>
{% elsif post.media.type == "embed" %}
  <iframe src="{{ post.media.value }}"></iframe>
{% endif %}

Reach for a oneof when a field is genuinely “either/or” — for example content that may be an image or a video. If you instead want a repeating list of structured items, use child fields.

meta

A meta field holds free-form, schema-less data. Use it when the shape of the data isn’t fixed ahead of time — arbitrary settings, custom attributes, or nested metadata that doesn’t warrant its own object definition.

[page]
title = "string"
config = "meta"

A meta value can contain strings, numbers, booleans, dates, arrays, and nested tables, mixed and nested freely. You write it as ordinary toml:

title = "Home"
[config]
theme = "dark"
featured = true
max_items = 6
tags = ["news", "updates"]
[config.contact]
email = "[email protected]"

Because meta is unstructured, archival does not validate its contents (though you can constrain individual keys with a validator). When rendering, access keys with dot notation and iterate arrays like any other liquid value:

<body class="theme-{{ page.config.theme }}">
  {% if page.config.featured %}<span class="badge">Featured</span>{% endif %}
  <ul>
    {% for tag in page.config.tags %}<li>{{ tag }}</li>{% endfor %}
  </ul>
  <a href="mailto:{{ page.config.contact.email }}">Contact</a>
</body>

liquid in field values

For complex cases, field values may contain liquid, which may reference the site's archival content.

Wherever a template outputs a value, archival checks the value for liquid and, if it finds any, renders it right there — in the middle of rendering your page. That is what evaluates liquid you wrote inside your content, so a {{ site_url }} typed into a field is rendered, not printed.

This applies to every field value that reaches your output as text — string, markdown, enum values, strings inside meta, and fields on child objects. Every value below renders the site url:

title = "Hosted at {{ site_url }}"
status = "{{ site_url }}"

[meta]
note = "also {{ site_url }}"

[[links]]
label = "and {{ site_url }}"

Because the value renders in place, it sees exactly what the surrounding template sees at that point: your objects, all the template variables, and anything the template itself has created. Control flow ({% if %}, {% for %}) works just like it does in a template.

That includes template-local variables. A value output inside a loop can use the loop variable and forloop:

{% for post in posts %}{{ post.body }}{% endfor %}
body = "Posted by {{ post.author }} — entry #{{ forloop.index }}"

and a value output after an {% assign %} can use what was assigned:

{% assign featured = posts | first %}{{ featured.body }}
body = "Read more from {{ featured.author }}"

There are some subtleties to be aware of:

Unknown variables fail the build. A {{ }} in your content that names something which doesn’t exist is a build error, not an empty string — exactly as it would be in a template. This is the same rule that makes secret fields fail loudly.

A value has to hold complete liquid. A block your value opens must be closed by the same value. A field holding only {% if x %}, with the matching {% endif %} in your page template, is a parse error.

Only one extra level renders. Liquid produced by a value isn’t rendered again — it’s emitted literally. If field a holds {{ post.b }} and field b holds {{ site_url }}, rendering a outputs the text {{ site_url }}, not the url. A field that refers to itself terminates for the same reason.

In markdown fields, markdown is converted first. A markdown field becomes HTML before its liquid is rendered, so your liquid lands inside whatever HTML comrak produced — a tag on its own line ends up wrapped in a <p>. More importantly, comrak escapes double quotes to &quot;, which liquid can’t parse, so use single quotes for string literals in markdown fields:

{{ post.date | date: '%Y' }}

Escaping. To keep liquid syntax literal in a field value, wrap it in a raw tag — which is why the examples throughout these docs are written that way:

{% raw %}
This {{ site_url }} is printed, not rendered.
{% endraw %}