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, ''') } /** 从列表返回中提取 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 = ` ${urls .map( (u) => ` ${escapeXml(origin + u.loc)} ${u.lastmod} ${u.changefreq} ${u.priority} ` ) .join('\n')} ` setHeader(event, 'Content-Type', 'application/xml') return xml })