Content

Define content types (Articles, Events, Products), write entries in a rich editor, and publish them. Publishing turns each entry into a static HTML page on Firebase Hosting, with a list page per type.

What it is

Content has three layers:

LayerWhat it isExample
Content typeA kind of content: its name, URL slug, custom fields and template folderArticles at /articles
EntryOne item of a type: title, body, cover image, tags, author, SEO fields and the type's custom fields/articles/getting-started
PageThe static HTML that publishing writes to Hosting for an entry, plus one list page per type/articles/getting-started.html, /articles/index.html

Every entry has a draft and, once published, a published copy. You edit the draft; publishing copies it to the published copy and rebuilds the pages. The admin list shows each entry as Draft, Published or Edited (published, with changes since).

The standard pages (About, Contact, FAQ, Privacy Policy, Terms, Cookie Policy) are entries too, of a built-in type called Info Pages at /info, so admins edit them like any entry. See Standard pages.

Static pages are a separate, simpler thing: hand-written HTML files in src/custom/site/pages/ (Arc CMS ships sample privacy and cookie policies in public/_site/pages/). See Static pages.

Turn it on or off

Content is a switchable feature with the id content. It is on by default and needs no other feature. To turn it off, list it in src/custom/features.ts:

export const CUSTOM_FEATURES: FeatureChoice = {
    off: ['content'],
};

When it is off:

Firestore data is not deleted. See Feature ids for every id.

Set it up

The setup wizard creates Articles (slug articles, default template) and the Info Pages type with the standard pages as drafts (see Standard pages). To add your own type:

  1. Open Content > Content types (/admin/contents/content-types; /admin/contents on its own goes there too) and choose Add Content Type.

  2. Give it a plural name, a singular name and a slug. The slug is the URL (/events) and the Firestore collection names (arc_events, arc_events_drafts), so pick it once.

  3. Pick a Content Template (or keep Default Template). See Templates.

  4. Add custom fields (below) and save.

For planning types and fields for a website, see Define content types.

Content type settings

SettingWhat it does
Name, singular name, descriptionShown in the admin and on the public list page (heading and subtitle). Each can be translated per language.
SlugThe URL segment and the collection names. Changing it later hides every existing entry, which stays in the old collections.
IconThe Font Awesome icon beside the type in the admin menu
Content TemplateThe template folder whose partials.html, list.html and detail.html render this type (see Templates), plus any layouts an entry can pick (Layouts). With public pages off it is labelled Card template, and only its partials.html is used, for the type's cards on the home page.
Generate public pagesOff: entries are stored and published in Firestore, but no entry gets a page of its own: no HTML page, list page, sitemap entry, feed or search entry. The type can still show as cards on the home page, through its card template, with no links on them (see Build your home page). Use it for parts of a page that editors change, such as services, a hero or testimonials, and for data that only your app's own pages read. The slug is hidden while it is off.
Entry orderNewest first (the default) or Your own order, which admins set by dragging. The list page and the home page cards follow it; the sitemap, feeds and llms.txt stay newest first. See Put entries in your own order.
Searchable custom fieldsPlain text custom fields that search also indexes (see Search). Shown when you edit the type.
Structured dataWhich schema.org type the page's structured data uses, and which fields fill it (see SEO and discoverability). Shown when you add a type and when you edit it.

Which columns the entry list shows is chosen with Columns on the entry list itself, and saved on the type for every admin.

Custom field types

You type a field's name; its key is made from it and fixed once the type is saved, so renaming the field later changes only its label. The key is the slug, a hyphen, then the name in lowercase with hyphens: "Card Icon" on events is events-card-icon. Types made with older versions may have an underscore after the slug (events_card_icon). A template can use the full key or the short name (card-icon), on a detail page and inside the items loop of a list or card block alike. The short name never replaces a built-in: a field named "Title" does not shadow title.

The content type's view page has a Template Reference with keys to click and copy. Each field there comes with the snippet its type needs, the same as the In a template column below.

Field types reference

Every custom field value is stored in the entry's customFields map under the field's key. The examples use the short name of a field on an events type.

TypeThe editor showsStoredIn a template
textA one-line text boxThe text<p>{{ subtitle }}</p>
numberA number boxThe number as text, such as "25"<span>{{ price }}</span>. As it is text, data-arc-if hides only an empty value: 0 still shows.
richtextA rich editor, with Expand for a full-screen oneHTML<div [innerHTML]="agenda"></div> or <div data-arc-bind="agenda"></div>. {{ agenda }} shows the tags as text.
dateA date picker, for a day onlyYYYY-MM-DD, such as 2026-10-05. A value saved with an older version may include a time.{{ deadline }} prints the value as stored. <time data-arc-bind="deadline"></time> sets datetime and shows "October 5, 2026" in the page's language.
datetimeA date and time pickerYYYY-MM-DDTHH:mm, such as 2026-10-05T18:30{{ starts }} prints the value as stored. In <time data-arc-bind="starts"> it shows the date only, in the page's language.
booleanAn on and off switchtrue or false; nothing until it is first switched<span data-arc-if="featured">Featured</span>. {{ featured }} prints true or false.
dropdownA select list of the options you typed, separated by commasThe chosen option's text{{ level }} prints "Beginner"
radioOne radio button per optionThe chosen option's text{{ format }} prints "Online"
checkboxOne tick box per optionA list of the ticked options{{ topics }} prints them joined with commas and no space ("Design,Code"). The list cannot be looped, and data-arc-if counts an empty list as set.
dropdown, radio or checkbox with Use data from another collectionThe published entries of another content type, labelled by the chosen display fieldThe chosen entry's id (a list of ids for a checkbox), plus a copy of the fields ticked under Additional Fields to Save{{ speaker }} prints the id. On a detail page, {{ ref_events-speaker.title }} (always the full key) gives the copy; see Options from another content type.
imageA picker that opens the Media ManagerThe picked image's URL<img src="{{ photo_m }}" alt="">. photo is the image as picked; photo_s, photo_m, photo_l and photo_xl are its sizes (see Image sizes).
fileA picker that opens the Media Manager's Files (PDF, Word, Excel, PowerPoint, CSV, text). With Several files ticked: Add file picks several at once, and arrows set their order.A list of files, also for one: each { url, title, type, bytes, pages }<a href="{{ brochure_url }}">{{ brochure_title }}</a> for the first file, plus brochure_type ("PDF"), brochure_size ("2.3 MB") and brochure_pages, anywhere. Every file: a loop, data-arc-loop="brochure", with {{ url }}, {{ title }}, {{ type }}, {{ kind }} (pdf, for a class name), {{ size }} and {{ pages }} in each row (detail pages)
iconA picker that opens the Media Manager's iconsThe icon's class list, inline SVG, label and name<i class="{{ card-icon }}" aria-hidden="true"></i>, plus card-icon_svg, card-icon_label and card-icon_name
colorA color picker beside a box for the hex codeA hex color, such as #1a73e8<h2 style="color: {{ brand }}">, plus brand_rgb and brand_rgb_values, or data-arc-style-background="brand"
infocardRepeating cards, each an icon or image, a headline and a short textA list of rowsA loop: data-arc-loop="info-cards", with {{ headline }} in each row
galleryRepeating photos or YouTube videos with captions; Add photos picks several at onceA list of rowsA loop: data-arc-loop="gallery", with {{ caption }} in each row
labelvalueRepeating label and value pairs side by side, under a section headingA list of rows; the heading beside it, under the key plus _heading{{ details_heading }}, then a loop: data-arc-loop="details", with {{ label }} and {{ value }}
faqRepeating questions, each a question and a plain text answer, under a section headingA list of rows; the heading under the key plus _heading{{ faq_heading }}, then a loop: data-arc-loop="faq", with {{ question }} and <div data-arc-bind="answer_html"> in each row. A page with FAQ rows also gets FAQPage structured data (see SEO and discoverability).
maplocationRepeating places, each a name and an address found on a map with a pin you can drag, under a section headingA list of rows with the address and its coordinates; the heading under the key plus _heading{{ locations_heading }}, then a loop: data-arc-loop="locations", with <iframe data-arc-bind="map_embed"> in each row

The five repeating types (infocard, gallery, labelvalue, faq, maplocation) render only on detail pages, not inside the items loop of a list or card block. Every key their rows carry is listed under Custom field bindings.

Options from another content type

A dropdown, radio or checkbox field with Use data from another collection on offers the published entries of another content type:

Field settings

In a second language the editor translates only prose: text and richtext fields, the headline and text of an infocard, a gallery caption, the heading, labels and values of a labelvalue, the heading, questions and answers of a faq, and the heading, names and addresses of a maplocation. Every other field, including options, dates, images, files, icons and the rows' order and pictures, is shared by all languages and edited on the default language's tab.

Use it

Write and publish an entry

  1. Open Content > {your type} and choose Add {type}, such as Add Article.

  2. Write the title and body in the rich editor (TipTap: headings, lists, tables, task lists, links, images from the media library (or straight from a PDF's pages, see From a PDF), YouTube embeds, text color and alignment). An image dropped or pasted into the body is uploaded to the media library first and goes in at the M size, with a progress box while it uploads; a PDF or other document goes to Files and in as a file card (a link showing its title, type and size); see Drop or paste into the editor. Attach files in the / menu links files already in the library. Fill in the cover image, summary, tags, author, SEO fields and custom fields. When the type's template folder has layouts, Layout picks the one this entry uses (see Layouts).

  3. The editor saves the draft as you work. Use Preview to see the draft rendered on the site. Drag the dotted grip in the gap between the writing area and the settings panel to make the panel wider or narrower; the editor remembers the width for each content type on this computer, and a double-click puts it back.

  4. Choose Publish. The editor shows the deploy status of this publish (Deploying to hosting..., then Deployed to hosting or Deploy failed, with the reason when you point at it) and offers a retry if the release failed. A page whose template folder is not on the live site yet fails with "Template folder 'x' is not on the live site. Deploy the website, then publish again." In npm run dev, Live site differs beside Publish means your site files are not the live ones yet, so the page would be built from the old ones: deploy the website first. Click it for the whole message. On a phone it is only the warning icon; tap it for the message.

Other actions:

ActionWhereResult
Publish againEditorThe published copy and pages are rebuilt from the current draft
UnpublishEntry listThe published copy and its pages are removed; the draft stays
DeleteEntry listThe draft, the published copy with its translations, and its pages are removed
Version historyEditor, History tabEvery publish is kept. Preview an old version and restore it into the editor, then publish.
Bulk ImportEntry listCreate many entries from an Excel or CSV file (below)
ArrangeEntry list, for a type in your own orderDrag entries into the order the site shows them (below)

There is no scheduled publishing: an entry goes live when you publish it. An entry that still holds outline text starting with [Replace: (the standard pages start with it) asks before it is published.

Put entries in your own order

Newest first suits articles and news. For services, team members or FAQs you choose the order:

  1. Add or edit the type and set Entry order to Your own order. The entries keep the order they had, newest first, until you change it.

  2. On the type's entry list, choose Arrange (it shows only for a type in your own order).

  3. Drag the entries into place, or use the arrows, and save. Drafts are listed too and keep their place for when they are published.

Saving republishes the type's list page and, when it shows the type, the home page, in one release. Moving an entry does not mark it Edited. A new entry goes after the arranged ones until you arrange it. Above 200 entries the dialog reminds you that the list page shows the first 100.

Standard pages

Every site needs an About page, a way to get in touch and its policies. The setup wizard creates a content type called Info Pages (slug info, template folder info, your own order) with six drafts, in this order:

PageAddressStarts with
About/info/aboutWho you are, what you do, why
Contact/info/contactThe Contact layout (below); a line on how to reach you; an Opening hours info box; Show contact details and Show contact form on
Frequently asked questions/info/faqThree FAQ rows
Privacy Policy/info/privacy-policyAn outline of the usual sections, with your name, email and address from Settings, About
Terms/info/termsAn outline of the usual sections
Cookie Policy/info/cookie-policyWhat cookies are and the visitor's choices, with a prompt to list the cookies your site uses

Every prompt to fill in starts with [Replace:, and the editor asks before publishing a page that still has one. The outlines are not legal advice: have the policies checked for where you work.

The Info Pages type has five fields that each page turns on as it needs: Locations (a map), FAQ, Show contact details (the email, phone, address and social links from Settings, About), Show contact form (only with the contact feature on, see Contact form) and Info boxes (an Info Cards field for the Contact layout's own boxes, such as opening hours). The info template folder shows each section only when it is set.

The Contact page starts in the Contact layout (info/detail-contact.html, see Layouts), in three rows:

  1. The title and the body, full width.
  2. For each place in Locations: the map on the left, its name, address and directions on the right.
  3. On the left, boxes: the email and phone from Settings, About (with Show contact details on), the social links, then the page's Info boxes. On the right, the contact form.

A row with nothing to show is left out, and on a phone everything stacks in one column. The other pages use the standard info/detail.html; any of them can switch to the Contact layout, and Contact back to Standard, with Layout in the editor.

Once published, the pages are linked from the default footer in their arranged order, and the signup terms notice, the contact form's privacy line and the cookie banner link to /info/terms, /info/privacy-policy and /info/cookie-policy. Publishing or unpublishing a page, changing its title or arranging the pages republishes the whole site, so every footer agrees.

An install from before the standard pages existed adds them with Add standard pages on Content > Content types. It only adds what is missing, so it is safe to use again, and it adds nothing when a type of your own already uses the slug info. On an Info Pages type made by an earlier version it also adds the standard fields the type lacks (Info boxes), after your own fields; the button shows until the type has them. It never changes a page that exists, so an existing Contact page keeps its look until you pick Layout: Contact in the editor. A type created before the name Info Pages keeps its own name, Pages; rename it under Content types if you like. Delete a page you do not need, or leave it unpublished.

How publishing works

  1. The editor saves the draft to arc_{slug}_drafts and writes a queue item (publish, unpublish or delete) to _publish_queue.

  2. The processPublishQueue function (functions/src/publishQueue/processPublishQueue.ts) copies the draft to arc_{slug} with publishedOn set to the time of publishing, adds a version to PublishedHistory, copies translations and updates the search index.

  3. It renders the detail page in every language the entry has, the Markdown twin, and the type's list page (and the home page when it shows the type), from the type's template (see Templates). With the seo feature on, it also rebuilds the sitemap, feeds, robots.txt and llms.txt.

  4. All files go out as one Firebase Hosting release, through the Hosting REST API. The result is written to the published entry (deployStatus, deployError), which the editor shows. When the entry's own page cannot be built, the rest of the release (list page, sitemap, feeds) still goes out, and the entry is marked failed with the reason rather than deployed.

PageFile on Hosting
Entry, default language/{type}/{slug}.html and /{type}/{slug}.md
Entry, other language/{lang}/{type}/{slug}.html and /{lang}/{type}/{slug}.md
List page/{type}/index.html (and /{lang}/{type}/index.html per enabled language)

Hosting serves these files directly. The app's routes /{type} and /{type}/{slug} (src/app/pages/[contentTypeSlug]/) are the fallback: they render the same template in the browser for anything not yet on Hosting, for previews, and when you run the app locally.

To rebuild every published page, the static pages and the SEO files at once, run the seed (it also runs at the end of npm run deploy:dev and npm run deploy:prod):

npm run seed:dev

For production: npm run seed:prod.

A plain firebase deploy --only hosting replaces the Hosting release with only the files it uploaded, so every page publishing wrote disappears, and the app's fallback still answers those addresses, so nothing looks broken at first. Deploy the website with npm run deploy, which keeps them. After a plain deploy, run npm run seed:dev (or seed:prod) to put the pages back. See Deploy.

Bulk import entries

On a type's entry list, Bulk Import reads the first sheet of an .xlsx or .csv file. It maps columns by header to Title (required), Content, Summary, URL Slug, Tags, Cover Image URL, SEO Title, Meta Description, Canonical URL, Featured, Author and the type's custom fields, and offers a sample file to download. You import as drafts or publish straight away. An author named in the Author column is matched by name, or created. Repeating fields (info cards, gallery, label and value, FAQ, map location) cannot be imported.

For moving whole collections between projects, see Data import and export.

Static pages

A static page is an HTML file in src/custom/site/pages/, such as src/custom/site/pages/privacy-policy.html, served at /p/privacy-policy. One with the same name as an Arc CMS sample in public/_site/pages/ replaces it. It can carry <arc-header> and <arc-footer>, a <title> and meta tags.

The walkthrough is Write static pages.

Extend it

What it stores

WhereWhat
ContentTypes/{id}Type definitions: name, slug, fields, template folder, settings (including entryOrder, and standard: 'pages' on the Info Pages type), translations of names and field labels
arc_{slug}_drafts/{id}Drafts, with lastPublishedAt once published, and sortOrder in a type in your own order (publishing copies it). Subcollection translations/{lang}.
arc_{slug}/{id}Published copies, with publishedOn and the deploy status fields. Subcollections translations/{lang}, PublishedHistory (one document per publish) and DeploymentLogs.
Tags_{slug}/{id}The type's tags (see Authors and tags)
_publish_queue/{id}Queue items, deleted once processed
HostingThe generated pages listed under How publishing works

An entry stores its custom field values in the customFields map, keyed by field key. Full field lists are in Data model.

Cloud FunctionKindDoes
processPublishQueueFirestore trigger on _publish_queuePublishes, updates, unpublishes and deletes entries and their pages, and applies a new entry order
onSiteSettingsWrittenFirestore trigger on SettingsQueues a home page republish when about, site, localization or misc changes, or a whole site republish when an about change shows on other pages through data-arc-site
onContentTypeDeletedFirestore trigger on ContentTypesDeletes the type's draft, published and tag collections, with each entry's translations, version history and deploy logs, and their search entries, unless another type still uses the same slug
seedStaticPagesCallable, also run by the seed scriptRebuilds every published page, the static pages and the SEO files

All deploy with the arccms- prefix. See Cloud Functions.

Who can do what

DataReadWrite
ContentTypes, arc_{slug}, Tags_{slug}AnyoneAdmins (and the reserved editor role)
arc_{slug}_drafts and their translationsAdmins and editorsAdmins and editors
PublishedHistoryAdminsCloud Functions only
_publish_queueNobody (functions only)Admins and editors

The Content admin screens are admin only. See Roles and claims.

Troubleshooting

SymptomCause and fix
Published, but the page is not on the siteCheck the deploy status in the editor. Website publishing off means the install publishes nothing to Hosting (ARC_HOSTING_SITE=none). On Deploy failed, hover it for the error and read the processPublishQueue logs; the retry button beside it re-queues the publish. If processPublishQueue is not deployed, nothing happens at all.
All generated pages vanished after a deployA hosting-only deploy dropped them. Run npm run seed:dev or npm run seed:prod.
Preview on the live site shows the published versionHosting serves the static file for a published address before the app loads, whatever the query string. Preview edits to a published entry from a local run (Run it locally).
An older entry is missing from the list pageThe generated list page holds the 100 newest published entries (the first 100 in your own order).
A new entry shows last, not firstThe type is in your own order, where a new entry goes after the arranged ones. Use Arrange.
Add standard pages says the address /info is takenA type of your own already has the slug info. Nothing was added. Rename your type's slug, or build those pages in your own type.
A deleted content type's pages are still onlineDeleting a type deletes its Firestore data, not its files on Hosting. Unpublish its entries first, then delete the type.
"URL slug already exists"Slugs are unique within a type. Change the slug in the editor.
"Slug already exists" when saving a content typeAnother content type uses that slug, and two types with one slug would share their entries. Choose another slug.

Deleting a content type deletes every entry of that type, draft and published, and its tags. It cannot be undone, and the confirmation says how many entries go. Export the type first (Data import and export). The one exception is a type that shares its slug with another type, possible only on a site set up before slugs were checked: the entries stay for the other type.