All skills

Craft CMS 5 front-end Twig development — atomic design, template architecture, components, Vite buildchain. Covers atoms/molecules/organisms, props/extends/block patterns, layout chains, view routing, content builders, image presets, Tailwind named-key collections, multi-brand CSS tokens, JavaScript boundaries (Alpine/DataStar/Vue, tabs, accordions), Vite asset loading, and front-end auth (login, registration, password reset, profiles). Triggers on: {% include ... only %}, {% embed %}, _atoms/_molecules/_organisms/_views/_builders, component--variant.twig, _component--props.twig, collect({}), utilities prop, data-brand theming, hero/card components, Matrix block rendering, craft.vite.script, vite.php, vite.config.ts, buildchain, per-page scripts, Blitz static/page caching, ImageOptimize, Imager-X, responsive images, srcset, image transforms, SEOmatic meta/OpenGraph/JSON-LD, Sprig, htmx, multi-language, hreflang, localization, Formie form styling, login/registration form, RSS/Atom/JSON feeds, XML sitemap, search page, .search(), headless GraphQL, Next.js/Nuxt/Astro integration, example-templates command, render builder, fluent BaseTag {{ tag.render() }}, progressive enhancement. Always use when creating, editing, or reviewing Craft front-end Twig templates, components, layouts, views, builders, buildchain, or front-end auth — including plugin template integration (Blitz, SEOmatic, Sprig, Formie, Imager-X). Do NOT trigger for PHP plugin/module development (craftcms) or content modeling (craft-content-modeling).

Use this Skill: https://skilld.dev/gh/michtio/craftcms-claude-skills/craft-site

This session only. Nothing lands on disk.

referencessearch.md

≈2.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Search

How Craft CMS 5's search system works for site builders: search syntax, Twig search queries, search indexing, configuration, and rebuilding. For extending search with custom searchable attributes (plugin development), see the craftcms skill's elements.md. For advanced search needs (Typesense, Algolia, Meilisearch), consider dedicated search service plugins.

Documentation

Common Pitfalls

  • Enabling subLeft in defaultSearchTermOptions — forces LIKE '%term%' on every search, destroying performance on large datasets. Use only when absolutely needed for specific queries.
  • Expecting search to find 1-2 character words — MySQL's default innodb_ft_min_token_size is 3. Words shorter than this are not indexed. Configure in MySQL, not Craft.
  • Not rebuilding the search index after content modeling changes — new fields marked as searchable need resave with --update-search-index to populate the index.
  • Relying on field-scoped search (fieldHandle::term) against a reused field — the index can't tell instances apart, so summary::daisy may match a byline instance of the same field. See Reused fields are invisible to instance-scoped search.
  • Passing raw user input to .search() without understanding enumeration risk — search syntax allows field-targeted queries (body:secret) that may reveal content structure.

Contents

Search Syntax

Users and template queries can use these search operators:

Syntax Meaning Example
term Contains word salty
term1 term2 Contains both (AND) salty dog
term1 OR term2 Contains either salty OR sweet
-term Does not contain salty -dog
"exact phrase" Contains exact phrase "salty dog"
field:term Field contains word (substring) body:salty
field::term Field contains exact word body::salty
field::"exact phrase" Field contains exact phrase body::"salty dog"
-field:* Field is empty -body:*
field:* Field is not empty body:*
*term Ends with (wildcard left) *alty
term* Starts with (wildcard right) salt*
*term* Contains substring *alt*

field refers to the field handle (e.g., body, summary, title).

Search in Twig

Basic search

{% set results = craft.entries()
    .section('blog')
    .search(query)
    .all() %}

When .search() is used, results are ordered by search score by default.

Search form

{# search/_form.twig #}
<form action="{{ url('search') }}" method="get">
    <input type="search"
           name="q"
           value="{{ craft.app.request.getQueryParam('q') }}"
           placeholder="Search...">
    <button type="submit">Search</button>
</form>

Search results page

{# search/index.twig #}
{% set query = craft.app.request.getQueryParam('q') %}

{% if query %}
    {% set results = craft.entries()
        .search(query)
        .orderBy('score')
        .limit(20)
        .all() %}

    {% if results | length %}
        {% for entry in results %}
            <article>
                <h2><a href="{{ entry.url }}">{{ entry.title }}</a></h2>
                {% if entry.summary is defined and entry.summary %}
                    <p>{{ entry.summary | striptags | truncate(200) }}</p>
                {% endif %}
            </article>
        {% endfor %}
    {% else %}
        <p>No results found for "{{ query }}".</p>
    {% endif %}
{% endif %}

Combining search with other filters

{% set results = craft.entries()
    .section('blog')
    .relatedTo(category)
    .search(query)
    .all() %}

Search works alongside all other query parameters. The .search() param adds a score-based ordering and keyword matching filter.

Paginated search results

{% set query = craft.app.request.getQueryParam('q') %}

{% paginate craft.entries()
    .search(query)
    .limit(10) as pageInfo, results %}

{% for entry in results %}
    {# ... #}
{% endfor %}

{% include '_partials/pagination' with { pageInfo: pageInfo } only %}

Multi-element search

{# Search across entries and assets #}
{% set entries = craft.entries().search(query).all() %}
{% set assets = craft.assets().search(query).all() %}

There is no built-in cross-element-type search query. Search each type separately and merge/interleave results in Twig if needed.

Search Configuration

General config settings

In config/general.php:

Setting Default Purpose
defaultSearchTermOptions [] Default options applied to all search queries

Options within defaultSearchTermOptions:

Option Default Effect
subLeft false Match beginning of words (%term). Expensive.
subRight true Match end of words (term%). Safe default.
exclude false Negate the term
exact false Require exact word match
// Only enable subLeft when you really need it
->defaultSearchTermOptions(['subLeft' => true])

Search component config

In config/app.php:

'components' => [
    'search' => [
        'useFullText' => true,        // MySQL only, ignored on PostgreSQL
        'minFullTextWordLength' => 3,  // Must match MySQL's innodb_ft_min_token_size
    ],
],

useFullText enables MySQL's FULLTEXT index for search. When true, Craft uses MATCH() AGAINST() queries. When false, falls back to LIKE queries (slower on large datasets).

minFullTextWordLength must match your MySQL server's innodb_ft_min_token_size setting. If MySQL is configured for minimum 4-character words but Craft thinks it's 3, short searches silently return no results.

Search Indexing

How indexing works

Craft stores search keywords in the searchindex table:

Column Purpose
elementId The indexed element
siteId Site context
attribute Attribute or field handle
fieldId Field ID (null for native attributes)
keywords Normalized keyword text

When an element is saved, Craft clears and rebuilds all search index rows for that element. Keywords are normalized (lowercased, stripped of punctuation) before insertion.

What is indexed by default

Element Type Default Indexed Attributes
Entries title, slug
Assets filename, extension, kind
Users username, firstName, lastName, fullName, email
Tags/Categories title

Custom fields are indexed when "Use this field's values as search keywords" is enabled in the field layout designer.

Making custom fields searchable

In the field layout designer (CP), click a field's gear icon and enable "Use this field's values as search keywords." This setting is per-field-layout-element, not per-field — the same field can be searchable in one layout and not in another.

After enabling, resave affected elements to populate the index.

Reused fields are invisible to instance-scoped search

The search index does not distinguish between instances of a reused field. If one field definition is instanced under different handles (e.g. summary and byline), a field-scoped search can't tell them apart: summary::daisy may also match entries that mention "daisy" in their byline, and vice versa. This is a documented limitation — the index records keywords against the field, not the instance. If you rely on distinct field-scoped keywords, give those fields separate definitions rather than reusing one (see the craft-content-modeling skill → Field Instances). Note this is specific to search; element-query filtering by the overridden handle (.summary(x).byline(y)) works fine.

Rebuilding the Search Index

Command When to Use
ddev craft resave/entries --update-search-index After enabling search on existing fields
ddev craft resave/entries --section=blog --update-search-index Rebuild for a specific section
ddev craft resave/entries --update-search-index --queue Async rebuild via queue
ddev craft resave/all --update-search-index Full rebuild across every element type

resave/entries (and resave/assets, resave/users) with --update-search-index is selective — it only re-indexes the specified element type. resave/all --update-search-index re-saves every element type, rebuilding the entire search index.

Score and Ranking

Search results include a relevance score that determines default ordering. Approximate relative weights (varies by search mode and Craft version):

Match Type Approximate Weight
Title match Very high
Exact word match Very high
Phrase match High
Substring match Medium
Body match Base

Ordering by score

{# Results ordered by relevance (default when .search() is used) #}
{% set results = craft.entries().search(query).all() %}

{# Explicit score ordering #}
{% set results = craft.entries().search(query).orderBy('score').all() %}

{# Override: order by date instead of score #}
{% set results = craft.entries().search(query).orderBy('postDate DESC').all() %}

Accessing search scores

Each element in search results has a searchScore attribute:

{% for entry in results %}
    {# searchScore is a float, higher = more relevant #}
    {# Only populated when .search() is used in the query — null otherwise #}
    <p>{{ entry.title }} (score: {{ entry.searchScore }})</p>
{% endfor %}

When to use a dedicated search service

Craft's built-in search is adequate for most sites. Consider a dedicated service (Typesense, Algolia, Meilisearch) when you need:

  • Fuzzy matching and typo tolerance
  • Faceted search and filtering
  • Search across 100k+ elements with sub-100ms response
  • Autocomplete/suggestions
  • Synonym handling

Source: SKILL.md on GitHub

1 warning16d3 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill is generally safe and follows professional Craft CMS development practices. A low-severity risk regarding indirect prompt injection was identified due to the typical architectural pattern of rendering rich-text content from the CMS database into the front-end templates.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: MEDIUM · 1 issue

Signed by skilld at d3a91c1. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 2 months ago

README badge

README badge for michtio/craftcms-claude-skills/craft-site