Files
hjc-web/server/api/sitemap.xml.ts
T
gxwebsoft 2b69686795 feat(app): 添加多模板关于我们页面及相关路由和404页面
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引
- 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理
- 实现/article、/case、/product及/page动态路由兼容列表与详情展示
- 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置
- 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持
- 模板增强支持CMS单页内容加载及SEO信息动态设置
- 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
2026-09-08 12:13:44 +08:00

146 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}
/** 从列表返回中提取 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
})