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:
| Layer | What it is | Example |
|---|---|---|
| Content type | A kind of content: its name, URL slug, custom fields and template folder | Articles at /articles |
| Entry | One item of a type: title, body, cover image, tags, author, SEO fields and the type's custom fields | /articles/getting-started |
| Page | The 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:
- The Content admin menu (content types, entries, authors) goes, and
/admin/contents/...,/admin/authorsand/p/...show the not-found page. - The public content routes (
/{type},/{type}/{slug}and their language-prefixed twins) are not registered. - The Cloud Functions in
functions/src/features/content.tsare not exported, so a full functions deploy deletes them:processPublishQueue,onSiteSettingsWritten,onContentTypeDeletedandseedStaticPages. The draft search queue, which needs both content and search, goes too. - The setup wizard does not create the Articles type or the standard pages, and data export offers no content collections.
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:
Open Content > Content types (
/admin/contents/content-types;/admin/contentson its own goes there too) and choose Add Content Type.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.Pick a Content Template (or keep Default Template). See Templates.
Add custom fields (below) and save.
For planning types and fields for a website, see Define content types.
Content type settings
| Setting | What it does |
|---|---|
| Name, singular name, description | Shown in the admin and on the public list page (heading and subtitle). Each can be translated per language. |
| Slug | The URL segment and the collection names. Changing it later hides every existing entry, which stays in the old collections. |
| Icon | The Font Awesome icon beside the type in the admin menu |
| Content Template | The 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 pages | Off: 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 order | Newest 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 fields | Plain text custom fields that search also indexes (see Search). Shown when you edit the type. |
| Structured data | Which 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.
| Type | The editor shows | Stored | In a template |
|---|---|---|---|
text | A one-line text box | The text | <p>{{ subtitle }}</p> |
number | A number box | The number as text, such as "25" | <span>{{ price }}</span>. As it is text, data-arc-if hides only an empty value: 0 still shows. |
richtext | A rich editor, with Expand for a full-screen one | HTML | <div [innerHTML]="agenda"></div> or <div data-arc-bind="agenda"></div>. {{ agenda }} shows the tags as text. |
date | A date picker, for a day only | YYYY-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. |
datetime | A date and time picker | YYYY-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. |
boolean | An on and off switch | true or false; nothing until it is first switched | <span data-arc-if="featured">Featured</span>. {{ featured }} prints true or false. |
dropdown | A select list of the options you typed, separated by commas | The chosen option's text | {{ level }} prints "Beginner" |
radio | One radio button per option | The chosen option's text | {{ format }} prints "Online" |
checkbox | One tick box per option | A 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 collection | The published entries of another content type, labelled by the chosen display field | The 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. |
image | A picker that opens the Media Manager | The 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). |
file | A 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) |
icon | A picker that opens the Media Manager's icons | The 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 |
color | A color picker beside a box for the hex code | A hex color, such as #1a73e8 | <h2 style="color: {{ brand }}">, plus brand_rgb and brand_rgb_values, or data-arc-style-background="brand" |
infocard | Repeating cards, each an icon or image, a headline and a short text | A list of rows | A loop: data-arc-loop="info-cards", with {{ headline }} in each row |
gallery | Repeating photos or YouTube videos with captions; Add photos picks several at once | A list of rows | A loop: data-arc-loop="gallery", with {{ caption }} in each row |
labelvalue | Repeating label and value pairs side by side, under a section heading | A 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 }} |
faq | Repeating questions, each a question and a plain text answer, under a section heading | A 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). |
maplocation | Repeating places, each a name and an address found on a map with a pin you can drag, under a section heading | A 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:
- It stores the chosen entry's id, so
{{ key }}prints the id, not the title. - The id, the display field and the fields ticked under Additional Fields to Save are copied onto the entry. On a detail page the copy is
ref_plus the full key:{{ ref_events-speaker.title }},ref_events-speaker.urlSlug,ref_events-speaker.coverImage. For a checkbox the copy is a list, which{{ }}cannot print. - Any field of the other type, built-in or custom, can be the display field or a copied field.
- Only published entries are offered, so every link has a page to go to.
- Publishing rebuilds the copy from the linked entry as it is published at that moment, so a page shows the entry's current title and fields. When the linked entry changes later, publish the page again (or use Republish website). A linked entry that is no longer published is left out, so wrap its block in
data-arc-if="ref_events-speaker".
Field settings
- Required is checked only when you press Publish, on the default language. Save as draft, the editor's own saving and Preview accept an empty field.
- Type can be changed after the type is saved, but values already stored are not converted. A text value under a field changed to
numberorimagestays the same text, so check the entries that have one. - Deleting a field takes it out of the editor, but its values stay in each entry's
customFields, and a template that names the key still shows them. - The key is locked once the type is saved, because entries refer to it.
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
Open Content > {your type} and choose Add {type}, such as Add Article.
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).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.
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:
| Action | Where | Result |
|---|---|---|
| Publish again | Editor | The published copy and pages are rebuilt from the current draft |
| Unpublish | Entry list | The published copy and its pages are removed; the draft stays |
| Delete | Entry list | The draft, the published copy with its translations, and its pages are removed |
| Version history | Editor, History tab | Every publish is kept. Preview an old version and restore it into the editor, then publish. |
| Bulk Import | Entry list | Create many entries from an Excel or CSV file (below) |
| Arrange | Entry list, for a type in your own order | Drag 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:
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.
On the type's entry list, choose Arrange (it shows only for a type in your own order).
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:
| Page | Address | Starts with |
|---|---|---|
| About | /info/about | Who you are, what you do, why |
| Contact | /info/contact | The 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/faq | Three FAQ rows |
| Privacy Policy | /info/privacy-policy | An outline of the usual sections, with your name, email and address from Settings, About |
| Terms | /info/terms | An outline of the usual sections |
| Cookie Policy | /info/cookie-policy | What 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:
- The title and the body, full width.
- For each place in Locations: the map on the left, its name, address and directions on the right.
- 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
The editor saves the draft to
arc_{slug}_draftsand writes a queue item (publish,unpublishordelete) to_publish_queue.The
processPublishQueuefunction (functions/src/publishQueue/processPublishQueue.ts) copies the draft toarc_{slug}withpublishedOnset to the time of publishing, adds a version toPublishedHistory, copies translations and updates the search index.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
seofeature on, it also rebuilds the sitemap, feeds,robots.txtandllms.txt.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.
| Page | File 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.
- In the app,
/p/{name}fetches/pages/{name}.html, adds the site header and footer, and applies the title and meta tags. - On Hosting,
/p/{name}redirects (301) to/pages/{name}. - The seed builds a finished copy, with header, footer and site stylesheets, at
/pages/{name}/index.htmlforprivacy-policyandcookie-policy. That list is fixed infunctions/src/pages/seedStaticPages.ts.
The walkthrough is Write static pages.
Extend it
- Change how pages look: give the type its own template folder. See Templates.
- Read content in your own app code: published entries are in
arc_{slug}and readable by anyone, so a page insrc/custom/can query them directly. A type with Generate public pages off is the way to keep structured data for an app without generating pages. See Add pages and routes. - React to publishing in your own function: put a trigger on
arc_{slug}/{id}infunctions/src/custom/. See Write your own Cloud Functions. - New field types and changes to the publish pipeline are core changes. See Keep core generic.
What it stores
| Where | What |
|---|---|
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 |
| Hosting | The 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 Function | Kind | Does |
|---|---|---|
processPublishQueue | Firestore trigger on _publish_queue | Publishes, updates, unpublishes and deletes entries and their pages, and applies a new entry order |
onSiteSettingsWritten | Firestore trigger on Settings | Queues 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 |
onContentTypeDeleted | Firestore trigger on ContentTypes | Deletes 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 |
seedStaticPages | Callable, also run by the seed script | Rebuilds every published page, the static pages and the SEO files |
All deploy with the arccms- prefix. See Cloud Functions.
Who can do what
| Data | Read | Write |
|---|---|---|
ContentTypes, arc_{slug}, Tags_{slug} | Anyone | Admins (and the reserved editor role) |
arc_{slug}_drafts and their translations | Admins and editors | Admins and editors |
PublishedHistory | Admins | Cloud Functions only |
_publish_queue | Nobody (functions only) | Admins and editors |
The Content admin screens are admin only. See Roles and claims.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Published, but the page is not on the site | Check 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 deploy | A hosting-only deploy dropped them. Run npm run seed:dev or npm run seed:prod. |
| Preview on the live site shows the published version | Hosting 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 page | The generated list page holds the 100 newest published entries (the first 100 in your own order). |
| A new entry shows last, not first | The 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 taken | A 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 online | Deleting 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 type | Another 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.