My landing page, written in Astro hayden.moe
0
fork

Configure Feed

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

feat: moving to leaflet

+2 -250
-5
src/atproto/agent.ts
··· 1 - import { AtpAgent } from '@atproto/api'; 2 - 3 - export const atpAgent = (locals: App.Locals) => new AtpAgent({ 4 - service: locals.runtime.env.ATP_SERVICE, 5 - });
-21
src/atproto/dataToView.ts
··· 1 - import type { WhtwndBlogEntryRecord, WhtwndBlogEntryView } from "../types"; 2 - import { uriToRkey } from "./uriToRkey"; 3 - 4 - export const whtwndBlogEntryRecordToView = ({ 5 - uri, 6 - cid, 7 - value, 8 - }: { 9 - uri: string; 10 - cid: string; 11 - value: WhtwndBlogEntryRecord; 12 - }): WhtwndBlogEntryView => { 13 - return { 14 - rkey: uriToRkey(uri), 15 - cid, 16 - title: value.title, 17 - content: value.content, 18 - createdAt: value.createdAt, 19 - banner: value.ogp?.url ?? undefined, 20 - }; 21 - };
-31
src/atproto/getPost.ts
··· 1 - import type { WhtwndBlogEntryRecord, WhtwndBlogEntryView } from "src/types"; 2 - import { atpAgent } from "./agent"; 3 - import { whtwndBlogEntryRecordToView } from "./dataToView"; 4 - import { getCachedPost, setCachedPost } from "src/kv"; 5 - 6 - export const getPost = async (ctx: App.Locals, rkey: string, skipCache?: boolean) => { 7 - const cachedRes = await getCachedPost(ctx, rkey); 8 - if (!skipCache && cachedRes) { 9 - return cachedRes; 10 - } 11 - 12 - const repo = ctx.runtime.env.ATP_IDENTIFIER; 13 - const res = await atpAgent(ctx).com.atproto.repo.getRecord({ 14 - collection: 'com.whtwnd.blog.entry', 15 - repo, 16 - rkey, 17 - }); 18 - 19 - if (!res.success) { 20 - throw new Error('Failed to get post.'); 21 - } 22 - 23 - const post = whtwndBlogEntryRecordToView({ 24 - uri: res.data.uri, 25 - cid: res.data.cid?.toString() ?? '', 26 - value: res.data.value as WhtwndBlogEntryRecord, 27 - }) as WhtwndBlogEntryView; 28 - 29 - await setCachedPost(ctx, post); 30 - return post; 31 - };
-35
src/atproto/getPosts.ts
··· 1 - import type { WhtwndBlogEntryRecord, WhtwndBlogEntryView } from "src/types"; 2 - import { atpAgent } from "./agent"; 3 - import { whtwndBlogEntryRecordToView } from "./dataToView"; 4 - import { getCachedPosts, setCachedPost, setCachedPosts } from "src/kv"; 5 - 6 - export const getPosts = async (ctx: App.Locals, cursor: string | undefined, skipCache?: boolean) => { 7 - const cachedRes = await getCachedPosts(ctx); 8 - if (!skipCache && cachedRes) { 9 - return cachedRes; 10 - } 11 - 12 - const repo = ctx.runtime.env.ATP_IDENTIFIER; 13 - const res = await atpAgent(ctx).com.atproto.repo.listRecords({ 14 - collection: 'com.whtwnd.blog.entry', 15 - repo, 16 - cursor, 17 - }); 18 - 19 - if (!res.success) { 20 - throw new Error('Failed to get posts.'); 21 - } 22 - 23 - const posts = res.data.records.map(data => whtwndBlogEntryRecordToView({ 24 - uri: data.uri, 25 - cid: data.cid?.toString() ?? '', 26 - value: data.value as WhtwndBlogEntryRecord, 27 - })) as WhtwndBlogEntryView[]; 28 - 29 - for (const post of posts) { 30 - await setCachedPost(ctx, post); 31 - } 32 - await setCachedPosts(ctx, posts); 33 - 34 - return posts; 35 - }
-8
src/atproto/uriToRkey.ts
··· 1 - export const uriToRkey = (uri: string): string => { 2 - const rkey = uri.split('/').pop() 3 - if (!rkey) { 4 - throw new Error('Failed to get rkey from uri.') 5 - } else { 6 - return rkey 7 - } 8 - }
-33
src/kv/index.ts
··· 1 - import type { WhtwndBlogEntryView } from "src/types"; 2 - 3 - export const getCachedPosts = async (context: App.Locals) => { 4 - const res = await context.runtime.env.CACHE.get('post:all', 'json'); 5 - if (!res) { 6 - return null; 7 - } 8 - return res as WhtwndBlogEntryView[]; 9 - }; 10 - 11 - export const setCachedPosts = async (context: App.Locals, posts: WhtwndBlogEntryView[]) => { 12 - await context.runtime.env.CACHE.put( 13 - 'post:all', 14 - JSON.stringify(posts), 15 - { expirationTtl: 60 }, 16 - ); 17 - }; 18 - 19 - export const getCachedPost = async (context: App.Locals, rkey: string) => { 20 - const res = await context.runtime.env.CACHE.get(`post:${rkey}`, 'json'); 21 - if (!res) { 22 - return null; 23 - } 24 - return res as WhtwndBlogEntryView; 25 - }; 26 - 27 - export const setCachedPost = async (context: App.Locals, post: WhtwndBlogEntryView) => { 28 - await context.runtime.env.CACHE.put( 29 - `post:${post.rkey}`, 30 - JSON.stringify(post), 31 - { expirationTtl: 60 * 10 }, 32 - ); 33 - };
+1 -39
src/pages/posts/[rkey].astro
··· 1 1 --- 2 - export const prerender = false; 3 - 4 - import { createMarkdownProcessor } from '@astrojs/markdown-remark'; 5 - 6 - import { getPost } from '~/atproto/getPost'; 7 - import Shell from '~/layouts/shell.astro'; 8 - import FormattedDate from '~/components/formatted-date.astro'; 9 - import { loadWasm } from 'shiki'; 10 - 11 - const rkey = Astro.params.rkey; 12 - 13 - // @ts-ignore 14 - loadWasm(await import('shiki/onig.wasm')); 15 - 16 - const markdownProcessor = await createMarkdownProcessor(); 17 - const post = await getPost(Astro.locals, rkey!); 18 - const content = await markdownProcessor.render(post.content) 19 - const shortContent = post.content.slice(0, 200).trimEnd() + '...'; 2 + return Astro.redirect('https://hayden.leaflet.pub'); 20 3 --- 21 - <Shell title={post.title} description={shortContent}> 22 - <header class="max-w-2xl mx-auto w-full"> 23 - <h1 class="font-bold before-hash-1">{post.title}</h1> 24 - <span class="text-base03"> 25 - <FormattedDate date={new Date(Date.parse(post.createdAt))} /> 26 - </span> 27 - </header> 28 - 29 - <hr /> 30 - 31 - <div class="prose max-w-2xl mx-auto pb-5" set:html={content.code} /> 32 - 33 - <hr /> 34 - 35 - <p class="max-w-2xl mx-auto pb-5 prose"> 36 - Thanks for reading along, I hope you enjoyed this post. 37 - If you did, maybe consider following me on <a href="https://bsky.app/profile/hayden.moe">Bluesky</a>, 38 - and if you're feeling generous, maybe consider <a href="https://ko-fi.com/haydenuwu">buying me a coffee</a>. 39 - I'm trying to write more this year, so I'll see you in the next post. 👋 40 - </p> 41 - </Shell>
+1 -48
src/pages/posts/index.astro
··· 1 1 --- 2 - export const prerender = false; 3 - 4 - import { getPosts } from '~/atproto/getPosts'; 5 - import Shell from '~/layouts/shell.astro'; 6 - import FormattedDate from '~/components/formatted-date.astro'; 7 - 8 - const posts = await getPosts(Astro.locals, '', false); 9 - const postsFiltered = posts.filter(p => !p.content?.startsWith('NOT_LIVE')); 10 - 11 - const postsShortened = postsFiltered.map(p => { 12 - if (p.content?.length! > 200) { 13 - p.content = p.content?.slice(0, 200).trimEnd() + '...'; 14 - } 15 - return p; 16 - }); 2 + return Astro.redirect('https://hayden.leaflet.pub'); 17 3 --- 18 - <Shell title="hayden@web ~/posts"> 19 - <div class="mx-auto max-w-2xl flex flex-col"> 20 - <header class="flex flex-col"> 21 - <h1 class="font-bold before-hash-1">Posts</h1> 22 - <p class="text-base03"> 23 - Showing {posts.length} posts of {posts.length}. 24 - </p> 25 - </header> 26 - 27 - <div class="flex flex-col divide-y divide-muted"> 28 - {postsShortened.map((post) => ( 29 - <div class="py-4"> 30 - <article> 31 - <header> 32 - <h2 class="text-pink font-bold"> 33 - <a href={`/posts/${post.rkey}/`}>{post.title}</a> 34 - </h2> 35 - <p class="text-base03"> 36 - <span class="date"> 37 - <FormattedDate date={new Date(Date.parse(post.createdAt))} /> 38 - </span> 39 - </p> 40 - </header> 41 - </article> 42 - 43 - <section class="post__summary prose max-w-6xl"> 44 - { post.content } 45 - </section> 46 - </div> 47 - ))} 48 - </div> 49 - </div> 50 - </Shell>
-30
src/types.ts
··· 1 - export interface WhtwndBlogEntryRecord { 2 - $type: 'com.whtwnd.blog.entry' 3 - content?: string 4 - createdAt: string 5 - theme?: string 6 - title: string 7 - ogp?: { 8 - height: number | null 9 - url: string | null 10 - width: number | null 11 - } 12 - } 13 - 14 - export interface WhtwndBlogEntryView { 15 - rkey: string 16 - cid: string 17 - title: string 18 - content?: string 19 - createdAt: string 20 - banner?: string 21 - } 22 - 23 - export interface BskyProfileView { 24 - did: string 25 - handle: string 26 - displayName: string 27 - avatar: string 28 - description: string 29 - banner: string 30 - }