/** * 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') }