a tool for shared writing and social publishing
0
fork

Configure Feed

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

render full html to rss feed

+216 -183
+188
app/lish/[did]/[publication]/[rkey]/PostContent.tsx
··· 1 + import { 2 + PubLeafletBlocksHeader, 3 + PubLeafletBlocksImage, 4 + PubLeafletBlocksText, 5 + PubLeafletBlocksUnorderedList, 6 + PubLeafletBlocksWebsite, 7 + PubLeafletDocument, 8 + PubLeafletPagesLinearDocument, 9 + } from "lexicons/api"; 10 + import { blobRefToSrc } from "src/utils/blobRefToSrc"; 11 + import { TextBlock } from "./TextBlock"; 12 + 13 + export function PostContent({ 14 + blocks, 15 + did, 16 + }: { 17 + blocks: PubLeafletPagesLinearDocument.Block[]; 18 + did: string; 19 + }) { 20 + return ( 21 + <div className="postContent flex flex-col"> 22 + {blocks.map((b, index) => { 23 + return <Block block={b} did={did} key={index} />; 24 + })} 25 + </div> 26 + ); 27 + } 28 + 29 + let Block = ({ 30 + block, 31 + did, 32 + isList, 33 + }: { 34 + block: PubLeafletPagesLinearDocument.Block; 35 + did: string; 36 + isList?: boolean; 37 + }) => { 38 + let b = block; 39 + 40 + // non text blocks, they need this padding, pt-3 sm:pt-4, which is applied in each case 41 + let className = ` 42 + postBlockWrapper 43 + pt-1 44 + ${isList ? "isListItem pb-0 " : "pb-2 last:pb-3 last:sm:pb-4 first:pt-2 sm:first:pt-3"} 45 + ${b.alignment === "lex:pub.leaflet.pages.linearDocument#textAlignRight" ? "text-right" : b.alignment === "lex:pub.leaflet.pages.linearDocument#textAlignCenter" ? "text-center" : ""} 46 + `; 47 + 48 + switch (true) { 49 + case PubLeafletBlocksUnorderedList.isMain(b.block): { 50 + return ( 51 + <ul className="-ml-[1px] sm:ml-[9px] pb-2"> 52 + {b.block.children.map((child, index) => ( 53 + <ListItem 54 + item={child} 55 + did={did} 56 + key={index} 57 + className={className} 58 + /> 59 + ))} 60 + </ul> 61 + ); 62 + } 63 + case PubLeafletBlocksWebsite.isMain(b.block): { 64 + return ( 65 + <a 66 + href={b.block.src} 67 + target="_blank" 68 + className={` 69 + externalLinkBlock flex relative group/linkBlock 70 + h-[104px] w-full bg-bg-page overflow-hidden text-primary hover:no-underline no-underline 71 + hover:border-accent-contrast shadow-sm 72 + block-border 73 + `} 74 + > 75 + <div className="pt-2 pb-2 px-3 grow min-w-0"> 76 + <div className="flex flex-col w-full min-w-0 h-full grow "> 77 + <div 78 + className={`linkBlockTitle bg-transparent -mb-0.5 border-none text-base font-bold outline-none resize-none align-top border h-[24px] line-clamp-1`} 79 + style={{ 80 + overflow: "hidden", 81 + textOverflow: "ellipsis", 82 + wordBreak: "break-all", 83 + }} 84 + > 85 + {b.block.title} 86 + </div> 87 + 88 + <div 89 + className={`linkBlockDescription text-sm bg-transparent border-none outline-none resize-none align-top grow line-clamp-2`} 90 + > 91 + {b.block.description} 92 + </div> 93 + <div 94 + style={{ wordBreak: "break-word" }} // better than tailwind break-all! 95 + className={`min-w-0 w-full line-clamp-1 text-xs italic group-hover/linkBlock:text-accent-contrast text-tertiary`} 96 + > 97 + {b.block.src} 98 + </div> 99 + </div> 100 + </div> 101 + {b.block.previewImage && ( 102 + <div 103 + className={`linkBlockPreview w-[120px] m-2 -mb-2 bg-cover shrink-0 rounded-t-md border border-border rotate-[4deg] origin-center`} 104 + style={{ 105 + backgroundImage: `url(${blobRefToSrc(b.block.previewImage?.ref, did)})`, 106 + backgroundPosition: "center", 107 + }} 108 + /> 109 + )} 110 + </a> 111 + ); 112 + } 113 + case PubLeafletBlocksImage.isMain(b.block): { 114 + return ( 115 + <img 116 + height={b.block.aspectRatio?.height} 117 + width={b.block.aspectRatio?.width} 118 + className={`!pt-3 sm:!pt-4 ${className}`} 119 + src={blobRefToSrc(b.block.image.ref, did)} 120 + /> 121 + ); 122 + } 123 + case PubLeafletBlocksText.isMain(b.block): 124 + return ( 125 + <div className={` ${className}`}> 126 + <TextBlock facets={b.block.facets} plaintext={b.block.plaintext} /> 127 + </div> 128 + ); 129 + case PubLeafletBlocksHeader.isMain(b.block): { 130 + if (b.block.level === 1) 131 + return ( 132 + <h1 className={`${className}`}> 133 + <TextBlock {...b.block} /> 134 + </h1> 135 + ); 136 + if (b.block.level === 2) 137 + return ( 138 + <h2 className={`${className}`}> 139 + <TextBlock {...b.block} /> 140 + </h2> 141 + ); 142 + if (b.block.level === 3) 143 + return ( 144 + <h3 className={`${className}`}> 145 + <TextBlock {...b.block} /> 146 + </h3> 147 + ); 148 + // if (b.block.level === 4) return <h4>{b.block.plaintext}</h4>; 149 + // if (b.block.level === 5) return <h5>{b.block.plaintext}</h5>; 150 + return ( 151 + <h6 className={`${className}`}> 152 + <TextBlock {...b.block} /> 153 + </h6> 154 + ); 155 + } 156 + default: 157 + return null; 158 + } 159 + }; 160 + 161 + function ListItem(props: { 162 + item: PubLeafletBlocksUnorderedList.ListItem; 163 + did: string; 164 + className?: string; 165 + }) { 166 + return ( 167 + <li className={`!pb-0 flex flex-row gap-2`}> 168 + <div 169 + className={`listMarker shrink-0 mx-2 z-[1] mt-[14px] h-[5px] w-[5px] rounded-full bg-secondary`} 170 + /> 171 + <div className="flex flex-col"> 172 + <Block block={{ block: props.item.content }} did={props.did} isList /> 173 + {props.item.children?.length ? ( 174 + <ul className="-ml-[7px] sm:ml-[7px]"> 175 + {props.item.children.map((child, index) => ( 176 + <ListItem 177 + item={child} 178 + did={props.did} 179 + key={index} 180 + className={props.className} 181 + /> 182 + ))} 183 + </ul> 184 + ) : null} 185 + </div> 186 + </li> 187 + ); 188 + }
+4 -6
app/lish/[did]/[publication]/[rkey]/TextBlock.tsx
··· 1 - "use client"; 2 1 import { UnicodeString } from "@atproto/api"; 3 2 import { PubLeafletRichtextFacet } from "lexicons/api"; 4 - import { useMemo } from "react"; 5 3 6 4 type Facet = PubLeafletRichtextFacet.Main; 7 5 export function TextBlock(props: { plaintext: string; facets?: Facet[] }) { 8 6 const children = []; 9 - let richText = useMemo( 10 - () => new RichText({ text: props.plaintext, facets: props.facets || [] }), 11 - [props.plaintext, props.facets], 12 - ); 7 + let richText = new RichText({ 8 + text: props.plaintext, 9 + facets: props.facets || [], 10 + }); 13 11 let counter = 0; 14 12 for (const segment of richText.segments()) { 15 13 let link = segment.facet?.find(PubLeafletRichtextFacet.isLink);
+3 -174
app/lish/[did]/[publication]/[rkey]/page.tsx
··· 3 3 import { AtUri } from "@atproto/syntax"; 4 4 import { ids } from "lexicons/api/lexicons"; 5 5 import { 6 - PubLeafletBlocksHeader, 7 - PubLeafletBlocksImage, 8 - PubLeafletBlocksText, 9 - PubLeafletBlocksWebsite, 10 - PubLeafletBlocksUnorderedList, 11 6 PubLeafletDocument, 12 7 PubLeafletPagesLinearDocument, 13 8 } from "lexicons/api"; 14 9 import { Metadata } from "next"; 15 10 import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 16 - import { TextBlock } from "./TextBlock"; 17 11 import { ThemeProvider } from "components/ThemeManager/ThemeProvider"; 18 - import { BlobRef, BskyAgent } from "@atproto/api"; 12 + import { BskyAgent } from "@atproto/api"; 19 13 import { SubscribeWithBluesky } from "app/lish/Subscribe"; 20 - import { blobRefToSrc } from "src/utils/blobRefToSrc"; 14 + import { PostContent } from "./PostContent"; 21 15 22 16 export async function generateMetadata(props: { 23 17 params: Promise<{ publication: string; did: string; rkey: string }>; ··· 138 132 ) : null} 139 133 </div> 140 134 </div> 141 - <div className="postContent flex flex-col"> 142 - {blocks.map((b, index) => { 143 - return <Block block={b} did={did} key={index} />; 144 - })} 145 - </div> 135 + <PostContent blocks={blocks} did={did} /> 146 136 <hr className="border-border-light mb-4 mt-2" /> 147 137 <SubscribeWithBluesky 148 138 isPost ··· 158 148 </ThemeProvider> 159 149 ); 160 150 } 161 - 162 - let Block = ({ 163 - block, 164 - did, 165 - isList, 166 - }: { 167 - block: PubLeafletPagesLinearDocument.Block; 168 - did: string; 169 - isList?: boolean; 170 - }) => { 171 - let b = block; 172 - 173 - // non text blocks, they need this padding, pt-3 sm:pt-4, which is applied in each case 174 - let className = ` 175 - postBlockWrapper 176 - pt-1 177 - ${isList ? "isListItem pb-0 " : "pb-2 last:pb-3 last:sm:pb-4 first:pt-2 sm:first:pt-3"} 178 - ${b.alignment === "lex:pub.leaflet.pages.linearDocument#textAlignRight" ? "text-right" : b.alignment === "lex:pub.leaflet.pages.linearDocument#textAlignCenter" ? "text-center" : ""} 179 - `; 180 - 181 - switch (true) { 182 - case PubLeafletBlocksUnorderedList.isMain(b.block): { 183 - return ( 184 - <ul className="-ml-[1px] sm:ml-[9px] pb-2"> 185 - {b.block.children.map((child, index) => ( 186 - <ListItem 187 - item={child} 188 - did={did} 189 - key={index} 190 - className={className} 191 - /> 192 - ))} 193 - </ul> 194 - ); 195 - } 196 - case PubLeafletBlocksWebsite.isMain(b.block): { 197 - return ( 198 - <a 199 - href={b.block.src} 200 - target="_blank" 201 - className={` 202 - externalLinkBlock flex relative group/linkBlock 203 - h-[104px] w-full bg-bg-page overflow-hidden text-primary hover:no-underline no-underline 204 - hover:border-accent-contrast shadow-sm 205 - block-border 206 - `} 207 - > 208 - <div className="pt-2 pb-2 px-3 grow min-w-0"> 209 - <div className="flex flex-col w-full min-w-0 h-full grow "> 210 - <div 211 - className={`linkBlockTitle bg-transparent -mb-0.5 border-none text-base font-bold outline-none resize-none align-top border h-[24px] line-clamp-1`} 212 - style={{ 213 - overflow: "hidden", 214 - textOverflow: "ellipsis", 215 - wordBreak: "break-all", 216 - }} 217 - > 218 - {b.block.title} 219 - </div> 220 - 221 - <div 222 - className={`linkBlockDescription text-sm bg-transparent border-none outline-none resize-none align-top grow line-clamp-2`} 223 - > 224 - {b.block.description} 225 - </div> 226 - <div 227 - style={{ wordBreak: "break-word" }} // better than tailwind break-all! 228 - className={`min-w-0 w-full line-clamp-1 text-xs italic group-hover/linkBlock:text-accent-contrast text-tertiary`} 229 - > 230 - {b.block.src} 231 - </div> 232 - </div> 233 - </div> 234 - {b.block.previewImage && ( 235 - <div 236 - className={`linkBlockPreview w-[120px] m-2 -mb-2 bg-cover shrink-0 rounded-t-md border border-border rotate-[4deg] origin-center`} 237 - style={{ 238 - backgroundImage: `url(${blobRefToSrc(b.block.previewImage?.ref, did)})`, 239 - backgroundPosition: "center", 240 - }} 241 - /> 242 - )} 243 - </a> 244 - ); 245 - } 246 - case PubLeafletBlocksImage.isMain(b.block): { 247 - return ( 248 - <img 249 - height={b.block.aspectRatio?.height} 250 - width={b.block.aspectRatio?.width} 251 - className={`!pt-3 sm:!pt-4 ${className}`} 252 - src={blobRefToSrc(b.block.image.ref, did)} 253 - /> 254 - ); 255 - } 256 - case PubLeafletBlocksText.isMain(b.block): 257 - return ( 258 - <div className={` ${className}`}> 259 - <TextBlock facets={b.block.facets} plaintext={b.block.plaintext} /> 260 - </div> 261 - ); 262 - case PubLeafletBlocksHeader.isMain(b.block): { 263 - if (b.block.level === 1) 264 - return ( 265 - <h1 className={`${className}`}> 266 - <TextBlock {...b.block} /> 267 - </h1> 268 - ); 269 - if (b.block.level === 2) 270 - return ( 271 - <h2 className={`${className}`}> 272 - <TextBlock {...b.block} /> 273 - </h2> 274 - ); 275 - if (b.block.level === 3) 276 - return ( 277 - <h3 className={`${className}`}> 278 - <TextBlock {...b.block} /> 279 - </h3> 280 - ); 281 - // if (b.block.level === 4) return <h4>{b.block.plaintext}</h4>; 282 - // if (b.block.level === 5) return <h5>{b.block.plaintext}</h5>; 283 - return ( 284 - <h6 className={`${className}`}> 285 - <TextBlock {...b.block} /> 286 - </h6> 287 - ); 288 - } 289 - default: 290 - return null; 291 - } 292 - }; 293 - 294 - function ListItem(props: { 295 - item: PubLeafletBlocksUnorderedList.ListItem; 296 - did: string; 297 - className?: string; 298 - }) { 299 - return ( 300 - <li className={`!pb-0 flex flex-row gap-2`}> 301 - <div 302 - className={`listMarker shrink-0 mx-2 z-[1] mt-[14px] h-[5px] w-[5px] rounded-full bg-secondary`} 303 - /> 304 - <div className="flex flex-col"> 305 - <Block block={{ block: props.item.content }} did={props.did} isList /> 306 - {props.item.children?.length ? ( 307 - <ul className="-ml-[7px] sm:ml-[7px]"> 308 - {props.item.children.map((child, index) => ( 309 - <ListItem 310 - item={child} 311 - did={props.did} 312 - key={index} 313 - className={props.className} 314 - /> 315 - ))} 316 - </ul> 317 - ) : null} 318 - </div> 319 - </li> 320 - ); 321 - }
+21 -3
app/lish/[did]/[publication]/rss/route.ts
··· 2 2 import { get_publication_data } from "app/api/rpc/[command]/get_publication_data"; 3 3 import { Feed } from "feed"; 4 4 import fs from "fs/promises"; 5 - import { PubLeafletDocument, PubLeafletPublication } from "lexicons/api"; 5 + import { 6 + PubLeafletDocument, 7 + PubLeafletPagesLinearDocument, 8 + PubLeafletPublication, 9 + } from "lexicons/api"; 6 10 import { NextRequest, NextResponse } from "next/server"; 11 + import { createElement } from "react"; 7 12 import { supabaseServerClient } from "supabase/serverClient"; 13 + import { PostContent } from "../[rkey]/PostContent"; 8 14 9 15 export async function GET( 10 16 req: Request, ··· 12 18 params: Promise<{ publication: string; did: string }>; 13 19 }, 14 20 ) { 21 + let renderToStaticMarkup = await import("react-dom/server").then( 22 + (module) => module.renderToStaticMarkup, 23 + ); 15 24 let params = await props.params; 16 25 let { result: publication } = await get_publication_data.handler( 17 26 { ··· 40 49 publication?.documents_in_publications.forEach((doc) => { 41 50 if (!doc.documents) return; 42 51 let record = doc.documents?.data as PubLeafletDocument.Record; 43 - let rkey = new AtUri(doc.documents?.uri).rkey; 52 + let uri = new AtUri(doc.documents?.uri); 53 + let rkey = uri.rkey; 44 54 if (!record) return; 55 + let firstPage = record.pages[0]; 56 + let blocks: PubLeafletPagesLinearDocument.Block[] = []; 57 + if (PubLeafletPagesLinearDocument.isMain(firstPage)) { 58 + blocks = firstPage.blocks || []; 59 + } 45 60 feed.addItem({ 46 61 title: record.title, 47 62 description: record.description, 48 63 date: record.publishedAt ? new Date(record.publishedAt) : new Date(), 49 64 id: `https://${pubRecord.base_path}/${rkey}`, 50 65 link: `https://${pubRecord.base_path}/${rkey}`, 66 + content: renderToStaticMarkup( 67 + createElement(PostContent, { blocks, did: uri.host }), 68 + ), 51 69 }); 52 70 }); 53 71 return new Response(feed.rss2(), { 54 72 headers: { 55 - "Content-Type": "application/rss+xml", 73 + "Content-Type": "text/xml", 56 74 }, 57 75 }); 58 76 }