Appearance
SEO
Head meta, robots.txt, sitemaps, webmaster tools and instant indexing, compatible with Rank Math; imports Yoast SEO.
kitloom/seo · admins: kitloom/seo-filament, kitloom/seo-nova
Video lessons
Recorded in the Filament admin; the Nova admin works the same way.
SEO the way Rank Math does it, on Rank Math's own data: a site moved from WordPress keeps its titles, robots, sitemap settings and verification codes as they were, and Rank Math can read what this package writes.
- The head of every page: title and description from the record's meta or its type's template, robots, the canonical address (with its page number on page 2 and further, the shop page's included), Open Graph and Twitter tags, and on the front page the webmaster verification codes. A shop's Cart, Checkout and My account pages (
woocommerce_*_page_id) arenoindex, followwhatever they say, as WooCommerce and Rank Math's WooCommerce module keep them. The Shop page is headed as Rank Math heads it: its own title orpt_page_title(no page number), and Noindex archive subpages closes its page 2 and further, as any paged address. - schema.org markup: the site's owner, the website with its search, the page, its breadcrumbs, and an article where the type asks for one.
- Breadcrumbs with Rank Math's labels and the primary category.
- Sitemaps at Rank Math's addresses, an HTML sitemap shortcode, robots.txt, Google's HTML verification file.
- Instant indexing: IndexNow, as Rank Math, and Google's Indexing API.
The admin is kitloom/seo-filament or kitloom/seo-nova.
In the admin
- SEO → General Settings: the breadcrumbs' separator and labels, the verification codes of Google, Bing and the other webmaster tools (the code or the whole meta tag), Google's HTML file and the text of robots.txt.
- SEO → Titles & Meta: the title and description templates of the home page, of each post type and taxonomy, of authors and of the other pages, with variables such as
%title%and%sep%; whether each is open to search engines; the site's owner for schema.org (Local SEO). - SEO → Sitemap Settings: which post types, taxonomies and authors get a sitemap (and which roles' authors do not), how many links a file holds, and what the HTML sitemap shortcode lists.
- SEO → Indexing Console, Settings, History: submit addresses to IndexNow and Google by hand, choose what is submitted on its own when it is published or changed, and see what was sent and what the services answered.
- The SEO block on a post, page or term form: its own title, description, robots, canonical address, breadcrumb title, primary category and social previews (Facebook and X apart). Empty fields fall back to the templates of Titles & Meta; variables in the social titles and texts are filled in too. While no X field is filled, X shows the Facebook title, text and image, as Rank Math does with "Use Data from Facebook Tab"; one X field of its own turns that off for the record.
Where things are kept
| What | Where |
|---|---|
| Titles & Meta | option rank-math-options-titles |
| Breadcrumbs, verification codes, robots.txt | option rank-math-options-general |
| Sitemap settings | option rank-math-options-sitemap |
| IndexNow key and post types, Google's service account | option rank-math-options-instant-indexing |
| A record's, a term's or an author's own head | meta rank_math_title, rank_math_description, rank_math_robots, rank_math_canonical_url, rank_math_focus_keyword, rank_math_breadcrumb_title, rank_math_facebook_title, _description, _image, rank_math_twitter_title, _description, _image (an author: title, description and robots) |
| The primary category | meta rank_math_primary_category; of another taxonomy rank_math_primary_{taxonomy} (rank_math_primary_product_cat) |
| IndexNow history | option rank_math_indexnow_log |
The options are arrays with on/off switches, as the plugin writes them — serialized as Rank Math left them, JSON once saved here. A form that edits some keys keeps the rest. Every page writes through the settings' writer as the signed-in user, so the settings' own permissions are asked and a module listening to SettingWriting may refuse a value, as on any settings page.
Three things Rank Math does not have are the site's own options: Google's HTML file (kitloom_google_html_file), the taxonomies submitted for indexing (kitloom_instant_indexing_taxonomies) and Google's indexing history (kitloom_google_indexing_log).
php
use Kitloom\Seo\RankMath\Options;
app(Options::class)->get(Options::TITLES, 'title_separator', '-');
app(Options::class)->isOn(Options::SITEMAP, 'authors_sitemap');
app(Options::class)->put(Options::GENERAL, ['breadcrumbs_home_label' => 'Start']);A record's head meta — title, description, focus keyword, robots, canonical, social — goes into its revisions, so restoring one brings back what the search results showed (SeoRevisionedMeta).
Page parts
| Part | Key | Gives |
|---|---|---|
Site\Head | head | title, description, robots (index, follow), canonical, verification, openGraph, twitter |
Site\Schema | schema | @context and @graph; nothing on a missing page |
The site's breadcrumbs part gets Rank Math's settings: the home label, the 404 label, the search and archive formats, a record's own crumb title, and the separator (breadcrumbs_separator, entities decoded) — beside the list, as breadcrumbsSeparator for a Vue theme; left empty, each theme draws its own.
The theme prints the title, the description, robots and the canonical link itself — with fallbacks of its own, and in an Inertia theme kept in step on client visits. The rest of the head — verification codes, Open Graph, Twitter cards and the schema.org graph (the seo::meta view) — the module puts where the theme marks @siteHead (kitloom/site), so a theme needs to know nothing about seo. In an Inertia theme they are those of the first page loaded; crawlers and link previews always load a page whole.
A module adds its own nodes to the graph:
php
Event::listen(SchemaBuilding::class, function (SchemaBuilding $event): void {
$event->add(['@type' => 'Product', '@id' => $event->ids['webpage'].'#product', 'name' => '…']);
$event->change($event->ids['webpage'], fn (array $node): array => [...$node, 'about' => ['@id' => '…']]);
});Variables
The title and description templates, and the site's widget slots, are filled in with Rank Math's variables: the site (%sitename%, %sitedesc%, %sep% — the separator of Titles & Meta), the record (%title%, %excerpt%, %date(Y-m-d)%, %modified%, %name%, %category%, %tags%, %customterm(genre)%, %customfield(isbn)%, %focuskw%, %thumb(size=medium)%), a term or an author (%term%, %term_description%, %user_description%), and the page (%search_query%, %page%, %pagenumber%, %pagetotal%, %pt_plural%, %currentyear%). A variable that is not known, or has no value, stays as written.
The package adds them to Kitloom\Site\Content\Variables after the sources other modules and themes tag with Variables::TAG (a Kitloom\Site\Contracts\VariableSource): those are asked first, so a theme may give %date% its own meaning.
The primary category
The category chosen in the editor goes into %category% of the permalink, the breadcrumbs and the %category% variable, while the post is still in it; otherwise the site's own choice applies. The package binds Kitloom\Site\Contracts\PrimaryTerms.
Addresses
| Address | What |
|---|---|
/sitemap_index.xml | the index of the sitemaps that are on and have something in them |
/{type}-sitemap.xml, /{type}-sitemap2.xml | a post type, a taxonomy or author, items_per_page addresses a file |
/main-sitemap.xsl | the stylesheet; a theme's sitemap/stylesheet view replaces it |
/robots.txt | robots_txt_content, otherwise WordPress's default with the sitemap; a site closed to search engines closes everything |
/google{code}.html | Google's verification file |
/{key}.txt | the IndexNow key file |
The web server hands out a file in public/ before the application is asked, so a static public/robots.txt — or a google….html verification file or an IndexNow key file put there by hand — wins over these addresses: the robots settings, the Sitemap line and a site closed to search engines never apply. Remove such a file; the skeletons ship without one.
A sitemap leaves out drafts, password-protected records, what is closed to indexing by its own robots or by its type's, terms without published posts and the authors of the excluded roles. Off, empty or unknown is the site's 404 page. Sitemaps are cached for SEO_SITEMAP_CACHE_TTL seconds and built again once a record, a term or an option is saved.
On a site in several languages (kitloom/languages) every entry lists the page in its languages as <xhtml:link rel="alternate" hreflang="…"> — the same addresses the page's own hreflang gives, each language named by its locale (en-CA, fr-CA), x-default among them — and each translation's address (/fr/about/) is an entry of its own in the same file — dated by the post or its translation, whichever changed later — as Rank Math writes it with Polylang or WPML. A post or a term is listed in the languages it has a published translation into; archives and authors in every language. A file still holds items_per_page records, so with translations it holds more addresses. Without a languages module the sitemap is exactly what it was.
The styled view of a sitemap in a browser is the browser's XSLT reading /main-sitemap.xsl. Chrome is removing XSLT (from its stable release around November 2026): there the sitemap shows as plain XML. Search engines read the XML and never used the stylesheet, so nothing changes for them. For a sitemap people read, put the HTML sitemap shortcode, [rank_math_html_sitemap], on a page; the theme draws it from its data (below) — none of the bundled themes does yet.
[rank_math_html_sitemap] (and [sitemap]) gives the theme sections — name, label, items of title, url, lastmod — in the order html_sitemap_sort asks for.
Coming from Yoast SEO
A site that used Yoast SEO keeps its SEO: Yoast's data is taken once into the keys above, and from then on the site works with those. While Yoast's data is in the database, SEO → General Settings has an Import from Yoast SEO button (in Nova, a switch applied after the save), shown to whoever may change General Settings, Titles & Meta and Sitemap Settings. A site with up to 300 records with Yoast's head (posts, terms and authors; SEO_IMPORT_AT_ONCE) is imported while you wait; a larger one goes to the queue, and the admin tells you how it went once it is done — a run stopped halfway keeps what it wrote, and importing again takes the rest. The same from the console, a dry run first:
bash
php artisan seo:import --from=yoast --dry-run # what would be taken, and what has no place here
php artisan seo:import --from=yoastWhat is taken:
- Every post, page and term: the SEO title and meta description, the focus keyphrase, the canonical address, the breadcrumbs title, the social title, description and image, the X title, description and image, the primary category (and the primary term of another taxonomy, a product's).
- Robots of a record: left at Yoast's defaults, nothing is written and the type's robots apply. Once anything was chosen, the record gets its own list: index or noindex (for "default", what Yoast said of the type), nofollow, and No Image Index, No Archive, No Snippet.
- Authors: the title, description and "do not show this author's archives" of their profile, for their archive.
- Settings: the title separator and website name; the title and description templates of the homepage, every post type, its archive, every taxonomy, authors, dates, search and 404; which of them are hidden from search engines, and with that which get a sitemap; the article type of a post type; breadcrumbs; the organization or person behind the site with its logo; the verification codes of Google, Bing, Baidu, Yandex and Pinterest; the X card type.
- Variables are renamed as Rank Math names them —
%%title%%becomes%title%, Yoast's%%category%%(every category)%categories%,%%primary_category%%%category%,%%cf_isbn%%%customfield(isbn)%. One without a counterpart (%%caption%%,%%userid%%) is taken out, as Yoast showed nothing there either; the report says where.
What was set here already stays: Yoast fills only what is empty, and the report counts what it left alone. A type's robots are its switch and its list together — a Titles & Meta page saved once has the switch, so Yoast's choice for that type is not taken. Running it again changes nothing. Yoast's own rows are not touched or removed.
Not taken, and named in the report: what kitloom has no setting for (the date archive's description, removing /category/ from addresses, social profiles, a default share image), Yoast's scores and analysis, and its own screens' switches. Yoast Premium's redirects are kitloom/redirects': php artisan redirects:import --from=yoast. A robots.txt Yoast edited was a file, not data: copy what it said into Edit robots.txt.
Instant indexing
What a signed-in user publishes, changes or takes down goes to IndexNow for the post types in bing_post_types and to Google for those in post_types, after the response, on the queue if there is one. Terms go for the chosen taxonomies. A record closed to indexing is not submitted. Imports, seeders and the console submit nothing.
bash
php artisan seo:submit-urls https://example.com/hello/
php artisan seo:submit-urls https://example.com/gone/ --google --deletedGoogle's quota (200 a day by default, SEO_GOOGLE_INDEXING_QUOTA) is counted per Pacific day.
AI agents
With kitloom/ai installed, an agent reads and changes the head meta of a record or a term — seo_get, seo_update — under plain names (title, description, robots, canonical_url, facebook_image, primary_category…), written by HeadMeta as its user: reading or editing it is editing the record, so only a record its user may edit, and only of a type the site declares. An empty value removes the meta and the type's template applies again. describe_site tells what each name means. kitloom-seo.ai.register false — no SEO tools.
Configuration
config/kitloom-seo.php (php artisan vendor:publish --tag=kitloom-seo-config): which addresses the package serves, the sitemap cache, instant indexing, how large an import from Yoast SEO runs while the admin waits, and the shortcodes.