2b69686795
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引 - 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理 - 实现/article、/case、/product及/page动态路由兼容列表与详情展示 - 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置 - 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持 - 模板增强支持CMS单页内容加载及SEO信息动态设置 - 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
146 lines
4.4 KiB
TypeScript
146 lines
4.4 KiB
TypeScript
import { defineEventHandler, getQuery, setHeader } from 'h3'
|
||
import { useRuntimeConfig } from '#imports'
|
||
import { getTenantFromContext } from '../utils/tenant'
|
||
import { $fetch } from 'ofetch'
|
||
|
||
/**
|
||
* 动态站点地图
|
||
* GET /api/sitemap.xml
|
||
* 根据当前租户的页面列表动态生成 sitemap.xml
|
||
*
|
||
* 覆盖范围:
|
||
* - 静态页:首页 + 三大模块列表根页(/article /product /case)
|
||
* - CMS 单页(/page slug 对应的顶层路由,如 /about /contact)
|
||
* - 文章 / 产品 / 案例 详情页(仅已发布 status=0、未删除 deleted=0)
|
||
*/
|
||
interface SitemapUrl {
|
||
loc: string
|
||
priority: string
|
||
changefreq: string
|
||
lastmod: string
|
||
}
|
||
|
||
function escapeXml(s: string): string {
|
||
return s
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''')
|
||
}
|
||
|
||
/** 从列表返回中提取 list 数组(兼容 article 原始包装与 product/case 归一化结构) */
|
||
function extractList(res: any): any[] {
|
||
if (!res) return []
|
||
const data = res.data !== undefined ? res.data : res
|
||
if (data && Array.isArray(data.list)) return data.list
|
||
if (Array.isArray(res?.data?.list)) return res.data.list
|
||
if (Array.isArray(data)) return data
|
||
return []
|
||
}
|
||
|
||
/** 从列表项中提取唯一 id(各模块主键字段名不同) */
|
||
function pickId(item: any, keys: string[]): string | number | null {
|
||
for (const k of keys) {
|
||
if (item?.[k] != null) return item[k]
|
||
}
|
||
return null
|
||
}
|
||
|
||
export default defineEventHandler(async (event) => {
|
||
const config = useRuntimeConfig()
|
||
const ctx = getTenantFromContext(event, config)
|
||
const query = getQuery(event)
|
||
const modulesApiBase = config.public.modulesApiBase as string
|
||
|
||
const origin = `https://${ctx.host || ''}`
|
||
const now = new Date().toISOString()
|
||
|
||
const urls: SitemapUrl[] = [
|
||
{ loc: '/', priority: '1.0', changefreq: 'daily', lastmod: now },
|
||
{ loc: '/article', priority: '0.9', changefreq: 'daily', lastmod: now },
|
||
{ loc: '/product', priority: '0.9', changefreq: 'daily', lastmod: now },
|
||
{ loc: '/case', priority: '0.9', changefreq: 'daily', lastmod: now }
|
||
]
|
||
|
||
// ===== 单页 slug(/about /contact 等顶层路由) =====
|
||
try {
|
||
const res = await $fetch<{ data?: { list?: any[] } } | any[]>('/cms/cms-website/pageAll', {
|
||
baseURL: modulesApiBase,
|
||
headers: { TenantId: ctx.tenantId }
|
||
})
|
||
const list = Array.isArray(res) ? res : res?.data?.list || res?.data || []
|
||
for (const p of list) {
|
||
if (p?.slug && p.slug !== 'index') {
|
||
urls.push({
|
||
loc: `/${p.slug}`,
|
||
priority: '0.8',
|
||
changefreq: 'weekly',
|
||
lastmod: p.updateTime || now
|
||
})
|
||
}
|
||
}
|
||
} catch {
|
||
// 单页接口异常不影响详情页收录
|
||
}
|
||
|
||
// ===== 文章 / 产品 / 案例详情(分页拉取,仅已发布) =====
|
||
const fetchDetailUrls = async (
|
||
apiPath: string,
|
||
urlPrefix: string,
|
||
idKeys: string[]
|
||
) => {
|
||
const limit = 500
|
||
let page = 1
|
||
// 安全上限:单 sitemap 文件 URL 上限 50,000
|
||
while (urls.length < 50000) {
|
||
let list: any[] = []
|
||
try {
|
||
const res = await $fetch(apiPath, {
|
||
baseURL: modulesApiBase,
|
||
headers: { TenantId: ctx.tenantId },
|
||
query: { page, limit, status: 0, deleted: 0 }
|
||
})
|
||
list = extractList(res)
|
||
} catch {
|
||
break
|
||
}
|
||
|
||
for (const item of list) {
|
||
const idVal = pickId(item, idKeys)
|
||
if (idVal == null) continue
|
||
urls.push({
|
||
loc: `/${urlPrefix}/${idVal}`,
|
||
priority: '0.7',
|
||
changefreq: 'weekly',
|
||
lastmod: item.updateTime || item.publishTime || item.createTime || now
|
||
})
|
||
}
|
||
|
||
if (list.length < limit) break
|
||
page++
|
||
}
|
||
}
|
||
|
||
await fetchDetailUrls('/cms/cms-article/page', 'article', ['id', 'articleId'])
|
||
await fetchDetailUrls('/cms/cms-product/page', 'product', ['id', 'productId'])
|
||
await fetchDetailUrls('/cms/cms-case/page', 'case', ['id', 'caseId', 'caseItemId'])
|
||
|
||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||
${urls
|
||
.map(
|
||
(u) => ` <url>
|
||
<loc>${escapeXml(origin + u.loc)}</loc>
|
||
<lastmod>${u.lastmod}</lastmod>
|
||
<changefreq>${u.changefreq}</changefreq>
|
||
<priority>${u.priority}</priority>
|
||
</url>`
|
||
)
|
||
.join('\n')}
|
||
</urlset>`
|
||
|
||
setHeader(event, 'Content-Type', 'application/xml')
|
||
return xml
|
||
})
|