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 page title metadata

+63 -2
+44 -2
app/[doc_id]/page.tsx
··· 1 + import { Metadata, ResolvingMetadata } from "next"; 2 + import * as Y from "yjs"; 3 + import * as base64 from "base64-js"; 4 + 1 5 import { Fact, ReplicacheProvider } from "src/replicache"; 2 6 import { Database } from "../../supabase/database.types"; 3 7 import { Attributes } from "src/replicache/attributes"; ··· 7 11 import { ThemeProvider } from "components/ThemeManager/ThemeProvider"; 8 12 import { MobileFooter } from "components/MobileFooter"; 9 13 import { PopUpProvider } from "components/Toast"; 14 + import { YJSFragmentToString } from "components/TextBlock/RenderYJSFragment"; 10 15 11 16 export const preferredRegion = ["sfo1"]; 12 17 export const dynamic = "force-dynamic"; ··· 17 22 process.env.SUPABASE_SERVICE_ROLE_KEY as string, 18 23 { cookies: {} }, 19 24 ); 20 - export default async function DocumentPage(props: { 25 + type Props = { 21 26 params: { doc_id: string }; 22 - }) { 27 + }; 28 + export default async function DocumentPage(props: Props) { 23 29 let { data } = await supabase.rpc("get_facts", { root: props.params.doc_id }); 24 30 let initialFacts = (data as unknown as Fact<keyof typeof Attributes>[]) || []; 25 31 return ( ··· 39 45 </ReplicacheProvider> 40 46 ); 41 47 } 48 + 49 + export async function generateMetadata(props: Props): Promise<Metadata> { 50 + let { data } = await supabase.rpc("get_facts", { root: props.params.doc_id }); 51 + let initialFacts = (data as unknown as Fact<keyof typeof Attributes>[]) || []; 52 + let blocks = initialFacts 53 + .filter( 54 + (f) => f.attribute === "card/block" && f.entity === props.params.doc_id, 55 + ) 56 + .map((_f) => { 57 + let block = _f as Fact<"card/block">; 58 + let type = initialFacts.find( 59 + (f) => f.entity === block.data.value && f.attribute === "block/type", 60 + ) as Fact<"block/type"> | undefined; 61 + if (!type) return null; 62 + return { ...block.data, type: type.data.value, parent: block.entity }; 63 + }) 64 + .flatMap((f) => (f ? [f] : [])) 65 + .sort((a, b) => (a.position > b.position ? 1 : -1)) 66 + .filter((b) => b.type === "text" || b.type === "heading"); 67 + 68 + let metadata: Metadata = { title: "Untitled Leaflet" }; 69 + let firstBlock = blocks[0]; 70 + if (firstBlock?.type === "heading") { 71 + let content = initialFacts.find( 72 + (f) => f.entity === firstBlock.value && f.attribute === "block/text", 73 + ) as Fact<"block/text"> | undefined; 74 + if (content) { 75 + let doc = new Y.Doc(); 76 + const update = base64.toByteArray(content.data.value); 77 + Y.applyUpdate(doc, update); 78 + let nodes = doc.getXmlElement("prosemirror").toArray(); 79 + metadata.title = YJSFragmentToString(nodes[0]); 80 + } 81 + } 82 + return metadata; 83 + }
+19
components/TextBlock/RenderYJSFragment.tsx
··· 86 86 if (d.attributes?.em) style.fontStyle = "italic"; 87 87 return style; 88 88 } 89 + 90 + export function YJSFragmentToString( 91 + node: XmlElement | XmlText | XmlHook, 92 + ): string { 93 + if (node.constructor === XmlElement) { 94 + return node 95 + .toArray() 96 + .map((f) => YJSFragmentToString(f)) 97 + .join(""); 98 + } 99 + if (node.constructor === XmlText) { 100 + return (node.toDelta() as Delta[]) 101 + .map((d) => { 102 + return d.insert; 103 + }) 104 + .join(" "); 105 + } 106 + return ""; 107 + }