Hooks & filters
Every llmstxt_* hook is a stable extension point. Drop code into a theme’s functions.php, a must-use plugin, or a site-specific plugin - the plugin picks it up without any configuration changes.
Filters
llmstxt_template_llmstxt
Filters the site-index template string before generation runs. Use this to restructure the top-level llms.txt layout in code rather than through the admin UI.
| Parameter | Type | Description |
|---|---|---|
$template | string | The full Mustache template for the site index |
Returns: string - the modified template.
Example: Add a custom ## Podcasts section to the index when a podcast CPT is registered.
add_filter( 'llmstxt_template_llmstxt', function ( $template ) {
if ( ! post_type_exists( 'podcast' ) ) {
return $template;
}
$section = "\n## Podcasts\n{{podcasts_list}}\n";
// Insert before the closing footer line
return str_replace( '---', $section . '---', $template );
} );
llmstxt_template_{type}
Filters the per-item template for a specific content type. The {type} portion is replaced with the content type slug when the hook fires.
| Parameter | Type | Description |
|---|---|---|
$template | string | The Mustache template for this content type |
$post | WP_Post | The post object being rendered (not available for term types) |
Returns: string - the modified template.
Valid {type} values:
| Hook name | Applies to |
|---|---|
llmstxt_template_post | Blog posts |
llmstxt_template_page | Pages |
llmstxt_template_product | WooCommerce products |
llmstxt_template_product_cat | WooCommerce product categories |
llmstxt_template_cpt_default | All other public custom post types |
llmstxt_template_tax_default | All other public custom taxonomies |
Example 1: Append a custom _review_score meta field to every post entry.
add_filter( 'llmstxt_template_post', function ( $template, $post ) {
$score = get_post_meta( $post->ID, '_review_score', true );
if ( $score ) {
$template .= "\n**Review score:** {$score}/10\n";
}
return $template;
}, 10, 2 );
Example 2: Add warranty information to products in a specific category, using a conditional block so it only appears when the field is non-empty.
add_filter( 'llmstxt_template_product', function ( $template, $post ) {
if ( ! has_term( 'electronics', 'product_cat', $post->ID ) ) {
return $template;
}
$warranty_block = "\n{{#meta_warranty_years}}**Warranty:** {{meta_warranty_years}} years{{/meta_warranty_years}}\n";
return $template . $warranty_block;
}, 10, 2 );
meta_warranty_yearsmust be listed under Settings → Advanced → Custom Field Mapping (a Pro feature) for the variable to resolve.
Export visibility
Public llms.txt and .md output only includes posts that pass export visibility checks. Before any filter runs, the plugin enforces publish status, password protection, and scheduled-future dates.
Posts marked noindex in a supported SEO plugin are excluded automatically. Built-in support covers:
- Yoast SEO
- Rank Math
- SEOPress
- All in One SEO (AIOSEO)
- Slim SEO
If SEO meta is missing or ambiguous, the post is included by default - the plugin does not over-exclude.
Use the filters below to add support for other SEO plugins, override a noindex decision, or apply custom editorial rules.
llmstxt_seo_noindex_check
Runs after built-in SEO plugin checks. Use this to extend noindex detection or override a result before the final export gate.
| Parameter | Type | Description |
|---|---|---|
$is_noindex | bool | true when any built-in SEO check flagged the post as noindex |
$post | WP_Post | The post being evaluated |
Returns: bool - true to treat the post as noindex (exclude from public output), false to allow export.
When to use:
- Add support for an SEO plugin not in the built-in list
- Override a false positive (force-include a noindexed post in llms.txt)
- Apply a custom editorial exclusion rule via post meta
Example 1 - add The SEO Framework support: The site runs The SEO Framework (not in the built-in list). Its noindex flag is stored in _genesis_noindex:
add_filter( 'llmstxt_seo_noindex_check', function ( bool $is_noindex, WP_Post $post ): bool {
if ( $is_noindex ) {
return true;
}
if ( function_exists( 'the_seo_framework' ) ) {
$tsf_noindex = get_post_meta( $post->ID, '_genesis_noindex', true );
if ( '1' === $tsf_noindex ) {
return true;
}
}
return false;
}, 10, 2 );
Example 2 - force-include a strategic landing page: A PPC landing page is set to noindex in Yoast (to keep it out of Google), but the marketing team still wants AI assistants to discover it via llms.txt. Return false for that specific post:
add_filter( 'llmstxt_seo_noindex_check', function ( bool $is_noindex, WP_Post $post ): bool {
// Post ID 482: /spring-promo - noindex in Yoast, but include in AI output.
if ( 482 === $post->ID ) {
return false;
}
return $is_noindex;
}, 10, 2 );
This filter runs before
llmstxt_post_is_publicly_exportable. Returningfalsehere does not bypass publish status, password protection, or scheduled-future checks - those are always enforced.
llmstxt_post_is_publicly_exportable
Final gate for whether a post may appear in public llms.txt / .md output. Runs after all built-in checks and llmstxt_seo_noindex_check.
| Parameter | Type | Description |
|---|---|---|
$exportable | bool | Default true when all prior checks passed |
$post | WP_Post | The post being evaluated |
Returns: bool - false to exclude the post from public output.
When to use: Custom exclusion rules that are not SEO-related - membership tiers, editorial flags, CPT-specific policies.
Example - exclude posts flagged by editorial workflow: Editors mark sponsor content or thin pages with a custom _exclude_from_llms meta field:
add_filter( 'llmstxt_post_is_publicly_exportable', function ( bool $exportable, WP_Post $post ): bool {
if ( get_post_meta( $post->ID, '_exclude_from_llms', true ) ) {
return false;
}
return $exportable;
}, 10, 2 );
This filter only runs when the post has already passed publish, password, future-date, and SEO noindex checks. It cannot resurrect drafts or password-protected posts.
llmstxt_post_file_mode Pro
Controls whether an individual .md file is written to disk or served as a live WordPress endpoint, decided per-post. Overrides the site-wide and per-type setting from the Settings tab.
| Parameter | Type | Description |
|---|---|---|
$mode | string | Current mode: 'physical' or 'endpoint' |
$post_id | int | The post being evaluated |
$type | string | Content type slug (e.g. 'post', 'product') |
Returns: string - 'physical' or 'endpoint'.
Example 1 - category-based routing: Serve products in the sale category as live endpoints (so price changes are always current), write everything else to disk.
add_filter( 'llmstxt_post_file_mode', function ( $mode, $post_id, $type ) {
if ( 'product' === $type && has_term( 'sale', 'product_cat', $post_id ) ) {
return 'endpoint';
}
return $mode;
}, 10, 3 );
Example 2 - opt-in via post meta: Let editors flag individual posts as live-endpoint by setting a _llmstxt_live custom field to 1.
add_filter( 'llmstxt_post_file_mode', function ( $mode, $post_id, $type ) {
if ( get_post_meta( $post_id, '_llmstxt_live', true ) ) {
return 'endpoint';
}
return $mode;
}, 10, 3 );
Example 3 - recency-based routing: Write posts older than 30 days to disk; serve recently updated content as live endpoints so edits appear immediately without a regeneration run.
add_filter( 'llmstxt_post_file_mode', function ( $mode, $post_id, $type ) {
$modified = get_post_modified_time( 'U', true, $post_id );
if ( $modified && ( time() - $modified ) < ( 30 * DAY_IN_SECONDS ) ) {
return 'endpoint';
}
return $mode;
}, 10, 3 );
llmstxt_acf_repeater_output Pro
Filters the Markdown string produced when rendering an ACF repeater field referenced via {{acf_fieldname}} in a template. Requires ACF (free or Pro) to be active.
| Parameter | Type | Description |
|---|---|---|
$output | string | The default Markdown output for the repeater |
$rows | array | All row data from the repeater field |
$sub_fields | array | ACF sub-field definitions |
$field_name | string | The ACF field name (e.g. 'faq_items') |
Returns: string - the replacement Markdown.
Example: Render an faq_items repeater (with question and answer sub-fields) as a proper Q&A section instead of a flat list.
add_filter( 'llmstxt_acf_repeater_output', function ( $output, $rows, $sub_fields, $field_name ) {
if ( 'faq_items' !== $field_name || empty( $rows ) ) {
return $output;
}
$md = "## Frequently Asked Questions\n\n";
foreach ( $rows as $row ) {
$q = esc_html( $row['question'] ?? '' );
$a = esc_html( $row['answer'] ?? '' );
$md .= "**Q: {$q}**\n\n{$a}\n\n";
}
return $md;
}, 10, 4 );
llmstxt_acf_flexible_content_output Pro
Filters the Markdown string produced when rendering an ACF flexible content field referenced via {{acf_fieldname}} in a template. Requires ACF Pro to be active.
| Parameter | Type | Description |
|---|---|---|
$output | string | The default Markdown output for the flexible content field |
$layouts_value | array | The resolved layout data (layout name + field values) |
$layouts_config | array | ACF layout configuration definitions |
$field_name | string | The ACF field name (e.g. 'page_sections') |
Returns: string - the replacement Markdown.
Example: Apply per-layout Markdown formatting for a page_sections flexible content field - headings for hero layouts, blockquotes for pullquote layouts.
add_filter( 'llmstxt_acf_flexible_content_output', function ( $output, $layouts_value, $layouts_config, $field_name ) {
if ( 'page_sections' !== $field_name ) {
return $output;
}
$md = '';
foreach ( $layouts_value as $layout ) {
switch ( $layout['acf_fc_layout'] ) {
case 'hero':
$md .= "# {$layout['heading']}\n\n{$layout['body']}\n\n";
break;
case 'pullquote':
$md .= "> {$layout['quote']}\n> - {$layout['attribution']}\n\n";
break;
default:
$md .= "{$layout['text']}\n\n";
}
}
return $md ?: $output;
}, 10, 4 );
Actions
Project LLMs.txt exposes two kinds of actions:
- Regeneration triggers (
llmstxt_regenerate*) - fire these from import tools, custom scripts, or other plugins to start regeneration. - Post-generation callbacks (
llmstxt_scheduled_regeneration) - hook these to run work after regeneration completes.
Regeneration triggers
Fire any of these actions with do_action() to trigger regeneration from code. They respect the same debounce and lock logic as auto-regeneration - if generation is already in progress, the request queues gracefully via WP Cron rather than erroring.
| Action | Regenerates | Tier |
|---|---|---|
llmstxt_regenerate | llms.txt + llms-full.txt | Free |
llmstxt_regenerate_index | llms.txt only | Free |
llmstxt_regenerate_full | llms-full.txt only | Free |
llmstxt_regenerate_md | All individual .md files | Pro (no-op on free) |
llmstxt_regenerate_post | Single post ($post_id) | Free |
llmstxt_regenerate
Regenerates both llms.txt and llms-full.txt. No arguments are passed.
Example: Trigger a full regeneration from a custom deploy script.
do_action( 'llmstxt_regenerate' );
llmstxt_regenerate_index
Regenerates llms.txt only. No arguments are passed.
Example: Rebuild the site index without re-scraping full page content.
do_action( 'llmstxt_regenerate_index' );
llmstxt_regenerate_full
Regenerates llms-full.txt only. No arguments are passed.
Example: Refresh the full-content file after a template change.
do_action( 'llmstxt_regenerate_full' );
llmstxt_regenerate_md Pro
Regenerates all individual .md files from cached post content. No arguments are passed. On the free tier this is a silent no-op.
Example: Rewrite physical .md files without rebuilding the index files.
do_action( 'llmstxt_regenerate_md' );
llmstxt_regenerate_post
Regenerates a single post by ID. Validates that $post_id is a positive integer and that the post exists; silently no-ops otherwise.
| Parameter | Type | Description |
|---|---|---|
$post_id | int | The WordPress post ID to regenerate |
Example: Regenerate one post after a programmatic update.
do_action( 'llmstxt_regenerate_post', 42 );
llmstxt_regenerate_postuses a per-post lock. If that post is already being generated, the request is queued via WP Cron instead of running immediately.
WP All Import
WP All Import (and similar bulk import tools) often bypass normal WordPress publish hooks, so auto-regeneration may not fire. Hook pmxi_after_xml_import and fire a regeneration action when the import finishes.
llmstxt_regenerate rebuilds both llms.txt and llms-full.txt. Swap in llmstxt_regenerate_index, llmstxt_regenerate_full, llmstxt_regenerate_md, or llmstxt_regenerate_post if you only need part of the output updated.
After any import completes
// Regenerate llms.txt after any WP All Import run completes
add_action( 'pmxi_after_xml_import', function( $import_id, $import ) {
do_action( 'llmstxt_regenerate' );
}, 10, 2 );
After a specific import completes
// Regenerate llms.txt after specific WP All Import run completes
function after_xml_import( $import_id, $import ) {
// Only run if import ID is 5.
if ($import_id == 5) {
do_action( 'llmstxt_regenerate' );
}
}
add_action( 'pmxi_after_xml_import', 'after_xml_import', 10, 2 );
llmstxt_scheduled_regeneration Pro
Fires after the debounced background regeneration completes - 5 minutes after a publish, update, or delete triggered it. No arguments are passed.
Hook into this to run any post-generation work: purge CDN caches, notify external services, log regeneration events, or ping a URL so downstream systems know fresh content is available.
Example: Purge a Cloudflare cache zone after the files are regenerated.
add_action( 'llmstxt_scheduled_regeneration', function () {
$zone_id = defined( 'CF_ZONE_ID' ) ? CF_ZONE_ID : '';
$api_token = defined( 'CF_API_TOKEN' ) ? CF_API_TOKEN : '';
if ( ! $zone_id || ! $api_token ) {
return;
}
wp_remote_post(
"https://api.cloudflare.com/client/v4/zones/{$zone_id}/purge_cache",
[
'headers' => [
'Authorization' => "Bearer {$api_token}",
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( [
'files' => [
home_url( '/llms.txt' ),
home_url( '/llms-full.txt' ),
],
] ),
]
);
} );
Define
CF_ZONE_IDandCF_API_TOKENas constants inwp-config.phpto keep credentials out of the database.