this repo has no description
0
fork

Configure Feed

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

og image on posts + per-page purge

+146 -29
+10 -1
src/app/opengraph-image.tsx
··· 2 2 import { join } from "node:path"; 3 3 import { ImageResponse } from "next/og"; 4 4 5 + export const dynamic = "force-static"; 5 6 export const size = { 6 7 width: 1200, 7 8 height: 630, ··· 26 27 > 27 28 mozzius.dev 28 29 </h1> 29 - <h1 style={{ fontSize: 32 }}>a webbed site</h1> 30 + <h1 31 + style={{ 32 + fontSize: 32, 33 + fontStyle: "italic", 34 + fontFamily: '"Libre Baskerville"', 35 + }} 36 + > 37 + a webbed site 38 + </h1> 30 39 </div> 31 40 ), 32 41 {
+64
src/app/post/[rkey]/opengraph-image.tsx
··· 1 + import { readFile } from "node:fs/promises"; 2 + import { join } from "node:path"; 3 + import { ImageResponse } from "next/og"; 4 + 5 + import { getPost } from "#/lib/api"; 6 + 7 + export const dynamic = "force-static"; 8 + 9 + export const size = { 10 + width: 1200, 11 + height: 630, 12 + }; 13 + export const contentType = "image/png"; 14 + 15 + export default async function OpenGraphImage({ 16 + params, 17 + }: { 18 + params: Promise<{ rkey: string }>; 19 + }) { 20 + const { rkey } = await params; 21 + 22 + const fontData = await readFile( 23 + join(process.cwd(), "./src/app/fonts/LibreBaskerville-Italic.ttf"), 24 + ).then((res) => Uint8Array.from(res).buffer); 25 + 26 + const post = await getPost(rkey); 27 + 28 + return new ImageResponse( 29 + ( 30 + <div tw="h-full w-full bg-white flex flex-col justify-center items-center px-20"> 31 + <h1 32 + style={{ 33 + fontFamily: '"Libre Baskerville"', 34 + fontSize: 80, 35 + textTransform: "uppercase", 36 + fontWeight: 700, 37 + fontStyle: "italic", 38 + textAlign: "center", 39 + }} 40 + > 41 + {post.value.title} 42 + </h1> 43 + <h1 44 + style={{ 45 + fontSize: 32, 46 + fontStyle: "italic", 47 + fontFamily: '"Libre Baskerville"', 48 + }} 49 + > 50 + mozzius.dev 51 + </h1> 52 + </div> 53 + ), 54 + { 55 + ...size, 56 + fonts: [ 57 + { 58 + name: "Libre Baskerville", 59 + data: fontData, 60 + }, 61 + ], 62 + }, 63 + ); 64 + }
+10 -27
src/app/post/[rkey]/page.tsx
··· 3 3 import { type Metadata } from "next"; 4 4 import Image from "next/image"; 5 5 import Link from "next/link"; 6 - import { type ComWhtwndBlogEntry } from "@atcute/client/lexicons"; 7 6 import { Code as SyntaxHighlighter } from "bright"; 8 7 import readingTime from "reading-time"; 9 8 import rehypeSanitize from "rehype-sanitize"; ··· 11 10 import { Footer } from "#/components/footer"; 12 11 import { PostInfo } from "#/components/post-info"; 13 12 import { Code, Paragraph, Title } from "#/components/typography"; 14 - import { getPosts } from "#/lib/api"; 15 - import { bsky, MY_DID } from "#/lib/bsky"; 13 + import { getPost, getPosts } from "#/lib/api"; 14 + import { MY_DID } from "#/lib/bsky"; 16 15 17 16 export const dynamic = "force-static"; 18 17 export const revalidate = 3600; // 1 hour ··· 24 23 }): Promise<Metadata> { 25 24 const { rkey } = await params; 26 25 27 - const post = await bsky.get("com.atproto.repo.getRecord", { 28 - params: { 29 - repo: MY_DID, 30 - rkey: rkey, 31 - collection: "com.whtwnd.blog.entry", 32 - }, 33 - }); 34 - 35 - const entry = post.data.value as ComWhtwndBlogEntry.Record; 26 + const post = await getPost(rkey); 36 27 37 28 return { 38 - title: entry.title + " — mozzius.dev", 29 + title: post.value.title + " — mozzius.dev", 39 30 authors: [{ name: "Samuel", url: `https://bsky.app/profile/${MY_DID}` }], 40 - description: `by Samuel · ${readingTime(entry.content).text}`, 31 + description: `by Samuel · ${readingTime(post.value.content).text}`, 41 32 }; 42 33 } 43 34 ··· 48 39 }) { 49 40 const { rkey } = await params; 50 41 51 - const post = await bsky.get("com.atproto.repo.getRecord", { 52 - params: { 53 - repo: MY_DID, 54 - rkey: rkey, 55 - collection: "com.whtwnd.blog.entry", 56 - }, 57 - }); 58 - 59 - const entry = post.data.value as ComWhtwndBlogEntry.Record; 42 + const post = await getPost(rkey); 60 43 61 44 return ( 62 45 <div className="grid grid-rows-[20px_1fr_20px] justify-items-center min-h-dvh py-8 px-4 xs:px-8 pb-20 gap-16 sm:p-20"> ··· 70 53 <ArrowLeftIcon className="inline size-4 align-middle mb-px mr-1" /> 71 54 Back 72 55 </Link> 73 - <Title>{entry.title}</Title> 56 + <Title>{post.value.title}</Title> 74 57 <PostInfo 75 - content={entry.content} 76 - createdAt={entry.createdAt} 58 + content={post.value.content} 59 + createdAt={post.value.createdAt} 77 60 includeAuthor 78 61 /> 79 62 <div className="diagonal-pattern w-full h-3" /> ··· 149 132 ), 150 133 }} 151 134 > 152 - {entry.content} 135 + {post.value.content} 153 136 </Markdown> 154 137 </article> 155 138 </main>
+16
src/app/post/[rkey]/purge/actions.ts
··· 1 + "use server"; 2 + 3 + import { revalidatePath } from "next/cache"; 4 + import { redirect } from "next/navigation"; 5 + 6 + export async function purgeCache(formData: FormData) { 7 + const rkey = formData.get("rkey")! 8 + const password = formData.get("password"); 9 + if (password === process.env.PURGE_PASSWORD) { 10 + revalidatePath(`/post/${rkey}`, "page"); 11 + redirect(`/post/${rkey}`) 12 + } else { 13 + console.error(`Invalid password: ${password}`); 14 + redirect(`/post/${rkey}/purge?error=yeah`) 15 + } 16 + }
+31
src/app/post/[rkey]/purge/page.tsx
··· 1 + import { purgeCache } from "./actions"; 2 + 3 + export default async function Purge({ 4 + params, 5 + searchParams, 6 + }: { 7 + params: Promise<{ rkey: string }>; 8 + searchParams: Promise<{ error?: string }>; 9 + }) { 10 + const { rkey } = await params; 11 + const { error } = await searchParams; 12 + return ( 13 + <form 14 + action={purgeCache} 15 + method="post" 16 + className="flex flex-col gap-4 p-2 mt-24 mx-auto max-w-96" 17 + > 18 + <input type="hidden" name="rkey" value={rkey} /> 19 + <input 20 + type="password" 21 + name="password" 22 + placeholder="Password" 23 + className="p-2 border rounded-xs" 24 + /> 25 + {error && <p className="text-red-500">wrong password mate</p>} 26 + <button type="submit" className="p-2 border rounded-xs"> 27 + Purge 28 + </button> 29 + </form> 30 + ); 31 + }
+1 -1
src/app/purge/actions.ts
··· 11 11 redirect('/') 12 12 } else { 13 13 console.error(`Invalid password: ${password}`); 14 - redirect('/?error=yeah') 14 + redirect('/purge?error=yeah') 15 15 } 16 16 }
+14
src/lib/api.ts
··· 25 25 const post = record.value as ComWhtwndBlogEntry.Record; 26 26 return post.visibility === "public"; 27 27 } 28 + 29 + export async function getPost(rkey: string) { 30 + const post = await bsky.get("com.atproto.repo.getRecord", { 31 + params: { 32 + repo: MY_DID, 33 + rkey: rkey, 34 + collection: "com.whtwnd.blog.entry", 35 + }, 36 + }); 37 + 38 + return post.data as ComAtprotoRepoListRecords.Record & { 39 + value: ComWhtwndBlogEntry.Record; 40 + }; 41 + }