diff --git a/package.json b/package.json index 2d58e204..fb42d0d8 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "idb-keyval": "6.2.0", "ioredis": "5.2.4", "js-yaml": "4.1.0", + "jsonfeed-to-rss": "^3.0.6", "mdast-util-to-string": "3.1.0", "mdast-util-toc": "6.1.0", "nanoid": "4.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f754812..9fc81a6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,6 +61,7 @@ specifiers: idb-keyval: 6.2.0 ioredis: 5.2.4 js-yaml: 4.1.0 + jsonfeed-to-rss: ^3.0.6 lint-staged: 13.0.3 mdast-util-to-string: 3.1.0 mdast-util-toc: 6.1.0 @@ -141,6 +142,7 @@ dependencies: idb-keyval: 6.2.0 ioredis: 5.2.4 js-yaml: 4.1.0 + jsonfeed-to-rss: 3.0.6 mdast-util-to-string: 3.1.0 mdast-util-toc: 6.1.0 nanoid: 4.0.0 @@ -357,6 +359,12 @@ packages: to-fast-properties: 2.0.0 dev: false + /@bret/truthy/1.0.1: + resolution: {integrity: sha512-mFVhqy/yrh+BZwzqQQptGcTq5SJ+T0uhWQ/hmBkU0RFV3YN2UkcDhxvAzUuMDibOi6s66YM01BUGAi1y/92QGw==} + dependencies: + existy: 1.0.1 + dev: false + /@codemirror/autocomplete/6.3.0_cjhreg6sef6bi4tr3erat4xvva: resolution: {integrity: sha512-4jEvh3AjJZTDKazd10J6ZsCIqaYxDMCeua5ouQxY8hlFIml+nr7le0SgBhT3SIytFBmdzPK3AUhXGuW3T79nVg==} peerDependencies: @@ -1671,6 +1679,10 @@ packages: use-sync-external-store: 1.2.0_react@18.2.0 dev: false + /@textlint/ast-node-types/4.4.3: + resolution: {integrity: sha512-qi2jjgO6Tn3KNPGnm6B7p6QTEPvY95NFsIAaJuwbulur8iJUEenp1OnoUfiDaC/g2WPPEFkcfXpmnu8XEMFo2A==} + dev: false + /@types/bn.js/4.11.6: resolution: {integrity: sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==} dependencies: @@ -2439,6 +2451,10 @@ packages: hasBin: true dev: true + /add-zero/1.0.0: + resolution: {integrity: sha512-WpPiUgy7h9Kd7NY0aTuhfx7vjub3XYbZCq1W2e/LMvUsEmYK/hz8xgFDmd0GnKpk44HXFwIFu1hEOivc+MzJ0Q==} + dev: false + /aes-js/3.0.0: resolution: {integrity: sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==} dev: false @@ -2841,6 +2857,10 @@ packages: text-encoding-utf-8: 1.0.2 dev: false + /boundary/1.0.1: + resolution: {integrity: sha512-AaLhxHwYVh55iOTJncV3DE5o7RakEUSSj64XXEWRTiIhlp7aDI8qR0vY/k8Uw0Z234VjZi/iG/WxfrvqYPUCww==} + dev: false + /brace-expansion/1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -3078,6 +3098,15 @@ packages: resolution: {integrity: sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==} dev: false + /clean-deep/3.4.0: + resolution: {integrity: sha512-Lo78NV5ItJL/jl+B5w0BycAisaieJGXK1qYi/9m4SjR8zbqmrUtO7Yhro40wEShGmmxs/aJLI/A+jNhdkXK8mw==} + engines: {node: '>=4'} + dependencies: + lodash.isempty: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.transform: 4.6.0 + dev: false + /clean-stack/2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -3236,6 +3265,16 @@ packages: /concat-map/0.0.1: resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + /concat-stream/2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.0 + typedarray: 0.0.6 + dev: false + /consola/2.15.3: resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} dev: true @@ -4516,6 +4555,10 @@ packages: strip-final-newline: 3.0.0 dev: true + /existy/1.0.1: + resolution: {integrity: sha512-YeYTp9rIyoArnYubrTXHUOpuxwSdlQEctcw9zUgViSof/uJE+LlsiZb9BpIrCqLRcjiCCeF5cxX3wcfKZMqVzg==} + dev: false + /expand-template/2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -5549,6 +5592,23 @@ packages: minimist: 1.2.7 dev: true + /jsonfeed-to-rss/3.0.6: + resolution: {integrity: sha512-lbEMYwPsofOBoUIW3QteZw+ogVUpWbnDZIaWgLvK8+3bZLeCW7m6luk9N2AT/Ax1plHy8nXMZo6TvcRILkDHpA==} + dependencies: + '@bret/truthy': 1.0.1 + add-zero: 1.0.0 + clean-deep: 3.4.0 + existy: 1.0.1 + lodash.get: 4.4.2 + lodash.merge: 4.6.2 + podcast-categories: 2.0.0 + sentence-splitter: 3.2.2 + striptags: 3.2.0 + trim-left: 1.0.1 + trim-right: 1.0.1 + xmlbuilder: 15.1.1 + dev: false + /jsonify/0.0.1: resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} dev: false @@ -5707,18 +5767,33 @@ packages: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} dev: false + /lodash.get/4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + dev: false + /lodash.isarguments/3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} dev: false + /lodash.isempty/4.4.0: + resolution: {integrity: sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==} + dev: false + + /lodash.isplainobject/4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: false + /lodash.merge/4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - dev: true /lodash.throttle/4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} dev: false + /lodash.transform/4.6.0: + resolution: {integrity: sha512-LO37ZnhmBVx0GvOU/caQuipEh4GN82TcWv3yHlebGDgOxbxiwwzW5Pcx2AcvpIv2WmvmSMoC492yQFNhy/l/UQ==} + dev: false + /lodash/4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -6620,6 +6695,10 @@ packages: es-abstract: 1.20.4 dev: true + /object_values/0.1.2: + resolution: {integrity: sha512-tZgUiKLraVH+4OAedBYrr4/K6KmAQw2RPNd1AuNdhLsuz5WP3VB7WuiKBWbOcjeqqAjus2ChIIWC8dSfmg7ReA==} + dev: false + /ohmyfetch/0.4.21: resolution: {integrity: sha512-VG7f/JRvqvBOYvL0tHyEIEG7XHWm7OqIfAs6/HqwWwDfjiJ1g0huIpe5sFEmyb+7hpFa1EGNH2aERWR72tlClw==} dependencies: @@ -6875,6 +6954,10 @@ packages: engines: {node: '>=10.13.0'} dev: false + /podcast-categories/2.0.0: + resolution: {integrity: sha512-WcntiTmj4ERPwaevHeR59Lc6yk37YVwksflBaCu7/J2ChjPA02J703rZMVq4gnoa+XHFHqpjK+qSc8Fv6N62EA==} + dev: false + /postcss-import/14.1.0_postcss@8.4.19: resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} engines: {node: '>=10.0.0'} @@ -7662,6 +7745,16 @@ packages: dependencies: lru-cache: 6.0.0 + /sentence-splitter/3.2.2: + resolution: {integrity: sha512-hMvaodgK9Fay928uiQoTMEWjXpCERdKD2uKo7BbSyP+uWTo+wHiRjN+ZShyI99rW0VuoV4Cuw8FUmaRcnpN7Ug==} + hasBin: true + dependencies: + '@textlint/ast-node-types': 4.4.3 + concat-stream: 2.0.0 + object_values: 0.1.2 + structured-source: 3.0.2 + dev: false + /set-blocking/2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} dev: false @@ -7968,6 +8061,16 @@ packages: acorn: 8.8.1 dev: true + /striptags/3.2.0: + resolution: {integrity: sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==} + dev: false + + /structured-source/3.0.2: + resolution: {integrity: sha512-Ap7JHfKgmH40SUjumqyKTHYHNZ8GvGQskP34ks0ElHCDEig+bYGpmXVksxPSrgcY9rkJqhVMzfeg5GIpZelfpQ==} + dependencies: + boundary: 1.0.1 + dev: false + /style-mod/4.0.0: resolution: {integrity: sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==} dev: false @@ -8167,6 +8270,16 @@ packages: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: false + /trim-left/1.0.1: + resolution: {integrity: sha512-quyxKUsLsY7+lgue8TQ6R8C4kwTe5No3/fvliPNTZd8CIK1KXFaIw+Z7SwAWiWUF5Ge5j7ppbjml3EKa02uPWA==} + engines: {node: '>=0.10.0'} + dev: false + + /trim-right/1.0.1: + resolution: {integrity: sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==} + engines: {node: '>=0.10.0'} + dev: false + /trough/2.1.0: resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==} dev: false @@ -8246,6 +8359,10 @@ packages: is-typedarray: 1.0.0 dev: false + /typedarray/0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + dev: false + /typescript/4.9.3: resolution: {integrity: sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==} engines: {node: '>=4.2.0'} @@ -8949,6 +9066,11 @@ packages: utf-8-validate: 5.0.10 dev: false + /xmlbuilder/15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + dev: false + /xtend/4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} diff --git a/src/components/common/SEOHead.tsx b/src/components/common/SEOHead.tsx index 03c4557a..dd1e3956 100644 --- a/src/components/common/SEOHead.tsx +++ b/src/components/common/SEOHead.tsx @@ -27,16 +27,22 @@ export const SEOHead: React.FC<{ /> )} + diff --git a/src/components/site/SiteHeader.tsx b/src/components/site/SiteHeader.tsx index b8826c18..b82a7d3c 100644 --- a/src/components/site/SiteHeader.tsx +++ b/src/components/site/SiteHeader.tsx @@ -69,6 +69,11 @@ export const SiteHeader: React.FC<{ icon: , url: `/feed`, }, + { + text: "RSS", + icon: , + url: `/feed/xml`, + }, ] return ( diff --git a/src/components/ui/UniLink.tsx b/src/components/ui/UniLink.tsx index 6eacffb6..fc54a7fe 100644 --- a/src/components/ui/UniLink.tsx +++ b/src/components/ui/UniLink.tsx @@ -18,7 +18,8 @@ export const UniLink: React.FC<{ return {children} } - const isExternal = href && (/^https?:\/\//.test(href) || href === "/feed") + const isExternal = + href && (/^https?:\/\//.test(href) || href.startsWith("/feed")) if (isExternal) { return ( diff --git a/src/lib/json-feed.ts b/src/lib/json-feed.ts new file mode 100644 index 00000000..750e1152 --- /dev/null +++ b/src/lib/json-feed.ts @@ -0,0 +1,47 @@ +import { GetServerSideProps } from "next" +import { fetchGetSite } from "~/queries/site.server" +import { fetchGetPagesBySite } from "~/queries/page.server" +import { PageVisibilityEnum } from "~/lib/types" +import { getSiteLink } from "~/lib/helpers" +import { QueryClient } from "@tanstack/react-query" +import { renderPageContent } from "~/markdown" + +export const getJsonFeed = async (domainOrSubdomain: string, path: string) => { + const queryClient = new QueryClient() + + const site = await fetchGetSite(domainOrSubdomain, queryClient) + const pages = await fetchGetPagesBySite( + { + site: domainOrSubdomain, + type: "post", + visibility: PageVisibilityEnum.Published, + keepBody: true, + }, + queryClient, + ) + + const link = getSiteLink({ + subdomain: site.username || "", + }) + return { + version: "https://jsonfeed.org/version/1", + title: site.name, + description: site.description, + icon: link + site.avatars?.[0], + home_page_url: link, + feed_url: `${link}${path}`, + items: pages.list?.map((page: any) => ({ + id: page.id, + title: page.title, + content_html: + page.body?.content && + renderPageContent(page.body?.content, true).contentHTML, + summary: page.summary?.content, + url: `${link}/${page.slug || page.id}`, + image: `${link}${page.cover}`, + date_published: page.date_published, + date_modified: page.date_updated, + tags: page.tags, + })), + } +} diff --git a/src/markdown/index.ts b/src/markdown/index.ts index 7bf8bbfc..e70f24eb 100644 --- a/src/markdown/index.ts +++ b/src/markdown/index.ts @@ -55,7 +55,6 @@ const rehypePrism = rehypePrismGenerator(refractor) export const renderPageContent = ( content: string, html?: boolean, - enableToc?: boolean, ): Rendered => { const env: MarkdownEnv = { excerpt: "", diff --git a/src/models/page.model.ts b/src/models/page.model.ts index 73c601fa..f0de37e0 100644 --- a/src/models/page.model.ts +++ b/src/models/page.model.ts @@ -206,6 +206,7 @@ export async function getPagesBySite( cursor?: string | null tags?: string[] useStat?: boolean + keepBody?: boolean }, customUnidata?: Unidata, ) { @@ -297,7 +298,11 @@ export async function getPagesBySite( await Promise.all( pages?.list.map(async (page) => { await expandPage(page, input.useStat) - delete page.body + + if (!input.keepBody) { + delete page.body + } + return page }), ) diff --git a/src/pages/_site/[site]/feed/index.tsx b/src/pages/_site/[site]/feed/index.tsx index 7d0b1f27..57374e80 100644 --- a/src/pages/_site/[site]/feed/index.tsx +++ b/src/pages/_site/[site]/feed/index.tsx @@ -1,57 +1,13 @@ import { GetServerSideProps } from "next" -import { fetchGetSite } from "~/queries/site.server" -import { fetchGetPagesBySite } from "~/queries/page.server" -import { PageVisibilityEnum } from "~/lib/types" -import { getSiteLink } from "~/lib/helpers" -import { QueryClient } from "@tanstack/react-query" -import { renderPageContent } from "~/markdown" +import { getJsonFeed } from "~/lib/json-feed" export const getServerSideProps: GetServerSideProps = async (ctx) => { - const queryClient = new QueryClient() ctx.res.setHeader("Content-Type", "application/feed+json; charset=utf-8") ctx.res.setHeader("Access-Control-Allow-Methods", "GET") ctx.res.setHeader("Access-Control-Allow-Origin", "*") const domainOrSubdomain = ctx.params!.site as string - const site = await fetchGetSite(domainOrSubdomain, queryClient) - const pages = await fetchGetPagesBySite( - { - site: domainOrSubdomain, - type: "post", - visibility: PageVisibilityEnum.Published, - }, - queryClient, - ) - - const link = getSiteLink({ - subdomain: site.username || "", - }) - ctx.res.write( - JSON.stringify({ - version: "https://jsonfeed.org/version/1.1", - title: site.name, - description: site.description, - icon: link + site.avatars?.[0], - home_page_url: link, - feed_url: `${link}/feed`, - items: pages.list?.map((page: any) => ({ - id: page.id, - title: page.title, - content_html: - page.body?.content && - renderPageContent(page.body?.content, true).contentHTML, - summary: page.summary?.content, - url: `${link}/${page.slug || page.id}`, - image: `${link}${page.cover}`, - date_published: page.date_published, - date_modified: page.date_updated, - // authors: page.authors?.map((author: any) => ({ - // name: author, - // })), - tags: page.tags, - })), - }), - ) + ctx.res.write(JSON.stringify(await getJsonFeed(domainOrSubdomain, "/feed"))) ctx.res.end() return { diff --git a/src/pages/_site/[site]/feed/xml.tsx b/src/pages/_site/[site]/feed/xml.tsx new file mode 100644 index 00000000..3b5cda09 --- /dev/null +++ b/src/pages/_site/[site]/feed/xml.tsx @@ -0,0 +1,24 @@ +import { GetServerSideProps } from "next" +// @ts-ignore +import jsonfeedToRSS from "jsonfeed-to-rss" +import { getJsonFeed } from "~/lib/json-feed" + +export const getServerSideProps: GetServerSideProps = async (ctx) => { + ctx.res.setHeader("Content-Type", "application/xml; charset=utf-8") + ctx.res.setHeader("Access-Control-Allow-Methods", "GET") + ctx.res.setHeader("Access-Control-Allow-Origin", "*") + const domainOrSubdomain = ctx.params!.site as string + + ctx.res.write( + jsonfeedToRSS(await getJsonFeed(domainOrSubdomain, "/feed/xml")), + ) + ctx.res.end() + + return { + props: {}, + } +} + +const SiteFeed: React.FC = () => null + +export default SiteFeed