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