diff --git a/packages/karbon/src/runtime/api/feed.ts b/packages/karbon/src/runtime/api/feed.ts
index 4ef62878..a20539c2 100644
--- a/packages/karbon/src/runtime/api/feed.ts
+++ b/packages/karbon/src/runtime/api/feed.ts
@@ -1,5 +1,6 @@
import { gql } from '@apollo/client/core/index.js'
import { useStoripressClient } from '../composables/storipress-client'
+import type { _NormalizeArticle } from './normalize-article'
const GetDesk = gql`
query GetDesk($slug: String) {
@@ -21,7 +22,7 @@ const GetDesk = gql`
}
`
-export async function listFeedArticles() {
+export async function listFeedArticles(): Promise<_NormalizeArticle[]> {
const allArticles = (await $fetch('/_storipress/posts/__all.json')) ?? []
return allArticles
}
diff --git a/packages/karbon/src/runtime/lib/__tests__/__snapshots__/feed.spec.ts.snap b/packages/karbon/src/runtime/lib/__tests__/__snapshots__/feed.spec.ts.snap
index 5dc76592..267d7798 100644
--- a/packages/karbon/src/runtime/lib/__tests__/__snapshots__/feed.spec.ts.snap
+++ b/packages/karbon/src/runtime/lib/__tests__/__snapshots__/feed.spec.ts.snap
@@ -44,3 +44,29 @@ exports[`createFeed > can create feed with default config 1`] = `
"
`;
+
+exports[`generateAtomFeed > can generate atom feed 1`] = `
+"
+
+ https://example.com/
+ site name
+ 2024-04-24T00:00:00.000Z
+ https://github.com/jpmonette/feed
+
+
+ site description
+ © site name 2024 All Rights Reserved
+
+
+ https://example.com/posts/slug
+
+ 2024-04-24T00:00:00.000Z
+
+
+
+ author
+
+ 2024-04-24T00:00:00.000Z
+
+"
+`;
diff --git a/packages/karbon/src/runtime/lib/__tests__/feed.spec.ts b/packages/karbon/src/runtime/lib/__tests__/feed.spec.ts
index a352e1d3..4157c09f 100644
--- a/packages/karbon/src/runtime/lib/__tests__/feed.spec.ts
+++ b/packages/karbon/src/runtime/lib/__tests__/feed.spec.ts
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { addFeedPageLinks, createFeed } from '../feed'
+import { addFeedPageLinks, createFeed, generateAtomFeed } from '../feed'
beforeEach(() => {
vi.useFakeTimers()
@@ -29,6 +29,32 @@ describe('createFeed', () => {
})
})
+describe('generateAtomFeed', () => {
+ it('can generate atom feed', () => {
+ const atom = generateAtomFeed({
+ articles: [
+ {
+ id: '1',
+ slug: 'slug',
+ authors: [{ name: 'author' }] as any,
+ title: 'title',
+ html: 'html',
+ plaintext: 'plaintext',
+ updated_at: '2024-04-24T00:00:00.000Z',
+ published_at: '2024-04-24T00:00:00.000Z',
+ },
+ ],
+ getArticleURL: () => '/posts/slug',
+ siteName: 'site name',
+ siteDescription: 'site description',
+ siteUrl: 'https://example.com',
+ feedUrl: 'atom.xml',
+ })
+
+ expect(atom).toMatchSnapshot()
+ })
+})
+
const ATOM_FIXTURE = `
diff --git a/packages/karbon/src/runtime/lib/feed.ts b/packages/karbon/src/runtime/lib/feed.ts
index fc68a722..7a6f55d4 100644
--- a/packages/karbon/src/runtime/lib/feed.ts
+++ b/packages/karbon/src/runtime/lib/feed.ts
@@ -1,6 +1,7 @@
import { Feed } from 'feed'
-import { joinURL, withQuery, withTrailingSlash, withoutTrailingSlash } from 'ufo'
+import { encodePath, joinURL, withQuery, withTrailingSlash, withoutTrailingSlash } from 'ufo'
import { XMLBuilder, XMLParser } from 'fast-xml-parser'
+import type { _NormalizeArticle } from '../api/normalize-article'
export function createFeed({
siteUrl,
@@ -26,7 +27,7 @@ export function createFeed({
})
}
-export function addFeedPageLinks(atomXml: string, siteUrl: string, currentPage: number, maxPage: number) {
+export function addFeedPageLinks(atomXml: string, siteUrl: string, currentPage: number, maxPage: number): string {
const option = {
ignoreAttributes: false,
attributeNamePrefix: '@_',
@@ -53,3 +54,54 @@ export function addFeedPageLinks(atomXml: string, siteUrl: string, currentPage:
})
return buildAtomXml
}
+
+export type FeedArticle = Pick<
+ _NormalizeArticle,
+ 'id' | 'slug' | 'plaintext' | 'authors' | 'updated_at' | 'published_at' | 'title' | 'html'
+>
+
+export interface GenerateAtomFeedInput {
+ articles: FeedArticle[]
+ siteUrl: string
+ siteName: string
+ siteDescription: string
+ feedUrl: string
+ getArticleURL: (article: FeedArticle) => string
+}
+
+export function generateAtomFeed({
+ articles,
+ siteUrl,
+ siteName,
+ siteDescription,
+ feedUrl,
+ getArticleURL,
+}: GenerateAtomFeedInput): string {
+ const feed = createFeed({
+ siteUrl,
+ siteName,
+ siteDescription,
+ feedUrl,
+ })
+
+ articles
+ .filter((article) => article.published_at)
+ .forEach((article) => {
+ const id = encodePath(getArticleURL(article))
+ feed.addItem({
+ title: article.title,
+ id: joinURL(siteUrl, id),
+ link: joinURL(siteUrl, id),
+ description: article.plaintext.slice(0, 120),
+ date: new Date(article.updated_at),
+ published: new Date(article.published_at),
+ author:
+ article.authors?.map((author) => ({
+ name: author.name,
+ })) || [],
+ content: article.html,
+ })
+ })
+
+ return feed.atom1()
+}
diff --git a/packages/karbon/src/runtime/routes/atom-desk.xml.ts b/packages/karbon/src/runtime/routes/atom-desk.xml.ts
index 3ac38deb..645c7542 100644
--- a/packages/karbon/src/runtime/routes/atom-desk.xml.ts
+++ b/packages/karbon/src/runtime/routes/atom-desk.xml.ts
@@ -1,8 +1,6 @@
import { defineEventHandler, sendNoContent, setHeader } from 'h3'
-import { Feed } from 'feed'
-import { encodePath, joinURL, withTrailingSlash } from 'ufo'
import path from 'pathe'
-import { getDeskWithSlug, listArticles } from '@storipress/karbon/internal'
+import { generateAtomFeed, getDeskWithSlug, listArticles } from '@storipress/karbon/internal'
import { useRuntimeConfig } from '#imports'
import urls from '#sp-internal/storipress-urls.mjs'
@@ -21,39 +19,20 @@ export default defineEventHandler(async (e) => {
return sendNoContent(e, 404)
}
- const deskIds: string[] = desk.desks?.map(({ id }: { id: string }) => id) ?? []
+ const subDesks: string[] = desk.desks?.map(({ id }: { id: string }) => id) ?? []
const runtimeConfig = useRuntimeConfig()
- const articles = await listArticles({ desk_ids: deskIds })
+ const articles = await listArticles({ desk_ids: [desk.id, ...subDesks] })
const siteUrl = runtimeConfig.public.siteUrl as string
- const feed = new Feed({
- id: withTrailingSlash(runtimeConfig.public.siteUrl as string),
- link: withTrailingSlash(runtimeConfig.public.siteUrl as string),
- title: runtimeConfig.public.siteName as string,
- description: runtimeConfig.public.siteDescription as string,
- updated: new Date(),
- feedLinks: {
- atom: joinURL(siteUrl, `/atom/${fileName}`),
- },
- copyright: `© ${runtimeConfig.public.siteName} ${new Date().getFullYear()} All Rights Reserved`,
- })
- articles.forEach((article) => {
- const id = encodePath(urls.article.toURL(article, urls.article._context))
- feed.addItem({
- title: article.title,
- id: joinURL(siteUrl, id),
- link: joinURL(siteUrl, id),
- description: article.plaintext.slice(0, 120),
- date: new Date(article.published_at),
- author:
- article.authors?.map((author) => ({
- name: author.name,
- })) || [],
- content: article.html,
- })
+ const atomXml = generateAtomFeed({
+ articles,
+ siteUrl,
+ siteName: runtimeConfig.public.siteName as string,
+ siteDescription: runtimeConfig.public.siteDescription as string,
+ feedUrl: `/atom/${fileName}`,
+ getArticleURL: (article) => urls.article.toURL(article, urls.article._context),
})
-
- return feed.atom1()
+ return atomXml
})
diff --git a/packages/karbon/src/runtime/routes/atom.xml.ts b/packages/karbon/src/runtime/routes/atom.xml.ts
index 157da6dc..837be9b5 100644
--- a/packages/karbon/src/runtime/routes/atom.xml.ts
+++ b/packages/karbon/src/runtime/routes/atom.xml.ts
@@ -1,74 +1,42 @@
import type { H3Event } from 'h3'
import { defineEventHandler, getQuery, setHeader } from 'h3'
-import type { RuntimeConfig } from '@nuxt/schema'
-import { encodePath, joinURL } from 'ufo'
-import { addFeedPageLinks, createFeed, listFeedArticles } from '@storipress/karbon/internal'
-import type { Author } from '../composables/page-meta'
+import type { FeedArticle } from '@storipress/karbon/internal'
+import { addFeedPageLinks, generateAtomFeed, listFeedArticles } from '@storipress/karbon/internal'
+import type { _NormalizeArticle } from '../api/normalize-article'
import { useRuntimeConfig } from '#imports'
import urls from '#sp-internal/storipress-urls.mjs'
-interface TArticle {
- title: string
- id: string
- link: string
- description: string
- content: string
- date: Date
- authors: Author[]
- plaintext: string
- html: string
- published_at: string
- updated_at: string
-}
-
export default defineEventHandler(async (event) => {
setHeader(event, 'Content-Type', 'text/xml; charset=UTF-8')
if (!process.dev) setHeader(event, 'Cache-Control', 'max-age=600, must-revalidate')
const runtimeConfig = useRuntimeConfig()
- return await generateAtomFeed(runtimeConfig, event)
-})
-
-const ARTICLES_PER_PAGE = 100
+ const siteUrl = runtimeConfig.public.siteUrl as string
-async function generateAtomFeed(runtimeConfig: RuntimeConfig, event: H3Event) {
- const articles = await listFeedArticles()
+ const { currentPageArticles, currentPage, maxPage } = paginateArticle(event, await listFeedArticles())
- const siteUrl = runtimeConfig.public.siteUrl as string
- const feed = createFeed({
+ const atomXml = generateAtomFeed({
+ articles: currentPageArticles,
siteUrl,
siteName: runtimeConfig.public.siteName as string,
siteDescription: runtimeConfig.public.siteDescription as string,
feedUrl: 'atom.xml',
+ getArticleURL: (article) => urls.article.toURL(article, urls.article._context),
})
+ const buildAtomXml = addFeedPageLinks(atomXml, siteUrl, currentPage, maxPage)
+
+ return buildAtomXml
+})
+
+const ARTICLES_PER_PAGE = 100
+
+function paginateArticle(event: H3Event, articles: FeedArticle[]) {
const queryString = getQuery(event)
const page = Number(queryString.page) || 1
const maxPage = Math.ceil(articles.length / ARTICLES_PER_PAGE)
const currentPage = page > maxPage ? maxPage : page
const currentPageArticles = articles.slice((currentPage - 1) * ARTICLES_PER_PAGE, currentPage * ARTICLES_PER_PAGE)
- currentPageArticles
- .filter((article: TArticle) => article.published_at)
- .forEach((article: TArticle) => {
- const id = encodePath(urls.article.toURL(article, urls.article._context))
- feed.addItem({
- title: article.title,
- id: joinURL(siteUrl, id),
- link: joinURL(siteUrl, id),
- description: article.plaintext.slice(0, 120),
- date: new Date(article.updated_at),
- published: new Date(article.published_at),
- author:
- article.authors?.map((author) => ({
- name: author.name,
- })) || [],
- content: article.html,
- })
- })
-
- const atomXml = feed.atom1()
-
- const buildAtomXml = addFeedPageLinks(atomXml, siteUrl, currentPage, maxPage)
- return buildAtomXml
+ return { currentPageArticles, currentPage, maxPage }
}