Appearance
实现 robots.txt 与 sitemap 工具
分类:general | 标签:robots、sitemap、SEO 生成 robots.txt 与 sitemap.xml,解析 sitemap
2.1 功能说明
生成 robots.txt 与 sitemap.xml,解析 sitemap
核心能力
- 生成 robots.txt
- Allow
- Disallow
- 生成 sitemap.xml
- 不输出
- 解析 sitemap.xml
2.1.1 使用指南
功能说明
围绕站点收录做的三件事:按规则生成 robots.txt、把一批网址打包成标准 sitemap.xml、以及把别人的 sitemap 拆回纯网址列表。三个标签页互相独立,全部在浏览器本地完成,不会读取或访问任何线上站点。
一、生成 robots.txt
robots.txt 用来告诉搜索引擎哪些路径可以抓、哪些不要抓。按 User-agent 分组配置,每组下可添加任意条 Allow 或 Disallow 规则:
- User-agent:填
*表示对所有爬虫生效,也可以针对特定爬虫单独建一组,写更严格或更宽松的规则。 - Disallow:禁止抓取的路径,以斜杠开头。填
/表示整站禁止,留空表示不限制。 - Allow:在被禁止的目录中开一个例外,通常配合 Disallow 使用。
- Crawl-delay:建议的抓取间隔秒数,用于减轻服务器压力,设为 0 时不输出该行。注意部分主流搜索引擎会忽略这一指令。
- Sitemap:站点地图的完整地址,建议填写绝对路径,可以写多行。
三个预设按钮可以快速铺底:全站开放、全站禁止、以及屏蔽常见后台与接口目录。
二、生成 sitemap.xml
每行填一个网址,也可以在网址后面用逗号或制表符附加两个可选字段,顺序为「最后修改日期, 优先级」,例如 https://example.com/about,2026-08-01,0.8。没有单独指定的条目会使用下方设置的默认值。
- lastmod:最后修改日期,推荐 YYYY-MM-DD 格式,可一键填入今天。
- priority:相对优先级,取值 0 到 1,只表示站内页面之间的相对重要性。
- changefreq:预计更新频率,可选择不输出。
- 不以 http 或 https 开头的行会被忽略,界面会提示被忽略的行数;网址中的特殊字符会做 XML 转义。
三、解析 sitemap.xml
粘贴任意 sitemap.xml 内容,工具会提取全部 loc 标签中的网址,同时读取对应的 lastmod 与 priority。支持去重和按字母排序,结果可复制、下载为 TXT,或导出成包含三列的 CSV 便于进一步整理。即使粘贴的是不完整的 XML 片段,也能尽量提取出其中的网址。
使用建议
单个 sitemap 文件建议不超过 5 万条网址且未压缩时小于 50MB,超出时应拆分成多个文件并用 sitemap 索引文件组织。robots.txt 与 sitemap.xml 都要放在站点根目录才能被正确读取。
2.2 代码实现
2.2.1 组件结构
本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-input、el-button 等 Element Plus 组件与用户交互,所有数据均在浏览器本地处理,不会上传服务器。
2.2.2 核心逻辑概览
<script setup> 中定义的主要函数/方法:
addGroup()removeGroup()addRule()removeRule()applyPreset()today()escapeXml()loadSitemapSample()unescapeXml()downloadParsedCsv()copyText()download()
2.2.3 关键实现代码
以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。
vue
import { ref, reactive, computed } from 'vue'
import { ElMessage } from 'element-plus'
import ToolPageShell from '@/components/ToolPageShell.vue'
const activeTab = ref('robots')
/* ---------- 一、robots.txt ---------- */
const groups = reactive([
{
ua: '*',
delay: 0,
rules: [{ type: 'Disallow', path: '/admin/' }]
}
])
const sitemapUrls = ref('')
const robotsComment = ref('')
function addGroup() {
groups.push({ ua: '*', delay: 0, rules: [{ type: 'Disallow', path: '/' }] })
}
function removeGroup(index) {
if (groups.length === 1) return
groups.splice(index, 1)
}
function addRule(gi) {
groups[gi].rules.push({ type: 'Disallow', path: '/' })
}
function removeRule(gi, ri) {
groups[gi].rules.splice(ri, 1)
}
function applyPreset(kind) {
groups.splice(0, groups.length)
if (kind === 'open') {
groups.push({ ua: '*', delay: 0, rules: [{ type: 'Disallow', path: '' }] })
} else if (kind === 'block') {
groups.push({ ua: '*', delay: 0, rules: [{ type: 'Disallow', path: '/' }] })
} else {
groups.push({
ua: '*',
delay: 0,
rules: [
{ type: 'Disallow', path: '/admin/' },
{ type: 'Disallow', path: '/api/' },
{ type: 'Disallow', path: '/login' },
{ type: 'Disallow', path: '/*?s=' },
{ type: 'Allow', path: '/api/public/' }
]
})
}
ElMessage.success('预设已应用')
}
const robotsText = computed(() => {
const lines = []
if (robotsComment.value.trim()) lines.push('# ' + robotsComment.value.trim())
groups.forEach((g, index) => {
if (index > 0 || lines.length) lines.push('')
lines.push(`User-agent: ${g.ua.trim() || '*'}`)
g.rules.forEach(r => {
lines.push(`${r.type}: ${r.path.trim()}`)
})
if (g.delay > 0) lines.push(`Crawl-delay: ${g.delay}`)
})
const maps = sitemapUrls.value
.split(/\r?\n/)
.map(s => s.trim())
.filter(Boolean)
if (maps.length) {
lines.push('')
maps.forEach(m => lines.push(`Sitemap: ${m}`))
}
return lines.join('\n') + '\n'
})
/* ---------- 二、生成 sitemap.xml ---------- */
const freqOptions = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never']
const sitemapInput = ref('')
const defaultFreq = ref('')
const defaultPriority = ref(0.5)
const usePriority = ref(true)
const useLastmod = ref(true)
const defaultLastmod = ref('')
function today() {
const d = new Date()
const pad = n => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
}
const sitemapParsed = computed(() => {
const entries = []
let invalid = 0
sitemapInput.value
.split(/\r?\n/)
.map(l => l.trim())
.filter(Boolean)
.forEach(line => {
const parts = line
.split(/[,\t]/)
.map(p => p.trim())
.filter(p => p !== '')
const loc = parts[0]
if (!/^https?:\/\//i.test(loc)) {
invalid++
return
}
let lastmod = ''
let priority = ''
parts.slice(1).forEach(p => {
if (/^\d{4}-\d{2}-\d{2}/.test(p)) lastmod = p
else if (/^0?\.\d+$|^[01](\.0+)?$/.test(p)) priority = p
})
entries.push({ loc, lastmod, priority })
})
return { entries, invalid }
})
const sitemapEntries = computed(() => sitemapParsed.value.entries)
const sitemapInvalid = computed(() => sitemapParsed.value.invalid)
function escapeXml(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
const sitemapXml = computed(() => {
const entries = sitemapEntries.value
if (!entries.length) return ''
const out = []
out.push('<?xml version="1.0" encoding="UTF-8"?>')
out.push('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">')
entries.forEach(e => {
out.push(' <url>')
out.push(` <loc>${escapeXml(e.loc)}</loc>`)
if (useLastmod.value) {
const lm = e.lastmod || defaultLastmod.value.trim()
if (lm) out.push(` <lastmod>${escapeXml(lm)}</lastmod>`)
}
if (defaultFreq.value) out.push(` <changefreq>${defaultFreq.value}</changefreq>`)
if (usePriority.value) {
const pr = e.priority || Number(defaultPriority.value).toFixed(1)
out.push(` <priority>${escapeXml(pr)}</priority>`)
}
out.push(' </url>')
})
out.push('</urlset>')
return out.join('\n') + '\n'
})
function loadSitemapSample() {
sitemapInput.value = [
'https://example.com/',
'https://example.com/about,2026-08-01,0.8',
'https://example.com/products,2026-07-20,0.9',
'https://example.com/blog/hello-world,2026-06-15'
].join('\n')
defaultLastmod.value = today()
}
/* ---------- 三、解析 sitemap.xml ---------- */
const parseInput = ref('')
const parseDedupe = ref(true)
const parseSort = ref(false)
function unescapeXml(str) {
return String(str)
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/&#(\d+);/g, (m, code) => String.fromCodePoint(Number(code)))
.replace(/&/g, '&')
}
const parsedUrls = computed(() => {
const text = parseInput.value
if (!text.trim()) return []
// 逐个 url 块解析,保证 loc 与同块内的 lastmod、priority 对应
const blocks = text.match(/<url\b[\s\S]*?<\/url>/gi)
let list = []
if (blocks && blocks.length) {
blocks.forEach(block => {
const locMatch = block.match(/<loc[^>]*>([\s\S]*?)<\/loc>/i)
if (!locMatch) return
const lastmod = block.match(/<lastmod[^>]*>([\s\S]*?)<\/lastmod>/i)
const priority = block.match(/<priority[^>]*>([\s\S]*?)<\/priority>/i)
list.push({
loc: unescapeXml(locMatch[1].trim()),
lastmod: lastmod ? unescapeXml(lastmod[1].trim()) : '',
priority: priority ? unescapeXml(priority[1].trim()) : ''
})
})
} else {
// 兼容 sitemap 索引文件或不完整片段,直接抓取所有 loc
const locs = text.match(/<loc[^>]*>([\s\S]*?)<\/loc>/gi) || []
list = locs.map(item => ({
loc: unescapeXml(item.replace(/<[^>]+>/g, '').trim()),
lastmod: '',
priority: ''
}))
}
list = list.filter(item => item.loc)
if (parseDedupe.value) {
const seen = new Set()
list = list.filter(item => {
if (seen.has(item.loc)) return false
seen.add(item.loc)
return true
})
}
if (parseSort.value) list = [...list].sort((a, b) => a.loc.localeCompare(b.loc))
return list
})
const parsedText = computed(() => parsedUrls.value.map(item => item.loc).join('\n'))
const parseSummary = computed(() => {
const n = parsedUrls.value.length
if (!n) return '未提取到任何 loc 标签,请确认粘贴的内容是 sitemap.xml'
const withDate = parsedUrls.value.filter(item => item.lastmod).length
return `提取到 ${n} 条 URL,其中 ${withDate} 条带有 lastmod`
})
function downloadParsedCsv() {
if (!parsedUrls.value.length) return
const escape = v => {
const s = String(v || '')
return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s
}
const lines = ['loc,lastmod,priority']
parsedUrls.value.forEach(item => {
lines.push([escape(item.loc), escape(item.lastmod), escape(item.priority)].join(','))
})
download('\ufeff' + lines.join('\r\n'), 'sitemap-urls.csv', 'text/csv;charset=utf-8')
}
/* ---------- 公共方法 ---------- */
async function copyText(text) {
if (!text) return
try {
await navigator.clipboard.writeText(text)
ElMessage.success('已复制到剪贴板')
} catch {
ElMessage.warning('复制失败,请手动选择文本复制')
}
}
function download(content, filename, mime) {
if (!content) return
const blob = new Blob([content], { type: mime })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
ElMessage.success('已开始下载')
}2.3 效果截图
