105 lines
3.5 KiB
JavaScript
105 lines
3.5 KiB
JavaScript
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, '"')
|
|
.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 [
|
|
' <item>',
|
|
` <title>${escapeXml(post.title || 'Untitled')}</title>`,
|
|
` <link>${escapeXml(postUrl)}</link>`,
|
|
` <guid isPermaLink="true">${escapeXml(postUrl)}</guid>`,
|
|
` <pubDate>${escapeXml(formatRssDate(publishedAt))}</pubDate>`,
|
|
description ? ` <description>${escapeXml(description)}</description>` : '',
|
|
' </item>'
|
|
].filter(Boolean).join('\n')
|
|
}
|
|
|
|
/**
|
|
* 공개 RSS XML을 만든다.
|
|
* @param {string} selfPath - 현재 피드 경로
|
|
* @param {string} fallbackSiteUrl - 사이트 설정 URL이 없을 때 사용할 요청 URL
|
|
* @returns {Promise<string>} 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 [
|
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
'<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">',
|
|
' <channel>',
|
|
` <title>${escapeXml(settings.title)}</title>`,
|
|
` <link>${escapeXml(siteUrl)}</link>`,
|
|
` <description>${escapeXml(settings.description)}</description>`,
|
|
' <language>ko</language>',
|
|
` <lastBuildDate>${escapeXml(formatRssDate(lastBuildDate))}</lastBuildDate>`,
|
|
` <atom:link href="${escapeXml(feedUrl)}" rel="self" type="application/rss+xml" />`,
|
|
items,
|
|
' </channel>',
|
|
'</rss>'
|
|
].filter(Boolean).join('\n')
|
|
}
|