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.

referencesfeeds.md

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

Feeds, RSS & XML Output

How to build RSS, Atom, JSON Feed, and XML sitemap templates in Craft CMS 5. Pure Twig — no plugins required for basic feeds. For SEOmatic-managed sitemaps, see plugins/seomatic.md. For JSON API endpoints, see plugins/element-api.md.

Documentation

Common Pitfalls

  • Forgetting {% header %} for content type — browsers will render XML as HTML without the correct Content-Type header.
  • Using |date instead of |rss or |atom — RSS and Atom have specific date format requirements. The dedicated filters handle them correctly.
  • Not wrapping HTML content in <![CDATA[...]]> — HTML entities in RSS <description> will break XML parsers.
  • Including draft or disabled entries in feeds — always use the default query (which excludes them) or explicitly filter with .status('live').

Contents

RSS 2.0 Feed

{# templates/_feeds/rss.twig #}
{% header "Content-Type: application/rss+xml; charset=utf-8" %}
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>{{ siteName }}</title>
        <link>{{ siteUrl }}</link>
        <description>{{ siteName }} RSS Feed</description>
        <language>{{ currentSite.language }}</language>
        <atom:link href="{{ url('feed/rss') }}" rel="self" type="application/rss+xml" />
        <lastBuildDate>{{ now | rss }}</lastBuildDate>

        {% set entries = craft.entries()
            .section('blog')
            .orderBy('postDate DESC')
            .limit(20)
            .all() %}

        {% for entry in entries %}
            <item>
                <title>{{ entry.title | e('html') }}</title>
                <link>{{ entry.url }}</link>
                <guid isPermaLink="true">{{ entry.url }}</guid>
                <pubDate>{{ entry.postDate | rss }}</pubDate>

                {% if entry.summary is defined and entry.summary %}
                    <description><![CDATA[{{ entry.summary }}]]></description>
                {% endif %}

                {% if entry.featuredImage is defined %}
                    {% set image = entry.featuredImage.one() %}
                    {% if image %}
                        <enclosure url="{{ image.url }}"
                                   length="{{ image.size }}"
                                   type="{{ image.mimeType }}" />
                    {% endif %}
                {% endif %}

                {% if entry.categories is defined %}
                    {% for category in entry.categories.all() %}
                        <category>{{ category.title | e('html') }}</category>
                    {% endfor %}
                {% endif %}
            </item>
        {% endfor %}
    </channel>
</rss>

Adding to <head>

{# In your layout #}
<link rel="alternate"
      type="application/rss+xml"
      title="{{ siteName }} RSS"
      href="{{ url('feed/rss') }}">

Atom Feed

{# templates/_feeds/atom.twig #}
{% header "Content-Type: application/atom+xml; charset=utf-8" %}
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <title>{{ siteName }}</title>
    <link href="{{ siteUrl }}" />
    <link href="{{ url('feed/atom') }}" rel="self" />
    <id>{{ siteUrl }}</id>
    <updated>{{ now | atom }}</updated>

    {% set entries = craft.entries()
        .section('blog')
        .orderBy('postDate DESC')
        .limit(20)
        .all() %}

    {% for entry in entries %}
        <entry>
            <title>{{ entry.title | e('html') }}</title>
            <link href="{{ entry.url }}" />
            <id>{{ entry.url }}</id>
            <published>{{ entry.postDate | atom }}</published>
            <updated>{{ entry.dateUpdated | atom }}</updated>

            {% if entry.summary is defined and entry.summary %}
                <summary type="html"><![CDATA[{{ entry.summary }}]]></summary>
            {% endif %}

            {% if entry.author %}
                <author>
                    <name>{{ entry.author.fullName | e('html') }}</name>
                </author>
            {% endif %}
        </entry>
    {% endfor %}
</feed>

JSON Feed

{# templates/_feeds/feed.json.twig #}
{% header "Content-Type: application/feed+json; charset=utf-8" %}
{% set entries = craft.entries()
    .section('blog')
    .orderBy('postDate DESC')
    .limit(20)
    .all() %}

{{ {
    version: 'https://jsonfeed.org/version/1.1',
    title: siteName,
    home_page_url: siteUrl,
    feed_url: url('feed.json'),
    language: currentSite.language,
    items: entries | map(entry => {
        id: entry.url,
        url: entry.url,
        title: entry.title,
        date_published: entry.postDate | atom,
        date_modified: entry.dateUpdated | atom,
        summary: entry.summary is defined ? entry.summary | striptags : null,
        authors: entry.author ? [{ name: entry.author.fullName }] : [],
    }),
{# |raw is safe here — json_encode output is not user-controlled HTML #}
} | json_encode(constant('JSON_PRETTY_PRINT') b-or constant('JSON_UNESCAPED_SLASHES')) | raw }}

XML Sitemap

For SEOmatic-managed sitemaps (recommended for most sites), see plugins/seomatic.md. For a custom Twig-based sitemap:

{# templates/sitemap.xml.twig #}
{% header "Content-Type: application/xml; charset=utf-8" %}
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    {# Homepage #}
    <url>
        <loc>{{ siteUrl }}</loc>
        <lastmod>{{ now | atom }}</lastmod>
        <changefreq>daily</changefreq>
        <priority>1.0</priority>
    </url>

    {# All sections with URLs #}
    {% for section in craft.app.entries.allSections() %}
        {% if section.hasUrls %}
            {% set entries = craft.entries()
                .section(section.handle)
                .orderBy('dateUpdated DESC')
                .all() %}

            {% for entry in entries %}
                <url>
                    <loc>{{ entry.url }}</loc>
                    <lastmod>{{ entry.dateUpdated | atom }}</lastmod>
                </url>
            {% endfor %}
        {% endif %}
    {% endfor %}
</urlset>

Sitemap index (for large sites)

{# templates/sitemap-index.xml.twig #}
{% header "Content-Type: application/xml; charset=utf-8" %}
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    {% for section in craft.app.entries.allSections() %}
        {% if section.hasUrls %}
            {% set latest = craft.entries().section(section.handle).orderBy('dateUpdated DESC').one() %}
            {% if latest %}
                <sitemap>
                    <loc>{{ url("sitemap-#{section.handle}.xml") }}</loc>
                    <lastmod>{{ latest.dateUpdated | atom }}</lastmod>
                </sitemap>
            {% endif %}
        {% endif %}
    {% endfor %}
</sitemapindex>

Custom Routes

In config/routes.php:

return [
    'feed/rss' => ['template' => '_feeds/rss'],
    'feed/atom' => ['template' => '_feeds/atom'],
    'feed.json' => ['template' => '_feeds/feed.json'],
    'sitemap.xml' => ['template' => 'sitemap.xml'],
    'sitemap-index.xml' => ['template' => 'sitemap-index.xml'],

    // Dynamic section feed
    'feed/<section:{handle}>' => ['template' => '_feeds/rss'],
];

Named parameters (like <section:{handle}>) are available as template variables.

Multi-site feed routes

For multi-site, routes are site-specific in the CP (Settings > Routes) or use site-prefixed patterns:

return [
    'en' => [
        'feed/rss' => ['template' => '_feeds/rss'],
    ],
    'nl' => [
        'feed/rss' => ['template' => '_feeds/rss'],
    ],
];

Date Filters

Filter Format Example Output
|rss RFC 2822 Wed, 31 May 2023 12:23:09 -0700
|atom ISO 8601 2023-05-31T12:23:09-07:00

Both filters accept an optional timezone parameter:

{{ entry.postDate | rss('UTC') }}
{{ entry.postDate | atom('America/New_York') }}
{{ entry.postDate | rss(false) }}  {# Preserve the date's existing timezone #}

Use |atom for JSON Feed dates, Atom feeds, and sitemaps. Use |rss for RSS 2.0 feeds.

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