144 lines
4.5 KiB
JavaScript
144 lines
4.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(/\/+$/, '')
|
|
|
|
/**
|
|
* URL을 절대 URL로 변환한다.
|
|
* @param {unknown} value - 원본 URL
|
|
* @param {string} siteUrl - 사이트 URL
|
|
* @returns {string} 절대 URL
|
|
*/
|
|
const toAbsoluteUrl = (value, siteUrl) => {
|
|
const url = String(value || '').trim()
|
|
|
|
if (!url) {
|
|
return ''
|
|
}
|
|
|
|
if (/^https?:\/\//i.test(url)) {
|
|
return url
|
|
}
|
|
|
|
if (url.startsWith('//')) {
|
|
return `https:${url}`
|
|
}
|
|
|
|
if (url.startsWith('/')) {
|
|
return `${siteUrl}${url}`
|
|
}
|
|
|
|
return `${siteUrl}/${url.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 썸네일 URL을 반환한다.
|
|
* @param {Object} post - 게시물
|
|
* @param {string} siteUrl - 사이트 URL
|
|
* @returns {string} 썸네일 절대 URL
|
|
*/
|
|
const getPostThumbnailUrl = (post, siteUrl) => toAbsoluteUrl(post.ogImage || post.featuredImage, siteUrl)
|
|
|
|
/**
|
|
* 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 thumbnailUrl = getPostThumbnailUrl(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>`,
|
|
thumbnailUrl ? ` <media:thumbnail url="${escapeXml(thumbnailUrl)}" />` : '',
|
|
thumbnailUrl ? ` <media:content url="${escapeXml(thumbnailUrl)}" medium="image" />` : '',
|
|
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" xmlns:media="http://search.yahoo.com/mrss/">',
|
|
' <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')
|
|
}
|