SEO and discoverability
Every published page tells search engines and AI assistants what it is, who wrote it and where its sources are, and the site publishes the files crawlers look for. Authors get a Checks tab that scores each draft for being found and quoted.
What it is
The publish pipeline writes these with every published page. Nothing has to be switched on. The home page gets the head tags and the site's structured data too, without overwriting the ones it sets itself (see Build your home page).
| Part | What it gives |
|---|---|
| Head tags | Title, meta description, canonical URL, Open Graph and Twitter card tags, and hreflang links when the page exists in more than one language. |
| Structured data | schema.org JSON-LD: the site owner (Organization or Person), the WebSite with a search action, a breadcrumb trail, and the page itself as an Article or the type its content type is mapped to. FAQ, How-to and Definition blocks in the body become their own nodes, and so do the rows of an FAQ custom field. |
| Authors and sources | A byline with the author's box, a Sources list at the end, both sent to search engines as the author and citations. |
| Markdown twins | The same page as Markdown, at the same address with .md (/articles/my-post.md), linked from the HTML head. AI tools read it at a fraction of the tokens. |
| Related items | Up to four related pages at the end of each page, chosen from the search index by title and tags. Needs Search. |
| Checks tab | A score out of 100 for each draft, with the one thing to fix per failing check. Advice only: it never blocks publishing. |
The seo feature adds the site-wide files and the settings for them:
| File | What it holds |
|---|---|
/sitemap.xml | The home page, each content type's list page, every published page in every language it exists in (with hreflang alternates), and every static page (Arc CMS's privacy and cookie policies and the app's own, see Write static pages). lastmod is the updated date, else the publish date. |
/robots.txt | Everything allowed except /_site/ (the site's source files: templates, header, page sources), a Disallow: / group for each AI crawler switched off, and the sitemap address. |
/llms.txt | A Markdown index of the site (llmstxt.org): the site name and description, then a section per content type linking its pages' Markdown twins in the default language, newest first, up to 500 pages in all, then links to the home page and the sitemap. |
/llms-full.txt | Every default-language Markdown twin in one file, newest first, up to 2 MB. |
/{type}/feed.xml | An RSS 2.0 feed per content type with its 20 newest items. |
/{key}.txt | The IndexNow key file, which proves the site to the engines. |
After each publish, IndexNow tells Bing (which feeds ChatGPT search and Copilot), Yandex, Seznam and Naver which pages changed. Google does not take part; the sitemap covers it. For a website walkthrough, see Get found by search engines and AI.
Turn it on or off
The site-wide files are the switchable feature seo. 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: ['seo'],
};
When it is off, Admin, Settings, Discoverability goes, the regenerateSeoFiles function goes at the next deploy of the whole functions codebase, and neither publishing nor npm run seed writes the sitemap, robots.txt, llms.txt, llms-full.txt, feeds or IndexNow key, and nothing is pinged. Files an earlier publish or seed wrote stay on Hosting until the next website deploy: with seo off it leaves them out of the release, which removes them. So after turning it off, deploy the website once (npm run deploy -- --only hosting --project <alias>), then check that /robots.txt and /sitemap.xml no longer show the old files. Head tags, structured data, Markdown twins, authors, sources and the Checks tab are part of Content and stay. See Feature ids.
Set it up
Say who owns the site. In Admin, Settings, About, fill in the site name, the production URL, whether the site is an organization or a person, the logo, a description, the Social and profile links (one URL per line), a contact email and a public phone. They become the Organization or Person node on every page (the phone as
telephone, the links assameAs), and the same details can be printed on the site itself (see Site info), and the production URL is the base of every absolute link in the files above. Empty fields are left out, never guessed.Add authors. Content, Authors. Mark one as the default and new content gets that author. See Authors and tags.
Map content types (optional). Add or edit a content type and choose its Structured data type: Article (the default), Blog post, News article, Web page, Product, Service, Event, Person, Recipe or How-to guide. Web page suits pages that are not articles, such as About or a policy; the standard pages use it. Map the type's properties to your custom fields; anything unmapped is left out.
Choose the crawler policy. Admin, Settings, Discoverability. Every crawler is allowed until you switch it off. Choose whether to publish
/llms.txtand/llms-full.txtand whether to use IndexNow (both on by default). Press Save and apply to site to write the files now, or Save to let the next publish write them.Verify from the outside. Arc CMS keeps no crawler log, so register the site in Google Search Console and Bing Webmaster Tools (which also shows IndexNow submissions).
The crawlers on the settings page come in two groups:
| Group | Crawlers | Switching one off |
|---|---|---|
| Search and answer bots | OAI-SearchBot, ChatGPT-User, Claude-SearchBot, Claude-User, PerplexityBot, Perplexity-User, Bingbot, Googlebot, DuckAssistBot | Removes the site from those answers and results. |
| Training bots | GPTBot, ClaudeBot, Google-Extended, Applebot-Extended, CCBot, meta-externalagent, Bytespider, Amazonbot | A policy choice. It does not change whether assistants cite the site. |
Use it
When the files change
Each publish or unpublish of a content type with public pages rewrites the sitemap, the feeds, robots.txt, llms.txt and (with IndexNow on) the IndexNow key file, and releases them to Hosting together with the pages. IndexNow is pinged only after that release succeeds. Save and apply to site rewrites robots.txt, llms.txt and the key file at once, without a publish.
In the editor
| Where | What |
|---|---|
| Basic tab | Author: the byline and the schema.org author. Cover image: the social and search thumbnail. |
| SEO tab | SEO title, meta description, Sources (the pages this piece draws on), Mark as updated today, which sets the "Last updated" date shown on the page and sent as the modified date, and Canonical URL, empty unless the original is on another page. An empty canonical, and one on a local address such as localhost, is published as the page's own address on the Production URL. |
| Body toolbar | The block menu inserts an FAQ, Key takeaways, How-to steps or Definition block; typing / offers them too. Each becomes structured data. Key takeaways become the page's abstract. |
| Checks tab | The score, each check, and pages on this site the body does not link to yet, each with Insert link. |
A content type with an FAQ custom field (see Field types reference) adds a FAQPage node from the rows of its first FAQ field that have both a question and an answer. Since 2023 Google shows FAQ results only for some government and health sites, so do not expect them in search; the node is still valid, and AI assistants read it.
What the Checks tab scores
High checks weigh 3, medium 2, low 1. The score is the weight passed out of the weight checked. It reads the default language.
| Check | Passes when | Weight |
|---|---|---|
| Answer first | The first paragraph has 15 to 70 words. | High |
| Question heading | At least one h2, h3 or h4 ends with a question mark. | High |
| Blocks | The body has an FAQ or Key takeaways block. | High |
| Title length | The title has 1 to 70 characters. | Medium |
| Meta description | 50 to 160 characters. | Medium |
| List or table | The body has one. | Medium |
| Internal links | Three or more links to this site. | Medium |
| Author | An author is picked. | Medium |
| Sources | At least one source with an http or https address. | Medium |
| Length | 300 words or more. | Low |
| Cover image | One is set. | Low |
| Tags | Two or more. | Low |
| Freshness | Published or marked updated in the last 12 months. | Low |
Write for Google and AI assistants
Assistants quote passages, not pages. A passage gets quoted when it answers a question on its own, sits under a heading that matches how people ask, holds something checkable, and comes from a page with a named author, a date and sources. Google rewards the same things. Share this with the people who write:
- Answer first. The first paragraph under the title, and under each heading, answers in 15 to 70 words. Detail comes after.
- Write headings as questions. "How much does it cost in India?" beats "Pricing".
- Use facts, not adjectives. "Supports 12 languages including Hindi and Tamil" can be quoted; "world-class support" cannot.
- Give it structure. A list for steps, a table for comparisons, an FAQ block for the questions you keep getting.
- Say who wrote it and when. Pick an author. Press Mark as updated today only after a real revision, never for a typo fix.
- Cite sources. Add the pages you drew on under Sources.
- Link to your own pages. Three or more; use the suggestions in the Checks tab.
Do not stuff keywords, rewrite what is already everywhere on the web, mark a page updated without changing it, or write an FAQ of questions nobody asks. Aim for a score above 80, then publish. A day later, test the page with Google's Rich Results Test, and ask an assistant your heading's question to see whether it quotes you. If it does not, sharpen the opening paragraph.
AI assistant visits
When Google Analytics is connected, the dashboard counts visits referred by AI assistants such as ChatGPT and Perplexity. See Analytics.
Extend it
An app extends discoverability through its own content and templates:
- Structured data for your content: map a content type to a schema.org type and its custom fields in the type editor (above). No code.
- Templates: a custom detail template can show the author box, the "Last updated" date, the Sources list and Related items with the same bindings as
public/_site/templates/default/detail.html. See Detail page bindings.
New schema.org types, content blocks, crawlers, AI referrers and checks are changes to Arc CMS itself, not to an app (see Keep core generic). Types, blocks and crawlers live in a functions file with a browser copy; change both, since a static page and its in-app twin must agree (a test compares the two lists for types and crawlers):
| To add | Change |
|---|---|
| A schema.org type | functions/src/shared/schema-types.ts and its copy src/shared/constants/schema-types.ts; build the node in functions/src/shared/schema-mapping.ts and src/shared/utils/schema-mapping.ts. The type editor lists it by itself. |
| A content block | ARC_BLOCKS in src/shared/components/tiptap-editor/service/arc-block-extension.ts; read it in functions/src/shared/content-blocks.ts and src/shared/utils/content-blocks.ts; style it in the default template. |
| A crawler | CRAWLERS in functions/src/shared/crawlers.ts and src/shared/constants/crawlers.ts, in the search or training group. Existing sites get it allowed. |
| An AI referrer | AI_REFERRERS in functions/src/shared/ai-referrers.ts. |
| A check | A rule() in src/shared/utils/discoverability-checklist.ts, and the keys admin.contents.checklist.{id} and admin.contents.checklist.{id}_fix in every admin translation file, then npm run i18n:keys. |
The pipeline never invents a value: a property that cannot be filled truthfully is left out. Keep new types and blocks that way.
What it stores
| Where | What |
|---|---|
Settings/discoverability | The default author id, the crawler policy (allowed or not, by crawler id), whether llms.txt is on, and IndexNow on or off with its key (made on first use). |
Settings/about | The site's identity for the Organization or Person node. |
Authors | Name, bio, photo, job title and profile links. Nothing private. |
| Each content document | authorId, references, updatedOn, seoTitle, metaDescription; the content type's schema mapping. |
| Firebase Hosting | The files in What it is and each page's .md twin, written by the functions. Nothing is kept in Storage. |
Cloud Functions: the regenerateSeoFiles callable (the seo feature). The rest runs inside the publish queue. See Cloud Functions.
Who can do what
| Action | Who |
|---|---|
Read and change Discoverability settings, run regenerateSeoFiles | Admins. |
| Change About settings, add and edit authors | Admins. |
| Read authors and About | Anyone: the same details are printed on every public page. |
| Set author, sources and dates on content; see the Checks tab | Admins: the content screens are admin only (see Users and roles). |
| Read the sitemap, robots.txt, llms.txt, feeds and twins | Anyone: they are public files on Hosting. |
Troubleshooting
Settings changed but robots.txt did not. Save applies on the next publish; the functions also cache the settings for five minutes. Press Save and apply to site.
Pages and files vanish after a plain firebase deploy --only hosting, which releases only what the CLI uploaded. npm run deploy keeps every file the functions wrote. If they did vanish, rebuild them with npm run seed:dev (or npm run seed:prod). See Deploy.
| Symptom | Fix |
|---|---|
| Absolute links point at the wrong host | Without a production URL in Admin, Settings, About, the Hosting default address is used. Set it, then publish. |
| IndexNow sends nothing | Check it is on in Discoverability. Each submission, and each refusal with its status, is in the function log under "IndexNow". A 4xx means the key file is not served at the production URL: press Save and apply to site. |
| A structured data property is missing | Its source field is empty or unmapped. Nothing is invented. |
| No Related items or link suggestions | Search is off, or the index is empty: rebuild it in Admin, Settings, Search. |
| A page is missing from llms.txt | It has no URL slug or title, its type has no public pages, or the 500-page limit was reached before it. |