145 lines
4.5 KiB
JavaScript
145 lines
4.5 KiB
JavaScript
import { getSiteSettings, listPages, listPosts, listTags } from '../repositories/content-repository'
|
|
|
|
/**
|
|
* 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 {string} path - 사이트 내부 경로
|
|
* @param {string} siteUrl - 사이트 URL
|
|
* @returns {string} 절대 URL
|
|
*/
|
|
const toSiteUrl = (path, siteUrl) => `${siteUrl}${path.startsWith('/') ? path : `/${path}`}`
|
|
|
|
/**
|
|
* sitemap 날짜 문자열을 만든다.
|
|
* @param {unknown} value - ISO 날짜 문자열
|
|
* @returns {string} sitemap lastmod 문자열
|
|
*/
|
|
const formatSitemapDate = (value) => {
|
|
const date = value ? new Date(value) : new Date()
|
|
|
|
if (Number.isNaN(date.getTime())) {
|
|
return new Date().toISOString()
|
|
}
|
|
|
|
return date.toISOString()
|
|
}
|
|
|
|
/**
|
|
* sitemap URL 항목 XML을 만든다.
|
|
* @param {Object} item - URL 항목
|
|
* @param {string} item.loc - 절대 URL
|
|
* @param {string} [item.lastmod] - 마지막 수정 시각
|
|
* @param {string} [item.changefreq] - 변경 빈도
|
|
* @param {string} [item.priority] - 우선순위
|
|
* @returns {string} URL 항목 XML
|
|
*/
|
|
const buildUrlItem = ({ loc, lastmod = '', changefreq = '', priority = '' }) => [
|
|
' <url>',
|
|
` <loc>${escapeXml(loc)}</loc>`,
|
|
lastmod ? ` <lastmod>${escapeXml(formatSitemapDate(lastmod))}</lastmod>` : '',
|
|
changefreq ? ` <changefreq>${escapeXml(changefreq)}</changefreq>` : '',
|
|
priority ? ` <priority>${escapeXml(priority)}</priority>` : '',
|
|
' </url>'
|
|
].filter(Boolean).join('\n')
|
|
|
|
/**
|
|
* 공개 sitemap XML을 만든다.
|
|
* @param {string} fallbackSiteUrl - 사이트 설정 URL이 없을 때 사용할 요청 URL
|
|
* @returns {Promise<string>} sitemap XML
|
|
*/
|
|
export const buildSitemap = async (fallbackSiteUrl = '') => {
|
|
const [settings, posts, pages, tags] = await Promise.all([
|
|
getSiteSettings(),
|
|
listPosts({ includeMembership: false }),
|
|
listPages(),
|
|
listTags({ tagType: 'managed' })
|
|
])
|
|
const siteUrl = normalizeSiteUrl(settings.siteUrl || fallbackSiteUrl)
|
|
const now = new Date().toISOString()
|
|
const publicPosts = posts.filter((post) => !post.noindex)
|
|
const visibleTags = tags.filter((tag) => Number(tag.postCount || 0) > 0)
|
|
const items = [
|
|
{
|
|
loc: toSiteUrl('/', siteUrl),
|
|
lastmod: settings.updatedAt || now,
|
|
changefreq: 'daily',
|
|
priority: '1.0'
|
|
},
|
|
{
|
|
loc: toSiteUrl('/posts', siteUrl),
|
|
lastmod: publicPosts[0]?.updatedAt || now,
|
|
changefreq: 'daily',
|
|
priority: '0.8'
|
|
},
|
|
{
|
|
loc: toSiteUrl('/tags', siteUrl),
|
|
lastmod: visibleTags[0]?.updatedAt || now,
|
|
changefreq: 'weekly',
|
|
priority: '0.5'
|
|
},
|
|
...publicPosts.map((post) => ({
|
|
loc: toSiteUrl(`/post/${encodeURIComponent(post.slug)}`, siteUrl),
|
|
lastmod: post.updatedAt || post.publishedAt || post.createdAt,
|
|
changefreq: 'weekly',
|
|
priority: post.isFeatured ? '0.9' : '0.7'
|
|
})),
|
|
...pages.map((page) => ({
|
|
loc: toSiteUrl(`/pages/${encodeURIComponent(page.slug)}`, siteUrl),
|
|
lastmod: page.updatedAt || page.createdAt,
|
|
changefreq: 'monthly',
|
|
priority: '0.6'
|
|
})),
|
|
...visibleTags.map((tag) => ({
|
|
loc: toSiteUrl(`/tag/${encodeURIComponent(tag.slug)}`, siteUrl),
|
|
lastmod: tag.lastUsedAt || tag.updatedAt || now,
|
|
changefreq: 'weekly',
|
|
priority: '0.5'
|
|
}))
|
|
]
|
|
|
|
return [
|
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
items.map(buildUrlItem).join('\n'),
|
|
'</urlset>'
|
|
].join('\n')
|
|
}
|
|
|
|
/**
|
|
* robots.txt 본문을 만든다.
|
|
* @param {string} fallbackSiteUrl - 사이트 설정 URL이 없을 때 사용할 요청 URL
|
|
* @returns {Promise<string>} robots.txt 본문
|
|
*/
|
|
export const buildRobotsTxt = async (fallbackSiteUrl = '') => {
|
|
const settings = await getSiteSettings()
|
|
const siteUrl = normalizeSiteUrl(settings.siteUrl || fallbackSiteUrl)
|
|
|
|
return [
|
|
'User-agent: *',
|
|
'Disallow: /admin',
|
|
'Disallow: /api/',
|
|
'Disallow: /settings',
|
|
'Disallow: /signin',
|
|
`Sitemap: ${toSiteUrl('/sitemap.xml', siteUrl)}`,
|
|
''
|
|
].join('\n')
|
|
}
|