angeo / module-llms-txt
Magento 2 module for AI Engine Optimization (AEO). Generates spec-compliant llms.txt and llms-full.txt per llmstxt.org standard, plus streaming JSONL for vector indexing. Multi-store, multi-website, CLI, cron, async admin UI, Page Builder-aware sanitization, customer-group pricing, atomic writes, ETag/Cache-Control, .md mirrors.
Angeo LLMs.txt — Magento 2 Module
AI Engine Optimization (AEO) for Magento 2 / Adobe Commerce. Generates
spec-compliant llms.txt, llms-full.txt, and JSONL files so ChatGPT,
Claude, Gemini, Perplexity, and other LLM-powered crawlers can ingest your
catalog efficiently.
Current version: 4.3.0 — fixes an empty blockquote in the generated
files, finishes theSitemap:line in agents.md, and adds router integration
tests. No breaking changes.4.2.0 — reads salable status through Multi-Source
Inventory when MSI is installed, so availability is correct on stores with
more than one stock. No breaking changes.4.1.0 — operational release on top of 4.0.0: skip a
store's catalog pass when nothing changed, per-entity markdown mirror
invalidation on save, and anangeo:llms:cleancommand. No breaking changes
from 4.0.0.4.0.0 does two things: it carries out the
deprecations announced in 3.2.0, and it brings the module in line with
llms.txt v2 (llmstxt.org, 10 August 2026) — link relations, links that
point at markdown versions of pages, and the end of## Optionalas a
mechanical instruction. It also adds availability, image and attribute
export for AI shopping agents, policy links inagents.md, and an agentic
discovery sitemap. It contains breaking changes — read the upgrade notes
in CHANGELOG.md before deploying, especially if any other
module extends the generated files.
What this module does
After install, your storefront serves:
| URL | What it is |
|---|---|
https://shop/llms.txt |
Spec-compliant llmstxt.org file (compact markdown) |
https://shop/llms-full.txt |
Same structure, full sanitized descriptions inline |
https://shop/llms.jsonl |
One JSON record per line, for vector indexing |
https://shop/{url-key}.md |
On-the-fly Markdown mirror of any product/category/CMS page |
https://shop/agents.md |
Operator's manual for AI agents: rules, policies, surfaces |
https://shop/sitemap_agentic_discovery.xml |
Sitemap declaring the files above |
Product, category and CMS pages also carry the llms.txt v2 link relations in
their <head>, so an agent holding a page URL can find both its markdown
version and the llms.txt describing it:
<link rel="alternate" type="text/markdown" href="https://shop/blue-shirt.html.md"/>
<link rel="describedby" href="https://shop/llms.txt"/>
Both markdown URL forms resolve: page.html.md (v1) and page.md (added in
v2). The mirror controller matches either against url_rewrite, with or
without your store's URL suffix.
Generation happens via cron (daily by default), CLI, or the admin "Generate
Now" button. The output is streamed to disk with bounded memory, atomically
renamed on completion, and served with proper ETag / Cache-Control headers.
Why this module exists
LLM crawlers can ingest a typical Magento storefront — full theme, JS, image
sprites, navigation chrome — but that's wasteful for everyone. The
llmstxt.org standard defines a clean text format
optimized for AI ingestion: stable links, structured headings, descriptions
in their natural prose form rather than buried in product cards.
This module produces that format for Magento, with care taken for the things
Magento makes hard: multi-store layout, Page Builder content, CMS directive
resolution, customer-group pricing, and very large catalogs.
A note on the spec
This module follows llmstxt.org v2 (10 August 2026).
Two things are worth stating plainly, because they are commonly misreported:
llms-full.txtis not part of the specification. It is a convention
popularised by Mintlify. The spec keeps llms.txt small and puts the detail
behind links. The format is supported here because it is genuinely useful
for smaller catalogues, but enable it knowingly — it is 5–50× larger and no
crawler is obliged to understand it.- There is no W3C standard for llms.txt. Several articles describe a
"W3C working draft" that forbids llms.txt at subpaths and requires a version
header. No such draft exists — only an open strategy issue from 2025 — and
v2 explicitly permits subpath files, with the most specific one winning.
That is what makes per-store-code files such as/de/llms.txtvalid.
Installation
composer require angeo/module-llms-txt:^4.0
bin/magento module:enable Angeo_LlmsTxt
bin/magento setup:upgrade
bin/magento setup:di:compile # only in production mode
bin/magento setup:static-content:deploy # only in production mode
bin/magento cache:flush
Then generate your first batch:
bin/magento angeo:llms:generate
Visit https://your-store.tld/llms.txt.
Configuration reference
All settings live at Stores → Configuration → Angeo → LLMs.txt.
General
| Field | Default | Notes |
|---|---|---|
| Enable | Yes | Master switch. |
| Exclude This Scope | No | Available at website + store scope. Skips generation for this scope. |
| Store Summary | — | One-line summary used as the spec-compliant blockquote. If empty, falls back to Design → HTML Head → Default Description. |
| Attribution Signature | Yes | One-line Generated by Angeo LlmsTxt … markdown footer on llms.txt / llms-full.txt / agents.md / .md mirrors (never in JSONL). Credits the free module; feel free to disable — nothing else changes. |
| Generate agents.md | Yes | Operator's manual for AI agents at /agents.md: interaction rules + links to the store's machine-readable surfaces (UCP profile and MCP endpoint auto-linked when angeo/module-ucp / angeo/module-mcp-server are installed). Also appends a "For Agents & Developers" section to llms.txt. |
Content
| Field | Default | Notes |
|---|---|---|
| Include Categories | Yes | |
| Include CMS Pages | Yes | |
| Include Products | Yes | |
Products under ## Optional |
No | Changed in 4.0.0. llms.txt v2 removed the mechanical meaning of ## Optional — it no longer tells any tool what to drop, it is only a convention for secondary links. Products are a store's primary content, so they get their own ## Products section. |
| Product Limit | 5000 | 0 = unlimited. |
| Exclude Out-of-Stock Products | No | |
| CMS Identifiers to Exclude | no-route, enable-cookies, privacy-policy-cookie-restriction-mode |
Comma- or newline-separated. |
| Customer Group for Pricing | NOT LOGGED IN | Which group's final price (with special / group prices) is exposed. |
Output formats
| Field | Default | Notes |
|---|---|---|
| Generate llms.txt | Yes | |
| Generate llms-full.txt | No | 5–50× larger; enable only if you actually want it. |
| Generate JSONL | Yes | One record per line; embeds-ready. |
Serve /url-key.md Mirrors |
No | Per-entity Markdown rendering; on-the-fly, no disk. |
| Generate agents.md | Yes | Operator's manual for AI agents. |
| Emit Link Relations | Yes | llms.txt v2 discoverability: rel="alternate" type="text/markdown" and rel="describedby" in <head>, plus a Link: header on the mirrors. Requires mirrors. |
| Link to Markdown Mirrors in llms.txt | Yes | v2 asks that llms.txt links lead to LLM-friendly content, so entity links use the .md URL. JSONL keeps url and adds md_url beside it. Requires mirrors. |
| Serve Agentic Discovery Sitemap | Yes | /sitemap_agentic_discovery.xml, listing only the formats actually enabled for the store. |
Product data
Availability and price are the two facts an AI shopping agent needs before it
recommends anything. Before 4.0.0 the module exported only price.
| Field | Default | Notes |
|---|---|---|
| Include Availability | Yes | in_stock in JSONL, In stock / Out of stock in the markdown formats. Loaded in batch per collection page — no query per product. |
| Availability Source | Auto | Auto reads salable status through MSI when installed, the legacy stock index otherwise. Matters only if you run more than one stock — with a single Default Stock both answers are identical. Legacy forces the old index. |
| Include Image URL | Yes | Absolute URL of the base image (original file, not a resized cache variant). |
| Brand Attribute Code | — | e.g. manufacturer. Exported under the dedicated brand key so agents need not guess which attribute carries it. |
| Additional Attributes | — | Comma- or newline-separated codes, e.g. color, size, material. Dropdown values export as store-view labels, not option IDs. Empty values are dropped per product. Keep the list short — each code adds a column to every collection page. |
Agents.md content
Each field takes an absolute URL or a path relative to the store base URL, so
a CMS page identifier such as shipping-policy works as-is. They render as a
## Key pages section in agents.md; the section is omitted entirely when
nothing is configured, so the file never carries an empty heading.
| Field | Notes |
|---|---|
| Delivery / Returns / Privacy / Terms / About / Support | The policy pages an agent should quote rather than summarise from memory. |
Content sanitization
| Field | Default | Notes |
|---|---|---|
| Resolve CMS Directives | Yes | Renders {{widget}}, {{block}}, {{var}} via Magento's frontend filter. |
| Page Builder Strategy | Exclude | See below. |
| Excluded Content-Types | products, banner, slider, slide, video, map, buttons, button-item, block, dynamic-block, divider, spacer |
Used under Exclude strategy. |
| Allowed Content-Types | text, heading, html, tabs, tab-item, row, column, column-group |
Used under Allow strategy. |
Page Builder strategies
| Strategy | Effect |
|---|---|
| Preserve | Keep all Page Builder content; only strip wrapper attributes. |
| Exclude | Drop elements whose data-content-type is in the excluded list. Default. |
| Allow | Drop everything EXCEPT data-content-type in the allowed list. |
| Strip | Drop ALL elements that carry a data-content-type attribute. |
The filter parses content with DOMDocument (not regex), so nested Page
Builder containers are handled correctly. Known content-types include:
row, column-group, column, tabs, tab-item, text, heading,
html, image, video, map, divider, spacer, buttons, button-item,
banner, slider, slide, products, block, dynamic-block.
Performance
| Field | Default | Notes |
|---|---|---|
| Skip Generation When Nothing Changed | No | Skips a store's whole catalog pass when its products, categories, CMS pages and module config all pre-date the last successful run. Off by default — see the caveat below. |
| Invalidate Markdown Mirror on Save | Yes | Drops an entity's cached .md mirror when it is saved or deleted, like Magento does for the cached HTML page. Costs one url_rewrite lookup per save. |
| Collection Page Size | 500 | Products loaded per collection page while streaming. Lower if you hit memory limits; raise only on hosts with generous PHP memory. |
Read this before enabling Skip Generation When Nothing Changed.
Change detection reads entity timestamps. It therefore does not see
stock movements —cataloginventory_stock_itemhas no timestamp column —
nor prices changed by catalog price rules or scheduled updates, which do not
touch the product row. Since 4.0.0 exports availability, a store with moving
stock would publish stale in-stock flags. Enable it only if your catalog
changes through product saves.angeo:llms:generate --forcealways rebuilds,
and if detection fails for any reason the store is regenerated rather than
skipped.
HTTP caching
| Field | Default | Notes |
|---|---|---|
| Cache-Control TTL (s) | 3600 | Sent as public, max-age=… on the served files. |
Cron
| Field | Default | Notes |
|---|---|---|
| Cron Expression | 0 2 * * * |
Daily at 02:00 server time. |
CLI commands
# Generate everything for all eligible stores
bin/magento angeo:llms:generate
# Single store, skip JSONL
bin/magento angeo:llms:generate --store=default --no-jsonl
# Rebuild even when nothing changed since the last run
bin/magento angeo:llms:generate --force
# Delete generated files and flush the .md mirror cache
bin/magento angeo:llms:clean --store=default
# Per-store/per-format last-run status
bin/magento angeo:llms:status
# Lint generated files against the llms.txt v2 spec
bin/magento angeo:llms:validate
# Same, but warnings fail the build — use this in CI
bin/magento angeo:llms:validate --strict
validate checks the H1, at most one blockquote summary, that no heading sits
between the summary and the first H2 (v2: "sections of any type except
headings"), that every section list item is a real markdown link with an
absolute URL, and that JSONL holds exactly one JSON object per line. It warns
when llms.txt links point at HTML pages while you are serving markdown
mirrors.
Extending — custom entity providers
Add your own section to the generated files (a "Brands" list, blog posts, a
store locator) by implementing Angeo\LlmsTxt\Api\EntityProviderInterface and
registering it on the pipeline.
The pipeline reads the catalog once per store and renders every enabled
format from that single pass. So a provider does not emit markdown or JSON —
it yields format-agnostic EntityRecordInterface records, and the renderers
turn each record into llms.txt, llms-full.txt and JSONL. You write the data
extraction once and get all three formats.
namespace Vendor\Module\Provider;
use Angeo\LlmsTxt\Api\Data\EntityRecordInterface;
use Angeo\LlmsTxt\Api\EntityProviderInterface;
use Angeo\LlmsTxt\Api\OutputContextInterface;
use Angeo\LlmsTxt\Model\Data\EntityRecord;
class BrandProvider implements EntityProviderInterface
{
public function isApplicable(OutputContextInterface $context): bool
{
return true;
}
public function provide(OutputContextInterface $context): iterable
{
// Must be memory-bounded: yield as you page, never build an array.
foreach ($this->brandRepo->getList((int) $context->getStore()->getId()) as $brand) {
yield new EntityRecord(
type: EntityRecordInterface::TYPE_CATEGORY,
entityId: (int) $brand->getId(),
name: (string) $brand->getName(),
url: (string) $brand->getUrl(),
content: (string) $brand->getDescription(),
);
}
}
}
<!-- etc/di.xml -->
<type name="Angeo\LlmsTxt\Model\Pipeline\SinglePassGenerator">
<arguments>
<argument name="entityProviders" xsi:type="array">
<item name="brands" xsi:type="object" sortOrder="50">Vendor\Module\Provider\BrandProvider</item>
</argument>
</arguments>
</type>
Order matters: the bundled StoreEntityProvider must stay first, because it
carries the H1 and the blockquote summary.
Two things to know about the records:
- Content is sanitized by you, once.
getContent()and
getShortContent()must already be plain text at the longest length any
format needs; renderers only truncate downwards. Inject
Angeo\LlmsTxt\Api\SanitizerInterfaceand run your HTML through it. EntityRecordtakes optional trailing arguments —
inStock,imageUrlandattributes(all added in 4.0.0). Named
arguments, as above, are the safe way to construct it.
To render a record differently, implement
Angeo\LlmsTxt\Api\FormatRendererInterface and replace the renderer for that
format in the renderers argument of the same di.xml type.
Migrating from the 3.x ProviderInterface
Api\ProviderInterface, Model\Provider\AbstractProvider and the three
format generators were removed in 4.0.0, together with the compatibility pass
that kept them running. A module still registering a provider on
Model\Generator\LlmsTxtGenerator (or its siblings) will fail at
bin/magento setup:di:compile with the missing class name.
| 3.x | 4.0.0 |
|---|---|
Api\ProviderInterface |
Api\EntityProviderInterface |
extends Model\Provider\AbstractProvider |
implements EntityProviderInterface (no base class) |
yield "## Brands\n\n" — format strings |
yield new EntityRecord(...) — one record per entity |
| One provider per format (three classes) | One provider, rendered into every format |
di.xml → LlmsTxtGenerator.providers |
di.xml → SinglePassGenerator.entityProviders |
escapeMarkdown() / encodeJsonl() from the base class |
Handled by the renderers; you no longer format output |
Section headers are emitted by the renderers based on getType(), so drop
your own ## Heading yields — otherwise they will appear twice.
Hyvä
Natively compatible. No compatibility module, no theme override, no
Tailwind rebuild.
The module ships no frontend JavaScript, no CSS and no LESS. Everything it
serves is either a plain-text file rendered by a controller (llms.txt,
llms-full.txt, llms.jsonl, agents.md, the .md mirrors, the agentic
sitemap) or two <link> tags in the document head. None of that touches the
theme layer, which is where Luma and Hyvä differ.
Specifically:
- The only frontend template,
head/link_relations.phtml, emits two<link>
elements. No jQuery, no Knockout, nox-magento-init, nodata-mage-init,
no CSS classes. It renders identically under both themes, so there is no
hyva_layout variant and no Hyvä-specific template in this module. - The three layout files attach to
head.additional, which Hyvä keeps as an
extension point — Hyvä's own theme uses it to injecthyva.phtml. - Nothing needs to be added to
hyva-themes.json. That registration exists so
a theme can scan a module's CSS for Tailwind classes; this module has no CSS
to scan. - No
setup:static-content:deployis required for the frontend on account of
this module —.phtmlfiles are not static content.
If you do want to change the markup, override
Angeo_LlmsTxt::head/link_relations.phtml in your theme as you would any
template. The block exposes getMarkdownUrl() and getLlmsTxtUrl().
To switch the head tags off entirely while keeping the served files, set
Output Formats → Emit Link Relations to No.
Extending — custom sanitizer filters
Insert your own filter between Page Builder and HTML stripping (e.g. to
remove <script> data attributes, redact phone numbers, etc.) by implementing
Angeo\LlmsTxt\Api\SanitizerFilterInterface and re-declaring the pipeline
in di.xml.
<type name="Angeo\LlmsTxt\Model\Sanitizer\Sanitizer">
<arguments>
<argument name="filters" xsi:type="array">
<item name="cms_directive" xsi:type="object">Angeo\LlmsTxt\Model\Sanitizer\Filter\CmsDirectiveFilter</item>
<item name="page_builder" xsi:type="object">Angeo\LlmsTxt\Model\Sanitizer\Filter\PageBuilderFilter</item>
<item name="redact_pii" xsi:type="object">Vendor\Module\Sanitizer\Filter\PiiRedactionFilter</item>
<item name="html" xsi:type="object">Angeo\LlmsTxt\Model\Sanitizer\Filter\HtmlFilter</item>
<item name="whitespace" xsi:type="object">Angeo\LlmsTxt\Model\Sanitizer\Filter\WhitespaceFilter</item>
</argument>
</arguments>
</type>
Events
Hook in via observers — three events are dispatched per store/format pass:
| Event | Data |
|---|---|
angeo_llms_generation_before |
store, format, context |
angeo_llms_generation_after |
store, format, file, bytes, items, duration |
angeo_llms_generation_failed |
store, format, error |
The events are still dispatched per store and per format, even though
generation is now a single pass — the payload shape is unchanged from 3.x, so
existing observers keep working.
Migrating from 2.x
- Old files in
media/llms/can be deleted (output now lives inmedia/angeo/llms/). - Any custom providers must be rewritten against
Api\EntityProviderInterface; the 3.xProviderInterfacewas removed in 4.0.0. See Migrating from the 3.xProviderInterfaceabove. - Drop any reverse-proxy / Nginx rewrites pointing at the old paths.
- Re-run Stores → Configuration → Angeo → LLMs.txt to set the new fields (Page Builder strategy, customer group, etc.).
- External tooling that called the GET
/admin/angeo_llms/generate/indexURL must switch to the CLI command (the admin endpoint is now POST + CSRF).
Beyond this module
The module gets your store's data into AI answer engines. Whether ChatGPT,
Claude, Gemini, or Perplexity actually cite you is a separate problem —
that's the part we do as a service:
- Free AEO scan — https://angeo.dev/scan: 9 externally-checked signals
(structured data, entity clarity, llms.txt correctness, crawlability, and
more) with a scored report for any Magento / Adobe Commerce store. Good
first step right after installing this module. - AEO audit & implementation — schema/entity optimization, citation
building, and AI-visibility measurement across LLM providers, done for you.
Case study: a demo store's AEO score going from 20% (Critical) to 86%
(Excellent) — see https://angeo.dev/. - Agencies — running this module on client stores? We partner with
Magento agencies on white-label AEO audits. Write to us.
angeo.dev is a registered member of the Anthropic Claude Partner Network.
License
MIT — see LICENSE.
Support
- GitHub Issues: https://github.com/angeo-dev/module-llms-txt/issues
- Email: [email protected]
- Free AEO scan: https://angeo.dev/scan
Changelog
All notable changes to Angeo_LlmsTxt are documented in this file.
The format follows Keep a Changelog,
and this project adheres to Semantic Versioning.
[4.3.0] — 2026-09-08
Loose ends. Three defects that were known and carried, plus the integration
tests. No breaking changes, no new configuration.
Fixed
- Empty blockquote in llms.txt and llms-full.txt. A store with no meta
description and no Custom Summary produced a bare>line. That is worse
than having no summary: to a parser it looks like the spec's summary and
carries nothing. The line is now omitted when the summary is empty, and
angeo:llms:validatereports a bare>as an error instead of counting it
as a valid summary — which it had been doing since 4.0.0. - The
Sitemap:line in agents.md never appeared. It was hardcoded empty
in 3.4.0 with a "resolved in a follow-up" comment.SitemapUrlResolvernow
reads the store's most recently generated sitemap and builds the URL. Reads
thesitemaptable directly rather than depending on Magento_Sitemap's
classes: that module is removable, and a missing sitemap should omit a line,
not fail generation. - Removed
Model\Config\Source\PageBuilderContentType, which nothing
referenced — notsystem.xml, notdi.xml, not any PHP file. It had been
dead since the sanitizer was reworked.
Added
-
Integration tests for the router: that
/llms.txtand
/sitemap_agentic_discovery.xmlare claimed rather than falling through to
the CMS no-route handler, and that with the module disabled/llms.txt
returns a plain 404 so a merchant can serve their own file from the web root.These need the Magento integration test framework and a test database. They
are not inphpunit.xmland not in the GitHub Actions workflow,
which runs without a Magento installation. SeeTest/Integration/README.md.
Notes
- One planned item is deliberately not here. Referencing
llms.txtand
agents.mdfromrobots.txtbelongs inangeo/module-robots-txt-aeo, which
owns that file and already does lossless RFC 9309 round-trip parsing. Writing
robots.txt from two modules is how merchants end up with duplicated
directives.
[4.2.0] — 2026-09-07
Correctness release for one specific case: stores running more than one stock.
No breaking changes.
Added
Availability Source(Product Data, default Auto). Salable status
is read through Multi-Source Inventory when MSI is installed, and through the
legacy stock index otherwise.Legacyforces the old index.Model\Stock\SalableStatusResolver.
Fixed
-
Availability could be wrong on multi-stock stores. 4.0.0 started
publishing salable status as a fact that an AI agent reads and repeats to a
shopper, but it read that fact fromcataloginventory_stock_status, which is
not the source of truth for a sales channel once a store has more than one
stock. Until 4.0.0 the same index was used only to filter out-of-stock
products, where a wrong answer merely meant a product appeared or did not.
Publishing "in stock" wrongly is worse.Single-source merchants — Default Source with Default Stock, which is most of
them — are unaffected either way: MSI keeps the legacy index in sync for the
default stock, so both paths give the same answer.
Notes
- The MSI lookup is batched per collection page
(AreProductsSalableInterface::execute()), never one call per product. - If MSI is asked and cannot answer, the module logs it and falls back to the
legacy index rather than publishing a guess. A SKU missing from a
successful MSI batch is treated as not salable, which is what it means. - MSI is optional. Adobe Commerce and Mage-OS both allow the
Magento_Inventory*
modules to be removed, so the dependency is resolved lazily inside
SalableStatusResolverand nowhere else — type-hinting the interface in a
constructor would breaksetup:di:compileon a store without MSI. Declared
in composersuggest, notrequire. - The out-of-stock filter still goes through the legacy stock helper, which
MSI plugs into. Verify on a multi-stock store that filtering and export agree.
[4.1.0] — 2026-09-07
Operational release. No breaking changes, no output-format changes — a 4.0.0
install can take this without touching configuration.
Added
-
Skip Generation When Nothing Changed(Performance, default No).
When enabled, the nightly run skips a store whose products, categories, CMS
pages and module configuration all pre-date its last successful generation —
no catalog pass at all. The check is fourMAX()reads on indexed timestamp
columns.It is off by default and that is deliberate. Detection reads entity
timestamps, so it does not see stock movements (cataloginventory_stock_item
has no timestamp column) or prices changed by catalog price rules and
scheduled updates. Since 4.0.0 exports availability, a store with moving
stock would publish stale data. Turn it on only if your catalog changes by
product saves. If detection fails for any reason, the store is regenerated. -
bin/magento angeo:llms:generate --force— rebuild regardless. -
Invalidate Markdown Mirror on Save(Performance, default Yes).
Saving or deleting a product, category or CMS page now drops that entity's
cached.mdmirror, the way Magento invalidates the cached HTML page.
Before this, an edited page kept serving its previous markdown until the
HTTP TTL expired — an hour by default, longer if the merchant raised it.
Only the affected entity's keys are removed, never the whole tag, so an
import does not throw away the entire mirror cache. -
bin/magento angeo:llms:clean [--store=…] [--force]— deletes the
generated files and flushes the mirror cache. For when a format is switched
off and its stale file is still being served, and for removing the module's
output before uninstalling. Asks for confirmation unless--force. -
Model\Cache\MdMirrorCacheKey— the mirror cache key rule, extracted so the
controller that fills the cache and the observer that invalidates it cannot
drift apart. Unit-tested. -
Model\Pipeline\ChangeDetector.
Changed
SinglePassGenerator::generateAll()andGenerationService::generateAll()
take a third optionalbool $forceargument. Existing calls are unaffected.Controller\Index\MdMirrortakes an optional trailingMdMirrorCacheKey.
Not in this release, and why
The roadmap called this "incremental generation", implying only changed
products would be rewritten. That is not achievable for these formats and the
wording was wrong. llms.txt, llms-full.txt and llms.jsonl are each a
single ordered file per store, streamed and atomically renamed; rewriting one
product inside one means rewriting the file, which means reading the catalog
again. A per-entity delta would require splitting the output into fragments and
concatenating them, which changes the on-disk contract and the served bytes for
no gain a merchant can observe.
What actually costs time is running the pass at all on a store that did not
change — and that is what this release removes. On stores that do change every
night, the pass still runs in full, and the honest lever there remains
Collection Page Size and the cron schedule.
[4.0.0] — 2026-09-07
Two things happen in this release. The deprecations announced in 3.2.0 are
carried out, and the module is brought in line with llms.txt v2
(llmstxt.org, 10 August 2026). Read the upgrade notes before deploying.
Removed — BREAKING
- The legacy generation pipeline. Single pass is now the only pipeline.
Deleted:Api\ProviderInterface,Model\Provider\AbstractProvider, the
eight bundled providers underModel\Provider\Llms\*and
Model\Provider\Jsonl\*,Model\Generator\AbstractGenerator,
LlmsTxtGenerator,LlmsFullTxtGenerator,JsonlGenerator, and
Model\Config\Source\GenerationMode. Performance → Generation Pipeline(angeo_llms/performance/generation_mode)
andConfig::getGenerationMode()/isSinglePassEnabled()/
MODE_LEGACY/MODE_SINGLE_PASS. A data patch deletes the stored rows.- The compatibility pass for legacy providers. A third-party module still
registering aProviderInterfaceimplementation on one of the deleted
generators will now fail atsetup:di:compilewith the missing class name.
Migrate toApi\EntityProviderInterface— see the README. - PHP 8.1 support. The floor is now 8.2, matching Magento 2.4.7+.
Added — llms.txt v2
- Link relations (
Formats → Emit Link Relations, default Yes).
v2's headline addition: given a page, an agent should be able to find its
markdown version and the llms.txt that covers it without guessing.<link rel="alternate" type="text/markdown">and
<link rel="describedby">in the<head>of product, category and CMS
pages, via a template block you can override in a theme;- the same
rel="describedby"as an HTTPLink:header on the served
markdown mirrors, so the relation also survives for non-HTML resources.
Requires markdown mirrors to be switched on.
- Links inside llms.txt point at markdown mirrors
(Formats → Link to Markdown Mirrors in llms.txt, default Yes when
mirrors are on). v2 asks that llms.txt links lead to LLM-friendly content.
JSONL keeps the canonicalurluntouched and addsmd_urlbeside it —
replacingurlwould break every consumer already indexing the feed. - Both markdown URL forms are supported and documented. v1 specified
page.html.md; v2 also allowspage.md. The mirror controller resolves
either againsturl_rewrite, with or without the store's URL suffix. angeo:llms:validaterewritten to the v2 rules, with--strictfor CI.
It now checks that no heading sits between the summary and the first H2
(v2: "sections of any type except headings"), that every section list item
is a real markdown link, that links are absolute, and it warns when links
point at HTML while mirrors are being served. Reads go through Magento's
Filesystem abstraction, so it works on Adobe Commerce Cloud.
Changed — BREAKING
Products under "## Optional"now defaults to No. v2 removed the
mechanical meaning of## Optional: it no longer instructs any tool to drop
those links, it is only a convention for secondary content. Products are a
store's primary content. Existing installs that saved an explicit value keep
it; installs relying on the default will see products move from
## Optional → ### Productsto a top-level## Products.Api\Data\EntityRecordInterfacegainedisInStock(),getImageUrl()and
getAttributes(). Implementations outside this module must add them.
Model\Data\EntityRecordtakes the three as optional trailing constructor
arguments, so existing instantiations keep working.- JSONL schema is now 4.0.0 (
etc/jsonl-schema.json) withmd_url,
in_stock,imageandattributes. All four are omitted when not
exported, so a consumer can tell "not exported" from "false" or "empty".
Added — product data for agents
Product Dataconfig group. Availability (default Yes), base image URL
(default Yes), a brand attribute code, and a free list of extra attribute
codes. Availability and price are the two facts an AI shopping agent needs
before it recommends anything; the module previously exported only price.- Stock status is loaded in batch per collection page
(Stock::addStockStatusToProducts), never one round-trip per product. - Dropdown attributes are exported as their store-view labels, not option IDs.
The configured brand attribute is re-keyed tobrandso agents do not have
to guess which attribute carries it. Empty values are dropped per product.
Added — agents.md and discovery
Agents.md Contentconfig group — delivery, returns, privacy, terms,
about and support links. Each takes an absolute URL or a path relative to
the base URL, so a CMS identifier such asshipping-policyworks as-is.
They render as a## Key pagessection; the section is omitted entirely
when nothing is configured, so the file never carries an empty heading.
Labels and values go through the same prompt-injection hygiene as the rest
of agents.md./sitemap_agentic_discovery.xml(Formats → Serve Agentic Discovery Sitemap, default Yes) — a small sitemap declaring only the agent-facing
files, mirroring what Shopify publishes. Lists only formats that are
actually enabled for the store: a sitemap pointing at a 404 is worse than
no sitemap. Submit it in Search Console next to the main sitemap.
Fixed
- Carried over from 3.4.1: the single-pass hub block read the output context
from a stream-array key that was never written, which aborted generation for
every store whose llms.txt had content. See the 3.4.1 entry.
Compatibility
- Hyvä: natively compatible, nothing to install. The new head block emits
two<link>tags and ships no JavaScript, CSS or LESS, so it renders
identically under Luma and Hyvä. The layout files attach to
head.additional, which Hyvä keeps as an extension point. No compatibility
module, nohyva_layout variant, nohyva-themes.jsonentry, no Tailwind
rebuild. OverrideAngeo_LlmsTxt::head/link_relations.phtmlin a theme if
you want different markup. - The layout files use
referenceBlockforhead.additional. It is declared
as a block inMagento_Theme, not a container (magento/magento2#16497);
referenceContainerresolves by name at runtime but logs in developer mode. - Magento 2.4.7+ / PHP 8.2–8.5.
Added — tooling
- GitHub Actions CI: lint,
Magento2coding standard, PHPStan and PHPUnit on
PHP 8.2, 8.3, 8.4 and 8.5. - Unit tests for
MarkdownUrland for the new agents.md sections.
Upgrade notes
- Before upgrading, if you run any third-party module that extends the
generated files, check whether it implementsAngeo\LlmsTxt\Api\ProviderInterface.
If it does, it must be migrated toApi\EntityProviderInterfacefirst —
otherwisesetup:di:compilewill fail. composer require angeo/module-llms-txt:^4.0bin/magento setup:upgrade && bin/magento setup:di:compilebin/magento cache:flush. No frontendstatic-content:deployis needed on
account of this release — the new template is a.phtml, which is not
static content.- Review Stores → Configuration → Angeo → LLMs.txt: the new
Product Data and Agents.md Content groups, and the three new fields
under Output Formats. bin/magento angeo:llms:generatethenbin/magento angeo:llms:validate --strict.- Diff the regenerated files against your 3.4.x output. Expected differences:
products no longer nested under## Optional, links pointing at.md
mirrors when those are enabled, and availability on the product lines.
[3.4.1] — 2026-09-07
Hotfix. Upgrade immediately if you run the single-pass pipeline.
Fixed
-
Single-pass generation aborted for every store whose llms.txt had
content.SinglePassGeneratorread the output context from a key that
was never written to the per-format stream array ($s['context']), so
rendering the 3.4.0 "For Agents & Developers" hub block raised anError.
The error was swallowed by the pass-levelcatch (\Throwable), which then
discarded all temporary streams for that store. Net effect with
Performance → Generation Pipeline = Single pass: llms.txt, llms-full.txt
and llms.jsonl were never written, the previous files kept being served
until they went stale, and the admin status panel showed a failure whose
message did not point at the cause.Stores running the default
legacypipeline were not affected —
the legacy generators resolve the context separately.
Notes
- No configuration, API, file-path or output-format changes. Drop-in upgrade
from 3.4.0:composer require angeo/module-llms-txt:3.4.1, then
bin/magento setup:upgrade && bin/magento cache:flushand re-run
bin/magento angeo:llms:generate. - 4.0.0 makes single-pass the only pipeline, so this fix is a prerequisite
for that release.
[3.4.0] — 2026-07-04
agents.md release — storefront parity with the convention Shopify rolled out
to all stores in May 2026. Fully additive, no breaking changes.
Added
- agents.md (
/agents.md, config:Formats → Generate agents.md,
default Yes) — an operator's manual addressed to AI agents: store
facts, machine-readable surfaces, catalog search template, and rules of
interaction (robots.txt authority, live-data preference, checkout on the
merchant's site). Generated by a standaloneAgentsMdGeneratorinvoked by
GenerationService, so it works identically under the legacy and
single-pass pipelines; atomic tmp+rename publish; status recorded in the
same repository the admin panel reads; stale file deleted when disabled. - Soft sibling-module detection (
SurfaceRegistry) — when
Angeo_Ucp/Angeo_McpServerare installed, agents.md and the llms.txt
hub block link the/.well-known/ucpprofile and the/mcpendpoint; no
hard Composer dependency. - "For Agents & Developers" hub block in llms.txt — emitted by both
pipelines right before the attribution signature (llms.txt format only),
mirroring Shopify's native llms.txt structure. Empty when no surfaces
exist — no orphan headings. - Prompt-injection hygiene in agents.md: owner-editable fields (store
name, summary) are stripped of markdown-structural characters before
rendering — agents are designed to trust this file, so it must not be a
vector. - Third signature variant (
utm_medium=agents-md) so the new surface is
measurable independently. - Unit tests:
AgentsMdBuilderTest(structure, conditional surfaces,
injection hygiene, hub block), extendedSignatureTest.
Notes for extension developers
AbstractGenerator,SinglePassGenerator, andGenerationServicegained
optional, defaulted constructor parameters only — 3.0–3.3 signatures keep
working.- New format constant
OutputContextInterface::FORMAT_AGENTS_MD;
FilePathResolvermaps it tomedia/angeo/llms/agents_{storeCode}.md.
The single-pass entity pipeline intentionally skips this format (it is
store-level metadata, not catalog content).
[3.3.0] — 2026-07-02
Attribution & discoverability release. No behavioral changes to generation
logic, file paths, events, or extension points. Fully backward compatible.
Added
- Attribution signature (
Model/Output/Signature, config:
General → Attribution Signature, default Yes). Appends a one-line
spec-compliant markdown footer —Generated by Angeo LlmsTxt vX.Y.Z …with
a link to the free AEO scan — tollms.txt,llms-full.txt, and the
on-the-fly.mdmirrors. Details:- never emitted into JSONL (one-JSON-record-per-line is a hard format
contract); - emitted by both pipelines (legacy and single-pass) byte-identically,
always as the last content in the file (after third-party legacy
providers in single-pass mode); - in
.mdmirrors it is appended before caching, so cached and fresh
responses are byte-identical andContent-Lengthstays correct; - only written when the file has real content (empty outputs still produce
no file at all); - not counted as an "item" in generation stats;
- can be disabled per store/website/default scope with no loss of features.
- never emitted into JSONL (one-JSON-record-per-line is a hard format
- Status panel next-step CTA — the admin status panel now shows a
one-line pointer to the free AEO scan and support contact under the table. - CLI next-step tip —
angeo:llms:generateprints a one-line pointer to
the free AEO scan after at least one successful store generation. - Unit tests for the signature contract
(Test/Unit/Model/Output/SignatureTest).
Fixed
angeo:llms:generateprinted a hardcoded, outdated banner
(Angeo LLMs.txt Generator 3.0). The banner now reads the new
Config::MODULE_VERSIONconstant, which is kept in sync with
composer.json.
Notes for extension developers
AbstractGenerator,SinglePassGenerator, and theMdMirrorcontroller
each gained one optional, defaulted constructor parameter
(?Signature $signature = null). Existing subclasses and DI configurations
compiled against the 3.0–3.2 signatures keep working unchanged.
[3.2.0] — 2026-06-10
Single-pass generation pipeline (opt-in). Fully backward compatible: the
default mode remains legacy, all pre-3.2 behavior, file paths, events, and
extension points keep working unchanged. Everything superseded is marked
@deprecated and will be removed in 4.0.0.
Added
- Single-pass pipeline (
Model/Pipeline/SinglePassGenerator). With
Stores → Configuration → Angeo LLMs.txt → Performance → Generation Pipeline = Single pass, each store's catalog is iterated once and every
enabled format (llms.txt, llms-full.txt, llms.jsonl) is rendered from that
one pass:- one frontend emulation per store (legacy: one per format),
- one url_rewrite warm-up per store (legacy: one per format),
- each entity loaded and sanitized exactly once (legacy: 2–3× per
product description), - all format files written in parallel streams with atomic rename, under one
per-store lock (media/angeo/llms/store_{code}.lock).
Combined with 3.1.1 this gives roughly 3× faster generation on top of the
3.1.1 gains, with identical output files.
- New
@apiextension points (implement these going forward):Api\EntityProviderInterface— yields format-agnostic entity records once
per entity (successor of the format-specificProviderInterface);Api\Data\EntityRecordInterface+Model\Data\EntityRecord— immutable
record DTO carrying already-sanitized content;Api\FormatRendererInterface— serializes records into one output format;Model\Output\FilePathResolver— the single source of truth for generated
file paths (used by both pipelines and the frontend controller);Model\Text\Truncator— shared word-boundary truncation (the Sanitizer
now delegates to it; behavior is byte-identical).
- Bundled single-pass providers/renderers registered via
di.xml
(SinglePassGenerator→entityProviders,renderers). Third parties add
their own items the same way. Model/Config/Source/GenerationMode+ new system.xml field
angeo_llms/performance/generation_mode(global scope, defaultlegacy).- Unit tests:
TruncatorTest, including the down-truncation invariant that
guarantees single-pass renderers reproduce legacy truncation byte-for-byte.
Backward compatibility
generation_modedefaults to legacy — upgrading changes nothing until
you opt in.- In single-pass mode the output files, on-disk paths, served URLs, generation
status records, and theangeo_llms_generation_before/after/failedevents
(dispatched per format) are identical to legacy. - Custom providers built on the legacy
ProviderInterfacekeep working in
both modes. In single-pass mode they are detected automatically (anything
registered on the legacy generators beyond the bundled providers) and
executed through a compatibility pass that appends their output to the
corresponding format stream. - The only semantic difference: the
itemscounter in generation status now
counts rendered records rather than raw stream chunks.
Deprecated (removal in 4.0.0)
Api\ProviderInterfaceandModel\Provider\AbstractProvider— implement
Api\EntityProviderInterfaceinstead.- All eight bundled legacy providers under
Model\Provider\Llms\*and
Model\Provider\Jsonl\*— superseded byModel\Pipeline\Provider\*+
format renderers. Model\Generator\AbstractGenerator,LlmsTxtGenerator,
LlmsFullTxtGenerator,JsonlGenerator— superseded by
SinglePassGenerator; file-path resolution moved toFilePathResolver.- The
legacygeneration mode itself: 4.0.0 ships single-pass as the only
pipeline and removes everything listed above.
Changed (internal, not @api)
Service\GenerationServiceroutes by generation mode; new constructor
dependency (SinglePassGenerator).Controller\Index\Indexresolves file paths viaFilePathResolverinstead
of the deprecated generators (constructor change).Model\Sanitizer\Sanitizeraccepts an optionalTruncator(defaults
internally — existing instantiations and tests are unaffected).AbstractGenerator::getProviders()added so the single-pass pipeline can
discover third-party legacy providers.
Upgrade notes
bin/magento setup:upgrade && bin/magento setup:di:compile- Optional but recommended: switch Performance → Generation Pipeline to
Single pass, runbin/magento angeo:llms:generate, and diff the
generated files against the legacy output for your data. - If you maintain custom providers, plan their migration to
EntityProviderInterfacebefore 4.0.0.
[3.1.1] — 2026-06-10
Performance release. No public-API changes; drop-in upgrade from 3.1.0.
Performance
- Out-of-stock filtering moved into SQL. Both
ProductProviders now use
StockHelper::addIsInStockFilterToCollection()(a JOIN on
cataloginventory_stock_status) instead of oneStockRegistryround-trip
per product. On a 100k-SKU catalog with Exclude Out-of-Stock enabled this
removes ~100,000 queries per format per store. - Prices come from the price index. Product collections call
addPriceData($customerGroupId, $websiteId); the final price (group-aware,
special-/tier-price-aware) is read from the joined
catalog_product_index_pricecolumn instead of invoking the PHP price
calculation chain per product — which for configurable/bundle products
lazy-loads child products (another hidden N+1). A per-product fallback to the
legacy calculation remains for rows missing from the index (e.g. reindex
pending). - Dedicated cron group
angeo_llmswithuse_separate_process=1
(newetc/cron_groups.xml). Long generation runs no longer block
default-group jobs (transactional emails, scheduled indexers, etc.). - Default
collection_page_sizelowered 1000 → 500. Each page holds full
HTML descriptions of every product in memory; 500 halves the peak without a
measurable throughput cost. Explicitly configured values are unaffected. - Duplicate-description sanitization skipped in
llms-full.txt: when
descriptionis byte-identical toshort_description(a common merchant
pattern), the content is sanitized once instead of twice.
Behavior notes
- Exclude Out-of-Stock is now strict: products whose stock status cannot be
resolved are excluded by the SQL filter, whereas 3.1.0 included them on
lookup failure ("default in stock"). With a healthy stock index the output
is identical. - Prices require the price index to be up to date (
bin/magento indexer:reindex catalog_product_price) — standard for any production store; stale index
rows fall back to the slow per-product calculation rather than emitting a
wrong price. - The cron job moved from group
defaultto groupangeo_llms. If your
crontab invokesbin/magento cron:runwith explicit--groupfilters, add
the new group. - Internal constructor change (not
@api): bothProductProviders now take
Magento\CatalogInventory\Helper\Stockinstead of
StockRegistryInterface. Recompile DI (setup:di:compile); if you extended
these concrete classes, update your constructors.
MSI note
Stock filtering still reads the legacy cataloginventory_stock_status table,
which MSI keeps in sync for the default stock. Multi-source/multi-stock setups
that need salable-quantity semantics per stock should override the providers —
now a single JOIN swap instead of a per-product call.
[3.1.0] — 2026-06-10
Security & hardening release following an external security code review.
Upgrading is strongly recommended for all installations, especially those
with the .md mirror feature enabled.
Security
- [HIGH]
.mdmirror no longer serves disabled or hidden entities
(information disclosure).Controller/Index/MdMirrornow verifies entity
state before rendering: products must be Enabled, catalog-visible, and
assigned to the current website; categories must be active; CMS pages must
be active. Previously a staleurl_rewriterow could expose embargoed,
recalled, or intentionally unpublished content — including price and full
description — at/{url_key}.md. Hidden entities now return the same 404
as unknown paths, so their existence is not confirmed. - [HIGH]
.mdmirror DoS mitigation. Rendered markdown is now cached in
the Magento cache (tagANGEO_LLMS_MD, TTL = configured HTTP Cache-Control
TTL), so crawls no longer re-trigger entity loads, CMS directive resolution,
and DOM-based sanitization on every request. Unknown paths are
negative-cached for 5 minutes to blunt enumeration sweeps; request paths
longer than 1024 bytes are rejected outright. The cache is flushed
automatically after every generation run, so mirrors never serve a stale
catalog state for a full TTL. - [HIGH] Frontend router no longer hijacks the
*.mdURL space
(route hijacking / availability). The routersortOrdermoved from 10 to
70 — after the urlrewrite (20), standard (30), and CMS (60) routers — so any
real merchant content whose URL ends in.mdalways wins; this module only
claims paths that would otherwise 404. The.mdbranch is additionally
gated on the md-mirror feature being enabled for the resolved store: when
the feature is off, the router declines the match instead of swallowing the
request with a 404. - [MEDIUM] Template-directive injection surface reduced for product content.
{{block}}/{{widget}}/{{var}}resolution inside product attribute
content (descriptions frequently imported from supplier/PIM feeds) is now
controlled by a separate flag,angeo_llms/sanitizer/resolve_directives_products,
default OFF. When off, directives found in product content are stripped —
never resolved and never leaked as source. CMS pages and categories keep the
existingresolve_directivesbehavior. On any directive-resolution failure
the filter now strips directive source instead of returning it raw. - [MEDIUM]
HtmlFilteroutput-encoding fixes (stored-XSS defense for
downstream consumers; secret-leak prevention):- HTML entities are decoded before the final tag-strip pass, then the
result is stripped again —<script>…</script>can no longer
materialize as live markup in the generated output. - Unterminated
<script>/<style>blocks (and unterminated HTML
comments) are removed to end-of-input, so inline JS — which can carry
analytics tokens or API keys — can never leak intollms.txt,
llms-full.txt, or.mdmirrors.
- HTML entities are decoded before the final tag-strip pass, then the
- [MEDIUM] Wholesale-price disclosure warning. The Customer Group for
Pricing admin field now carries an explicit warning that the generated
files are public and CDN-cacheable, and that selecting a logged-in / B2B
group publishes that group's negotiated pricing to the internet. - [LOW] Admin error messages no longer expose exception internals. The
"Generate Now" and "Schedule" actions log full exceptions to
var/log/system.logand show a generic message in the admin UI. - [LOW]
X-Content-Type-Options: nosniffis now sent on all.mdmirror
responses and all 404 responses (previously only on the file endpoint's
200 responses). - [LOW] Admin status panel embeds its polling URL via
json_encode()
instead of raw string interpolation inside a<script>block, per Magento
secure-rendering guidelines.
Fixed
- Large-file serving no longer loads the whole file into PHP memory.
Controller/Index/Indexstreams files above 4 MB to the client in 256 KB
chunks; concurrent requests for a multi-hundred-MBllms-full.txtcan no
longer exhaust the PHP memory limit.Content-Lengthis now always sent. - All file serving goes through Magento's
Filesystemabstraction —
no nativeis_file/filemtime/file_get_contentson raw paths —
making the endpoint compatible with Adobe Commerce Cloud remote storage
(AWS S3) drivers. - Generation status writes are now concurrency-safe.
GenerationStatusRepositoryperforms a locked read-modify-write (flock on a
sidecar lock file) followed by an atomic tmp-rename, so parallel
generators / cron / CLI runs can no longer lose each other's updates or
leave a truncatedstatus.json. - "Schedule (Async)" no longer piles up duplicate cron jobs. A new run is
only queued when noangeo_llms_generaterow is already pending or running;
the admin is informed otherwise. - Corrected a misleading comment in
MdMirror: the rewrite-lookup fallback
appends the configured.htmlURL suffix (it never tried a trailing slash).
Changed
UrlResolver::warmUp()streamsurl_rewriterows from the DB cursor
instead offetchAll(), roughly halving peak memory on very large rewrite
tables.- New public API:
AbstractGenerator::getRelativePath()(media-relative path
of the generated file; preferred overgetFilePath()for
Filesystem-abstraction readers).getFilePath()is retained for backward
compatibility. - New well-known shared-context key
OutputContextInterface::SHARED_ENTITY_TYPE; all bundled providers and the
.mdmirror publish it before sanitizing so filters can apply
entity-specific policies. Third-party providers are encouraged to do the
same. - Admin field comments updated (md-mirror caching behavior, directive
resolution semantics).
Added
- Config:
angeo_llms/sanitizer/resolve_directives_products(default0). - Cache tag
ANGEO_LLMS_MDfor rendered.mdmirrors (flush with
bin/magento cache:cleanor automatically on each generation run). - Unit tests:
HtmlFiltersecurity regressions (unterminated script blocks,
entity-encoded markup resurrection, legitimate<text preservation) and
CmsDirectiveFilterproduct-content gating.
Upgrade notes
- Run
bin/magento setup:upgrade && bin/magento cache:flushafter deploying. - If you relied on
{{widget}}/{{block}}directives inside product
descriptions being rendered into the generated files, re-enable this
explicitly at Stores → Configuration → Angeo → LLMs.txt → Content
Sanitization → Resolve Directives in Product Content after reviewing the
security note on that field. - If a customization called
AbstractGenerator::getFilePath()to read
generated files, consider migrating togetRelativePath()plus a
Filesystemmedia read-directory for remote-storage compatibility. - Behavior change: URLs ending in
.mdthat collide with real merchant
content are now served by that content (the mirror no longer takes
precedence). URLs of hidden or disabled entities now return 404.
[3.0.5] — 2026-06-04
Admin-config bugfix. Safe drop-in upgrade from 3.0.x.
Fixed
- System Config "Save Config" no longer throws
Cannot read properties of undefined (reading 'settings'). TheGeneratebuttonfrontend_model
template (generate_button.phtml) rendered two<form>elements inside
the admin system-config form (#config-edit-form). Nested forms are invalid
HTML: the browser re-parents the inner inputs/buttons onto the outer form, so
on Save the jQuery validator (jquery.validate.js metadataRules) iterated an
orphaned submit button that has no rule metadata and crashed, aborting the
whole submit. The buttons are now plaintype="button"elements that POST via
a JS-built form appended to<body>(outside the config form). CSRF
protection is unchanged — the form key is still submitted.
Install-blocking bugfix plus PHP 8.5 support. Safe drop-in upgrade from 3.0.x.
Fixed
setup:upgradeno longer fails XSD validation onetc/adminhtml/system.xml.
Two<comment>elements (cache_ttl_secondsandschedule) contained raw
<code>HTML without a CDATA wrapper.system_file.xsdonly allows amodel
child inside<comment>, so the literal markup tripped
Element 'code': This element is not expected. Expected is ( model )and
aborted module loading. Both comments are now wrapped in<![CDATA[ … ]]>,
matching every other HTML-bearing comment in the file.
Changed
- Added PHP 8.5 to the supported range (
…||~8.5.0). Intended for Magento
2.4.9+, which is the first line to support PHP 8.5; on 2.4.8 and earlier,
PHP 8.4 remains the recommended runtime.
Admin-config bugfix. No functional or API changes — safe drop-in upgrade
from 3.0.x.
Fixed
- System Config "Save Config" no longer throws a JS
TypeError. Three
numeric fields inetc/adminhtml/system.xmldeclared validation classes
that are not registered in Magento'smage/validationruleset
(validate-greater-than-zeroandinteger). On 2.4.8-p4 the admin form
validator (jquery.validate.jsmetadataRules) looks up
settingson each rule object; the missing rules resolved toundefined,
producingCannot read properties of undefined (reading 'settings')and
aborting the entire form submit. Replaced with registered rules:collection_page_size: →validate-digits validate-digits-range digits-range-0-1000000product_limit: →validate-digitscache_ttl_seconds: →validate-digits
[3.0.4] — 2026-06-03
Compatibility patch. No functional or API changes — safe drop-in upgrade
from 3.0.x.
Changed
- Lowered the minimum PHP to 8.1 (
~8.1.0||~8.2.0||~8.3.0||~8.4.0).
The module uses no PHP 8.2+ only syntax, so it runs on 2.4.5 / 2.4.6 stores
that are still on PHP 8.1 as well as on 2.4.7 / 2.4.8 (PHP 8.3 / 8.4). - Broadened dependency constraints to cover 2.4.5 through 2.4.8. Every
Magento dependency inrequirenow uses an open lower-bound (>=) pinned to
the major line that shipped with 2.4.5 — e.g.magento/framework: >=102.0
andmagento/module-url-rewrite: >=102.0. Because these major lines do not
change between 2.4.5 and 2.4.8, the module installs cleanly across all of
those minors. This replaces the earlier exact carets (such as the^101.2
onmodule-url-rewrite) that failed on 2.4.8, where that module ships as
102.x.
[3.0.2] — 2026-06-03
Marketplace-readiness patch. No functional or API changes — safe drop-in
upgrade from 3.0.0.
Fixed
- Replaced
md5()withhash('sha256', …)for ETag generation in the
file-serving controller. The Magento Coding Standard forbidsmd5(); the
ETag only needs to be stable and unique, so the switch is behaviour-neutral. - Removed error-silencing
@operators from filesystem calls
(fopen/flock/fclose) in the atomic-write lock helper and in the
validate command. Return values were already checked explicitly, so
dropping@changes no behaviour while clearing the coding-standard errors.
Changed
- Dependency constraints pinned to real 2.4.x major lines.
requirenow
uses caret ranges matching the actual published modules — notably
magento/module-url-rewrite: ^102.0(the 101.2 line never existed). This
resolves acomposer requirefailure on clean 2.4.8 installs. - Added an explicit
versionfield (3.0.1) tocomposer.jsonso the
package version matches the Marketplace submission form.
[3.0.0] — 2026-05-23
A full rebuild against the architectural review of 2.1.4. This release is
not drop-in compatible — see the Breaking Changes section below for
migration steps.
Breaking changes
ProviderInterface::provide()signature changed fromstringto
iterable<string>. Custom providers contributed by third-party modules
must now yield chunks rather than return one concatenated string. This is
the change that lets the generator stream to disk with bounded memory./llms-full.txtnow serves a genuinely-different file (full sanitized
descriptions inline). Previously, this URL silently aliased to/llms.txt,
which was misleading.- llms.txt header is now spec-compliant. A single blockquote summary line,
with currency / locale / base-URL moved to a plain markdown paragraph below.
The 2.x output used four blockquote lines, which broke llmstxt.org-spec
parsers. - Status tracking moved out of
core_config_dataand into
var/angeo_llms/status.json. Old status rows underangeo_llms/status/*
are no longer read. Drop them viabin/magento config:set --lock-env angeo_llms/status/... ""if you want a clean state, but it's harmless to leave them. media/llms/is no longer used as the file output directory; output now
lives undermedia/angeo/llms/. Old files can be deleted; remove any reverse-proxy rewrites pointing at the old path.- Admin "Generate" action moved to POST + CSRF. If you have any external
tooling that hit the old GET URL, switch to the CLI command instead. - Module namespace unchanged: still
Angeo\LlmsTxt. Composer package
name unchanged.
Added
- Page Builder element filter with four strategies — preserve, exclude,
allow, strip — driven by the element'sdata-content-typeattribute.
Default list of excluded types drops common visual-only elements
(products carousel, banner, slider, video, map, buttons, block,
dynamic-block, divider, spacer) so the output focuses on semantic text.
Configurable per-store at Stores → Configuration → Angeo → LLMs.txt →
Content Sanitization. - Streaming generation via PHP generators. Memory stays bounded at one
collection page (default 1000 products) regardless of catalog size. - Atomic writes: each file is written to
.tmp, then renamed. Readers
never see a half-written file. Generation locks via a separate.lockfile
withflock(LOCK_EX | LOCK_NB), so concurrent runs cannot corrupt output. - Cursor pagination by
entity_id ASC > $lastIdinstead of skip/limit, so
products inserted mid-run can neither be duplicated nor skipped. - Batch URL resolver loads every URL rewrite for a store in one query
(vs. the per-productgetProductUrl()query that 2.x triggered N times). - Real
llms-full.txtwith full sanitized descriptions inline. /{url_key}.mdmirrors — every product, category, and CMS page exposes
a clean Markdown rendering at its URL with.mdappended. Generated on the
fly; no extra disk storage.- CMS directive resolution —
{{widget}},{{block}},{{var}}, and
{{store}}directives are now rendered via Magento's standard frontend
filter before being stripped, instead of leaking as literal text. - Customer-group-aware pricing — admin can choose which customer group's
final price (with special-price and group-price applied) gets exposed. - HTTP caching —
ETag,Last-Modified,Cache-Control: public, max-age=,
X-Robots-Tag: noindex, follow, and 304 responses on conditional GETs. - Async admin action — Schedule (Async) inserts a
cron_schedulerow for
the next tick so admins don't have to wait through a synchronous generation. - Live admin status panel polling
/angeo_llms/status/indexevery 60s. - Three CLI commands:
bin/magento angeo:llms:generate [--store=…] [--no-jsonl] [--no-llms] [--no-full]bin/magento angeo:llms:statusbin/magento angeo:llms:validate [--store=…]
- JSONL JSON-Schema at
etc/jsonl-schema.jsonfor downstream pipelines. - Events:
angeo_llms_generation_before,angeo_llms_generation_after,
angeo_llms_generation_failed— for custom hooks. - PHPUnit test suite under
Test/Unit/.
Changed
frontend_default_meta_descriptionis now the fallback for the store
summary, before falling back to the generic stub.- Multi-store store-code routing handles the last URL path segment, so
/de/llms.txtworks on path-based stores. - Spec compliance: products go under
## Optionalby default (admin
toggleable) so context-budget-constrained clients can drop them. - Out-of-stock products excluded by an explicit
StockRegistrylookup
(configurable). - Logger context is now structured: every log line is prefixed
[Angeo LlmsTxt]and includes store/format keys.
Fixed
- Pseudo-locking in 2.x: a
'w'open truncates the file before the
flock()call, so two concurrent generations both saw an empty file and
the last writer won unpredictably. 3.0 uses a separate.lockfile. - CSRF-exposed admin generate: 2.x used a GET URL; 3.0 requires POST with
the form key. - Synchronous admin "Generate" timing out on large catalogs (now async option).
- N+1 URL rewrite queries: now batched.
- Literal
{{widget}}text appearing in 2.x output: now resolved. - Stale files for stores that became inactive or excluded: now cleaned up
on every generation run.
Removed
media/llms/legacy directory (see breaking-changes notes).- GET endpoint for admin generation.
- Documented-but-non-existent config fields from 2.x README.
[2.1.4] — Pre-rebuild baseline
Last release in the 2.x line. See the architectural review document for
the issues that motivated 3.0.0.
| Version | Stability | QA Status | Compatibility | Released |
|---|---|---|---|---|
| 4.3.3 | stable | Fail | Magento 2.4.7-2.4.9 Details | 2026-09-16 17:44:51 |
| 4.3.2 | stable | Fail | Not compatible Details | 2026-09-11 19:40:10 |
| 4.3.1 | stable | Not tested | Not yet tested Details | 2026-09-11 19:32:21 |
| 4.3.0 | stable | Fail | Magento 2.4.7-2.4.8 Details | 2026-09-11 16:48:10 |
| 3.4.0 | stable | Not tested | Not yet tested Details | 2026-09-11 16:17:36 |
| 3.2.0 | stable | Fail | Magento 2.4.7-2.4.9 Details | 2026-06-14 18:59:28 |
| 3.0.5 | stable | Fail | Magento 2.4.7-2.4.9 Details | 2026-06-04 19:39:51 |
| 3.0.4 | stable | Not tested | Not yet tested Details | 2026-06-03 18:23:19 |
| 3.0.3 | stable | Not tested | Not yet tested Details | 2026-06-03 18:04:49 |
| 3.0.2 | stable | Not tested | Not yet tested Details | 2026-06-03 17:46:25 |
| 3.0.1 | stable | Not tested | Not yet tested Details | 2026-06-03 16:17:59 |
| 3.0.0 | stable | Not tested | Not yet tested Details | 2026-05-29 20:31:58 |
| 2.1.4 | stable | Not tested | Not yet tested Details | 2026-05-06 04:36:21 |
| 2.1.3 | stable | Not tested | Not yet tested Details | 2026-04-30 07:41:35 |
| 2.1.2 | stable | Not tested | Not yet tested Details | 2026-04-30 05:05:25 |
| 2.1.1 | stable | Not tested | Not yet tested Details | 2026-04-29 20:38:27 |
| 2.1.0 | stable | Not tested | Not yet tested Details | 2026-04-29 20:07:40 |
| 2.0.0 | stable | Not tested | Not yet tested Details | 2026-04-16 18:52:27 |
| 1.1.2 | stable | Not tested | Not yet tested Details | 2026-03-20 18:38:35 |
| 1.1.1 | stable | Not tested | Not yet tested Details | 2026-03-18 18:33:44 |
Requires 13
| Package | Constraint |
|---|---|
| ext-json | * |
| ext-mbstring | * |
| magento/framework | ^103.0 |
| magento/module-backend | ^102.0 |
| magento/module-catalog | ^104.0 |
| magento/module-catalog-inventory | ^100.4 |
| magento/module-catalog-url-rewrite | ^100.4 |
| magento/module-cms | ^104.0 |
| magento/module-config | ^101.2 |
| magento/module-cron | ^100.4 |
| magento/module-store | ^101.0 |
| magento/module-url-rewrite | ^102.0 |
| php | ~8.1.0||~8.2.0||~8.3.0||~8.4.0||~8.5.0 |
Requires-dev 4
| Package | Constraint |
|---|---|
| bitexpert/phpstan-magento | ^0.43 |
| magento/magento-coding-standard | ^40.0 || ^41.0 |
| phpstan/phpstan | ^2.0 |
| phpunit/phpunit | ^10.5 |
Suggests 3
| Package | Reason |
|---|---|
| magento/module-inventory-sales-api | Read salable status through Multi-Source Inventory. Optional — the module falls back to the legacy stock index when MSI is not installed. |
| magento/module-page-builder | Enable to opt-in or opt-out of Page Builder content elements per content-type during sanitization |
| magento/module-shared-catalog | Adobe Commerce: integrate B2B shared catalogs so llms.txt only exposes the allowed catalog |
No QA results yet
QA pipelines haven't run for this version. Compatibility and quality results appear here once the vendor publishes a tagged release that gets ingested.
More from angeo
View vendorMagento 2 module for AI Engine Optimization (AEO). Injects AI crawler rules (OAI-SearchBot, GPTBot, ChatGPT-User, PerplexityBot, Perplexity-User, Google-Extended, ClaudeBot, anthropic-ai, Claude-User, Applebot, cohere-ai, Amazonbot, Meta-ExternalAgent) into robots.txt — without overwriting your existing configuration. Supports per-bot Allow/Disallow lists, Crawl-delay, Sitemap directives, multi-store, and a public Api\RobotsStatusInterface for cross-module integration with angeo/module-aeo-audit.
Live AI brand visibility audit for Magento 2. Queries ChatGPT, Claude, Perplexity, Gemini and Groq with brand-probing prompts and scores real-world AI recall, citation rate and recommendation presence. Extends angeo/module-aeo-audit v3 via CheckerInterface as the 16th signal, alongside the 15 built-in technical checks.
Magento 2 AEO (AI Engine Optimization) Audit. v3 covers 15 signals — robots.txt AI bots, llms.txt + llms.jsonl, Product / Organization / FAQ schema, merchant return + shipping policies, sitemap.xml, UCP profile, AI product feed, OG tags, canonical + hreflang, JSON-LD quality, well-known endpoint matrix, Core Web Vitals via CrUX. Score Trend dashboard, Admin UI, cron, dynamic fix commands, dependency-injected extension point for custom checkers.
Spec-compliant Universal Commerce Protocol (UCP) profile generator for Magento 2. Generates /.well-known/ucp at protocol version 2026-04-08 with ECDSA P-256 signing keys, declared capabilities, and proper cache headers. v0.1.x is profile-only — catalog, cart, checkout endpoints land in later releases.
Turn an existing module into recurring revenue.
If you already maintain a Magento 2 module on GitHub or GitLab, listing it on Packagento takes about five minutes. We mirror your tags, handle distribution signing, and route paid licenses through Stripe Connect, so you can keep shipping the way you already do.