# Everjust Website SEO

> SEO + discoverability for an everjust.app (Odoo 19) tenant's public marketing site, via the everjust_agent_mcp MCP as an admin / website-designer user. Use when the task is to set or backfill a page's meta title/description/keywords/OG image/canonical slug (website_meta_* + seo_name on website.page — related proxies onto its ir.ui.view), control sitemap membership or crawlability (website_indexed / website_published / date_publish), add the site's social_* profiles + social_default_image (the seven _SOCIAL_FIELDS feed the everjust_visibility schema.org JSON-LD sameAs on every page so AI engines cite it), edit robots_txt, verify Google Search Console, or enable IndexNow (the everjust_visibility.indexnow_key ir.config_parameter, confirm-gated, plus its opt-in cron). NOT for page CONTENT/layout ([[everjust-website]] / [[everjust-website-snippets]]) nor a customer's OWN-domain DNS/SSL (the Connect Domain product). Cross-references [[everjust-platform]], [[everjust-agent-mcp]], [[everjust-website]].

- Skill: `ever-just/everjust-website-seo` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ever-just/everjust-website-seo`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ever-just/everjust-website-seo/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Marketing & Growth
- Author: ever-just (https://skillmd.com/u/ever-just)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ever-just/everjust-website-seo

---


# EVERJUST Website SEO — Agent Skill

Make a live everjust.app tenant's **public marketing site discoverable** — by search
engines AND by AI answer engines (ChatGPT Search, Perplexity, Google AI Overviews).
You set **per-page SEO metadata**, control **sitemap / crawl** state, wire the site's
**social profiles** (which feed the schema.org JSON-LD the platform injects on every
page), edit **robots.txt**, verify **Google Search Console**, and enable **IndexNow**.
Everything runs through the **`everjust_agent_mcp`** server as the connected Odoo user,
so you need an **admin / website-designer** role (Odoo raises `AccessError` otherwise).

This is the discoverability layer that sits ON TOP of the page-editing skills. If the
task is to change a page's *copy or layout*, that's [[everjust-website]] (Tailwind-QWeb
ports) or [[everjust-website-snippets]] (the on-brand `s_cd_*` snippets). This skill is
only about the metadata, indexing, structured-data, and crawler-facing surfaces. For
opening a session against the right tenant DB and what each tool returns, see
[[everjust-agent-mcp]]; for the platform invariants (COW, the `/odoo` debrand,
`everjust.public_website`, re-sweep on `-u`, never self-escalate) read [[everjust-platform]]
first.

## When to use this skill

- **Set / backfill a page's SEO metadata** — `<title>`, meta description, keywords, the
  Open-Graph image, and the canonical URL slug (`seo_name`) — for one page or across the
  whole site (`website.page`).
- **Control the sitemap and crawlability** — `website_indexed` (is it in `/sitemap.xml`),
  `website_published` (is it live at all), and `date_publish` (schedule a go-live).
- **Wire the site's social profiles** — set `social_facebook` / `social_linkedin` /
  `social_twitter` / `social_github` / `social_instagram` / `social_youtube` /
  `social_tiktok` on the `website` so they become schema.org `sameAs` (a citation/authority
  signal AI reads), and set `social_default_image` as the fallback OG card.
- **Edit `robots.txt`** — the site-wide `website.robots_txt` (per-site override).
- **Verify Google Search Console** — the `google_search_console` token on `website`.
- **Enable IndexNow** — set the `everjust_visibility.indexnow_key` config param, serve the
  key file, and switch on the opt-in daily cron that pushes URLs to Bing/Yandex.
- **Understand the JSON-LD feed** — why the Organization + WebSite structured data on
  every page is auto-generated by `everjust_visibility` from the `website` record.

**Do NOT use this skill for**, and stop if the task is really:
- **Page content / layout / snippets** — copy, sections, CTAs, blocks. That's
  [[everjust-website]] (edit the Tailwind-QWeb arch) / [[everjust-website-snippets]]
  (the on-brand `s_cd_*` snippets). SEO metadata is *separate* from the arch even though
  it lands on the same view (see the model map).
- **The Connect Domain PRODUCT** (auto-DNS + SSL + reverse-proxy for a *customer's own*
  domain) — that's the control-plane under `Customdomain/app/`, unrelated to this Odoo
  site's SEO.
- **Registrar DNS for connectdomain.app itself** — GoDaddy / Route 53, see [[godaddy-api]].
  (Search Console *domain-property* verification via a DNS TXT record is registrar work;
  the `google_search_console` field here is the *URL-prefix* HTML-tag / meta method.)
- **The site's theme / palette / fonts** — SCSS in `everjust_theme` / `everjust_brand_website`,
  shipped in code, not MCP-editable.
- **Sending mail from a contact form** — delivery is the `everjust.mail.*` stack
  ([[everjust-mail-ops]]).

---

## The model map (real `_name`s + live-verified fields)

One tenant DB = one `website` (on connectdomain: **id 1**, name "Connect Domain",
domain `https://connectdomain.app`). SEO metadata is a **mixin whose fields are delegated
onto the page's VIEW** — this is the single most important structural fact, and it's why
COW matters for SEO too.

| Model | Role | Key fields (live-verified on `connectdomain`) |
|---|---|---|
| `website.seo.metadata` | The **SEO mixin**. `ir.ui.view` `_inherit`s it, so the metadata physically **stores on the view**. You rarely touch this model by name. | `website_meta_title` (char, **store=True**), `website_meta_description` (text, store=True), `website_meta_keywords` (char, store=True), `website_meta_og_img` (char — an image URL/path, store=True), `seo_name` (char — canonical URL slug, store=True), `is_seo_optimized` (bool, **computed+stored, read-only** = title+description present) |
| `website.page` | A **URL ↔ view binding + publish/index state**. Its SEO fields are **`related='view_id.website_meta_*'`, `store=False`, `compute=True`, writable** — writing them writes the **view**. | `url` (store=True), `name` (related `view_id.name`), `view_id` (m2o, store=True), `website_id` (related `view_id.website_id`), **`website_published`** (bool, **store=False/computed** — the publish state), **`website_indexed`** (bool, **store=True on the page** — sitemap membership), **`date_publish`** (datetime, store=True — schedule), `visibility` (related `view_id.visibility`), `is_seo_optimized` (related, **read-only**), and the writable delegated `website_meta_title` / `_description` / `_keywords` / `_og_img` / `seo_name` |
| `ir.ui.view` | **The QWeb template AND where the SEO metadata actually stores.** The `website_meta_*` / `seo_name` you set "on the page" persist here. Writing this **with the website in context copy-on-writes** a site-specific fork. | `arch`, `key`, `type='qweb'`, `website_id` (empty = module view; set = COW fork), plus the inherited `website_meta_*` / `seo_name` / `is_seo_optimized` |
| `website` | The **site record** — source of the site-wide social/OG/robots signals and the JSON-LD feed. | `robots_txt` (**html** — the editable robots.txt), `social_default_image` (**binary** — default OG card) + `has_social_default_image` (computed), `social_facebook` / `social_linkedin` / `social_twitter` / `social_github` / `social_instagram` / `social_youtube` / **`social_tiktok`** / `social_discord` (char URLs — **note which reach `sameAs`, below**), `google_search_console` (char — verification token), `google_analytics_key`, `plausible_site` / `plausible_shared_key`, `name`, `domain`, `company_id` |
| `ir.config_parameter` | **Structural config** (`confirm:true` to write). Holds `everjust_visibility.indexnow_key` (enables IndexNow) and `everjust.public_website` (the public-site gate, `'1'` here). | `key`, `value` |
| `ir.cron` | The **opt-in IndexNow cron**, xmlid `everjust_visibility.ir_cron_everjust_indexnow` — ships `active=False` (id 35 here). | `active`, `code` (=`model._everjust_indexnow_cron()`), `model_id`→`website`, `interval_number=1` `interval_type='days'` |

### The delegation gotcha (READ THIS FIRST)

`website.page.website_meta_title` is **`related='view_id.website_meta_title'`, `store=False`,
`compute=True`, writable** — a live-writable proxy. The value lands on the **page's
`ir.ui.view`**, the same view that holds the page's `arch`. Consequences:

- Setting page SEO via a **generic `update` on `website.page`** writes through to the
  view. Going through the `website.page` write is the correct, COW-safe path — **prefer it.**
- If you instead wrote `website_meta_*` **directly on a module `ir.ui.view`** (a view whose
  `website_id` is empty) without the site in context, you'd mutate the SHIPPED view for
  every site and it wouldn't COW — same trap as raw-editing arch ([[everjust-website]]).
  **Set SEO on the `website.page` row, never on a raw `ir.ui.view`.**
- `is_seo_optimized` is **computed and read-only** (true once title + description are both
  set). You never write it; you read it to confirm the page is done.

### How SEO renders (what wins at page load)

`website.layout` renders `<title>` and the `<meta>`/OG tags from these fields, **overriding
any `<title>` you put in the arch**. So set the page's title/description here, don't
hand-roll `<meta>` in the QWeb ([[everjust-website]] Pitfall 4). Per-page
`website_meta_og_img` wins for that page; if unset, the site falls back to
`website.social_default_image`, then the company `logo`. `seo_name` is the clean canonical
slug used in `<link rel="canonical">` and the SEO URL.

### The JSON-LD feed — auto-generated from the `website` record

The `everjust_visibility` addon (`auto_install=True` on any tenant that has a `website`)
inherits `website.layout` and injects, into **every public page's `<head>`** (via
`t-out="website._everjust_jsonld()"` xpathed `//head`), two
`<script type="application/ld+json">` blocks built by `website._everjust_jsonld()`:

- an **Organization** block: `name` (company name, else website name), `url` (base),
  `logo` (`{base}/web/image/res.company/{id}/logo`), **`sameAs`** (the site's set social
  URLs — see below), and `email` (only if `company.email` is set).
- a **WebSite** block with a `SearchAction` targeting `{base}/website/search?search={term}`.

You never author this markup. **You influence it by setting the `website` record's social
fields and the company name/email/logo.** The generator is wrapped in try/except and
returns empty `Markup("")` on ANY error (or when `base`/`name` is missing) — so it can
never break the layout, and a bad value fails silently (verify by fetching a page and
grepping for `application/ld+json`).

**Exactly the seven fields in the addon's `_SOCIAL_FIELDS` reach `sameAs`:**
`social_facebook`, `social_linkedin`, `social_twitter`, `social_github`,
`social_instagram`, `social_youtube`, **and `social_tiktok`**. **`social_discord` is a
field on the `website` record but is NOT in that list — it never reaches `sameAs`** (see
Pitfall 7). A value must `startswith("http")` to be emitted.

### The extra discoverability routes (`everjust_visibility`)

New public routes the addon adds (all `sitemap=False`, and they DON'T override Odoo's own
controllers): **`/llms.txt`** (an llmstxt.org-format index of published pages),
**`/.well-known/security.txt`** (RFC 9116, contact = `company.email`), and
**`/everjust-indexnow-key.txt`** (404 until a key is set, then serves the key). `robots.txt`
and `/sitemap.xml` remain **Odoo's own** — this addon deliberately does NOT override robots
(the AI-crawler Content-Signals override is a documented, un-shipped follow-up).

---

## The tool surface (dedicated vs. generic)

There is **no dedicated SEO tool** — SEO/indexing/social are all **generic ORM** writes
through `everjust_agent_mcp`. The dedicated `website_*` tools cover the adjacent content
ops. Route each recipe through the MCP against the tenant (see [[everjust-agent-mcp]]).

| Need | Tool | Notes |
|---|---|---|
| List pages + their published/indexed state | `website_pages` | Start here; returns url, view_id, published, indexed, in_menu, `website_specific`. |
| **Publish / index a page** | `website_publish` | `published` (default true) + optional `indexed`. The one dedicated tool that touches sitemap membership. |
| **Per-page SEO metadata** | generic `update` on `website.page` | Set `website_meta_title` / `_description` / `_keywords` / `_og_img` / `seo_name`. No confirm. |
| **Schedule a go-live** | generic `update` on `website.page` | `date_publish` (datetime) + `website_published`. |
| **Site-wide social / OG / robots / GSC** | generic `update` on `website` | `social_*`, `social_default_image`, `robots_txt`, `google_search_console`. No confirm. |
| **Enable IndexNow (set key)** | generic `create`/`update` on `ir.config_parameter` | `everjust_visibility.indexnow_key` — **`confirm:true`** (structural). Not classified secret. |
| **Turn on the IndexNow cron** | generic `update` on `ir.cron` | `active:true` on xmlid `everjust_visibility.ir_cron_everjust_indexnow` — **`confirm:true`** (structural). |
| **Submit to IndexNow once, now** | generic `call` on `website` | method `_everjust_indexnow_submit` — non-read → **`confirm:true`**. |
| Read a page's view (rarely needed) | generic `get` on `ir.ui.view` | Only to confirm what view a page points at. |

Confirm gates that apply here: **`delete` always needs `confirm:true`**; writing
`ir.config_parameter` and `ir.cron` (structural models) needs **`confirm:true`**; a
non-read `call` (e.g. the IndexNow submit) needs **`confirm:true`**. `website.page` /
`website` **updates are NOT confirm-structural**. Granting admin and writing *secret*
config params are **hard-blocked** regardless — `everjust_visibility.indexnow_key` is not
classified secret, so it's writable *with* confirm.

---

## Recipes

Route each through the `everjust_agent_mcp` MCP against the tenant ([[everjust-agent-mcp]]).

### 1. Audit the site's SEO / index state before touching anything

```jsonc
// tool: website_pages   (no args → the tenant's one website)
{}
// → { website:{id,name,domain},
//     pages:[{id,url,name,view_id,published,indexed,in_menu,website_specific}], _count }
```
Then pull the SEO columns the page tool doesn't return, in one read:
```jsonc
// tool: get  (generic, on website.page)
{ "model": "website.page", "ids": [/* all page ids */],
  "fields": ["url","name","website_published","website_indexed","date_publish",
             "is_seo_optimized","website_meta_title","website_meta_description",
             "website_meta_keywords","website_meta_og_img","seo_name","visibility"] }
```
Read `is_seo_optimized` per page — **false** means the page is missing a title or
description (the two the compute checks). That's your backfill worklist. Two live signals
worth spotting on connectdomain right now (**13 pages, 5 published, 11 `website_indexed`**):
pages that are **`website_indexed:true` but `website_published:false`** (drafts still marked
crawlable — harmless while unpublished, but they'll appear in `/sitemap.xml` the moment
they go live, so decide each deliberately), and **two rows with the same `url`** (`/` often
exists as both the module view and a COW fork — edit/set SEO on the site-specific one).

### 2. Set / backfill a page's SEO metadata

Write the delegated fields on the **page** (they proxy to its view — the correct path):
```jsonc
// tool: update  (on website.page)
{ "model": "website.page", "ids": [<page_id>],
  "values": {
    "website_meta_title": "Pricing — Connect Domain",                 // <title> + og:title
    "website_meta_description": "Bring-your-own-domain onboarding from $0. Automatic DNS, SSL and edge.",
    "website_meta_keywords": "custom domain, DNS, SSL, Entri alternative",
    "seo_name": "pricing",                                            // clean canonical slug
    "website_meta_og_img": "/web/image/website/1/social_default_image" // or a page-specific image URL
  } }
// is_seo_optimized flips true once title+description are both set (computed; read to confirm)
```
Keep titles ~50-60 chars and descriptions ~150-160; the rendered `<title>`/metas come from
here and **override any `<title>` in the arch** — never hand-roll `<meta>` in the QWeb.
`website_meta_og_img` expects an image **URL/path** (a `/web/image/...` route or a static
asset), NOT a binary. Leave it unset to fall back to the site's `social_default_image`,
then the company logo.

**Backfill across `website.page` (whole site, one pass):**
1. Read all pages (recipe 1) with `is_seo_optimized`, `website_published`, `name`, `url`.
2. Filter to `is_seo_optimized == false` **AND** `website_published == true` (fix the
   live/indexable pages first; drafts later).
3. For each, derive a title from `page.name` (mirror the tenant's existing pattern —
   several pages already carry `"{Name} | Connect Domain"`) and a one-line description
   from the page's purpose; set `seo_name` where the URL isn't already a clean slug.
4. `update` in a loop, then **re-run recipe 1** and confirm every published page now reads
   `is_seo_optimized:true`.

### 3. Control sitemap membership + publish + schedule

```jsonc
// publish + include in /sitemap.xml            // tool: website_publish
{ "url": "/pricing", "published": true, "indexed": true }
// keep live but REMOVE from the sitemap (e.g. a thin/duplicate page)
{ "url": "/welcome", "published": true, "indexed": false }
```
`website_published` = live vs draft (computed on the page); `website_indexed` = whether it
appears in `/sitemap.xml` / is offered to crawlers (**stored** on the page). They're
**independent** — a page can be live but out of the sitemap, or a draft flagged indexed.
Because `website_indexed` is a stored field, a raw `update` on `website.page` also works:
```jsonc
{ "model": "website.page", "ids": [<page_id>], "values": { "website_indexed": false } }
```
To **schedule** a future go-live, set `date_publish` and leave it published-forward:
```jsonc
// tool: update  (on website.page)
{ "model": "website.page", "ids": [<page_id>],
  "values": { "date_publish": "2026-08-01 13:00:00", "website_published": true } }
```

### 4. Wire the site's social profiles (feeds JSON-LD `sameAs`) + default OG image

Set the URL fields on the **`website`** record — the seven in `_SOCIAL_FIELDS` become
schema.org `sameAs` on every page's Organization block (an AI citation/authority signal):
```jsonc
// tool: update  (on website)
{ "model": "website", "ids": [1],
  "values": {
    "social_linkedin": "https://www.linkedin.com/company/connectdomain",
    "social_twitter":  "https://x.com/connectdomain",
    "social_github":   "https://github.com/ever-just",
    "social_youtube":  "https://www.youtube.com/@connectdomain",
    "social_tiktok":   "https://www.tiktok.com/@connectdomain"
  } }
```
`facebook / linkedin / twitter / github / instagram / youtube / tiktok` all reach `sameAs`;
**`social_discord` does not** (Pitfall 7). Each value **must start with `http`** or the
JSON-LD generator skips it. Set the default social share card (used when a page has no
`website_meta_og_img`): `social_default_image` is a **binary** — write a base64 image
string to it via `update` on `website` (or set it in backend Website settings);
`has_social_default_image` then reads true. **Verify JSON-LD after:** fetch a public page
and confirm two `application/ld+json` blocks with your `sameAs` (the generator swallows
errors silently, so "no error thrown" is not proof).

### 5. Edit robots.txt and verify Google Search Console

`robots_txt` is an **html** field on `website` (a per-site override of the default):
```jsonc
// tool: update  (on website)
{ "model": "website", "ids": [1],
  "values": {
    "robots_txt": "User-agent: *\nAllow: /\nSitemap: https://connectdomain.app/sitemap.xml" }
}
```
Keep the `Sitemap:` line pointing at the site's real `/sitemap.xml`. Don't `Disallow: /`
a live public site. Verify Google Search Console (URL-prefix HTML-tag method) by setting
its token — Odoo renders it as the verification `<meta>` in the head:
```jsonc
{ "model": "website", "ids": [1],
  "values": { "google_search_console": "<the token from Search Console>" } }
```
(Domain-property verification instead uses a registrar DNS TXT record — that's
[[godaddy-api]] / Route 53, not this field.)

### 6. Enable IndexNow (Bing/Yandex → also feeds ChatGPT Search) — CONFIRM-GATED

IndexNow is inert until you (a) set the key config param and (b) switch on the cron. The
key file at `/everjust-indexnow-key.txt` returns 404 until the key exists, then serves it.
```jsonc
// (a) set the key — any string; it's the shared secret AND the key-file body.
// tool: create on ir.config_parameter (or update if it already exists) — STRUCTURAL → confirm:true
{ "model": "ir.config_parameter",
  "values": { "key": "everjust_visibility.indexnow_key", "value": "<32-64 char key>" },
  "confirm": true }

// (b) enable the daily submission cron (ships active=False) — STRUCTURAL → confirm:true
// xmlid everjust_visibility.ir_cron_everjust_indexnow (id 35 here); code = model._everjust_indexnow_cron()
// tool: update on ir.cron
{ "model": "ir.cron", "ids": [<cron_id>], "values": { "active": true }, "confirm": true }
```
Once both are set, `/everjust-indexnow-key.txt` returns the key (confirm by fetching it),
and the cron calls `_everjust_indexnow_cron()` daily → for each website
`_everjust_indexnow_submit()` POSTs `_everjust_published_urls()` to
`https://api.indexnow.org/indexnow`. To submit **once immediately** without waiting for the
cron, call the submit method:
```jsonc
// tool: call  (non-read → confirm:true)
{ "model": "website", "ids": [1], "method": "_everjust_indexnow_submit", "confirm": true }
// → the HTTP status int (200/202) on success; False if no key is set or the POST failed
```
IndexNow reaches **Bing + Yandex, NOT Google** — Google ignores it; Google discovery is via
the sitemap + Search Console. Don't promise "instant Google indexing."

---

## Pitfalls

1. **SEO metadata stores on the page's VIEW, not the page table.** `website_meta_*` /
   `seo_name` on `website.page` are `related='view_id.…', store=False` (writable). Set them
   via `update` on the **`website.page`** row (correct, COW-safe path). **Never** write
   `website_meta_*` onto a raw module `ir.ui.view` (empty `website_id`) — that mutates the
   shipped view for every site and doesn't COW, the same trap as editing arch
   ([[everjust-website]] / [[everjust-platform]]).

2. **Set the title/description here, not in the QWeb `<head>`.** `website.layout` renders
   `<title>` and metas from `website_meta_*`, overriding any `<title>` in the arch. Don't
   hand-roll `<meta>` tags in the page body — they'll be shadowed. (Content edits are
   [[everjust-website]] / [[everjust-website-snippets]]; metadata is this skill.)

3. **`website_published` and `website_indexed` are independent** — and shaped differently:
   `website_published` is computed (publish state); `website_indexed` is **stored** on the
   page (sitemap membership). Publishing a page does not add it to the sitemap-intent you
   want, and marking `website_indexed` doesn't publish it. On this tenant **11 pages are
   indexed but only 5 published** — decide **both** per page (recipe 3), and beware drafts
   marked crawlable that will surface in `/sitemap.xml` the instant they publish.

4. **`is_seo_optimized` is computed and read-only.** It flips true only when BOTH title and
   description are set. Don't try to write it — write the two fields and re-read it to
   confirm. Use it as your backfill checklist (recipe 2).

5. **`og_img` is a URL/path; `social_default_image` IS a binary.** Per-page
   `website_meta_og_img` takes an image URL (`/web/image/...` or static). The site-wide
   fallback `website.social_default_image` is a binary (base64). Different field types,
   different write shapes. The fallback chain is page `og_img` → site
   `social_default_image` → company logo.

6. **You don't author the JSON-LD — you feed it.** The Organization/WebSite structured data
   on every page is generated by `website._everjust_jsonld()` from the `website` record
   (company name/email/logo + social `sameAs`). To change it, change those fields — do NOT
   inject your own `<script type="application/ld+json">` into a page arch (you'd get
   duplicate/competing blocks). The generator **swallows every error and emits empty
   markup**, so a bad social value fails silently — always verify by fetching a page and
   grepping `application/ld+json`.

7. **Seven social fields reach `sameAs`; `social_discord` does NOT.** The addon's
   `_SOCIAL_FIELDS` is `facebook, linkedin, twitter, github, instagram, youtube, tiktok`.
   **`social_discord` exists as a field on `website` but is NOT in that list**, so it never
   appears in the JSON-LD `sameAs` — set it if you want, but it won't be a citation signal.
   Values must `startswith("http")`. (If a task genuinely needs Discord in `sameAs`, that's
   an addon-code change to `_SOCIAL_FIELDS` in `everjust_visibility`, not an MCP write —
   flag it; see [[everjust-website-snippets]] for the "author QWeb, flag py/JS" boundary.)

8. **IndexNow is Bing/Yandex-only, confirm-gated, and OFF by default.** The key config
   param, the cron toggle, AND the one-shot `_everjust_indexnow_submit` call each need
   **`confirm:true`** (structural models / non-read call), and the cron ships
   `active=False`, so nothing submits until you explicitly enable BOTH the key and the cron.
   It never reaches Google. Setting the key also starts serving `/everjust-indexnow-key.txt`
   (was 404) — that's expected. Don't enable it on a site whose `/sitemap.xml` is wrong or
   whose pages aren't ready to be crawled.

9. **`everjust_visibility` does NOT override robots.txt — Odoo's `website.robots_txt` does.**
   The addon deliberately ships no robots override (the AI-crawler Content-Signals override
   is an un-shipped follow-up); its extra routes (`/llms.txt`, `/.well-known/security.txt`,
   `/everjust-indexnow-key.txt`) are all NEW and `sitemap=False`. To change robots, edit
   `website.robots_txt` (recipe 5); don't look for an addon route. Keep the `Sitemap:` line
   and don't `Disallow: /` a live public site.

10. **The public site (and thus all of this) only matters when
    `everjust.public_website == '1'`.** If `/` bounces to `/odoo` the site is app-only —
    SEO/sitemap/JSON-LD render for nobody. That gate is an `ir.config_parameter`
    (`confirm:true` to write) and flipping it is platform-ops, not SEO work
    ([[everjust-platform]]). On the connectdomain tenant it's `'1'` (public site live).

11. **You act with YOUR role, everything scoped by `website_id`.** SEO writes need admin /
    website-designer (else `AccessError`), run as the connected user, and are audit-logged.
    Odoo 19 note: the users' group field is **`group_ids`, not `groups_id`** — check your
    role via `group_ids` if you introspect. One `website` per tenant (id 1 here) — confirm
    you're on the right tenant DB before a backfill pass (see [[everjust-agent-mcp]] /
    [[everjust-platform]]).

## See also

- [[everjust-website]] — edit page CONTENT/layout (Tailwind-QWeb arch, COW, publish/menu/
  redirect). Its SEO recipe #6 is a subset; this skill is the full discoverability layer.
- [[everjust-website-snippets]] — the on-brand `s_cd_*` snippets + `html_builder` options
  and the "wrap-don't-rewrite" method for the marketing ports.
- [[everjust-agent-mcp]] — connect to the tenant; the generic `search`/`get`/`create`/
  `update`/`call`/`describe_model` + `website_*` tool surface and the confirm gates.
- [[everjust-platform]] — COW, the `/odoo` debrand, `everjust.public_website`, re-sweep on
  `-u`, structural-model confirm gates, never self-escalate, Odoo-19 `group_ids`.
- [[godaddy-api]] — registrar DNS (domain-property GSC verification, the site's own domain),
  a separate system from these fields.

