如何在 Next.js、WordPress 和 Shopify 中动态生成 llms.txt:实用开发者指南
为什么动态生成 llms.txt 文件对不断增长的平台至关重要
核心要点(BLUF):在高速内容平台上,静态 llms.txt 文件会在几小时内过时,在 AI 代理发现的内容与实际存在的内容之间造成严重差距。动态生成确保实时准确性,消除手动维护开销,并保证大型语言模型始终接收当前的站点结构、内容层级和资源位置——这对于每天发布数十个页面的企业级 WordPress 安装、Next.js 应用程序和 Shopify 商店来说是不可或缺的。
理解 llms.txt 规范和动态需求
llms.txt 文件作为机器可读的清单文件,引导 AI 爬虫、语言模型和自主代理浏览您网站的信息架构。与专注于爬取权限的 robots.txt 不同,llms.txt 提供语义结构、内容分类和专门为 LLM 消费优化的优先级信号。
在以下情况下,动态生成变得至关重要:
- 内容更新速度超过手动更新能力:电商目录每天添加 50+ 个产品、新闻网站每小时发布内容,或 SaaS 文档随每个发布周期更新
- 分类体系以编程方式演变:自动生成的分类页面、动态过滤系统或用户生成的内容层级
- 存在个性化层:地理变体、特定语言路由或需要条件性暴露的身份验证保护资源
- 多源内容聚合:无头 CMS 架构、提供内容 API 的微服务或联合数据源
WordPress 动态 llms.txt 实现
WordPress 为 43% 的网站提供支持,使其 llms.txt 实现模式变得至关重要。以下方法使用 template_redirect 动作在主题渲染之前拦截请求,确保最大性能和与缓存层的兼容性。
完整的 WordPress PHP 实现
<?php
/**
* WordPress 动态 llms.txt 生成器
* 添加到主题的 functions.php 或创建为插件
*/
add_action('template_redirect', 'serve_dynamic_llms_txt');
function serve_dynamic_llms_txt() {
// 仅响应 /llms.txt 请求
if ($_SERVER['REQUEST_URI'] !== '/llms.txt') {
return;
}
// 设置适当的响应头
header('Content-Type: text/plain; charset=utf-8');
header('X-Robots-Tag: noindex');
// 防止缓存真正动态的内容
header('Cache-Control: no-cache, must-revalidate');
// 开始输出
echo "# llms.txt - 动态生成\n";
echo "# 生成时间: " . current_time('c') . "\n\n";
// 站点元数据
echo "# 站点: " . get_bloginfo('name') . "\n";
echo "# 描述: " . get_bloginfo('description') . "\n";
echo "# URL: " . home_url() . "\n\n";
// 主要页面
echo "## 主导航\n\n";
$pages = get_pages(array('sort_column' => 'menu_order', 'hierarchical' => 0));
foreach ($pages as $page) {
echo "- [{$page->post_title}](" . get_permalink($page->ID) . ")\n";
}
echo "\n";
// 按分类的博客文章
echo "## 博客内容\n\n";
$categories = get_categories(array('hide_empty' => true, 'orderby' => 'count', 'order' => 'DESC'));
foreach ($categories as $category) {
echo "### {$category->name} ({$category->count} 篇文章)\n\n";
$posts = get_posts(array(
'category' => $category->term_id,
'numberposts' => 10,
'orderby' => 'date',
'order' => 'DESC'
));
foreach ($posts as $post) {
$date = get_the_date('Y-m-d', $post->ID);
echo "- [{$post->post_title}](" . get_permalink($post->ID) . ") - {$date}\n";
}
echo "\n";
}
// WooCommerce 产品集成
if (class_exists('WooCommerce')) {
echo "## 产品目录\n\n";
$product_categories = get_terms(array(
'taxonomy' => 'product_cat',
'hide_empty' => true,
'orderby' => 'count',
'order' => 'DESC',
'number' => 10
));
foreach ($product_categories as $cat) {
echo "- [{$cat->name}](" . get_term_link($cat) . ") - {$cat->count} 个产品\n";
}
echo "\n";
}
// 文档或自定义文章类型
$custom_post_types = get_post_types(array('public' => true, '_builtin' => false), 'objects');
foreach ($custom_post_types as $cpt) {
echo "## {$cpt->labels->name}\n\n";
$cpt_posts = get_posts(array(
'post_type' => $cpt->name,
'numberposts' => 20,
'orderby' => 'date',
'order' => 'DESC'
));
foreach ($cpt_posts as $post) {
echo "- [{$post->post_title}](" . get_permalink($post->ID) . ")\n";
}
echo "\n";
}
exit; // 防止 WordPress 继续执行
}
WordPress 性能优化策略
对于高流量的 WordPress 安装,实施以下缓存模式:
- 瞬态 API 缓存:将生成的输出存储在
set_transient('llms_txt_cache', $output, 3600)中,并在文章发布钩子上自动失效 - 对象缓存集成:为分布式环境利用 Redis 或 Memcached
- CDN 边缘缓存:为 Cloudflare 或 Fastly 边缘节点设置适当的
Cache-Control响应头(例如max-age=1800) - 选择性重新生成:挂接到
save_post、created_term和deleted_term动作,仅在内容更改时使缓存失效
Next.js 路由处理器实现(App Router)
Next.js 13+ App Router 提供了适合大型 llms.txt 文件的原生流式传输能力。以下实现使用带有 TypeScript 的路由处理器,以实现类型安全和边缘运行时兼容性。
Next.js TypeScript 实现
// app/llms.txt/route.ts
import { NextRequest, NextResponse } from 'next/server';
// 可选:启用边缘运行时以实现全球分发
// export const runtime = 'edge';
export async function GET(request: NextRequest) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
// 辅助函数用于写入块
const write = (text: string) => {
controller.enqueue(encoder.encode(text));
};
// 头部部分
write('# llms.txt - 动态站点地图\n');
write(`# 生成时间: ${new Date().toISOString()}\n`);
write(`# 基础 URL: ${process.env.NEXT_PUBLIC_SITE_URL}\n\n`);
// 从 CMS/数据库获取数据
try {
// 示例:从无头 CMS 获取
const pages = await fetch(`${process.env.CMS_API_URL}/pages`, {
headers: { 'Authorization': `Bearer ${process.env.CMS_API_KEY}` },
next: { revalidate: 3600 } // ISR 风格缓存
}).then(res => res.json());
write('## 主要页面\n\n');
pages.forEach((page: any) => {
write(`- [${page.title}](${process.env.NEXT_PUBLIC_SITE_URL}${page.slug})\n`);
});
write('\n');
// 带分页处理的博客文章
write('## 博客文章\n\n');
let page = 1;
let hasMore = true;
while (hasMore && page <= 10) { // 限制以防止无限循环
const posts = await fetch(
`${process.env.CMS_API_URL}/posts?page=${page}&per_page=50`,
{ next: { revalidate: 1800 } }
).then(res => res.json());
if (posts.length === 0) {
hasMore = false;
break;
}
posts.forEach((post: any) => {
const date = new Date(post.publishedAt).toISOString().split('T')[0];
write(`- [${post.title}](${process.env.NEXT_PUBLIC_SITE_URL}/blog/${post.slug}) - ${date}\n`);
});
page++;
}
write('\n');
// 产品目录(如果适用)
const products = await fetch(`${process.env.CMS_API_URL}/products?limit=100`)
.then(res => res.json())
.catch(() => []);
if (products.length > 0) {
write('## 产品\n\n');
// 按类别分组
const categorized = products.reduce((acc: any, product: any) => {
const cat = product.category || '未分类';
if (!acc[cat]) acc[cat] = [];
acc[cat].push(product);
return acc;
}, {});
Object.entries(categorized).forEach(([category, items]: [string, any]) => {
write(`### ${category}\n\n`);
items.forEach((product: any) => {
write(`- [${product.name}](${process.env.NEXT_PUBLIC_SITE_URL}/products/${product.slug})\n`);
});
write('\n');
});
}
// API 文档路由
write('## API 文档\n\n');
write(`- [API 参考](${process.env.NEXT_PUBLIC_SITE_URL}/docs/api)\n`);
write(`- [身份验证指南](${process.env.NEXT_PUBLIC_SITE_URL}/docs/auth)\n`);
write(`- [速率限制](${process.env.NEXT_PUBLIC_SITE_URL}/docs/limits)\n\n`);
} catch (error) {
write(`# 生成动态内容时出错: ${error}\n`);
}
controller.close();
}
});
return new NextResponse(stream, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'public, s-maxage=1800, stale-while-revalidate=3600',
'X-Robots-Tag': 'noindex',
},
});
}
Next.js 高级模式
增量静态再生(ISR)集成:通过在 fetch 调用中设置 revalidate 值,将路由处理器与 ISR 结合,允许边缘缓存同时保持新鲜度保证。
并行数据获取:使用 Promise.all() 同时获取多个内容源,减少总生成时间:
const [pages, posts, products] = await Promise.all([
fetchPages(),
fetchPosts(),
fetchProducts()
]);
条件性部分:实施功能标志或基于环境的条件,为预发布环境与生产环境暴露不同的内容结构。
Shopify 动态 llms.txt 实现
Shopify 的架构根据您的商店设置需要不同的方法。该平台的 Liquid 模板引擎和主题结构呈现出独特的挑战和机遇。
方法 1:自定义 Liquid 模板
在主题中创建新的页面模板:
{% comment %}
文件: templates/page.llms.liquid
在 Shopify 管理后台创建带有模板后缀 "llms" 的页面
通过以下方式访问: yourstore.com/pages/llms-txt
{% endcomment %}
{% layout none %}
{% content_for "content_type" %}text/plain{% endcontent_for %}
# llms.txt - {{ shop.name }}
# 生成时间: {{ "now" | date: "%Y-%m-%d %H:%M:%S %Z" }}
# 商店 URL: {{ shop.url }}
## 产品系列
{% for collection in collections %}
{% if collection.products_count > 0 %}
### {{ collection.title }} ({{ collection.products_count }} 个产品)
{% for product in collection.products limit: 20 %}
- [{{ product.title }}]({{ shop.url }}{{ product.url }}) - {{ product.price | money }}
{% endfor %}
{% endif %}
{% endfor %}
## 博客文章
{% for article in blogs.news.articles %}
- [{{ article.title }}]({{ shop.url }}{{ article.url }}) - {{ article.published_at | date: "%Y-%m-%d" }}
{% endfor %}
## 页面
{% for page in pages %}
- [{{ page.title }}]({{ shop.url }}{{ page.url }})
{% endfor %}
方法 2:Shopify 应用代理
对于企业级 Shopify Plus 商店,实施在服务器端生成 llms.txt 的应用代理:
- 配置应用代理:在 Shopify 应用设置中,将代理路径设置为
/apps/llms,指向您的服务器端点 - 服务器实现:构建使用 Shopify Admin API 获取当前库存、产品系列和内容的 Node.js/Python/Ruby 端点
- URL 重写:使用 Shopify Scripts 或主题修改将
/llms.txt重定向到/apps/llms/generate
// Express.js 应用代理处理器
app.get('/generate', async (req, res) => {
const shopifyClient = new Shopify.Clients.Rest(
req.query.shop,
process.env.SHOPIFY_ACCESS_TOKEN
);
res.setHeader('Content-Type', 'text/plain');
// 获取产品系列
const collections = await shopifyClient.get({
path: 'custom_collections',
});
let output = '# llms.txt\n\n## 产品系列\n\n';
for (const collection of collections.body.custom_collections) {
const products = await shopifyClient.get({
path: `collections/${collection.id}/products`,
query: { limit: 50 }
});
output += `### ${collection.title}\n\n`;
products.body.products.forEach(product => {
output += `- [${product.title}](https://${req.query.shop}/products/${product.handle})\n`;
});
output += '\n';
}
res.send(output);
});
方法 3:Shopify Hydrogen(无头)
对于 Hydrogen 店面,实施类似于 Next.js 的服务器路由:
// app/routes/llms[.]txt.tsx
import { LoaderFunction } from '@shopify/remix-oxygen';
export const loader: LoaderFunction = async ({ context }) => {
const { storefront } = context;
const { collections } = await storefront.query(`
query LLMSData {
collections(first: 50) {
nodes {
title
handle
products(first: 20) {
nodes {
title
handle
}
}
}
}
}
`);
let output = '# llms.txt\n\n';
collections.nodes.forEach(collection => {
output += `## ${collection.title}\n\n`;
collection.products.nodes.forEach(product => {
output += `- [${product.title}](/products/${product.handle})\n`;
});
output += '\n';
});
return new Response(output, {
headers: {
'Content-Type': 'text/plain',
'Cache-Control': 'public, max-age=3600'
}
});
};
验证和测试方法
确保动态生成的 llms.txt 文件满足规范要求并实现最佳性能需要系统化验证。
格式验证清单
- Markdown 合规性:验证正确的标题层级(H1 用于标题,H2 用于部分,H3 用于子部分)
- 链接完整性:所有 URL 必须是绝对路径、正确编码并返回 200 状态码
- 字符编码:UTF-8 编码,正确处理特殊字符、表情符号和国际文本
- 文件大小考虑:保持在 10MB 以下以实现最佳 LLM 处理;对于较大的站点实施分页或摘要
- 更新频率指示器:包含生成时间戳和更改频率提示
自动化测试脚本
import requests
import re
from urllib.parse import urlparse
def validate_llms_txt(url):
"""验证 llms.txt 格式和内容"""
response = requests.get(url)
# 检查响应头
assert response.status_code == 200, "文件无法访问"
assert 'text/plain' in response.headers.get('Content-Type', ''), "错误的内容类型"
content = response.text
lines = content.split('\n')
# 验证结构
assert lines[0].startswith('#'), "必须以标题开头"
# 提取并验证所有 URL
url_pattern = r'\[([^\]]+)\]\(([^\)]+)\)'
urls = re.findall(url_pattern, content)
print(f"找到 {len(urls)} 个链接")
# 样本验证(检查前 10 个链接)
for title, link in urls[:10]:
parsed = urlparse(link)
assert parsed.scheme in ['http', 'https'], f"无效的协议: {link}"
assert parsed.netloc, f"缺少域名: {link}"
# 可选:检查链接是否可访问
try:
link_response = requests.head(link, timeout=5, allow_redirects=True)
assert link_response.status_code < 400, f"损坏的链接: {link}"
except requests.RequestException as e:
print(f"警告: 无法验证 {link}: {e}")
# 检查必需的部分
assert '##' in content, "缺少部分标题"
# 验证文件大小
size_mb = len(content.encode('utf-8')) / (1024 * 1024)
assert size_mb < 10, f"文件过大: {size_mb:.2f}MB"
print("✓ 验证通过")
return True
# 使用方法
validate_llms_txt('https://yoursite.com/llms.txt')
性能监控
实施以下监控策略以确保动态生成不会影响站点性能:
- 响应时间跟踪:为超过 2 秒的生成时间设置警报
- 缓存命中率监控:跟踪缓存响应与重新生成响应的百分比
- 错误率日志记录:监控生成期间失败的数据库查询或 API 调用
- 资源利用率:测量高峰生成期间的 CPU 和内存使用情况
高级优化技术
条件性内容暴露
基于用户代理、地理位置或身份验证状态实施智能过滤:
// 带条件性部分的 Next.js 示例
export async function GET(request: NextRequest) {
const userAgent = request.headers.get('user-agent') || '';
const isGoogleBot = userAgent.includes('Googlebot');
const isLLMCrawler = userAgent.includes('GPTBot') || userAgent.includes('Claude');
// 根据爬虫类型暴露不同的内容深度
const maxItems = isLLMCrawler ? 1000 : isGoogleBot ? 500 : 100;
// 使用适当的限制生成内容
}
层级优先级
构建内容以首先呈现高价值页面,使用优先级指示器:
## 高优先级内容
- [2024 产品发布](/launch) - 优先级: 高,更新时间: 2024-01-15
- [文档首页](/docs) - 优先级: 高,更新时间: 2024-01-10
## 标准内容
- [博客归档](/blog) - 优先级: 中
多语言支持
对于国际站点,生成特定语言的 llms.txt 变体:
// WordPress 多语言示例
function serve_dynamic_llms_txt() {
$lang = isset($_GET['lang']) ? sanitize_text_field($_GET['lang']) : 'en';
if ($lang !== 'en') {
// 生成本地化版本
$posts = get_posts(array(
'lang' => $lang,
'numberposts' => 50
));
}
echo "# llms.txt ({$lang})\n\n";
// ... 其余生成代码
}
安全考虑
动态生成引入了必须解决的潜在安全向量:
- 速率限制:实施请求节流以防止资源耗尽攻击(例如,每个 IP 每小时 60 个请求)
- 输入清理:验证并转义所有动态内容以防止注入攻击
- 身份验证绕过防护:永远不要在 llms.txt 中暴露私有内容 URL,即使页面本身受到保护
- 信息泄露:避免透露内部系统路径、API 端点或敏感元数据
- DDoS 缓解:使用 CDN 级别保护并为上游服务故障实施断路器
维护和监控最佳实践
建立操作程序以确保长期可靠性:
- CI/CD 中的自动化测试:在部署管道中包含 llms.txt 验证
- 模板的版本控制:使用详细的提交消息跟踪生成逻辑的更改
- 警报阈值:为生成失败、性能下降或格式违规设置通知
- 定期审计:每月审查包含的内容、损坏的链接和结构准确性
- 文档:维护用于故障排除常见问题和更新生成逻辑的操作手册
结论
动态 llms.txt 生成将静态文件转变为准确代表您站点当前状态的活文档。通过为 WordPress、Next.js 和 Shopify 实施特定平台的解决方案,您可以确保 AI 代理始终接收关于您内容架构的权威、最新信息。对动态生成的投资通过改善 AI 可发现性、减少维护负担和增强对数字资产的语义理解而获得回报。随着 LLM 驱动的搜索和发现工具变得越来越普遍,动态 llms.txt 文件将从竞争优势转变为严肃网站的基线要求。