import { createPostSummary } from '../../composables/createPostSummary.js' import { getSiteSettings, listPosts } from '../repositories/content-repository' const RSS_POST_LIMIT = 50 /** * XML 텍스트 값을 이스케이프한다. * @param {unknown} value - XML에 넣을 값 * @returns {string} 이스케이프된 텍스트 */ const escapeXml = (value) => String(value || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') /** * 사이트 URL의 끝 슬래시를 제거한다. * @param {unknown} value - 사이트 URL * @returns {string} 정리된 사이트 URL */ const normalizeSiteUrl = (value) => String(value || '').trim().replace(/\/+$/, '') /** * RSS 날짜 문자열을 만든다. * @param {unknown} value - ISO 날짜 문자열 * @returns {string} RSS 날짜 문자열 */ const formatRssDate = (value) => { const date = value ? new Date(value) : new Date() if (Number.isNaN(date.getTime())) { return new Date().toUTCString() } return date.toUTCString() } /** * 게시물 공개 URL을 만든다. * @param {Object} post - 게시물 * @param {string} siteUrl - 사이트 URL * @returns {string} 게시물 URL */ const getPostUrl = (post, siteUrl) => `${siteUrl}/post/${encodeURIComponent(post.slug)}` /** * RSS item XML을 만든다. * @param {Object} post - 게시물 * @param {string} siteUrl - 사이트 URL * @returns {string} RSS item XML */ const buildRssItem = (post, siteUrl) => { const postUrl = getPostUrl(post, siteUrl) const description = createPostSummary(post.excerpt, post.content, { maxLength: 280, appendEllipsis: true }) const publishedAt = post.publishedAt || post.createdAt return [ ' ', ` ${escapeXml(post.title || 'Untitled')}`, ` ${escapeXml(postUrl)}`, ` ${escapeXml(postUrl)}`, ` ${escapeXml(formatRssDate(publishedAt))}`, description ? ` ${escapeXml(description)}` : '', ' ' ].filter(Boolean).join('\n') } /** * 공개 RSS XML을 만든다. * @param {string} selfPath - 현재 피드 경로 * @param {string} fallbackSiteUrl - 사이트 설정 URL이 없을 때 사용할 요청 URL * @returns {Promise} RSS XML */ export const buildRssFeed = async (selfPath = '/rss.xml', fallbackSiteUrl = '') => { const [settings, posts] = await Promise.all([ getSiteSettings(), listPosts({ includeMembership: false }) ]) const siteUrl = normalizeSiteUrl(settings.siteUrl || fallbackSiteUrl) const feedUrl = `${siteUrl}${selfPath}` const visiblePosts = posts.slice(0, RSS_POST_LIMIT) const lastBuildDate = visiblePosts[0]?.updatedAt || visiblePosts[0]?.publishedAt || new Date().toISOString() const items = visiblePosts.map((post) => buildRssItem(post, siteUrl)).join('\n') return [ '', '', ' ', ` ${escapeXml(settings.title)}`, ` ${escapeXml(siteUrl)}`, ` ${escapeXml(settings.description)}`, ' ko', ` ${escapeXml(formatRssDate(lastBuildDate))}`, ` `, items, ' ', '' ].filter(Boolean).join('\n') }