diff --git a/.env.example b/.env.example
index 2d53ff6492..148b144e5d 100644
--- a/.env.example
+++ b/.env.example
@@ -11,7 +11,7 @@ NEXT_PUBLIC_TWITTER_LINK="https://x.com/_xLog"
NEXT_PUBLIC_GITHUB_LINK="https://github.com/Crossbell-Box/xlog"
NEXT_PUBLIC_CSB_IO="https://crossbell.io"
NEXT_PUBLIC_CSB_SCAN="https://scan.crossbell.io"
-NEXT_PUBLIC_IPFS_GATEWAY="https://ipfs.4everland.xyz/ipfs/"
+NEXT_PUBLIC_IPFS_GATEWAY="https://ipfs.4everland.io/ipfs/"
NEXT_PUBLIC_MIRA_LINK="https://mira.crossbell.io"
NEXT_PUBLIC_WALLET_CONNECT_V2_PROJECT_ID=
diff --git a/package.json b/package.json
index 031775a454..c08a49be8f 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
"prisma:migrate:resolve": "prisma migrate resolve --preview-feature",
"prisma:generate": "prisma generate",
"prisma:studio": "prisma studio",
- "docker:db": "docker-compose -f docker-compose.db.yml up -d",
+ "docker:db": "docker compose -f docker-compose.db.yml up -d",
"spell-check": "cspell lint . --no-progress",
"analyze": "NODE_ENV=production ANALYZE=true BUNDLE_ANALYZE=browser next build",
"postinstall": "prisma generate",
diff --git a/src/app/[locale]/site/[site]/[slug]/page.tsx b/src/app/[locale]/site/[site]/[slug]/page.tsx
index 59b3d32fd7..0e3c584695 100644
--- a/src/app/[locale]/site/[site]/[slug]/page.tsx
+++ b/src/app/[locale]/site/[site]/[slug]/page.tsx
@@ -176,7 +176,13 @@ export default async function SitePagePage({
)}
- {page?.metadata?.content?.content}
+
") ||
+ "",
+ }}
+ />
>
) : (
diff --git a/src/app/[locale]/site/[site]/feed/route.ts b/src/app/[locale]/site/[site]/feed/route.ts
index b511bad193..2ec8b012de 100644
--- a/src/app/[locale]/site/[site]/feed/route.ts
+++ b/src/app/[locale]/site/[site]/feed/route.ts
@@ -2,6 +2,7 @@ import RSS from "rss"
import { SITE_URL } from "~/lib/env"
import { getSiteLink } from "~/lib/helpers"
+import { toGateway } from "~/lib/ipfs-parser"
import { NextServerResponse } from "~/lib/server-helper"
import { PageVisibilityEnum } from "~/lib/types"
import { getPagesBySite } from "~/models/page.model"
@@ -18,7 +19,11 @@ export async function GET(
},
) {
const site = await getSite(params.site)
- const pages = await getPagesBySite({
+
+ const url = new URL(request.url)
+ const includeShorts = url.searchParams.get("shorts") !== "false"
+
+ const posts = await getPagesBySite({
characterId: site?.characterId,
type: "post",
visibility: PageVisibilityEnum.Published,
@@ -26,7 +31,25 @@ export async function GET(
useHTML: true,
})
- const hasAudio = pages.list?.find((page) => page.metadata?.content?.audio)
+ let allPages = posts.list || []
+
+ if (includeShorts) {
+ const shorts = await getPagesBySite({
+ characterId: site?.characterId,
+ type: "short",
+ visibility: PageVisibilityEnum.Published,
+ keepBody: true,
+ useHTML: true,
+ })
+
+ allPages = [...(posts.list || []), ...(shorts.list || [])].sort((a, b) => {
+ const dateA = new Date(a.metadata?.content?.date_published || a.createdAt)
+ const dateB = new Date(b.metadata?.content?.date_published || b.createdAt)
+ return dateB.getTime() - dateA.getTime()
+ })
+ }
+
+ const hasAudio = allPages.find((page) => page.metadata?.content?.audio)
const email = site?.metadata?.content?.connected_accounts
?.map((account) => {
@@ -71,31 +94,78 @@ export async function GET(
},
}
: {}),
- items: pages.list?.map((page) => ({
- id: page.characterId + "-" + page.noteId,
- title: page.metadata?.content?.title || "Untitled",
- summary: page.metadata?.content?.summary,
- content_html: page.metadata?.content?.contentHTML,
- url: `${SITE_URL}/api/redirection?characterId=${page.characterId}¬eId=${page.noteId}`,
- image: page.metadata?.content?.cover,
- date_published: page.metadata?.content?.date_published,
- date_modified: page.updatedAt,
- tags: page.metadata?.content?.tags,
- author: site?.metadata?.content?.name,
- ...(page.metadata?.content?.audio && {
- _itunes: {
- image: page.metadata?.content?.cover,
- summary: page.metadata?.content?.summary,
- },
- attachments: [
- {
- url: page.metadata?.content?.audio,
- mime_type: "audio/mpeg",
- title: page.metadata?.content?.title,
+ items: allPages?.map((page) => {
+ const isShort = page.metadata?.content?.tags?.includes("short")
+ const title =
+ page.metadata?.content?.title ||
+ (isShort
+ ? `${new Date(page.metadata?.content?.date_published || page.createdAt).toLocaleDateString()}`
+ : "Untitled")
+
+ const allImages = isShort
+ ? (page.metadata?.content?.images || []).map((img: string) =>
+ toGateway(img),
+ )
+ : page.metadata?.content?.cover
+ ? [toGateway(page.metadata?.content?.cover)]
+ : []
+
+ const originalContent =
+ page.metadata?.content?.contentHTML ||
+ page.metadata?.content?.content ||
+ ""
+ const imagesHtml =
+ isShort && allImages.length > 0
+ ? allImages
+ .map(
+ (img: string) =>
+ `
`,
+ )
+ .join("")
+ : ""
+
+ const contentWithImages =
+ isShort && imagesHtml
+ ? `${imagesHtml}
${originalContent}`
+ : originalContent
+
+ return {
+ id: page.characterId + "-" + page.noteId,
+ title,
+ summary:
+ page.metadata?.content?.summary ||
+ (page.metadata?.content?.content
+ ? page.metadata.content.content.slice(0, 200) + "..."
+ : ""),
+ content_html: contentWithImages,
+ url: `${SITE_URL}/api/redirection?characterId=${page.characterId}¬eId=${page.noteId}`,
+ image: allImages[0],
+ ...(isShort &&
+ allImages.length > 0 && {
+ attachments: allImages.map((img: string) => ({
+ url: img,
+ })),
+ }),
+ date_published:
+ page.metadata?.content?.date_published || page.createdAt,
+ date_modified: page.updatedAt,
+ tags: page.metadata?.content?.tags,
+ author: site?.metadata?.content?.name,
+ ...(page.metadata?.content?.audio && {
+ _itunes: {
+ image: page.metadata?.content?.cover,
+ summary: page.metadata?.content?.summary,
},
- ],
- }),
- })),
+ attachments: [
+ {
+ url: page.metadata?.content?.audio,
+ mime_type: "audio/mpeg",
+ title: page.metadata?.content?.title,
+ },
+ ],
+ }),
+ }
+ }),
}
const feed = new RSS({
@@ -109,6 +179,8 @@ export async function GET(
feed_url: `${link}/feed`,
custom_namespaces: {
itunes: "http://www.itunes.com/dtds/podcast-1.0.dtd",
+ media: "http://search.yahoo.com/mrss/",
+ content: "http://purl.org/rss/1.0/modules/content/",
},
custom_elements: [
...(hasAudio
@@ -146,18 +218,60 @@ export async function GET(
],
})
- pages.list.forEach((page) => {
+ allPages.forEach((page) => {
+ const isShort = page.metadata?.content?.tags?.includes("short")
+ const title =
+ page.metadata?.content?.title ||
+ (isShort
+ ? `${new Date(page.metadata?.content?.date_published || page.createdAt).toLocaleDateString()}`
+ : "Untitled")
+
+ const allImages = isShort
+ ? (page.metadata?.content?.images || []).map((img: string) =>
+ toGateway(img),
+ )
+ : page.metadata?.content?.cover
+ ? [toGateway(page.metadata?.content?.cover)]
+ : []
+
+ const itemContent =
+ page.metadata?.content?.contentHTML ||
+ page.metadata?.content?.content ||
+ ""
+
+ const imagesHtml =
+ isShort && allImages.length > 0
+ ? allImages
+ .map(
+ (img: string) =>
+ `
`,
+ )
+ .join("")
+ : ""
+
+ const contentWithImages =
+ isShort && imagesHtml ? `${imagesHtml}
${itemContent}` : itemContent
+
+ const mediaContentElements = allImages.map((img: string) => ({
+ "media:content": { _attr: { url: img, type: "image", medium: "image" } },
+ }))
+
feed.item({
guid: page.characterId + "-" + page.noteId,
- title: page.metadata?.content?.title || "Untitled",
- description: page.metadata?.content?.summary,
+ title,
+ description:
+ page.metadata?.content?.summary ||
+ (page.metadata?.content?.content
+ ? page.metadata.content.content.slice(0, 200) + "..."
+ : ""),
custom_elements: [
{
- "content:encoded": page.metadata?.content?.contentHTML,
+ "content:encoded": contentWithImages,
},
+ ...mediaContentElements,
],
url: `${SITE_URL}/api/redirection?characterId=${page.characterId}¬eId=${page.noteId}`,
- date: new Date(page.metadata?.content?.date_published),
+ date: new Date(page.metadata?.content?.date_published || page.createdAt),
categories: page.metadata?.content?.tags,
author: site?.metadata?.content?.name,
enclosure: hasAudio
@@ -165,7 +279,12 @@ export async function GET(
url: page.metadata?.content?.audio,
type: "audio/mpeg",
}
- : undefined,
+ : allImages[0]
+ ? {
+ url: allImages[0],
+ type: "image/jpeg",
+ }
+ : undefined,
})
})
diff --git a/src/app/[locale]/site/[site]/feed/shorts/route.ts b/src/app/[locale]/site/[site]/feed/shorts/route.ts
new file mode 100644
index 0000000000..89d75fae1a
--- /dev/null
+++ b/src/app/[locale]/site/[site]/feed/shorts/route.ts
@@ -0,0 +1,184 @@
+import RSS from "rss"
+
+import { SITE_URL } from "~/lib/env"
+import { getSiteLink } from "~/lib/helpers"
+import { toGateway } from "~/lib/ipfs-parser"
+import { NextServerResponse } from "~/lib/server-helper"
+import { PageVisibilityEnum } from "~/lib/types"
+import { getPagesBySite } from "~/models/page.model"
+import { getSite } from "~/models/site.model"
+
+export async function GET(
+ request: Request,
+ {
+ params,
+ }: {
+ params: {
+ site: string
+ }
+ },
+) {
+ const site = await getSite(params.site)
+
+ const shorts = await getPagesBySite({
+ characterId: site?.characterId,
+ type: "short",
+ visibility: PageVisibilityEnum.Published,
+ keepBody: true,
+ useHTML: true,
+ })
+
+ const email = site?.metadata?.content?.connected_accounts
+ ?.map((account) => {
+ const match = account.match(/:\/\/account:(.*)@email/)
+ if (match) {
+ return match[1]
+ }
+ })
+ .find((email) => email)
+ const link = getSiteLink({
+ subdomain: site?.handle || "",
+ domain: site?.metadata?.content?.custom_domain,
+ })
+
+ const data = {
+ version: "https://jsonfeed.org/version/1",
+ title: `${site?.metadata?.content?.site_name || site?.metadata?.content?.name}`,
+ description: `${site?.metadata?.content?.name || site?.handle}`,
+ icon: site?.metadata?.content?.avatars?.[0],
+ home_page_url: `${link}/shorts`,
+ feed_url: `${link}/shorts/feed`,
+ items: shorts.list?.map((page) => {
+ const title =
+ page.metadata?.content?.title ||
+ `${new Date(page.metadata?.content?.date_published || page.createdAt).toLocaleDateString()}`
+
+ const allImages = (page.metadata?.content?.images || []).map(
+ (img: string) => toGateway(img),
+ )
+
+ const originalContent =
+ page.metadata?.content?.contentHTML ||
+ page.metadata?.content?.content ||
+ ""
+ const imagesHtml =
+ allImages.length > 0
+ ? allImages
+ .map(
+ (img: string) =>
+ `
`,
+ )
+ .join("")
+ : ""
+
+ const contentWithImages = imagesHtml
+ ? `${imagesHtml}
${originalContent}`
+ : originalContent
+
+ return {
+ id: page.characterId + "-" + page.noteId,
+ title,
+ summary:
+ page.metadata?.content?.summary ||
+ (page.metadata?.content?.content
+ ? page.metadata.content.content.slice(0, 200) + "..."
+ : ""),
+ content_html: contentWithImages,
+ url: `${SITE_URL}/api/redirection?characterId=${page.characterId}¬eId=${page.noteId}`,
+ image: allImages[0],
+ ...(allImages.length > 0 && {
+ attachments: allImages.map((img: string) => ({
+ url: img,
+ })),
+ }),
+ date_published:
+ page.metadata?.content?.date_published || page.createdAt,
+ date_modified: page.updatedAt,
+ tags: page.metadata?.content?.tags,
+ author: site?.metadata?.content?.name,
+ }
+ }),
+ }
+
+ const feed = new RSS({
+ title: `${site?.metadata?.content?.site_name || site?.metadata?.content?.name || "Untitled"}`,
+ description: `${site?.metadata?.content?.name || site?.handle}`,
+ image_url: site?.metadata?.content?.avatars?.[0],
+ site_url: `${link}/shorts`,
+ feed_url: `${link}/shorts/feed`,
+ custom_namespaces: {
+ media: "http://search.yahoo.com/mrss/",
+ content: "http://purl.org/rss/1.0/modules/content/",
+ },
+ })
+
+ shorts.list?.forEach((page) => {
+ const title =
+ page.metadata?.content?.title ||
+ `${new Date(page.metadata?.content?.date_published || page.createdAt).toLocaleDateString()}`
+
+ const allImages = (page.metadata?.content?.images || []).map(
+ (img: string) => toGateway(img),
+ )
+
+ const itemContent =
+ page.metadata?.content?.contentHTML ||
+ page.metadata?.content?.content ||
+ ""
+
+ const imagesHtml =
+ allImages.length > 0
+ ? allImages
+ .map(
+ (img: string) =>
+ `
`,
+ )
+ .join("")
+ : ""
+
+ const contentWithImages = imagesHtml
+ ? `${imagesHtml}
${itemContent}`
+ : itemContent
+
+ const mediaContentElements = allImages.map((img: string) => ({
+ "media:content": { _attr: { url: img, type: "image", medium: "image" } },
+ }))
+
+ feed.item({
+ guid: page.characterId + "-" + page.noteId,
+ title,
+ description:
+ page.metadata?.content?.summary ||
+ (page.metadata?.content?.content
+ ? page.metadata.content.content.slice(0, 200) + "..."
+ : ""),
+ custom_elements: [
+ {
+ "content:encoded": contentWithImages,
+ },
+ ...mediaContentElements,
+ ],
+ url: `${SITE_URL}/api/redirection?characterId=${page.characterId}¬eId=${page.noteId}`,
+ date: new Date(page.metadata?.content?.date_published || page.createdAt),
+ categories: page.metadata?.content?.tags,
+ author: site?.metadata?.content?.name,
+ enclosure: allImages[0]
+ ? {
+ url: allImages[0],
+ type: "image/jpeg",
+ }
+ : undefined,
+ })
+ })
+
+ const format =
+ new URLSearchParams(request.url.split("?")[1]).get("format") === "json"
+ ? "json"
+ : "xml"
+
+ const res = new NextServerResponse()
+ return res.status(200).rss(format === "json" ? data : feed.xml(), format)
+}
+
+export const dynamic = "force-dynamic"
+export const revalidate = 0
diff --git a/src/lib/env.ts b/src/lib/env.ts
index 48f4fedd98..e1c096b2ac 100644
--- a/src/lib/env.ts
+++ b/src/lib/env.ts
@@ -24,7 +24,7 @@ export const CSB_SCAN =
export const CSB_XCHAR =
process.env.NEXT_PUBLIC_CSB_XCHAR || "https://xchar.app"
export const IPFS_GATEWAY =
- process.env.NEXT_PUBLIC_IPFS_GATEWAY || "https://ipfs.4everland.xyz/ipfs/"
+ process.env.NEXT_PUBLIC_IPFS_GATEWAY || "https://ipfs.4everland.io/ipfs/"
export const IPFS_GATEWAY_FALLBACK =
process.env.NEXT_PUBLIC_IPFS_GATEWAY_FALLBACK ||
"https://gateway.ipfs.io/ipfs/"
diff --git a/src/lib/ipfs-parser.ts b/src/lib/ipfs-parser.ts
index 1a7e755fa8..eba0d5cf7b 100644
--- a/src/lib/ipfs-parser.ts
+++ b/src/lib/ipfs-parser.ts
@@ -24,7 +24,7 @@ export const toIPFS = (url: string) => {
.replaceAll("https://gateway.ipfs.io/ipfs/", IPFS_PREFIX)
.replaceAll("https://ipfs.io/ipfs/", IPFS_PREFIX)
.replaceAll("https://cf-ipfs.com/ipfs/", IPFS_PREFIX)
- .replaceAll("https://ipfs.4everland.xyz/ipfs/", IPFS_PREFIX)
+ .replaceAll("https://ipfs.4everland.io/ipfs/", IPFS_PREFIX)
.replaceAll("https://rss3.mypinata.cloud/ipfs/", IPFS_PREFIX)
.replaceAll("https://cloudflare-ipfs.com/ipfs/", IPFS_PREFIX)
.replaceAll("https://ipfs.xlog.app/ipfs/", IPFS_PREFIX)
@@ -36,7 +36,7 @@ export const toCid = (url: string) => {
.replaceAll("https://gateway.ipfs.io/ipfs/", "")
.replaceAll("https://ipfs.io/ipfs/", "")
.replaceAll("https://cf-ipfs.com/ipfs/", "")
- .replaceAll("https://ipfs.4everland.xyz/ipfs/", "")
+ .replaceAll("https://ipfs.4everland.io/ipfs/", "")
.replaceAll("https://rss3.mypinata.cloud/ipfs/", "")
.replaceAll("https://cloudflare-ipfs.com/ipfs/", "")
.replaceAll("https://ipfs.xlog.app/ipfs/", "")