/**
* Mandu SEO - Sitemap XML Rendering
*
* Sitemap 배열을 XML 문자열로 변환
*/
import type { Sitemap, SitemapEntry } from '../types'
/**
* XML 이스케이프
*/
function escapeXml(str: string): string {
return str
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
/**
* Date를 ISO 문자열로 변환
*/
function formatDate(date: string | Date): string {
if (date instanceof Date) {
return date.toISOString()
}
return date
}
/**
* 단일 sitemap 항목을 XML로 변환
*/
function renderSitemapEntry(entry: SitemapEntry): string {
const lines: string[] = [' ']
lines.push(` ${escapeXml(entry.url)}`)
if (entry.lastModified) {
lines.push(` ${formatDate(entry.lastModified)}`)
}
if (entry.changeFrequency) {
lines.push(` ${entry.changeFrequency}`)
}
if (entry.priority !== undefined) {
lines.push(` ${entry.priority.toFixed(1)}`)
}
// 이미지 sitemap 확장
if (entry.images && entry.images.length > 0) {
for (const image of entry.images) {
lines.push(' ')
lines.push(` ${escapeXml(image)}`)
lines.push(' ')
}
}
// 다국어 alternate 링크 (xhtml:link)
if (entry.alternates?.languages) {
for (const [lang, url] of Object.entries(entry.alternates.languages)) {
lines.push(` `)
}
}
lines.push(' ')
return lines.join('\n')
}
/**
* Sitemap 배열을 XML 문자열로 렌더링
*
* @param sitemap - Sitemap 항목 배열
* @returns XML 문자열
*
* @example
* ```typescript
* const xml = renderSitemap([
* { url: 'https://example.com', lastModified: new Date(), priority: 1.0 },
* { url: 'https://example.com/about', changeFrequency: 'monthly' },
* ])
* ```
*/
export function renderSitemap(sitemap: Sitemap): string {
const hasImages = sitemap.some(entry => entry.images && entry.images.length > 0)
const hasAlternates = sitemap.some(entry => entry.alternates?.languages)
// XML 네임스페이스 구성
const namespaces = ['xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"']
if (hasImages) {
namespaces.push('xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"')
}
if (hasAlternates) {
namespaces.push('xmlns:xhtml="http://www.w3.org/1999/xhtml"')
}
const lines: string[] = [
'',
``,
...sitemap.map(renderSitemapEntry),
'',
]
return lines.join('\n')
}
/**
* Sitemap Index 렌더링 (대규모 사이트용)
*
* @param sitemaps - 개별 sitemap URL 배열
* @returns XML 문자열
*/
export function renderSitemapIndex(
sitemaps: Array<{ url: string; lastModified?: string | Date }>
): string {
const lines: string[] = [
'',
'',
]
for (const sitemap of sitemaps) {
lines.push(' ')
lines.push(` ${escapeXml(sitemap.url)}`)
if (sitemap.lastModified) {
lines.push(` ${formatDate(sitemap.lastModified)}`)
}
lines.push(' ')
}
lines.push('')
return lines.join('\n')
}