Templates
A template is a plain HTML file with data-arc-* attributes and {{ }} placeholders. Arc CMS fills it with an entry's data to build its page. No framework knowledge is needed.
For the step-by-step path of designing a site's templates, see Design templates. This page is the reference: the tables below are the full list of what a template can use.
What it is
Each content type points at a template folder. A type without public pages uses only the folder's partials.html, for its cards on the home page.
| File | Renders | Required |
|---|---|---|
list.html | The type's list page, /{type} | No |
detail.html | One entry, /{type}/{slug} | No |
partials.html | A block of cards embedded in the home page (see Partials) | No |
detail-{name}.html | Another layout for one entry, such as detail-contact.html, chosen in the editor (see Layouts) | No |
A file a folder lacks is taken from the default folder, so a folder can hold just the one file it changes.
Arc CMS ships three folders in public/_site/templates/: default (used by any type without a folder of its own, so every type renders), articles, manuals and info (the standard pages, see Standard pages, with a Contact layout). An app's folders go in src/custom/site/templates/. A folder name uses lowercase letters, digits, hyphens and underscores, and starts with a letter or digit (case-studies, blog_2); a folder named otherwise is not served, and npm run dev warns about it. The build puts both sets together and serves them at /_site/templates/{folder}/{file}.html; a file in the app's folder at the same path as one of Arc CMS's replaces it. The folders offered in the admin are every folder the site has, listed by the build in /_site/site.json (see Use the custom space).
The same template is filled in two places, and both use the same attributes:
| Renderer | When | Where the template comes from |
|---|---|---|
| Publishing (Cloud Functions) | On publish, on Republish website, and on the seed. Writes the static pages Hosting serves. | The same file, read from the live site at /_site/templates/ and kept until a deploy changes it (the functions check /_site/site.json, which holds a hash of every file, at most every 10 seconds). A folder the live site does not have, or a file of it that is not a template, stops the publish with a message (see Troubleshooting). When the live site cannot give the default folder's file (hosting off, not deployed since /_site/ existed, or the file is not a template), the copy of Arc CMS's default templates built into the functions is used. |
| The app (browser) | For pages not yet on Hosting, previews, and local runs | The file served by the app at /_site/templates/, the same one publishing reads once deployed. A folder the site does not have, or a list or detail file that is not a template, falls back to the default folder's. A partials.html that is not a template shows nothing, and a default folder file of your own that is not a template leaves the page loading. |
A template is a fragment: no <html>, <head>, <body> or doctype. Only real tags count, in the file's first 2,000 characters: a comment, <style> or <script> that mentions <head> is fine. Publishing wraps it in a full document with the title, meta tags, structured data, language links and site stylesheets; see What publishing puts around your template.
Turn it on or off
Templates are part of the Content feature (content). With content off there are no content pages, so templates are not used.
Set it up
Copy a folder to start from, for example
public/_site/templates/default/tosrc/custom/site/templates/events/.Edit
src/custom/site/templates/events/list.htmlandsrc/custom/site/templates/events/detail.html. Delete a file you do not change; the default's is used.In Content > Content types, edit the type and pick the folder under Content Template. Run the app locally and open
/eventsto see the templates filled.Deploy Hosting (see Deploy), then rebuild the published pages so they use the new template:
npm run seed:devFor production:
npm run seed:prod. The fullnpm run deploy:devandnpm run deploy:prodrun the seed at their end. Republish website in Content > Content types also rebuilds every page.
Publishing reads the template from the live Hosting site, not from your working copy. A template you changed locally shows in the app at once, but published pages keep the old one until Hosting is deployed and the pages are rebuilt. Publishing sees a deploy within about 10 seconds.
To change the default look for every type without a folder, put your file at src/custom/site/templates/default/detail.html (or list.html, partials.html). Never edit public/_site/templates/: those are core files.
Use it
Attributes
| Syntax | Does |
|---|---|
{{ key }} | Inserts a value as text, in text or in any attribute (href="/{{ contentTypeSlug }}"). Dots reach inside objects ({{ author.name }}) and lists ({{ tags.0 }}). A missing value prints nothing. See Syntax details. |
data-arc-bind="key" | Fills the element. On <img> it sets src, and alt to the data's title when there is one (replacing the alt you wrote). On <iframe> it sets src, on <a> href (the link text stays), on <input>, <textarea> and <select> the value. On <time>, datetime gets the value as it is and the text becomes a long date in the page's language, such as "January 5, 2026" (English in the app) (a value that is not a date is printed as it is). Any other element gets the value as its content: a value that looks like HTML is kept as HTML, anything else is text. With no value, the element keeps what you wrote. |
[innerHTML]="key" | Sets the element's HTML (used for the body: [innerHTML]="content"). Reads top-level keys only: [innerHTML]="author.bio" does not work. With no value, the element keeps what you wrote. |
[attr]="key" | Sets any attribute from a value: [src], [href], [title], [data-id]. Dots work. With no value, the attribute is not added. |
data-arc-if="key" | Removes the element when the value is missing, empty, 0 or false. Dots work (data-arc-if="author.name"). |
data-arc-loop="name" | Repeats the container's first child once per row; other children are dropped. An empty or unknown loop empties the container. |
data-limit="N" | On a loop container: at most N rows. 0, or anything that is not a number, means no limit. |
data-arc-style-background="key" | Adds background-color from the value and color: #333 to the element's style |
data-arc-t="key" | Replaces the element's content with that UI string, as plain text, in the page's language. The content you wrote stays when there is no translation. |
data-arc-t-attr="placeholder:key" | The same for an attribute. Several pairs are separated by commas: data-arc-t-attr="placeholder:search_hint, title:search_title". |
data-arc-t-params | A JSON object of values for {{ }} in the translated string, such as data-arc-t-params='{"site": "Acme"}'. A {{ }} it does not fill is left for the template's data. |
class="arc-skeleton" | A loading look in the app. The class is removed from an element once it is filled; see Syntax details. |
A template is filled in this order: UI strings (data-arc-t) first, then site info (data-arc-site, below), then loops, then the page's own bindings. So a translated string may itself hold a placeholder such as {{ contentType }}, and a loop's row is translated once, before it is repeated. UI string keys and translations are covered in Languages.
Site info
The site's own details, typed once in Settings, About, print on any page with data-arc-site: in the header, the footer, the home page, every template (list, detail and partials) and static pages, both on published pages and in the app. A change in About republishes every page that uses them.
| Syntax | Does |
|---|---|
data-arc-site="name" | The site name. description is the About description. |
data-arc-site="email" | The public contact email. On an <a> it also sets href to mailto:. |
data-arc-site="phone" | The public phone, as typed. On an <a> it also sets href to tel: with only the digits and a leading +. |
data-arc-site="address" | The address, its line breaks kept |
data-arc-site="logo" | On an <img>: the logo as src, and the site name as alt when you wrote none |
data-arc-site="year" | The current year, for a copyright line. Always has a value. |
data-arc-site-if="key" | Keeps the element only when the site has that value, so a label or an icon goes with it. Besides the keys above it takes social (any social link), pages (any published standard page) and contact (any email, phone, address or social link, for a "How to reach us" block). |
data-arc-site-loop="social" | Repeats the container's first child once per social link, in the order they were typed. Each row has {{ url }}, {{ platform }} (instagram, x, linkedin; link for a site it does not know), {{ label }} (Instagram; the host name for an unknown site) and {{ icon }} (Font Awesome classes). |
data-arc-site-loop="pages" | Repeats the first child once per published standard page (About, Contact, the policies), in their arranged order. Each row has {{ url }} and {{ title }}, titled in the page's language. |
An element bound to a value the site does not have is removed. The social links are the Social and profile links of About, one URL per line; the platform is read from each address. Known sites are Facebook, Instagram, X, LinkedIn, YouTube, GitHub, TikTok, Pinterest, Threads, WhatsApp, Telegram and Medium.
<p data-arc-site-if="phone"><i class="fa-solid fa-phone"></i> <a data-arc-site="phone"></a></p>
<ul data-arc-site-loop="social">
<li><a href="{{ url }}" aria-label="{{ label }}"><i class="{{ icon }}"></i></a></li>
</ul>
Syntax details
| Case | What happens |
|---|---|
| Characters in a key | Letters, digits, _, - and .. Spaces inside the braces are optional: {{title}} and {{ title }} are the same, and {{ events-price }} works. |
| Filters and expressions | None. {{ title | upper }}, {{ a + b }} and {{ title || 'Untitled' }} are printed exactly as written. Change a value with CSS (text-transform) or a script. |
| Numbers and flags | Printed as text: 0 prints "0", a flag prints "true" or "false". |
| Lists | Printed as their items joined by commas with no space: a detail page's {{ tags }} prints "News,Tips". Reach one item by its position from 0: {{ tags.0 }}; in a card, where tags are objects, {{ tags.0.name }}. |
| Objects | Print [object Object]. Name a field inside: {{ author.name }}, not {{ author }}. |
| Escaping | {{ }} in text and in attributes is escaped, so a title holding <b> shows those characters. Inside <script> and <style> it is inserted raw: var title = "{{ title }}"; breaks on a title with a quote mark, so read values from the page's HTML in a script instead. [innerHTML], and a data-arc-bind value that looks like HTML, are inserted as HTML, unescaped. |
data-arc-if | A yes or no test of one value. Missing, empty text, 0 and false remove the element; anything else keeps it, including a list with no rows and an object. There is no negation or comparison: data-arc-if="!coverImage" or data-arc-if="lang == 'hi'" is read as a key, finds nothing and always removes the element. To show something only when a value is empty, use a flag such as hasRelated or CSS such as .cards:empty + .empty-state. |
| Inside a loop row | {{ }} and data-arc-if see only the row's own keys: {{ title }} is the row's title, and a page key the row does not have prints nothing. A data-arc-bind the row cannot fill is filled afterwards from the page's data. |
| A loop inside a loop row | Not supported: the inner container is emptied. For a card's tags use tagsHtml or tagsDisplay. |
| The same loop name twice | Both containers get the same rows, built from the first container's row and its data-limit; the second container's own row is ignored. |
data-arc-t with an icon inside | A translation replaces everything inside the element with text, so an icon inside it is lost. Keep icons outside: <i class="fas fa-arrow-right"></i> <span data-arc-t="read_more">Read</span>. |
arc-skeleton | Removed from an element when {{ }} in its own text, [innerHTML] or data-arc-bind fills it. The app styles it, and every unfilled data-arc-bind, as a grey shimmer with hidden text. Published pages have no style for it. |
Special elements
| Element | On a published page | In the app |
|---|---|---|
<arc-header></arc-header> | Replaced with the site header, where the tag is | The app draws the header at the top of the page, wherever the tag is |
<arc-footer></arc-footer> | Replaced with the site footer, where the tag is | The app draws the footer at the bottom of the page |
<arc-search></arc-search> | Replaced with the search box (see Search) anywhere in the page; removed when there is none | Works in the header and the footer; does nothing in a template |
<arc-language-switcher></arc-language-switcher> | Replaced with links to the page's other languages. Removed when the page exists in fewer than two languages: a detail page exists in the languages it is translated into, a list page in every enabled language. | Works in the header and the footer; does nothing in a template |
<arc-content-partials> | Filled with cards on the home page (see Partials); removed from content pages | Filled on the home page; does nothing in a template |
<arc-admin-edit-button> | An old tag, always removed | Does nothing |
Put <arc-header> and <arc-footer> in list.html and detail.html, never in partials.html. Keep the search box and language switcher in the header or footer, so they work in the app too.
Detail page bindings
Everything detail.html can use outside a loop. Every other field saved on the entry is there too, under its own name.
| Binding | Type | Example | Notes |
|---|---|---|---|
title | Text | My first post | |
summary | Text | A short introduction | The entry's Summary field |
urlSlug, id | Text | my-first-post | The entry's slug and document id |
coverImage | URL | https://…/cover.jpg | Plus coverImage_s to coverImage_xl (see Image sizes) |
metaDescription, seoTitle, canonicalUrl | Text | Ten tips for… | The SEO fields as entered. canonicalUrl is empty unless the author pointed the page at another one; the page's own address is in share |
content | HTML | <p>…</p> | The body. Bind with [innerHTML]="content". |
publishedOn, date | Text | January 5, 2026 | A long date in the page's language |
updatedOn, updatedOnDisplay | Text | February 2, 2026 | The revision date, empty unless the editor marked a revision after publishing |
readTime | Number | 5 | Minutes to read |
readingTime | Text | 5 min read | On a translated page, the min_read string with {{ readTime }} filled in; English when that string is missing. |
contentType, cat | Text | Articles | The type's name in the page's language |
contentTypeSlug | Text | articles | |
author.name, author.bio, author.photoUrl, author.jobTitle, author.url | Text, URL | Asha Rao | From the author's profile (see Authors and tags). All empty when there is no profile: no author, or a deleted one. Show the author box with data-arc-if="author.name". |
authorName | Text | Asha Rao | The profile's name; without a profile, the name stored on the entry, so a byline survives a deleted author |
tags | List | News,Tips | The tag names. For colored tags use the tags loop below. |
share.facebook, share.twitter, share.linkedin, share.whatsapp, share.email | URL | https://wa.me/?text=… | Share links for the page |
nextContent.title, nextContent.slug, nextContent.id, nextContent.summary | Text | Part two | The next entry linked in the editor. Missing when none, so data-arc-if="nextContent" hides the link. previousContent.* is the same for the previous entry. |
nextContent.url, previousContent.url | Text | /articles/part-two | App only: empty on a published page (see the warning below) |
hasReferences, hasRelated | Flag | true | For data-arc-if around the sources and related blocks |
references, related | List | The rows of the loops below. Always set, so test the flags instead. | |
lang, langPrefix | Text | hi, /hi | The page's language and its URL prefix (empty for the default language) |
| A custom field's key | Depends on the field | events-price, price | By its stored key and by its short key (see Custom field bindings) |
ref_{key} | Object | {{ ref_events-venue.title }} | A collection reference field, by its stored key: the chosen entry's id and the fields copied with it (see Content) |
Loops on a detail page:
| Loop | Row bindings | Notes |
|---|---|---|
data-arc-loop="tags" | name (text), color (hex, such as #6b7280) | One row per tag; the grey #6b7280 for a tag with no color |
data-arc-loop="references" | title (text), url (URL) | The sources the editor cited. Only http and https links, each once. |
data-arc-loop="related" | title, url, snippet, badge (text) | Up to four related entries in the page's language, found by search; none without the search feature. In the app they arrive a moment after the page. |
| A repeating custom field's key | That field's rows (see Custom field bindings) | By stored key or short key, in the order the editor set |
Use href="{{ langPrefix }}/{{ contentTypeSlug }}/{{ nextContent.slug }}" for a next link. nextContent.url exists only when the app renders the page, so a published page built with it has an empty link.
List page bindings
Outside the loop, list.html has:
| Binding | Type | Example | Notes |
|---|---|---|---|
contentType | Text | Articles | The type's name, in the page's language |
contentTypeSlug | Text | articles | |
contentTypeDescription, description | Text | Notes from the team | The type's description, in the page's language |
lang, langPrefix | Text | hi, /hi | The page's language and its URL prefix |
Inside data-arc-loop="items", one row per published entry, newest first or in the type's own order (see Put entries in your own order), at most 100. A list's rows and a partial's cards have the same bindings:
| Binding | Type | Example | Notes |
|---|---|---|---|
title, urlSlug, id | Text | My first post | The entry's title, slug and document id |
url | Text | /articles/my-post, /hi/articles/my-post | Its address, in the page's language. Empty for a type without public pages, so guard a link with data-arc-if="url". |
coverImage | URL | https://…/cover.jpg | Plus its sizes (see Image sizes) |
excerpt | Text | Ten tips for a calmer… | The meta description, or else the body, cut to its first 25 words with "..." |
content | HTML | <p>…</p> | The full body |
publishedOn | Text | Jan 5, 2026 | A short date in the page's language |
readTime | Number | 5 | Minutes to read |
authorName | Text | Asha Rao | The name stored on the entry |
author | Text | Asha Rao | The same name as text, not an object: {{ author.name }} is empty in a card |
tags | List | Tag objects with name and color. A loop cannot run inside a row; use the next two. | |
tagsHtml | HTML | <span class="tag-pill"…> | The first three tags as colored pills. Bind with data-arc-bind="tagsHtml": {{ tagsHtml }} would print the markup as text. |
tagsDisplay | Text | News, Tips, Guides | The first three tag names |
contentType, cat | Text | Articles | The type's name in the page's language |
contentTypeSlug | Text | articles | |
| A custom field's key | Depends on the field | events-price, price | By stored key and short key, with the derived keys below. A repeating field cannot be looped inside a row. |
An entry with a translation shows it on that language's pages; one without shows its default-language text.
Custom field bindings
A custom field binds by its stored key (events-price) and by its short key, the name without the type slug (price), on detail pages and inside the items loop of lists and partials. So one template can serve several types. The short key never replaces a built-in: a field named "Title" does not shadow title. Some types add derived keys beside the stored value:
| Field type | Bindings (for a field keyed x) |
|---|---|
icon | x (class list, such as fa-solid fa-star), x_svg (inline SVG), x_label, x_name |
color | x (hex), x_rgb (rgb(26, 115, 232)), x_rgb_values (26, 115, 232, for rgba()) |
image and the cover image | x plus x_s, x_m, x_l, x_xl |
file | The first file, anywhere (lists too): x_url, x_title, x_type ("PDF"), x_size ("2.3 MB"), x_pages. Every file, as a loop on detail pages: rows url, title, type, kind (pdf, word, excel, powerpoint, csv or text, for a class name), size, bytes, pages. |
| Any YouTube URL value | x_id, x_embed (youtube-nocookie.com), x_thumb (poster image) |
infocard (loop) | Rows: headline, info, icon or image, icon_svg, position |
gallery (loop) | Rows: image or video (with video_id, video_embed, video_thumb), caption |
labelvalue (loop) | Rows: label, value. The heading is x_heading, outside the loop. |
faq (loop) | Rows: question, answer (plain text), answer_html (the answer with blank lines as paragraphs and web addresses as links; bind with data-arc-bind="answer_html"). The heading is x_heading. |
maplocation (loop) | Rows: label, address, lat, lng, map_embed (OpenStreetMap iframe URL), map_directions, map_view. The heading is x_heading. |
Repeating fields render only on detail pages, not inside the items loop of a list or partial. The default detail template already loops fields with the keys gallery, details, faq and locations, so fields with those keys render with no template work. It also loops info_cards, but a new field's key is made from its name with hyphens in place of spaces and underscores, so a new field cannot get that key: use a loop of your own, as below.
<section class="info-cards" data-arc-loop="info-cards">
<article class="info-card">
<img data-arc-if="image" data-arc-bind="image" alt="">
<i data-arc-if="icon" class="{{ icon }}" aria-hidden="true"></i>
<h3 data-arc-bind="headline">Headline</h3>
<p data-arc-bind="info">Text</p>
</article>
</section>
A file field keyed brochure, as one download button, or a list of every file:
<a data-arc-if="brochure_url" href="{{ brochure_url }}" class="btn">Download {{ brochure_title }} ({{ brochure_type }}, {{ brochure_size }})</a>
<ul data-arc-loop="brochure">
<li class="file-{{ kind }}"><a href="{{ url }}">{{ title }}</a> {{ type }}, {{ size }}</li>
</ul>
Files linked in rich text are cards already: one link, <a class="arc-file" data-arc-file="pdf"> holding .arc-file-title and .arc-file-meta. main.css draws them with the type's colour on the left edge; restyle .arc-file in site.css to change them.
Layouts
Every entry of a type uses the folder's detail.html. For a page that needs its own look, such as a Contact page laid out differently from About, add a layout: another detail file in the same folder, named detail-{name}.html.
Put the file in the type's folder, such as
src/custom/site/templates/info/detail-contact.html. The name afterdetail-uses lowercase letters, digits,-and_. It is a template likedetail.html, with the same bindings.In the editor, open the entry. The Basic tab now shows Layout: Standard (the folder's
detail.html) and one option per layout, named from the file (detail-wide-hero.htmlis Wide hero). The picker shows only for a type whose folder has a layout, so most editors never see it.Pick the layout and publish. The choice is shared by every language of the entry.
A layout replaces only the entry's detail page: the folder's list.html and partials.html are unchanged. An app adds or replaces a layout file by file, like the other files, so src/custom/site/templates/info/detail-contact.html replaces Arc CMS's Contact layout and leaves its detail.html in place. The content type's view page lists the folder's layouts beside the template row.
A layout cannot have fields of its own: fields belong to the type. Use the site's details through site info, and the type's ordinary fields, such as an Info Cards field that only the pages using that layout fill in; on the other pages it stays empty and shows nothing.
When the live site does not have the chosen layout yet, publishing uses the folder's detail.html instead and notes it in the function log, so the page stays online in its standard look. Deploy the website, then publish the page again. The app's preview falls back the same way.
Image sizes
Every upload is stored at four sizes (see Media). Any image binding has one sibling per size, so a template can use a size the editor did not pick:
<img src="{{ coverImage_m }}"
srcset="{{ coverImage_s }} 300w, {{ coverImage_m }} 600w, {{ coverImage_l }} 900w, {{ coverImage_xl }} 1200w"
sizes="(max-width: 600px) 100vw, 600px" alt="{{ title }}">
The widths above are for the default maximum of 1200px; they follow the maximum set in Settings, Misc. Unsplash photos are resized by Unsplash to the same limits; an image pasted from elsewhere gets none.
Partials
partials.html renders a block of cards (such as "Latest articles") on the home page, where it is written as an element:
<arc-content-partials content-type="articles" count="4" section-title="Latest articles"></arc-content-partials>
| Attribute | On a published page | In the app |
|---|---|---|
content-type | The type's slug. Required: with no type or an unknown one, the element is removed. A type without public pages works: its cards have no links. | articles when left out |
count | How many cards, a whole number from 1 to 50 (a larger number gives 50). Default 4, also for 0 or text that is not a number. | Default 4, from the 50 newest entries |
section-title | The heading, bound as sectionTitle. Default "Latest {type name}", or the page's latest_of_type string, such as "नवीनतम {{ contentType }}". | The same |
template-folder | Another folder's partials.html. Default: the type's folder, else the default folder. A folder the live site does not have stops the home page's publish. | The same. A file that is not a template shows nothing. |
Outside the loop, a partial has:
| Binding | Type | Example | Notes |
|---|---|---|---|
sectionTitle | Text | Latest Articles | The section-title attribute, or its default |
listUrl | Text | /articles, /hi/articles | The type's list page, in the page's language. Empty for a type without public pages, so guard a View all link with data-arc-if="listUrl". |
hasItems | Flag | true | data-arc-if="hasItems" hides the block when the type has nothing published |
contentType, contentTypeSlug, contentTypeDescription | Text | Articles | As on a list page |
lang, langPrefix | Text | hi, /hi | The page's language and its URL prefix |
Inside data-arc-loop="items", each card has the list item bindings, in the type's order. The default partials.html makes the whole card a link only when it has a url. A card shows its translation on a translated home page, in the app too. A type without a partials.html of its own uses the default folder's cards. A partial's <style> and <script> stay where they are in the home page; in the app they run once, after the cards are first drawn. The element works on the home page only; publishing removes it from content pages.
Header, footer and styles
| What | Edit | Notes |
|---|---|---|
| Header and footer | src/custom/site/header.html, src/custom/site/footer.html (else Arc CMS's public/_site/header.html, public/_site/footer.html) | Plain HTML: no {{ }} or other bindings, only data-arc-t and site info (data-arc-site). The default footer lists the published standard pages (data-arc-site-loop="pages") and a copyright line with the year and site name; its rules in main.css apply only to the site footer (footer.site-footer, or a <footer> directly in <body>, <arc-footer> or a template), never to a footer inside a card or dialog. The app renders them as they are, filling data-arc-t, site info and the language of links; both turn <arc-search> and <arc-language-switcher> into the search box and the language links. Their scripts run in the app too, each time they are drawn (again when the page's language arrives), so a script binds to the elements it finds; a script file is loaded once. Publishing reads the same files from the live Hosting site at /_site/. |
| Site stylesheets | src/custom/site/site.css, served as /assets/css/site.css | Loaded on the website's pages and the sign-in page, never in the admin or member area. Published pages link every URL in cssUrls of Settings/site (by default Bootstrap 5, Font Awesome 6.5.1 and /assets/css/main.css, Arc CMS's own styles, a core file), then /assets/css/site.css, both stylesheets with a version that changes with the file. There is no admin screen for the list; change it in the Firestore console. The app always loads Bootstrap and Font Awesome, whatever the list says. |
| The site's own files | src/custom/site/assets/, served at /site/ | Every one is listed with a hash in /_site/site.json. Published pages and the app's preview add it to links (versionSiteUrls in functions/src/shared/site-urls.ts and src/app/core/site/site-urls.ts: src, href, poster, srcset), and the build adds it to url() in the app's stylesheets (scripts/arc-site.mjs), so a changed file is never served from a year-long browser cache. |
| A template's own styles and scripts | <style> and <script> in the template | On a published list or detail page, every <style> moves into the head and every <script> to the end of the body. The app runs a list or detail template's scripts once it has drawn it; it never runs a partial's. |
Icons from an icon field need Font Awesome in cssUrls; without it, bind x_svg instead of the class list. How a published page differs from the app's preview is listed in Preview versus live.
Extend it
- A new look for one type: a new folder, as in Set it up.
- New attributes or bindings are a core change: the two renderers,
functions/src/shared/template-hydration.tsandsrc/app/core/services/template-hydration.service.ts, must stay the same. See Keep core generic.
What it stores
| Where | What |
|---|---|
src/custom/site/templates/, public/_site/templates/ | The app's template folders and Arc CMS's, deployed with Hosting at /_site/templates/ |
src/custom/site/header.html, footer.html | Header and footer, deployed at /_site/header.html and /_site/footer.html |
/_site/site.json | Written by the build: the template folders and the files in each, each folder's layouts (layouts), with a hash of every file |
ContentTypes/{id}, field templateFolder | Which folder a type uses (empty or default means the default folder) |
An entry's layout | The layout an entry uses, by name (contact for detail-contact.html); empty for detail.html |
Settings/site | The stylesheet list cssUrls |
Templates have no Cloud Functions of their own; the content feature's functions render them (see Content).
Who can do what
Template files are changed by whoever can edit the repository and deploy Hosting. Choosing a type's folder is an admin action in the content type screen. Nothing about templates is kept in Firestore: earlier versions also read a templates collection and a Settings/partials document, set by hand in the Firebase console, and Arc CMS no longer reads either. Settings/site is admin only. See Security rules.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| The folder is not in the Content Template list | The folder must sit directly in src/custom/site/templates/, have a name of lowercase letters, digits, hyphens and underscores that starts with a letter or digit, and hold at least one of partials.html, list.html, detail.html or a detail-{name}.html layout. Restart npm run dev if it was running, or rebuild. |
| Publishing fails with "Template folder 'x' is not on the live site. Deploy the website, then publish again." | The type names a folder the live site does not have yet, or its file there is not a template (a whole HTML document). The editor shows the message on the item. Deploy the website, then publish again. |
| The app logs "is not a template fragment" | A file of the folder is a whole HTML document, or missing (a missing file answers with the app's own page, which is rejected). For a list or detail file the app shows the default template instead; a partial shows nothing; a default folder file of your own leaves the page loading. Make the file a fragment. |
| A binding renders empty | Check the key against the tables on this page. The admin's Template Reference on a content type's view page lists the same bindings by page, and the type's own fields with the snippet each needs. A binding that works on a detail page may not exist in a card (summary, author.name), and repeating fields render on detail pages only. A value is also empty on a page built before the field was filled: publish again. |
| Placeholder text such as "Caption" shows on the page | An element with data-arc-bind whose value is missing keeps the text you wrote (in the app it shows as a grey shimmer). Loops do the opposite: they keep only the first child as the row and drop the rest. Use {{ }}, which prints nothing when the value is missing, or guard optional parts with data-arc-if. |
| Changes show locally but not on the live site | Deploy Hosting, then rebuild the pages (npm run seed:dev or npm run seed:prod). |
A data-arc-site element is missing from the page | The value is empty in Settings, About, so the element was removed. Fill it in; the pages that use it are republished. |
| A page shows the standard layout, not the one chosen | The live site does not have that layout file yet (the function log says "Layout 'x' of template folder 'y' is not on the live site"). Deploy the website, then publish the page again. In npm run dev, check the file's name: detail-, then lowercase letters, digits, - or _. |
| The editor has no Layout picker | The type's folder has no detail-{name}.html, or the app was not restarted after adding one. Restart npm run dev. |
| Icons show as empty boxes | Font Awesome is not in cssUrls. Add it back, or bind the _svg form. |