Repository navigation
Ultimate SEO
Ultimate SEO is the SEO command centre for a Cactus site. It gathers every page the site serves - core pages and the content of any installed modules (Gazette posts, Shop products, product categories and collections, filter pages, Directory entries) - into one place, scores each one against a set of search-friendly rules, suggests fixes, applies the easy ones with one click, crawls the live site the way a search engine would, and puts the sitemap and robots controls in the admin where they belong.
Where it lives now. SEO used to take four sidebar links. It takes one - SEO - with Dashboard, Pages, Site audit, Structured data and Sitemap & robots as tabs across the top. Old links still work.
Install it and a new SEO section appears in the admin sidebar with five screens, plus an SEO tab on Settings.
Two permissions, set on your core roles from Users → Roles:
-
seo.view- see the dashboard, the pages list and audit results, and run the analyser. -
seo.manage- apply one-click fixes, run site audits, edit sitemap and robots rules, set up structured data, and change SEO settings.
The first screen is the state of the nation:
- Average SEO score across every analysed page, plus counts of pages missing descriptions, missing social images, missing focus keywords, and duplicate titles.
What counts as having a social image. A page has one if it would actually show a picture when somebody pastes its address into a message or a post, not merely if a picture was chosen for it by hand. A product counts if it has photographs. A product category counts if it has its own picture, or if the products on it do. A collection counts if the products on the shelf do. So a shop that has never once opened the social image box is not told off eighty-odd times for something it is already doing perfectly well.
- Quick wins - the lowest-scoring pages, one click away from their analysis.
- Recent site audits with error and warning counts.
- A very loud warning if the whole site is currently hidden from search engines (or the site status is coming-soon/maintenance, which blocks crawlers just as thoroughly). No point polishing metadata nobody is allowed to read.
One table of everything with a URL: core pages always, and - when the modules are installed - Gazette posts, Shop products, Shop product categories and product collections, filter pages, and Directory entries.
The last three are the ones people are surprised to find here, and they are usually the ones doing the heavy lifting. A product category at /shop/categories/office-chairs, a collection at /shop/collections/impulse and a filter page at /green-office-chairs are each a real address with its own page title and description, written to bring people in from a search - so they are listed, scored and filtered exactly like everything else. Whether a shop's products sit at /desk-name or /shop/products/desk-name is followed too; categories, collections and filter pages keep their own addresses either way.
Across the top sit the figures you opened the screen for: how many pages there are, the average score, how many are good, need work or are frankly poor, how many have never been analysed, how many have no meta description, and how many carry a score that is now out of date. Every one of those tiles is a filter. Reading "43 pages have no meta description" and then having to build that filter by hand is precisely what made this screen basic; click the tile instead.
Each row shows the page and its address, its type, publication status, the meta description (with its length, since length is the thing that goes wrong), the focus keyword, a count of failures and warnings, the score, and when it was last analysed.
Every column sorts. Click a heading to sort by it, click again to turn it over. Columns start the way round they are useful - worst score first, most problems first, longest-unlooked-at first, missing descriptions first - because nobody opens this screen to admire the pages that are already fine. Pages with no score at all, no keyword or no date stay at the bottom whichever way the arrow points: absent is not the same as worst, and a wall of blanks at the top would bury what you asked to see.
Under the tiles: filter by content type, by published or not, by score band, and by issue. The issue list is built from what the analyser actually found on your site, commonest first with a count beside each - No meta description (43), Duplicate title (7) - plus two of its own: Never analysed and Score out of date.
Score out of date is the useful one nobody thinks to ask for. It means the page has been edited since it was last analysed, so the score describes a version of the page that no longer exists. The row says so, and so does the detail panel.
The search box matches titles, addresses, focus keywords and meta description text at once.
Your filters, sort order and page size are remembered, so coming back to the screen picks up where you left it. Clear filters puts it all back.
- Page size of 25, 50, 100 or the lot, with the usual Previous/Next underneath, so a shop with four hundred products is a table rather than a scroll.
- Tick boxes on each row, and one in the heading that takes the whole page of results. Once anything is ticked, a bar appears with Analyse selected on it - and, when your filters match more than is on screen, Select all N matching.
- Export hands you the current view as a spreadsheet, in the order it is on screen, with the description, keyword, score, failure and warning counts and both dates. Useful for handing a list to whoever writes the copy.
Click a row and the detail panel opens:
- A Google-style result preview showing how the title and description will actually look in search results, truncation and all.
- A focus keyword box - the search phrase the page should win. The analyser scores keyword placement (title, description, slug, body copy) and density against it.
- Analyse runs a 20-odd-rule check: title and description length and uniqueness, slug hygiene, one-H1 structure, content depth, image alt text, internal links, readability, publication status.
- For core pages, the title and meta description are editable right there, with a Use suggestion button that drafts a description from the page's own copy. Saving writes through properly - the page editor and the next publish both see the change.
- For module content, editing happens where that content lives - the panel deep-links straight to the right editor in Gazette, Shop or Directory. The analysis still applies either way.
Clicking Analyse on four hundred products one at a time is nobody's idea of an afternoon. The button beside the filters does the lot:
- Analyse all N scores every page in the table and saves the results, with a progress bar and a Stop button for when you change your mind. It works through the list in small batches, so a big catalogue is a longer wait rather than a broken one.
- Narrow the list first with any of the filters and the button follows suit - it reads Analyse these N and only touches what the filters match, whether or not it is on the page you are looking at. Filter to Never analysed and press it for the obvious first pass; filter to Score out of date and press it to bring everything back up to date after a busy week of editing.
- Or tick individual rows and use Analyse selected, for when only a handful are worth the wait.
- Focus keywords are left exactly as they are. A bulk run re-scores pages, it never re-words them.
- When it finishes you get the count and the average score, and the Dashboard's figures catch up with it.
Run it again whenever you have added a batch of content - re-analysing an already-scored page simply replaces its score.
The Pages screen analyses what is stored; the audit checks what is actually served. It fetches your published pages over HTTP - the same view a search engine gets - and reports:
- Broken pages and timeouts
- Missing or overlong titles, missing meta descriptions
- Stray
noindexdirectives - Heading problems, missing image alt text, thin content
- Missing Open Graph tags and canonical links
- Canonical tags pointing at another site entirely, or at nothing usable
- Missing or unreadable structured data
- Missing viewport tag, or a page that never says what language it is in
- Slow responses
Run it on demand from the button, or let it run itself weekly (Mondays, 4am, when it is least likely to be in the way). A crawl is bounded by a time budget as well as a page limit, so a large site reports a partial run rather than failing outright. History is kept per run with per-page issue lists. The page limit per crawl is configurable in settings (default 50).
- Blocked paths - add robots.txt Disallow rules from the admin. They join the ones Cactus already blocks (admin, setup, API paths) and take effect immediately.
- Extra sitemap entries - your pages and module content are in the sitemap automatically; anything else you want crawled can be added here with optional priority and change frequency.
- Both screens link straight to the live
/sitemap.xmland/robots.txtso you can see the result.
Fixed in 0.1.6. Both lists were being collected only from modules that serve public pages under a prefix of their own, which this module does not - so the rules you added here were saved, listed back to you, and then had no effect whatsoever on the live
/sitemap.xmland/robots.txt. They apply now. If you added a Disallow rule at any point and wondered why the page was still being crawled, that is why, and it is worth a look at the list to check you still mean all of it.
New in 0.1.12. People increasingly find a shop by asking an assistant rather than by searching. This tab is about being findable that way - and about having a say in it, because "AI crawler" covers three quite different visitors and only one of them takes without giving anything back.
Nothing here changes what your site publishes until you change it, with one exception said plainly below.
Four things, and all four switches are on from the start:
-
/llms.txt- one Markdown page listing everything on the site, with a line of summary each. This is the file assistants look for first. -
/llms-full.txt- the same index with the shorter pages written out in full. Products are deliberately left out: a catalogue of any size makes a file nobody would finish fetching. They are still in the index, one line each, with their own address. The index names this file, so a reader that has only fetched the index knows it is there. - Who you are, at the top of the index - your registered name, your company registration number, VAT number, address, the countries you sell to, your opening hours and how to reach a human. All of it comes straight from the Structured data tab, so there is one set of facts rather than two that drift apart. It sits above the page lists on purpose: an assistant reads the top of the file first, and it will not put a business in front of somebody it cannot identify. If you would rather not publish it there, the switch turns it off - but it is the single most useful thing on this page for getting recommended, and everything in it is already on your pages in a form search engines read. It only goes out while Publish the organisation details on every page is switched on over on Structured data: if you have decided those facts are not for publishing, they are not published here either.
-
A Markdown twin of every page - the same page at the same address with
.mdon the end./aboutalso answers at/about.md,/task-chairat/task-chair.md, and the home page at/index.md. No menus, no buttons, no scripts: the words, the headings, the links, the questions the page answers and - for a product - the price, the stock, the specification, the variations and whether there is a 3D model of it.
The questions come too. Whatever your product, category and collection pages answer in their FAQ section is written into the copy as well, merged exactly as the page merges it: the item's own questions first, then its category's, then its parent categories', then the shop-wide list, with a nearer answer beating a further one and a level that has been told not to inherit stopping the walk. These are the most quotable lines you have - what it weighs, whether it comes built, how long delivery takes - and until now they were the one thing on a product page an assistant reading the copy could not see. Switch the shop's FAQs off and they are not in the copies either.
The specification is the one on the page. The copy lists the attributes you flagged for the product page's Specification panel, under the names the panel shows and in the order it shows them. It used to list every attribute a product carried, flagged or not, which meant supplier catalogue codes, internal reference numbers and whatever else lives back there went out to every assistant that fetched the file, while the page itself showed none of it. If a product has nothing flagged for the panel, its copy has no specification table - same as the page.
Prices in a copy are the prices on the page. If your shop stores its prices one way and shows them another - typed without VAT, shown with it, or the other way round - the Markdown copy does the same sum the shop does and carries your own wording after the figure ("ex VAT", "inc. VAT"). An assistant quoting a copy quotes what the shopper will actually see.
Who supplies it is your business. Product copies name the supplier behind each item, which suits a site that wants the brand said out loud and suits a reseller rather less. The switch turns it off. It is on to begin with because that is what the copies already did, and an update has no business quietly removing something your site was publishing.
You choose which kinds of content get a twin, and you can write a sentence or two at the top of the index saying what the site is. Untick a content type and it drops out of the index and its twins are removed at the next rebuild.
An update rebuilds the copies on its own. When a Cactus update changes what goes into a copy - a new section, something that should never have been in there - every existing copy is out of date, and the overnight job used to skip them all: it only ever asked "has this page been edited since?", and a product nobody has touched in a year never had been. So an install could take an update, run the new code, and go on serving copies written by the old one indefinitely. It now also asks which version wrote each copy, and rewrites anything written by an older one, a night or two at a time until it has caught up. Nothing to press. Rebuild everything is still there if you would rather not wait.
Nothing works until you have rebuilt once. A fresh install, or an install that has just switched these on, has no copies built yet - so the index lists nothing, the .md addresses answer "not found", the agent endpoint says it has nothing to show and no page gets its breadcrumbs. The screen says so in as many words when it happens. Press Rebuild everything once and the overnight job keeps up on its own after that.
Why "rebuild". The twins are built ahead of time and stored, not generated when somebody asks for one. Building a large catalogue's worth takes real time and real money; serving one that is already built is a single lookup. So they are rebuilt overnight, whenever you save a summary, and whenever you press the button - and the overnight rebuild only touches pages that have actually changed since last time, so on a night when nothing was edited it costs almost nothing. The practical effect is that an edit made today is in the Markdown copy tomorrow morning; press the button if you want it there sooner. Two buttons: Rebuild what has changed, and Rebuild everything for after a change to how the pages are put together rather than to what they say. A very large catalogue may not finish in one go; it says so, and picks up where it stopped next time.
Two switches that share one lookup, so having both costs no more than having either. Both off by default, because unlike everything above them they put a small database read on the render of every public page - which shows on your Vercel bill as Fluid Active CPU. A cached page does not pay it twice, and a page with no Markdown copy built does not pay it at all.
Publish structured data on every page. This is the machine-readable summary that puts the trail of links under your name in a search result, and that an assistant reads to work out what a page actually is. What goes out depends on the page:
| Page | What it publishes |
|---|---|
| Anything with a trail above it | Breadcrumbs - Home > Office Chairs > Task Chairs > this one |
| A blog post | Its headline, summary, picture, author and dates |
| A category or collection | That it is a listing page, and the first thirty things on it |
| A directory entry | That it is a business, with its address, telephone and website |
| A product | Breadcrumbs only - see below |
Your shop already publishes the product details on a product page, with the tax handled and the prices withheld when you have set the shop to hide them. This deliberately does not repeat them: two different prices for one item is the fastest way to have a shopping feed suspended. Breadcrumbs are the half nobody was publishing.
Tell each page's reader where its Markdown copy is. A line in the page's head pointing at the same address with .md on the end, so an assistant finds the plain-text version without having to guess that it exists. Needs the Markdown twins switched on above; it greys out if they are not, because a link to a page that answers "not found" is worse than no link.
Neither switch changes anything until a page has a Markdown copy built - they read the same rebuilt-overnight table everything else does, so a brand new page gets both the morning after it is published, or straight away if you press Rebuild what has changed.
Every crawler on this screen is currently allowed, because that is what your site does today and an update has no business quietly changing it. They are grouped by what they are actually for, which is the distinction no single robots.txt line can make:
- The ones that send you customers - they index your pages so an assistant can recommend you and link to you. Blocking these is how a business disappears from answers people are already asking.
- The ones fetching a page for somebody right now - somebody asked their assistant about you and it went to look. Block one and the person is told your site would not answer.
- The ones that take and give nothing back - they collect your pages to train a model. No link, no visit, no credit. Plenty of owners block the lot, and it costs nothing in search: Google's and Apple's training opt-outs are separate from their ordinary search crawlers, so turning them off does not affect either company's search results.
There is a Block the lot button on the third group for owners who want exactly that and no further reading.
Below that, what they may do with what they read: three declarations - may we be shown in results and answers, may our pages be used to answer a question being asked right now, may our pages be used to train a model. These are a statement of intent rather than a lock on the door, published as a Content-Signal line that a growing number of companies have agreed to honour. Each starts unanswered, because saying nothing and saying no are different answers and it is not for this module to put words in your mouth.
Off by default, and it costs money. Switched on, this counts AI crawler visits and people arriving from an assistant, and answers the question everybody asks next: is any of this working? Each AI visit is one small database write, which shows on your Vercel bill as Fluid Active CPU. An ordinary human visitor costs nothing extra - the check that decides is two string comparisons and never touches the database - and the write happens after the page has already been sent.
Counts are kept per day, per assistant and per page, for as long as you choose.
Off by default, and it costs money. This turns on a read-only endpoint at /api/m/ultimate-seo/mcp that an assistant can use to search your catalogue and read any page in one call instead of crawling the site. It is read-only in the strict sense: there is no tool there that changes anything, and it can see only what the site already publishes to anybody with a browser.
Five things an agent can ask for: who runs this site (the same business details that go at the top of the index, returns policy included - it is told to ask this first, before it recommends you to anybody), what kinds of content there are and how much of each, a search across the lot, any one page in full, and the catalogue narrowed down.
That last one is the one that gets you recommended. Somebody does not ask their assistant "what do you sell"; they ask for an office chair under £250 they can have this week. An index of four hundred products does not answer that without the assistant fetching all four hundred - which it will not do, so it answers from a site that made it easier. So an agent can ask for products by price range, whether they are in stock, which category or collection they are in, and have them back cheapest first. A listing running £200 to £400 counts as being under £250, because £250 buys one. Every result comes back with its price, your tax wording and its stock on the line, so nothing has to be fetched twice to compare two things.
Every request is a function call - Invocations and Fluid Active CPU on your Vercel bill. An agent can make a great many of them in a short space of time, and unlike a person it does not get bored. That is the whole reason it starts switched off. When it is on, the address is named in /llms.txt so an agent that reads the index can find it.
One line per page, written by you, that goes at the top of that page's Markdown twin and into its line in the index. It is the line a model quotes when it only has room for one. Costs nothing, and the box appears on the Pages tab beside everything else about that page; saving one rebuilds that page's twin there and then.
The Pages tab also gains a Ready for AI column, which says whether each page has a Markdown copy built and whether it has a summary.
New in 0.1.10. Fill this in once and it goes out on every page of the site. Before, the only way to tell search engines who you were was to drag a block onto a page, and it then applied to that page and no other.
What goes out - three switches, all off until you turn them on:
- Organisation details on every page - the record search engines use to build a knowledge panel: your name, logo, description, contact details, address, registration numbers and official profile links.
- Website details on every page - names the site itself and ties it to the organisation above, so the two read as one record rather than two unrelated claims.
-
A search box in search results - lets Google offer a search box for your site directly in its results. Needs a search address containing
{search_term_string}where the visitor's words go, and the website details switched on.
Returns are worth the five minutes. An assistant asked to recommend a supplier is really being asked what happens when the thing turns out to be wrong. Filling in the Returns card answers that before anybody asks, in a form both search engines and assistants read - and the same answer is said in plain English at the top of /llms.txt and handed to any agent that asks who runs this site.
What kind of organisation is this? - tick as many as are true, not just one. An online shop is both an Organisation and an Online shop, and saying so is how it qualifies for both sets of treatment in search results. Local business, Shop with premises and Professional service are for somewhere with a door, and ticking one of those three is what makes the opening hours and price range fields appear - putting them on the others is markup search engines throw away.
Everything else is a field. The whole record is fillable in, with nothing needing hand-written JSON:
| Section | Fields |
|---|---|
| Who you are | Name, also known as, registered legal name, description, home page, founded |
| Logo and photograph | Logo, logo width, logo height, logo caption, photograph |
| How to reach you | Email, phone, street address, town, county, postcode, country, areas served |
| Contact point | What it is for, email, phone, areas served, languages |
| Registration numbers | VAT number, tax ID, D-U-N-S number, ISO 6523 code, and one free "other number" pair - what it is, and the number |
| Official profiles | One URL per line |
| Returns | Whether you accept them, how long you allow, how they come back, who pays, where it applies, and the page it is written on |
| Premises | Opening hours, price range - only when a premises type is ticked |
Leave anything blank and it is simply left out. Leave the name blank and it uses the site name; leave the home page blank and it uses this site. The logo can be a full web address or a site path like /brand/logo.png; either way it goes out as a full address, because whatever reads it has no idea what site it came from.
A few things the screen decides for you, so you do not have to think about them:
-
A single value goes out as a single value. One area served is
"GB", not["GB"]. Both are correct; only one of them matches what you will see in every reference example when you go to check your own markup. - The contact point needs a way to be contacted. A contact type with no email and no phone number says nothing anybody can act on, so it is left out entirely until one of them is filled in.
- The "other number" needs both halves. A registration number nobody can name is a number nobody can use.
- A returns policy states itself or says nothing at all. Leave the first box on Not stated and none of it goes out - a policy nobody has typed in is not a policy of "no returns". Choose a set number of days and the day count appears; choose no time limit or not accepted and it does not, because a window on a policy that has no window is a contradiction, and a reader throws out the whole record rather than the half that disagreed. A return fee is published only with a currency beside it.
- The free-text "other number" is for anything with a name - a Companies House company number, a charity number, a licence number. The named boxes above it exist because VAT, D-U-N-S and ISO 6523 have proper homes of their own in the vocabulary.
The preview on the right is not a mock-up. It is the exact text every page will carry, built by the same code that builds the live one, so it cannot quietly drift into being a flattering approximation. Paste it into Google's Rich Results Test if you want a second opinion.
Nothing is published until you say so. Every switch starts off, including on a site updating from an earlier version. A half-filled organisation record on every page of the site is worse than none, so the module will not make that decision for you.
Two blocks also appear in the page builder, for the pages that need to say something different:
- Structured data (SEO) - invisible on the page, very visible to search engines. Describes an Organisation, Local business (with address, phone and opening hours), Website, or any custom JSON-LD you paste in. Organisation fields pre-fill from your saved SEO settings. Now mainly for a page that describes somebody other than you - a partner, a venue, an event; the site-wide settings above handle your own details better.
- FAQ (SEO) - a real, working FAQ accordion for visitors, with FAQPage markup underneath so the questions are eligible for rich results in search.
- Search engine visibility - the master switch (this is the same setting that used to live in general settings; it has moved in with the rest of the SEO controls, and still works from either home).
-
Organisation details - name, legal name, logo, official profile links and X/Twitter handle. The profile links feed the site-wide structured data as well as pre-filling the page-builder block. The X/Twitter handle is now published as the
twitter:sitetag, so shared links credit your account - it was stored and used by nothing before 0.1.10. - Analyser targets - title/description length ranges, keyword density band, and the audit page limit. The defaults follow current good practice.
- Repo: cactus-foundation-modules/ultimate-seo
- Requires core
0.5.1565+from 0.1.12 (thecore.agent-contentextension point, thelinkshalf of the head hook and the/llms.txt,/llms-full.txtand.mdaddresses all arrived there). 0.1.11 and earlier require0.5.1418+(the site-wide head hook the structured data is emitted through arrived in that release; earlier core builds also never collected this module's sitemap and robots entries at all). No environment variables; the weekly audit authenticates with the standardCRON_SECRET. - Tables are prefixed
seo_(settings singleton, per-page analysis, audit runs and issues, robots rules, sitemap entries, materialised Markdown documents, AI hit counts) and are torn down on uninstall.002_structured_data.sqladds thestructured_datacolumn to the settings singleton;003_ai_seo.sqladds theaicolumn,seo_page_meta.ai_abstract,seo_llm_documentsandseo_ai_hits. -
seo_llm_documentsis unique on(entity_type, entity_id)and onpath. The first is what makes a rebuild update a product that has moved slug rather than leaving a second row answering at the old address; the second is what stops two entities claiming one address, and a batch carrying such a clash is retried row by row so the loser is reported rather than taking the rebuild down. -
seo_ai_hitsis a running count keyed on(day, kind, agent, path), not a row per request: the table then grows with how many different pages the crawlers ask for rather than with how often they ask. - Integration is entirely through existing module hooks:
lib/sitemap.ts(getPublicSitemapEntries),lib/robots.ts(getPublicRobotsDisallow, plusgetPublicRobotsGroupsandgetPublicRobotsExtraLinesfrom core 0.5.1565),lib/head.ts(getPublicHead, new in core 0.5.1418 - see Authoring a module),lib/agent-content.tson thecore.agent-contentextension point, asettingsTabsmanifest entry, manifestpuckBlocks, and twocronJobsentries. - The Markdown twins are materialised by
lib/ai/materialise.ts- the weeklycron/aijob, the admin button, or a summary being saved - never on the request. Measured on a live catalogue: a hundred products with their variations, attributes and 3D models take about a second and a half, so a twenty-thousand-product shop rendered per fetch would be a bill rather than a feature. A rebuild compares each entity'supdatedAtagainst the storedsource_updated_atand skips what has not moved, stops at a forty-second budget rather than being killed mid-write, and only prunes after a complete pass. That budget is not a number of this module's choosing: a module route cannot set its ownmaxDuration, core's shared dispatcher for module API routes sets one ceiling of sixty seconds, and core's cron dispatcher then gives a job whatever is left of its own sixty-second tick. A budget set above that is not a longer run, it is a run that never finishes and is recorded as a failure, leaving a large catalogue silently short - which is what a four-minute budget between 0.1.12 and 0.1.13 would have done to any site big enough to need it. The same applies to the site audit, which budgets thirty seconds and will not start fetching a page it cannot also finish. -
lib/ai/html-to-markdown.tsis a small parser written for the purpose rather than a dependency, andlib/ai/puck-markdown.tswalks stored page-builder props generically - it knows no block types, because the palette differs per install - with a ProseMirror renderer for the rich-text fields that store an editor document rather than HTML. - The MCP endpoint reads the same materialised documents as everything else, so there is no second definition of "what this site says" to drift out of step with the first.
- Per-page structured data is built at request time from facts stored in
seo_llm_documents.page_facts(004_page_structured_data.sql), not from finished JSON-LD. Structured data is full of absolute URLs -@id,url,image, every breadcrumb step - and a hostname baked into twenty thousand rows is a hostname that has to be rebuilt out of them; this platform's one live site changed domain in its first year, and the same rows are read on preview deployments under another name again. The row holds names, paths and dates;lib/ai/json-ld.tsbuilds the blocks against whatever host the page is being served on, and is pure. - The page's own head contributions cost one indexed read, made only when the owner has switched one of the two on, wrapped in
cache()so it happens once per request whichever switch asked for it. The path comes from core'sx-cactus-pathrequest header (core 0.5.1565); an older core sets no header, and the module then simply publishes nothing extra rather than failing. - The JSON-LD builders in
lib/structured-data.tsare pure functions with no database or environment access, so the admin preview and the live page are the same code path rather than two renderings that agree until one is edited. - One-click fixes write only to core
InfoPagerows (columns plus the Puckroot.propsmirror in both draft and published data, so a later publish does not revert them). Module-owned content is analysed read-only and deep-linked to its own editor - this module never writes another module's tables.