a tool for shared writing and social publishing
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add Atom and JSON feeds + feed links in individual articles (#179)

* Add RSS alt links to head of individual articles

* Add Atom and JSON feeds + fix Content-Type mismatch for RSS feed

* Deduplicate feed metadata and generation code

* Move generateMetadata to layout.tsx for inheritance

authored by

penny and committed by
GitHub
46b1760d 74c9b6c5

+196 -127
+10 -15
app/lish/[did]/[publication]/[rkey]/page.tsx
··· 8 8 PubLeafletPublication, 9 9 } from "lexicons/api"; 10 10 import { Metadata } from "next"; 11 - import { AtpAgent, Agent, AtpBaseClient } from "@atproto/api"; 11 + import { AtpAgent } from "@atproto/api"; 12 12 import { QuoteHandler } from "./QuoteHandler"; 13 13 import { InteractionDrawer } from "./Interactions/InteractionDrawer"; 14 14 import { ··· 20 20 import { PostPage } from "./PostPage"; 21 21 import { PageLayout } from "./PageLayout"; 22 22 import { extractCodeBlocks } from "./extractCodeBlocks"; 23 - import { getIdentityData } from "actions/getIdentityData"; 24 - import { createOauthClient } from "src/atproto-oauth"; 25 23 26 24 export async function generateMetadata(props: { 27 25 params: Promise<{ publication: string; did: string; rkey: string }>; 28 26 }): Promise<Metadata> { 29 - let did = decodeURIComponent((await props.params).did); 27 + let params = await props.params; 28 + let did = decodeURIComponent(params.did); 29 + let publication = decodeURIComponent(params.publication); 30 30 if (!did) return { title: "Publication 404" }; 31 31 32 32 let [{ data: document }] = await Promise.all([ 33 33 supabaseServerClient 34 34 .from("documents") 35 35 .select("*") 36 - .eq( 37 - "uri", 38 - AtUri.make(did, ids.PubLeafletDocument, (await props.params).rkey), 39 - ) 36 + .eq("uri", AtUri.make(did, ids.PubLeafletDocument, params.rkey)) 40 37 .single(), 41 38 ]); 42 - 43 39 if (!document) return { title: "404" }; 44 - let record = document.data as PubLeafletDocument.Record; 40 + 41 + let docRecord = document.data as PubLeafletDocument.Record; 42 + 45 43 return { 46 - title: 47 - record.title + 48 - " - " + 49 - decodeURIComponent((await props.params).publication), 50 - description: record?.description || "", 44 + title: docRecord.title + " - " + publication, 45 + description: docRecord?.description || "", 51 46 }; 52 47 } 53 48 export default async function Post(props: {
+27
app/lish/[did]/[publication]/atom/route.ts
··· 1 + import { NextResponse } from "next/server"; 2 + import { generateFeed } from "../layout"; 3 + 4 + export async function GET( 5 + req: Request, 6 + props: { 7 + params: Promise<{ publication: string; did: string }>; 8 + }, 9 + ) { 10 + let renderToReadableStream = await import("react-dom/server").then( 11 + (module) => module.renderToReadableStream, 12 + ); 13 + const params = await props.params; 14 + const did = decodeURIComponent(params.did); 15 + const publication = decodeURIComponent(params.publication); 16 + const feed = await generateFeed(did, publication); 17 + 18 + if (feed instanceof NextResponse) { 19 + return feed; 20 + } 21 + 22 + return new Response(feed.atom1(), { 23 + headers: { 24 + "Content-Type": "application/atom+xml", 25 + }, 26 + }); 27 + }
+27
app/lish/[did]/[publication]/json/route.ts
··· 1 + import { NextResponse } from "next/server"; 2 + import { generateFeed } from "../layout"; 3 + 4 + export async function GET( 5 + req: Request, 6 + props: { 7 + params: Promise<{ publication: string; did: string }>; 8 + }, 9 + ) { 10 + let renderToReadableStream = await import("react-dom/server").then( 11 + (module) => module.renderToReadableStream, 12 + ); 13 + const params = await props.params; 14 + const did = decodeURIComponent(params.did); 15 + const publication = decodeURIComponent(params.publication); 16 + const feed = await generateFeed(did, publication); 17 + 18 + if (feed instanceof NextResponse) { 19 + return feed; 20 + } 21 + 22 + return new Response(feed.json1(), { 23 + headers: { 24 + "Content-Type": "application/feed+json", 25 + }, 26 + }); 27 + }
+121
app/lish/[did]/[publication]/layout.tsx
··· 1 + import { AtUri } from "@atproto/syntax"; 2 + import { Feed } from "feed"; 3 + import { 4 + PubLeafletDocument, 5 + PubLeafletPagesLinearDocument, 6 + PubLeafletPublication, 7 + } from "lexicons/api"; 8 + import { createElement } from "react"; 9 + import { renderToReadableStream } from "react-dom/server"; 10 + import { StaticPostContent } from "./[rkey]/StaticPostContent"; 11 + import { get_publication_data } from "app/api/rpc/[command]/get_publication_data"; 12 + import { supabaseServerClient } from "supabase/serverClient"; 13 + import { NextResponse } from "next/server"; 14 + import { Metadata } from "next"; 15 + 16 + export default async function PublicationLayout(props: { 17 + children: React.ReactNode; 18 + }) { 19 + return <>{props.children}</>; 20 + } 21 + 22 + export async function generateMetadata(params: { 23 + did: string; 24 + publication: string; 25 + }): Promise<Metadata> { 26 + if (!params.did || !params.publication) return { title: "Publication 404" }; 27 + let { result: publication } = await get_publication_data.handler( 28 + { 29 + did: decodeURIComponent(params.did), 30 + publication_name: decodeURIComponent(params.publication), 31 + }, 32 + { supabase: supabaseServerClient }, 33 + ); 34 + if (!publication) return { title: "Publication 404" }; 35 + 36 + let pubRecord = publication?.record as PubLeafletPublication.Record; 37 + 38 + return { 39 + title: pubRecord?.name || "Untitled Publication", 40 + description: pubRecord?.description || "", 41 + alternates: pubRecord?.base_path 42 + ? { 43 + types: { 44 + "application/rss+xml": `https://${pubRecord?.base_path}/rss`, 45 + "application/atom+xml": `https://${pubRecord?.base_path}/atom`, 46 + "application/json": `https://${pubRecord?.base_path}/json`, 47 + }, 48 + } 49 + : undefined, 50 + }; 51 + } 52 + 53 + export async function generateFeed( 54 + did: string, 55 + publication_name: string, 56 + ): Promise<Feed | NextResponse<unknown>> { 57 + let { result: publication } = await get_publication_data.handler( 58 + { 59 + did: did, 60 + publication_name: publication_name, 61 + }, 62 + { supabase: supabaseServerClient }, 63 + ); 64 + 65 + let pubRecord = publication?.record as PubLeafletPublication.Record; 66 + if (!publication || !pubRecord) 67 + return new NextResponse(null, { status: 404 }); 68 + 69 + const feed = new Feed({ 70 + title: pubRecord.name, 71 + description: pubRecord.description, 72 + id: `https://${pubRecord.base_path}`, 73 + link: `https://${pubRecord.base_path}`, 74 + language: "en", // optional, used only in RSS 2.0, possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes 75 + copyright: "", 76 + feedLinks: { 77 + rss: `https://${pubRecord.base_path}/rss`, 78 + atom: `https://${pubRecord.base_path}/atom`, 79 + json: `https://${pubRecord.base_path}/json`, 80 + }, 81 + }); 82 + 83 + await Promise.all( 84 + publication.documents_in_publications.map(async (doc) => { 85 + if (!doc.documents) return; 86 + let record = doc.documents?.data as PubLeafletDocument.Record; 87 + let uri = new AtUri(doc.documents?.uri); 88 + let rkey = uri.rkey; 89 + if (!record) return; 90 + let firstPage = record.pages[0]; 91 + let blocks: PubLeafletPagesLinearDocument.Block[] = []; 92 + if (PubLeafletPagesLinearDocument.isMain(firstPage)) { 93 + blocks = firstPage.blocks || []; 94 + } 95 + let stream = await renderToReadableStream( 96 + createElement(StaticPostContent, { blocks, did: uri.host }), 97 + ); 98 + const reader = stream.getReader(); 99 + const chunks = []; 100 + 101 + let done, value; 102 + while (!done) { 103 + ({ done, value } = await reader.read()); 104 + if (value) { 105 + chunks.push(new TextDecoder().decode(value)); 106 + } 107 + } 108 + 109 + feed.addItem({ 110 + title: record.title, 111 + description: record.description, 112 + date: record.publishedAt ? new Date(record.publishedAt) : new Date(), 113 + id: `https://${pubRecord.base_path}/${rkey}`, 114 + link: `https://${pubRecord.base_path}/${rkey}`, 115 + content: chunks.join(""), 116 + }); 117 + }), 118 + ); 119 + 120 + return feed; 121 + }
+1 -39
app/lish/[did]/[publication]/page.tsx
··· 1 1 import { supabaseServerClient } from "supabase/serverClient"; 2 - import { Metadata } from "next"; 3 - 4 - import { ThemeProvider } from "components/ThemeManager/ThemeProvider"; 5 - import { get_publication_data } from "app/api/rpc/[command]/get_publication_data"; 6 2 import { AtUri } from "@atproto/syntax"; 7 - import { 8 - PubLeafletDocument, 9 - PubLeafletPublication, 10 - PubLeafletThemeColor, 11 - } from "lexicons/api"; 3 + import { PubLeafletDocument, PubLeafletPublication } from "lexicons/api"; 12 4 import Link from "next/link"; 13 5 import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 14 6 import { BskyAgent } from "@atproto/api"; ··· 18 10 PublicationBackgroundProvider, 19 11 PublicationThemeProvider, 20 12 } from "components/ThemeManager/PublicationThemeProvider"; 21 - 22 - export async function generateMetadata(props: { 23 - params: Promise<{ publication: string; did: string }>; 24 - }): Promise<Metadata> { 25 - let params = await props.params; 26 - let did = decodeURIComponent(params.did); 27 - if (!did) return { title: "Publication 404" }; 28 - 29 - let { result: publication } = await get_publication_data.handler( 30 - { 31 - did, 32 - publication_name: decodeURIComponent(params.publication), 33 - }, 34 - { supabase: supabaseServerClient }, 35 - ); 36 - if (!publication) return { title: "404 Publication" }; 37 - 38 - let record = publication.record as PubLeafletPublication.Record | null; 39 - return { 40 - title: record?.name || "Untitled Publication", 41 - description: record?.description || "", 42 - alternates: record?.base_path 43 - ? { 44 - types: { 45 - "application/rss+xml": `https://${record?.base_path}/rss`, 46 - }, 47 - } 48 - : undefined, 49 - }; 50 - } 51 13 52 14 export default async function Publication(props: { 53 15 params: Promise<{ publication: string; did: string }>;
+10 -73
app/lish/[did]/[publication]/rss/route.ts
··· 1 - import { AtUri } from "@atproto/syntax"; 2 - import { get_publication_data } from "app/api/rpc/[command]/get_publication_data"; 3 - import { Feed } from "feed"; 4 - import fs from "fs/promises"; 5 - import { 6 - PubLeafletDocument, 7 - PubLeafletPagesLinearDocument, 8 - PubLeafletPublication, 9 - } from "lexicons/api"; 10 - import { NextRequest, NextResponse } from "next/server"; 11 - import { createElement } from "react"; 12 - import { supabaseServerClient } from "supabase/serverClient"; 13 - import { StaticPostContent } from "../[rkey]/StaticPostContent"; 1 + import { NextResponse } from "next/server"; 2 + import { generateFeed } from "../layout"; 14 3 15 4 export async function GET( 16 5 req: Request, ··· 21 10 let renderToReadableStream = await import("react-dom/server").then( 22 11 (module) => module.renderToReadableStream, 23 12 ); 24 - let params = await props.params; 25 - let { result: publication } = await get_publication_data.handler( 26 - { 27 - did: params.did, 28 - publication_name: decodeURIComponent(params.publication), 29 - }, 30 - { supabase: supabaseServerClient }, 31 - ); 13 + const params = await props.params; 14 + const did = decodeURIComponent(params.did); 15 + const publication = decodeURIComponent(params.publication); 16 + const feed = await generateFeed(did, publication); 32 17 33 - let pubRecord = publication?.record as PubLeafletPublication.Record; 34 - if (!publication || !pubRecord) 35 - return new NextResponse(null, { status: 404 }); 18 + if (feed instanceof NextResponse) { 19 + return feed; 20 + } 36 21 37 - const feed = new Feed({ 38 - title: pubRecord.name, 39 - description: pubRecord.description, 40 - id: `https://${pubRecord.base_path}`, 41 - link: `https://${pubRecord.base_path}`, 42 - language: "en", // optional, used only in RSS 2.0, possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes 43 - copyright: "", 44 - feedLinks: { 45 - rss: `https://${pubRecord.base_path}/rss`, 46 - }, 47 - }); 48 - 49 - await Promise.all( 50 - publication?.documents_in_publications.map(async (doc) => { 51 - if (!doc.documents) return; 52 - let record = doc.documents?.data as PubLeafletDocument.Record; 53 - let uri = new AtUri(doc.documents?.uri); 54 - let rkey = uri.rkey; 55 - if (!record) return; 56 - let firstPage = record.pages[0]; 57 - let blocks: PubLeafletPagesLinearDocument.Block[] = []; 58 - if (PubLeafletPagesLinearDocument.isMain(firstPage)) { 59 - blocks = firstPage.blocks || []; 60 - } 61 - let stream = await renderToReadableStream( 62 - createElement(StaticPostContent, { blocks, did: uri.host }), 63 - ); 64 - const reader = stream.getReader(); 65 - const chunks = []; 66 - 67 - let done, value; 68 - while (!done) { 69 - ({ done, value } = await reader.read()); 70 - if (value) { 71 - chunks.push(new TextDecoder().decode(value)); 72 - } 73 - } 74 - 75 - feed.addItem({ 76 - title: record.title, 77 - description: record.description, 78 - date: record.publishedAt ? new Date(record.publishedAt) : new Date(), 79 - id: `https://${pubRecord.base_path}/${rkey}`, 80 - link: `https://${pubRecord.base_path}/${rkey}`, 81 - content: chunks.join(""), 82 - }); 83 - }), 84 - ); 85 22 return new Response(feed.rss2(), { 86 23 headers: { 87 - "Content-Type": "text/xml", 24 + "Content-Type": "application/rss+xml", 88 25 }, 89 26 }); 90 27 }