From 359c8eddfebbd034c0e6c5a6225ac498a56e97a0 Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Fri, 28 Jun 2024 22:06:58 -0400 Subject: [PATCH] feat: rss + atom (#1092) --- packages/ui/app/src/index.ts | 2 +- .../src/next-app/utils/getBreadcrumbList.ts | 2 +- .../ui/app/src/next-app/utils/getSeoProp.ts | 8 +- packages/ui/docs-bundle/next.config.js | 2 + packages/ui/docs-bundle/package.json | 1 + .../src/pages/api/fern-docs/changelog.ts | 109 ++++++++++++++++++ pnpm-lock.yaml | 17 +++ 7 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 packages/ui/docs-bundle/src/pages/api/fern-docs/changelog.ts diff --git a/packages/ui/app/src/index.ts b/packages/ui/app/src/index.ts index 76fb036928..811509476f 100644 --- a/packages/ui/app/src/index.ts +++ b/packages/ui/app/src/index.ts @@ -7,7 +7,7 @@ export { useSetThemeColors } from "./docs/ThemeProvider"; export * from "./next-app/DocsPage"; export { NextApp } from "./next-app/NextApp"; export { getBreadcrumbList } from "./next-app/utils/getBreadcrumbList"; -export { getDefaultSeoProps } from "./next-app/utils/getSeoProp"; +export { getDefaultSeoProps, getFrontmatter } from "./next-app/utils/getSeoProp"; export { ApiDefinitionResolver } from "./resolver/ApiDefinitionResolver"; export { ApiTypeResolver } from "./resolver/ApiTypeResolver"; export * from "./resolver/types"; diff --git a/packages/ui/app/src/next-app/utils/getBreadcrumbList.ts b/packages/ui/app/src/next-app/utils/getBreadcrumbList.ts index 0bbcdbfba5..6866d38648 100644 --- a/packages/ui/app/src/next-app/utils/getBreadcrumbList.ts +++ b/packages/ui/app/src/next-app/utils/getBreadcrumbList.ts @@ -16,7 +16,7 @@ export function getBreadcrumbList( if (FernNavigation.isPage(node)) { const pageId = FernNavigation.utils.getPageId(node); if (pageId != null && pages[pageId] != null) { - const frontmatter = getFrontmatter(pages[pageId].markdown); + const [frontmatter] = getFrontmatter(pages[pageId].markdown); if (frontmatter["jsonld:breadcrumb"] != null) { const breadcrumb = JsonLd.BreadcrumbListSchema.safeParse(frontmatter["jsonld:breadcrumb"]); if (breadcrumb.success) { diff --git a/packages/ui/app/src/next-app/utils/getSeoProp.ts b/packages/ui/app/src/next-app/utils/getSeoProp.ts index 182759ce00..f647960344 100644 --- a/packages/ui/app/src/next-app/utils/getSeoProp.ts +++ b/packages/ui/app/src/next-app/utils/getSeoProp.ts @@ -18,12 +18,12 @@ function getFile(fileOrUrl: DocsV1Read.FileIdOrUrl, files: Record { + if (req.method !== "GET") { + return res.status(400).end(); + } + + let path = req.query["path"]; + const format = req.query["format"] ?? "rss"; + + if (typeof path !== "string" || typeof format !== "string") { + return res.status(400).end(); + } + + if (path.startsWith("/")) { + path = path.slice(1); + } + + path = decodeURIComponent(path); + + const xFernHost = getXFernHostNode(req); + const headers = new Headers(); + headers.set("x-fern-host", xFernHost); + + const url = buildUrlFromApiNode(xFernHost, req); + const docs = await loadWithUrl(url); + + if (docs == null) { + return res.status(404).end(); + } + + const root = FernNavigation.utils.convertLoadDocsForUrlResponse(docs); + const collector = NodeCollector.collect(root); + + const node = collector.slugMap.get(path); + + if (node?.type !== "changelog") { + return new NextResponse(null, { status: 404 }); + } + + const link = `https://${xFernHost}/${node.slug}`; + + const feed = new Feed({ + id: link, + link, + title: node.title, + copyright: `All rights reserved ${new Date().getFullYear()}`, + generator: "buildwithfern.com", + }); + + node.children.forEach((year) => { + year.children.forEach((month) => { + month.children.forEach((entry) => { + const item: Item = { + title: entry.title, + link: `https://${xFernHost}/${entry.slug}`, + date: new Date(entry.date), + }; + + const markdown = docs.definition.pages[entry.pageId].markdown; + if (markdown != null) { + const [frontmatter, content] = getFrontmatter(markdown); + item.description = frontmatter.description ?? frontmatter.subtitle ?? frontmatter.excerpt; + item.content = content; + + if (frontmatter.image != null) { + item.image = { url: frontmatter.image }; + } else if (frontmatter["og:image"] != null) { + if (frontmatter["og:image"].type === "url") { + item.image = { url: frontmatter["og:image"].value }; + } else if (frontmatter["og:image"].type === "fileId") { + const fileId = frontmatter["og:image"].value; + const file = docs.definition.files[fileId]; + if (file != null) { + item.image = { url: file }; + } + } else { + assertNever(frontmatter["og:image"]); + } + } + } + + feed.addItem(item); + }); + }); + }); + + if (format === "json") { + headers.set("Content-Type", "application/json"); + return res.json(feed.json1()); + } else if (format === "atom") { + headers.set("Content-Type", "application/atom+xml"); + return res.send(feed.atom1()); + } else { + headers.set("Content-Type", "application/rss+xml"); + return res.send(feed.rss2()); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f19679f93b..666569fe83 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1629,6 +1629,9 @@ importers: cssnano: specifier: ^6.0.3 version: 6.1.2(postcss@8.4.31) + feed: + specifier: ^4.2.2 + version: 4.2.2 jose: specifier: ^5.2.3 version: 5.2.4 @@ -17890,6 +17893,13 @@ packages: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} dev: false + /feed@4.2.2: + resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==} + engines: {node: '>=0.4.0'} + dependencies: + xml-js: 1.6.11 + dev: false + /fern-api@0.21.0: resolution: {integrity: sha512-/Zf/24Cg4YoRsbwJ8Joot0AcosQwUyvGQvZ6Qi0Cu/c83FfrWvQ73cb3ddJQCyzWhM06McgO5uguviSmgFyQWQ==} hasBin: true @@ -29316,6 +29326,13 @@ packages: utf-8-validate: optional: true + /xml-js@1.6.11: + resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} + hasBin: true + dependencies: + sax: 1.3.0 + dev: false + /xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'}