feat(seo): add sitemap generation and improve seo metadata
- Add sitemap generator script that scans markdown files and creates multilingual sitemap - Update build script to include sitemap generation - Add robots.txt and llms.txt files for crawlers - Enhance SEO metadata with better structured data and hreflang tags - Fix stage-0 URL in README
This commit is contained in:
@@ -122,7 +122,7 @@
|
|||||||
|
|
||||||
| 阶段 | 适用人群 | 核心技能 | 产出 |
|
| 阶段 | 适用人群 | 核心技能 | 产出 |
|
||||||
| :--- | :--- | :--- | :--- |
|
| :--- | :--- | :--- | :--- |
|
||||||
| [**第一阶段:零基础入门**](https://datawhalechina.github.io/easy-vibe/zh-cn/stage-1/) | 零基础/产品经理 | 学习地图、AI 编程入门、AI IDE、产品思维、原型设计、AI 能力集成 | 互动小游戏、完整的产品原型 |
|
| [**第一阶段:零基础入门**](https://datawhalechina.github.io/easy-vibe/zh-cn/stage-0/) | 零基础/产品经理 | 学习地图、AI 编程入门、AI IDE、产品思维、原型设计、AI 能力集成 | 互动小游戏、完整的产品原型 |
|
||||||
| [**第二阶段:全栈开发**](https://datawhalechina.github.io/easy-vibe/zh-cn/stage-2/) | 开发者 | 全栈开发、数据库、AI 集成、部署运维 | 可上线的全栈 AI 应用 |
|
| [**第二阶段:全栈开发**](https://datawhalechina.github.io/easy-vibe/zh-cn/stage-2/) | 开发者 | 全栈开发、数据库、AI 集成、部署运维 | 可上线的全栈 AI 应用 |
|
||||||
| [**第三阶段:高级开发**](https://datawhalechina.github.io/easy-vibe/zh-cn/stage-3/) | 进阶开发者 | Claude Code 进阶、多平台开发 | 生产级多平台应用 |
|
| [**第三阶段:高级开发**](https://datawhalechina.github.io/easy-vibe/zh-cn/stage-3/) | 进阶开发者 | Claude Code 进阶、多平台开发 | 生产级多平台应用 |
|
||||||
| [**附录:基础知识**](https://datawhalechina.github.io/easy-vibe/zh-cn/appendix/) | 全员 | 计算机基础、AI 入门、9 大知识领域 | 80+ 交互式专题 |
|
| [**附录:基础知识**](https://datawhalechina.github.io/easy-vibe/zh-cn/appendix/) | 全员 | 计算机基础、AI 入门、9 大知识领域 | 80+ 交互式专题 |
|
||||||
|
|||||||
+91
-16
@@ -98,6 +98,18 @@ const getSeoHead = (locale, title, description, path = '') => {
|
|||||||
const canonicalUrl = path ? `${siteUrl}${path}` : `${siteUrl}/${locale}/`
|
const canonicalUrl = path ? `${siteUrl}${path}` : `${siteUrl}/${locale}/`
|
||||||
const ogImageUrl = `${siteUrl}${base}logo.png`
|
const ogImageUrl = `${siteUrl}${base}logo.png`
|
||||||
|
|
||||||
|
// 从路径中提取页面相对路径(去掉语言前缀)
|
||||||
|
const getRelativePath = (fullPath, currentLocale) => {
|
||||||
|
if (!fullPath) return ''
|
||||||
|
const prefix = `/${currentLocale}/`
|
||||||
|
if (fullPath.startsWith(prefix)) {
|
||||||
|
return fullPath.slice(prefix.length)
|
||||||
|
}
|
||||||
|
return fullPath.replace(/^\//, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const relativePath = getRelativePath(path, locale)
|
||||||
|
|
||||||
const head = [
|
const head = [
|
||||||
['link', { rel: 'icon', href: `${base}logo.png`.replace('//', '/') }],
|
['link', { rel: 'icon', href: `${base}logo.png`.replace('//', '/') }],
|
||||||
[
|
[
|
||||||
@@ -143,19 +155,24 @@ const getSeoHead = (locale, title, description, path = '') => {
|
|||||||
['meta', { name: 'robots', content: 'index,follow' }],
|
['meta', { name: 'robots', content: 'index,follow' }],
|
||||||
['meta', { name: 'googlebot', content: 'index,follow' }],
|
['meta', { name: 'googlebot', content: 'index,follow' }],
|
||||||
['meta', { name: 'baiduspider', content: 'index,follow' }],
|
['meta', { name: 'baiduspider', content: 'index,follow' }],
|
||||||
|
['meta', { name: 'bingbot', content: 'index,follow' }],
|
||||||
['meta', { name: 'distribution', content: 'global' }],
|
['meta', { name: 'distribution', content: 'global' }],
|
||||||
['meta', { name: 'rating', content: 'general' }],
|
['meta', { name: 'rating', content: 'general' }],
|
||||||
['meta', { name: 'revisit-after', content: '7 days' }]
|
['meta', { name: 'revisit-after', content: '7 days' }]
|
||||||
]
|
]
|
||||||
|
|
||||||
// 添加 hreflang 标签
|
// 添加 hreflang 标签 - 指向相同页面的不同语言版本
|
||||||
Object.keys(localeMap).forEach((lang) => {
|
Object.keys(localeMap).forEach((lang) => {
|
||||||
|
let alternateUrl = `${siteUrl}/${lang}/`
|
||||||
|
if (relativePath) {
|
||||||
|
alternateUrl = `${siteUrl}/${lang}/${relativePath}`
|
||||||
|
}
|
||||||
head.push([
|
head.push([
|
||||||
'link',
|
'link',
|
||||||
{
|
{
|
||||||
rel: 'alternate',
|
rel: 'alternate',
|
||||||
hreflang: localeMap[lang].hreflang,
|
hreflang: localeMap[lang].hreflang,
|
||||||
href: `${siteUrl}/${lang}/`
|
href: alternateUrl
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
@@ -184,7 +201,10 @@ const getSeoHead = (locale, title, description, path = '') => {
|
|||||||
logo: {
|
logo: {
|
||||||
'@type': 'ImageObject',
|
'@type': 'ImageObject',
|
||||||
url: ogImageUrl
|
url: ogImageUrl
|
||||||
}
|
},
|
||||||
|
sameAs: [
|
||||||
|
'https://github.com/datawhalechina/easy-vibe'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
mainEntity: {
|
mainEntity: {
|
||||||
'@type': 'Course',
|
'@type': 'Course',
|
||||||
@@ -194,30 +214,85 @@ const getSeoHead = (locale, title, description, path = '') => {
|
|||||||
'@type': 'Organization',
|
'@type': 'Organization',
|
||||||
name: 'Datawhale',
|
name: 'Datawhale',
|
||||||
sameAs: 'https://github.com/datawhalechina/easy-vibe'
|
sameAs: 'https://github.com/datawhalechina/easy-vibe'
|
||||||
}
|
},
|
||||||
|
educationalLevel: 'Beginner to Advanced',
|
||||||
|
learningResourceType: 'Course'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
head.push(['script', { type: 'application/ld+json' }, JSON.stringify(jsonLd)])
|
head.push(['script', { type: 'application/ld+json' }, JSON.stringify(jsonLd)])
|
||||||
|
|
||||||
// 添加 BreadcrumbList 结构化数据
|
// 生成动态 BreadcrumbList 结构化数据
|
||||||
const breadcrumbJsonLd = {
|
const generateBreadcrumbList = () => {
|
||||||
'@context': 'https://schema.org',
|
const items = [
|
||||||
'@type': 'BreadcrumbList',
|
|
||||||
itemListElement: [
|
|
||||||
{
|
{
|
||||||
'@type': 'ListItem',
|
'@type': 'ListItem',
|
||||||
position: 1,
|
position: 1,
|
||||||
name: '首页',
|
name: locale === 'zh-cn' ? '首页' : 'Home',
|
||||||
item: siteUrl
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'@type': 'ListItem',
|
|
||||||
position: 2,
|
|
||||||
name: locale === 'zh-cn' ? '教程' : 'Tutorial',
|
|
||||||
item: `${siteUrl}/${locale}/`
|
item: `${siteUrl}/${locale}/`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (relativePath) {
|
||||||
|
// 解析路径生成面包屑
|
||||||
|
const pathParts = relativePath.split('/').filter(Boolean)
|
||||||
|
let currentPath = ''
|
||||||
|
|
||||||
|
// 路径分段名称映射
|
||||||
|
const segmentNames = {
|
||||||
|
'zh-cn': {
|
||||||
|
'stage-0': '幼儿园',
|
||||||
|
'stage-1': 'AI产品经理',
|
||||||
|
'stage-2': '初中级开发工程师',
|
||||||
|
'stage-3': '高级开发工程师',
|
||||||
|
'appendix': '附录',
|
||||||
|
'guide': '指南',
|
||||||
|
'frontend': '前端',
|
||||||
|
'backend': '后端',
|
||||||
|
'ai-capabilities': 'AI能力',
|
||||||
|
'core-skills': '核心技能',
|
||||||
|
'cross-platform': '跨平台开发',
|
||||||
|
'personal-brand': '个人品牌',
|
||||||
|
'ai-advanced': 'AI进阶'
|
||||||
|
},
|
||||||
|
'en': {
|
||||||
|
'stage-0': 'Kindergarten',
|
||||||
|
'stage-1': 'AI Product Manager',
|
||||||
|
'stage-2': 'Junior Developer',
|
||||||
|
'stage-3': 'Senior Developer',
|
||||||
|
'appendix': 'Appendix',
|
||||||
|
'guide': 'Guide',
|
||||||
|
'frontend': 'Frontend',
|
||||||
|
'backend': 'Backend',
|
||||||
|
'ai-capabilities': 'AI Capabilities',
|
||||||
|
'core-skills': 'Core Skills',
|
||||||
|
'cross-platform': 'Cross-platform',
|
||||||
|
'personal-brand': 'Personal Brand',
|
||||||
|
'ai-advanced': 'AI Advanced'
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const names = segmentNames[locale] || segmentNames['zh-cn']
|
||||||
|
|
||||||
|
pathParts.forEach((part, index) => {
|
||||||
|
currentPath += `/${part}`
|
||||||
|
const name = names[part] || part.replace(/-/g, ' ')
|
||||||
|
items.push({
|
||||||
|
'@type': 'ListItem',
|
||||||
|
position: index + 2,
|
||||||
|
name: name,
|
||||||
|
item: `${siteUrl}/${locale}${currentPath}/`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'BreadcrumbList',
|
||||||
|
itemListElement: items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const breadcrumbJsonLd = generateBreadcrumbList()
|
||||||
head.push(['script', { type: 'application/ld+json', class: 'breadcrumb-jsonld' }, JSON.stringify(breadcrumbJsonLd)])
|
head.push(['script', { type: 'application/ld+json', class: 'breadcrumb-jsonld' }, JSON.stringify(breadcrumbJsonLd)])
|
||||||
|
|
||||||
return head
|
return head
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Easy-Vibe - AI Vibe Coding Curriculum
|
||||||
|
# https://datawhalechina.github.io/easy-vibe
|
||||||
|
#
|
||||||
|
# This file helps AI models and agents understand our project structure
|
||||||
|
# Created for: OpenClaw, Claude, Cursor, Trae, and other AI coding assistants
|
||||||
|
|
||||||
|
== Project Overview ==
|
||||||
|
|
||||||
|
Easy-Vibe is an educational curriculum for learning AI Vibe Coding from zero to advanced levels.
|
||||||
|
It's built with VitePress and provides interactive tutorials in multiple languages.
|
||||||
|
|
||||||
|
== Learning Path ==
|
||||||
|
|
||||||
|
Stage 0 (Kindergarten): Learn AI programming through games
|
||||||
|
- Learning map visualization
|
||||||
|
- AI capabilities through interactive games
|
||||||
|
|
||||||
|
Stage 1 (AI Product Manager): Build AI-powered web application prototypes
|
||||||
|
- Finding great ideas
|
||||||
|
- AI IDE introduction (Cursor, Claude Code)
|
||||||
|
- Building prototypes
|
||||||
|
- Integrating AI capabilities
|
||||||
|
|
||||||
|
Stage 2 (Junior/Mid-level Developer): Full-stack development
|
||||||
|
- Frontend development
|
||||||
|
- Backend development with databases
|
||||||
|
- Deployment and DevOps
|
||||||
|
|
||||||
|
Stage 3 (Senior Developer): Cross-platform development
|
||||||
|
- WeChat mini-programs
|
||||||
|
- Android and iOS apps
|
||||||
|
- MCP (Model Context Protocol)
|
||||||
|
- RAG and LangGraph
|
||||||
|
|
||||||
|
== Content Structure ==
|
||||||
|
|
||||||
|
/docs/
|
||||||
|
- zh-cn/ (Simplified Chinese - primary, complete)
|
||||||
|
- en/ (English - complete)
|
||||||
|
- zh-tw/, ja-jp/, ko-kr/, etc. (partial translations)
|
||||||
|
- stage-0/, stage-1/, stage-2/, stage-3/ (curriculum stages)
|
||||||
|
- appendix/ (reference materials with interactive components)
|
||||||
|
|
||||||
|
== Key Files ==
|
||||||
|
|
||||||
|
- CLAUDE.md: Project-specific instructions for AI assistants
|
||||||
|
- package.json: Dependencies and scripts
|
||||||
|
- docs/.vitepress/config.mjs: Site configuration
|
||||||
|
|
||||||
|
== Contact ==
|
||||||
|
|
||||||
|
- GitHub: https://github.com/datawhalechina/easy-vibe
|
||||||
|
- Organization: Datawhale China
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# robots.txt for Easy-Vibe
|
||||||
|
# https://datawhalechina.github.io/easy-vibe
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
# Sitemap location
|
||||||
|
Sitemap: https://datawhalechina.github.io/easy-vibe/sitemap.xml
|
||||||
|
|
||||||
|
# Crawl-delay for polite crawling
|
||||||
|
Crawl-delay: 1
|
||||||
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -5,14 +5,15 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vitepress dev docs",
|
"dev": "vitepress dev docs",
|
||||||
"build": "vitepress build docs",
|
"build": "npm run sitemap && vitepress build docs",
|
||||||
"build:force": "vitepress build docs --force",
|
"build:force": "npm run sitemap && vitepress build docs --force",
|
||||||
"preview": "vitepress preview docs",
|
"preview": "vitepress preview docs",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"verify": "bash scripts/verify.sh",
|
"verify": "bash scripts/verify.sh",
|
||||||
"lint": "eslint docs/.vitepress/theme",
|
"lint": "eslint docs/.vitepress/theme",
|
||||||
"lint:fix": "eslint docs/.vitepress/theme --fix",
|
"lint:fix": "eslint docs/.vitepress/theme --fix",
|
||||||
"prepare": "husky"
|
"prepare": "husky",
|
||||||
|
"sitemap": "node scripts/generate-sitemap.mjs"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"easy-vibe",
|
"easy-vibe",
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Sitemap Generator for Easy-Vibe
|
||||||
|
* Generates sitemap.xml for all pages in the documentation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = path.dirname(__filename)
|
||||||
|
|
||||||
|
const docsDir = path.resolve(__dirname, '../docs')
|
||||||
|
const publicDir = path.resolve(__dirname, '../docs/public')
|
||||||
|
|
||||||
|
// 支持的语言
|
||||||
|
const locales = ['zh-cn', 'en', 'zh-tw', 'ja-jp', 'ko-kr', 'es-es', 'fr-fr', 'de-de', 'ar-sa', 'vi-vn']
|
||||||
|
|
||||||
|
// 基础 URL (根据部署环境动态确定)
|
||||||
|
const getBaseUrl = () => {
|
||||||
|
if (process.env.VERCEL_URL) {
|
||||||
|
return `https://${process.env.VERCEL_URL}`
|
||||||
|
}
|
||||||
|
if (process.env.EDGEONE_URL) {
|
||||||
|
return `https://${process.env.EDGEONE_URL}`
|
||||||
|
}
|
||||||
|
if (process.env.SITE_URL) {
|
||||||
|
return process.env.SITE_URL
|
||||||
|
}
|
||||||
|
return 'https://datawhalechina.github.io/easy-vibe'
|
||||||
|
}
|
||||||
|
|
||||||
|
const siteUrl = getBaseUrl()
|
||||||
|
|
||||||
|
// 扫描目录中的所有 markdown 文件
|
||||||
|
function scanMarkdownFiles(dir, basePath = '') {
|
||||||
|
const files = []
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name)
|
||||||
|
const relativePath = path.join(basePath, entry.name)
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
// 跳过特殊目录
|
||||||
|
if (entry.name === '.vitepress' || entry.name === 'node_modules' || entry.name === 'dist' || entry.name === 'public') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
files.push(...scanMarkdownFiles(fullPath, relativePath))
|
||||||
|
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||||
|
files.push(relativePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将 markdown 路径转换为 URL 路径
|
||||||
|
function mdPathToUrl(mdPath, locale) {
|
||||||
|
// 移除 .md 扩展名
|
||||||
|
let urlPath = mdPath.replace(/\.md$/, '')
|
||||||
|
|
||||||
|
// 如果是 index.md,只保留目录
|
||||||
|
if (urlPath.endsWith('/index')) {
|
||||||
|
urlPath = urlPath.slice(0, -6)
|
||||||
|
} else if (urlPath === 'index') {
|
||||||
|
urlPath = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建完整 URL
|
||||||
|
return `${siteUrl}/${locale}/${urlPath}${urlPath ? '/' : ''}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成 sitemap XML
|
||||||
|
function generateSitemap(urls) {
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
|
||||||
|
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
|
xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"\n'
|
||||||
|
xml += ' xmlns:xhtml="http://www.w3.org/1999/xhtml">\n'
|
||||||
|
|
||||||
|
for (const urlInfo of urls) {
|
||||||
|
xml += ' <url>\n'
|
||||||
|
xml += ` <loc>${escapeXml(urlInfo.loc)}</loc>\n`
|
||||||
|
xml += ` <lastmod>${now}</lastmod>\n`
|
||||||
|
xml += ` <changefreq>weekly</changefreq>\n`
|
||||||
|
xml += ` <priority>${urlInfo.priority.toFixed(1)}</priority>\n`
|
||||||
|
|
||||||
|
// 添加 hreflang alternates
|
||||||
|
for (const alternate of urlInfo.alternates) {
|
||||||
|
xml += ` <xhtml:link rel="alternate" hreflang="${alternate.hreflang}" href="${escapeXml(alternate.href)}"/>\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
xml += ' </url>\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
xml += '</urlset>\n'
|
||||||
|
return xml
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeXml(str) {
|
||||||
|
return str
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主函数
|
||||||
|
function main() {
|
||||||
|
console.log('🔍 Scanning documentation files...')
|
||||||
|
|
||||||
|
const allUrls = []
|
||||||
|
const localePaths = new Map()
|
||||||
|
|
||||||
|
// 首先扫描中文内容作为基准
|
||||||
|
const zhCnDir = path.join(docsDir, 'zh-cn')
|
||||||
|
let baseFiles = []
|
||||||
|
|
||||||
|
if (fs.existsSync(zhCnDir)) {
|
||||||
|
baseFiles = scanMarkdownFiles(zhCnDir)
|
||||||
|
} else {
|
||||||
|
// 如果没有 zh-cn 目录,扫描 docs 根目录
|
||||||
|
baseFiles = scanMarkdownFiles(docsDir).filter(f => !f.includes('/'))
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📄 Found ${baseFiles} base pages`)
|
||||||
|
|
||||||
|
// 为每个文件生成 URL 信息
|
||||||
|
for (const baseFile of baseFiles) {
|
||||||
|
// 跳过根目录的 index.md(特殊处理)
|
||||||
|
if (baseFile === 'index.md') continue
|
||||||
|
|
||||||
|
const urlInfo = {
|
||||||
|
loc: '',
|
||||||
|
priority: getPriority(baseFile),
|
||||||
|
alternates: []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为每个语言版本生成 alternate
|
||||||
|
for (const locale of locales) {
|
||||||
|
const localeDir = path.join(docsDir, locale)
|
||||||
|
const localeFile = path.join(localeDir, baseFile)
|
||||||
|
|
||||||
|
// 检查该语言版本是否存在
|
||||||
|
if (fs.existsSync(localeFile)) {
|
||||||
|
const url = mdPathToUrl(baseFile, locale)
|
||||||
|
urlInfo.alternates.push({
|
||||||
|
hreflang: getHreflangCode(locale),
|
||||||
|
href: url
|
||||||
|
})
|
||||||
|
|
||||||
|
// 设置主要语言版本为 zh-cn
|
||||||
|
if (locale === 'zh-cn') {
|
||||||
|
urlInfo.loc = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有至少一个语言版本存在
|
||||||
|
if (urlInfo.alternates.length > 0) {
|
||||||
|
// 如果没有 zh-cn 版本,使用第一个可用的
|
||||||
|
if (!urlInfo.loc) {
|
||||||
|
urlInfo.loc = urlInfo.alternates[0].href
|
||||||
|
}
|
||||||
|
allUrls.push(urlInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加首页
|
||||||
|
const homeAlternates = []
|
||||||
|
for (const locale of locales) {
|
||||||
|
homeAlternates.push({
|
||||||
|
hreflang: getHreflangCode(locale),
|
||||||
|
href: `${siteUrl}/${locale}/`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
allUrls.unshift({
|
||||||
|
loc: `${siteUrl}/zh-cn/`,
|
||||||
|
priority: 1.0,
|
||||||
|
alternates: homeAlternates
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`🌐 Generating sitemap with ${allUrls.length} URLs...`)
|
||||||
|
|
||||||
|
const sitemapXml = generateSitemap(allUrls)
|
||||||
|
const outputPath = path.join(publicDir, 'sitemap.xml')
|
||||||
|
fs.writeFileSync(outputPath, sitemapXml, 'utf-8')
|
||||||
|
|
||||||
|
console.log(`✅ Sitemap generated at ${outputPath}`)
|
||||||
|
console.log(`📊 Statistics:`)
|
||||||
|
console.log(` - Total URLs: ${allUrls.length}`)
|
||||||
|
console.log(` - Locales: ${locales.length}`)
|
||||||
|
console.log(` - Site URL: ${siteUrl}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPriority(filePath) {
|
||||||
|
if (filePath.includes('stage-0') || filePath.includes('stage-1')) return 0.9
|
||||||
|
if (filePath.includes('stage-2')) return 0.8
|
||||||
|
if (filePath.includes('stage-3')) return 0.8
|
||||||
|
if (filePath.includes('appendix')) return 0.7
|
||||||
|
return 0.6
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHreflangCode(locale) {
|
||||||
|
const map = {
|
||||||
|
'zh-cn': 'zh-CN',
|
||||||
|
'en': 'en',
|
||||||
|
'zh-tw': 'zh-TW',
|
||||||
|
'ja-jp': 'ja',
|
||||||
|
'ko-kr': 'ko',
|
||||||
|
'es-es': 'es',
|
||||||
|
'fr-fr': 'fr',
|
||||||
|
'de-de': 'de',
|
||||||
|
'ar-sa': 'ar',
|
||||||
|
'vi-vn': 'vi'
|
||||||
|
}
|
||||||
|
return map[locale] || locale
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user