Search and SEO
Site search
The global search on the front end is fed not from a single source but from providers. The core does not call any module directly (it does not import Modules\*); providers are collected with a tag. A module that is not active does not exist in the container, and the page stays up.
Writing a provider
final class ShopSearchProvider implements SearchProviderInterface
{
public function getKey(): string { return 'shop'; }
public function getLabel(): string { return 'shop.search.label'; }
public function getIcon(): string { return 'heroicons:shopping-bag'; }
public function getPriority(): int { return 50; }
public function search(string $term, string $locale, int $limit): SearchGroup
{
$hits = [];
foreach ($this->products->findByTerm($term, $locale, $limit) as $p) {
$hits[] = new SearchHit(title: $p->getName(), url: '/'.$locale.'/shop/'.$p->getSlug(), excerpt: $p->getSummary());
}
return new SearchGroup(key: 'shop', label: 'Products', icon: 'heroicons:shopping-bag', hits: $hits, moreUrl: '/'.$locale.'/shop?q='.urlencode($term));
}
}
When defined as a service (autoconfigure) the provider is collected automatically. GlobalSearchService calls each provider separately: if a provider fails, only that group drops, and the page does not return 500. When querying inside search(), pay attention to LIKE escapes (%, _, \) and never put user input directly into the query.
The built-in provider in the core: content (Nodes). Modules such as Blog, Forum and Docs add their own.
Limits
The built-in providers use simple text matching (LIKE). For very large content sets, a full-text index (MySQL FULLTEXT) or an external search engine can be written inside a provider; the contract does not prevent this.
Panel live search
Both Studio and AACP have a search in the top bar that returns results as you type. Three built-in sources: content, users and pages in the panel menu (menu items can be searched by name). To add a module, use AdminSearchProviderInterface:
final class ShopAdminSearch implements AdminSearchProviderInterface
{
public function getPriority(): int { return 40; }
public function search(string $term, string $panel, int $limit): ?array
{
if ($panel !== 'studio') { return null; } // give results only in Studio
return [ /* groups and result rows */ ];
}
}
Returning null means "I have no contribution in this panel".
The SEO module
The core knows only the SeoHeadRendererInterface contract: producing a full <head> is optional. The Seo module implements it; if the module is missing, the core settles for basic tags. The Seo module comes with the installation.
Title and description
core.default_meta_title_format(%%title%% – %%site_name%%) andseo.default_title/seo.default_descriptionare the general defaults.- Templates per content type:
seo.page.title_template,seo.blog.title_template,seo.forum.title_template… There is a separate title, description and keyword setting for each module's home page (seo.blog.home_title,seo.forum.home_description…). - The title and description can be overridden on individual content.
Open Graph, Twitter and verification
seo.default_og_image, seo.og_title, seo.og_description, seo.twitter_site, seo.twitter_card, seo.facebook_app_id. Search console verification codes: seo.google_site_verification, seo.bing_site_verification, seo.yandex_site_verification.
Structured data (schema.org)
SchemaOrgBuilder produces JSON-LD according to the content type. It is drawn in the template with the cp_schema_org Twig function. For organization information, seo.organization_name, seo.organization_description and social profile links (seo.same_as) are used.
Sitemap and robots.txt
| Address | What |
|---|---|
/sitemap.xml |
Sitemap |
/robots.txt |
Robots rules |
/llms.txt |
Site summary for AI crawlers (seo.llms_enabled, seo.llms_intro) |
/{lang}/blog/feed.xml |
Blog RSS feed (seo.feed_enabled) |
The sitemap is cached; the cp:seo:rebuild-sitemap command adds missing SEO defaults and clears the cache. The scheduled task seo.rebuild_sitemap does an hourly warm-up.
AI crawlers: seo.ai_search_bots (search-purpose bots) and seo.ai_training_bots (training-purpose bots) are managed separately. So you can allow your site to appear in AI searches while refusing its use in training models.
IndexNow
When content changes, it notifies Bing and Yandex with the IndexNow protocol which URLs have changed. seo.indexnow_enabled; the key file is served under /{key}.txt. Sending is done in batches every 10 minutes by the seo.indexnow task.
Redirects and 404 log
- Redirects (
SeoRedirect): 301/302 from an old address to the new one. This is the way to preserve old URLs when moving a site. - 404 log (
SeoNotFound): counts addresses that were not found; used to turn frequently seen ones into redirects. Rows not seen for 30 days are deleted by theseo.purge_not_foundtask. - The SEO audit screen (Studio) checks the SEO status of published content (the Blog module is required for blog post checks).
Public base address
The seo.public_base_url setting says which main domain canonical addresses are generated with when the site is behind a proxy/CDN.
SEO checklist for the module author
- Your content page should carry
<title>, description andcanonical. - If there are language versions, provide the
hreflangcounterparts. - If your page should be in the sitemap, add a sitemap provider for the module.
- If you change the address structure, leave a 301 map for the old addresses.
- Add a search provider (above).