A Astro blog hosted on Vercel

rework components and bluesky comments

+325 -387
+17
.prettierrc
··· 1 + { 2 + "plugins": ["prettier-plugin-astro"], 3 + "trailingComma": "es5", 4 + "tabWidth": 2, 5 + "useTabs": false, 6 + "semi": true, 7 + "singleQuote": true, 8 + "htmlWhitespaceSensitivity": "ignore", 9 + "overrides": [ 10 + { 11 + "files": "*.astro", 12 + "options": { 13 + "parser": "astro" 14 + } 15 + } 16 + ] 17 + }
+3 -3
package-lock.json
··· 5902 5902 } 5903 5903 }, 5904 5904 "node_modules/vite": { 5905 - "version": "6.3.5", 5906 - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", 5907 - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", 5905 + "version": "6.3.6", 5906 + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", 5907 + "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", 5908 5908 "license": "MIT", 5909 5909 "dependencies": { 5910 5910 "esbuild": "^0.25.0",
+149
src/components/BlueskyComments.svelte
··· 1 + <script lang="ts"> 2 + import { formatDate } from '@/utils'; 3 + import getBlogPostComments from '@/utils/getBlogPostComments'; 4 + 5 + interface Props { 6 + blogTitle: string; 7 + } 8 + 9 + let { blogTitle }: Props = $props(); 10 + 11 + const comments = getBlogPostComments(blogTitle); 12 + </script> 13 + 14 + <section class="bluesky-comments"> 15 + <h2>Comments 🗣️</h2> 16 + 17 + {#await comments} 18 + <p>Loading comments... ⏰</p> 19 + {:then comments} 20 + {#each comments as comment} 21 + <article aria-labelledby="{comment.cid}-author"> 22 + <header> 23 + <h2 class="author" id="{comment.cid}-author"> 24 + <a 25 + href={comment.authorUrl} 26 + rel="noopener noreferrer" 27 + target="_blank" 28 + aria-label="{comment.displayName}'s profile on Bluesky" 29 + > 30 + {comment.displayName} 31 + <span class="handle">(@{comment.handle})</span> 32 + </a> 33 + <time 34 + class="time" 35 + aria-label="Commented on" 36 + datetime={comment.createdAt} 37 + > 38 + {formatDate(new Date(comment.createdAt))} 39 + </time> 40 + </h2> 41 + </header> 42 + <p>{comment.text}</p> 43 + <footer class="counts"> 44 + <p class="likes"> 45 + <iconify-icon 46 + icon={'fa6-solid:heart'} 47 + height={'1rem'} 48 + ></iconify-icon> 49 + {comment.likeCount} Likes 50 + </p> 51 + <p class="replies"> 52 + <iconify-icon 53 + icon={'fa6-solid:comment'} 54 + height={'1rem'} 55 + ></iconify-icon> 56 + {comment.replyCount} Replies 57 + </p> 58 + <p class="replies"> 59 + <iconify-icon 60 + icon={'fa6-solid:repeat'} 61 + height={'1rem'} 62 + ></iconify-icon> 63 + {comment.replyCount} Quotes 64 + </p> 65 + <a href={comment.postUrl} rel="noopener noreferrer" target="_blank"> 66 + <iconify-icon 67 + icon={'fa6-solid:link'} 68 + height={'1rem'} 69 + ></iconify-icon> 70 + Link to Comment 71 + </a> 72 + </footer> 73 + </article> 74 + {:else} 75 + <p>No comments just yet 🐄</p> 76 + {/each} 77 + {:catch} 78 + <div> 79 + <p>There was an error getting the comments! ⚠️</p> 80 + <p> 81 + Please <a href="mailto:me@claycow.com">reach out</a> 82 + if this persists. ❤️ 83 + </p> 84 + </div> 85 + {/await} 86 + </section> 87 + 88 + <style> 89 + .bluesky-comments { 90 + display: flex; 91 + flex-direction: column; 92 + gap: 1rem; 93 + } 94 + 95 + article { 96 + display: flex; 97 + flex-direction: column; 98 + gap: 1rem; 99 + background-color: var(--surface0); 100 + padding: 1rem; 101 + border-radius: 0.5rem; 102 + } 103 + 104 + .author { 105 + display: flex; 106 + flex-direction: row; 107 + font-size: medium; 108 + } 109 + 110 + .author > a { 111 + flex-grow: 1; 112 + text-decoration: none; 113 + color: var(--text); 114 + } 115 + 116 + .author > a > .handle { 117 + color: var(--subtext0); 118 + } 119 + 120 + .author > .time { 121 + color: var(--subtext0); 122 + font-size: x-small; 123 + } 124 + 125 + footer { 126 + display: flex; 127 + flex-direction: row; 128 + gap: 2rem; 129 + } 130 + 131 + footer > p, 132 + footer > a { 133 + display: flex; 134 + flex-direction: row; 135 + align-items: center; 136 + gap: 0.5rem; 137 + text-decoration: none; 138 + color: var(--text); 139 + } 140 + 141 + footer > a { 142 + margin-left: auto; 143 + } 144 + 145 + h2, 146 + p { 147 + margin: 0; 148 + } 149 + </style>
src/components/atoms/Button.svelte src/components/Button.svelte
src/components/atoms/Link.astro src/components/Link.astro
-24
src/components/atoms/Link.svelte
··· 1 - <script lang="ts"> 2 - import type { HTMLAnchorAttributes } from "svelte/elements"; 3 - 4 - interface Props extends HTMLAnchorAttributes {}; 5 - 6 - let { children, ...props }: Props = $props(); 7 - </script> 8 - 9 - <a {...props}> 10 - {@render children?.()} 11 - </a> 12 - 13 - <style> 14 - a { 15 - display: flex; 16 - gap: 0.5rem; 17 - color: var(--text); 18 - text-decoration: none; 19 - } 20 - 21 - a:hover { 22 - color: var(--subtext0); 23 - } 24 - </style>
-2
src/components/atoms/index.ts
··· 1 - export { default as Link } from "./Link.astro"; 2 - export { default as Button } from "./Button.svelte";
+10
src/components/index.ts
··· 1 + export { default as BandcampWishlist } from "./BandcampWishlist.astro"; 2 + export { default as BlogPreviewCard } from "./BlogPreviewCard.astro"; 3 + export { default as Footer } from "./Footer.astro"; 4 + export { default as Head } from "./Head.astro"; 5 + export { default as LiberaPayDonate } from "./LiberaPayDonate.astro"; 6 + export { default as Link } from "./Link.astro"; 7 + export { default as Navigation } from "./Navigation.astro"; 8 + export { default as BlueskyComments } from "./BlueskyComments.svelte"; 9 + export { default as Button } from "./Button.svelte"; 10 + export { default as ThemeToggle } from "./ThemeToggle.svelte";
+1 -1
src/components/molecules/BandcampWishlist.astro src/components/BandcampWishlist.astro
··· 1 1 --- 2 - import { Link } from "@/components/atoms"; 2 + import { Link } from "@/components"; 3 3 --- 4 4 5 5 <Link
+1 -1
src/components/molecules/LiberaPayDonate.astro src/components/LiberaPayDonate.astro
··· 1 1 --- 2 - import { Link } from "@/components/atoms"; 2 + import { Link } from "@/components"; 3 3 --- 4 4 5 5 <Link
+1 -1
src/components/molecules/ThemeToggle.svelte src/components/ThemeToggle.svelte
··· 1 1 <script lang="ts"> 2 2 import type { HTMLButtonAttributes } from "svelte/elements"; 3 - import { Button } from "@/components/atoms"; 3 + import { Button } from "@/components"; 4 4 5 5 interface Props extends HTMLButtonAttributes {} 6 6
-3
src/components/molecules/index.ts
··· 1 - export { default as ThemeToggle } from "./ThemeToggle.svelte"; 2 - export { default as BandcampWishlist } from "./BandcampWishlist.astro"; 3 - export { default as LiberaPayDonate } from "./LiberaPayDonate.astro";
src/components/organisms/BlogPreviewCard.astro src/components/BlogPreviewCard.astro
-138
src/components/organisms/BlueskyComments.svelte
··· 1 - <script lang="ts"> 2 - import { formatDate } from "@/utils"; 3 - import getBlogPostComments from "@/utils/getBlogPostComments"; 4 - 5 - interface Props { 6 - blogTitle: string; 7 - } 8 - 9 - let { blogTitle }: Props = $props(); 10 - 11 - const comments = getBlogPostComments(blogTitle); 12 - </script> 13 - 14 - <section class="bluesky-comments"> 15 - <h2>Comments 🗣️</h2> 16 - 17 - {#await comments} 18 - <p>Loading comments... ⏰</p> 19 - {:then comments} 20 - {#each comments as comment} 21 - <article class="comment" aria-labelledby="comment-{comment.cid}-author"> 22 - <header> 23 - <h2 class="comment-author" id="comment-{comment.cid}-author"> 24 - <a 25 - href={`https://bsky.app/profile/${comment.author.did}`} 26 - rel="noopener noreferrer" 27 - target="_blank" 28 - aria-label="{comment.author.displayName}'s profile on Bluesky" 29 - > 30 - {comment.author.displayName} 31 - <span class="handle">(@{comment.author.handle})</span> 32 - </a> 33 - <time 34 - class="comment-time" 35 - aria-label="Commented on" 36 - datetime={comment.record.createdAt} 37 - > 38 - {formatDate(new Date(comment.record.createdAt))} 39 - </time> 40 - </h2> 41 - </header> 42 - <p class="comment-body">{comment.record.text}</p> 43 - <footer class="comment-counts"> 44 - <a 45 - href={`https://bsky.app/profile/${comment.author.did}`} 46 - rel="noopener noreferrer" 47 - target="_blank" 48 - > 49 - <p class="likes"> 50 - <iconify-icon icon={"fa6-solid:heart"} height={"1rem"} 51 - ></iconify-icon> 52 - {comment.likeCount} Likes 53 - </p> 54 - <p class="replies"> 55 - <iconify-icon icon={"fa6-solid:comment"} height={"1rem"} 56 - ></iconify-icon> 57 - {comment.replyCount} Replies 58 - </p> 59 - <p class="replies"> 60 - <iconify-icon icon={"fa6-solid:repeat"} height={"1rem"} 61 - ></iconify-icon> 62 - {comment.quoteCount} Quotes 63 - </p> 64 - </a> 65 - </footer> 66 - </article> 67 - {:else} 68 - <p>No comments just yet 🐄</p> 69 - {/each} 70 - {:catch} 71 - <div> 72 - <p>There was an error getting the comments! ⚠️</p> 73 - <p> 74 - Please <a href="mailto:me@claycow.com">reach out</a> if this persists. ❤️ 75 - </p> 76 - </div> 77 - {/await} 78 - </section> 79 - 80 - <style> 81 - .bluesky-comments { 82 - display: flex; 83 - flex-direction: column; 84 - gap: 1rem; 85 - } 86 - 87 - .bluesky-comments > h2 { 88 - margin-bottom: 0; 89 - } 90 - 91 - .bluesky-comments > article { 92 - background-color: var(--surface0); 93 - padding: 1rem; 94 - border-radius: 0.5rem; 95 - } 96 - 97 - .comment-author { 98 - display: flex; 99 - flex-direction: row; 100 - font-size: medium; 101 - margin: 0; 102 - } 103 - 104 - .comment-author > a { 105 - flex-grow: 1; 106 - text-decoration: none; 107 - color: var(--text); 108 - } 109 - 110 - .comment-author > a > .handle { 111 - color: var(--subtext0); 112 - } 113 - 114 - .comment-author > .comment-time { 115 - color: var(--subtext0); 116 - font-size: x-small; 117 - } 118 - 119 - .comment-body { 120 - margin: 1rem 0 0; 121 - } 122 - 123 - .comment-counts > a { 124 - display: flex; 125 - flex-direction: row; 126 - gap: 2rem; 127 - text-decoration: none; 128 - color: var(--text); 129 - } 130 - 131 - .comment-counts > a > p { 132 - display: flex; 133 - flex-direction: row; 134 - align-items: center; 135 - gap: 0.5rem; 136 - margin-bottom: 0; 137 - } 138 - </style>
+1 -1
src/components/organisms/Footer.astro src/components/Footer.astro
··· 1 1 --- 2 2 import { BLUESKY_LINK, TANGLED_SH_LINK, SIGNAL_LINK } from "@/consts"; 3 - import { Link } from "@/components/atoms"; 3 + import { Link } from "@/components"; 4 4 5 5 const today = new Date(); 6 6 ---
+20 -20
src/components/organisms/Head.astro src/components/Head.astro
··· 5 5 import type { CollectionEntry } from "astro:content"; 6 6 7 7 interface Props { 8 - title: string; 9 - description: string; 10 - image?: CollectionEntry<"gallery">["data"]["image"]; 8 + title: string; 9 + description: string; 10 + // image?: CollectionEntry<"gallery">["data"]["image"]; 11 11 } 12 12 13 13 const canonicalURL = new URL(Astro.url.pathname, Astro.site); 14 14 15 15 const { 16 - title, 17 - description, 18 - image 16 + title, 17 + description, 18 + // image 19 19 } = Astro.props; 20 20 --- 21 21 ··· 27 27 28 28 <!-- Font preloads --> 29 29 <link 30 - rel="preload" 31 - href="/fonts/atkinson-regular.woff" 32 - as="font" 33 - type="font/woff" 34 - crossorigin 30 + rel="preload" 31 + href="/fonts/atkinson-regular.woff" 32 + as="font" 33 + type="font/woff" 34 + crossorigin 35 35 /> 36 36 <link 37 - rel="preload" 38 - href="/fonts/atkinson-bold.woff" 39 - as="font" 40 - type="font/woff" 41 - crossorigin 37 + rel="preload" 38 + href="/fonts/atkinson-bold.woff" 39 + as="font" 40 + type="font/woff" 41 + crossorigin 42 42 /> 43 43 44 44 <!-- Canonical URL --> ··· 58 58 <meta property="og:title" content={title} /> 59 59 <meta property="og:description" content={description} /> 60 60 <meta property="og:site_name" content="claycow" /> 61 - { 61 + <!-- { 62 62 image && ( 63 63 <meta property="og:image" content={new URL(image.src, Astro.site)} /> 64 64 <meta property="og:image:url" content={new URL(image.src, Astro.site)} /> ··· 67 67 <meta property="og:image:height" content={image.height.toString()} /> 68 68 <meta property="og:image:alt" content={description} /> 69 69 ) 70 - } 70 + } --> 71 71 72 72 <!-- Twitter --> 73 73 <meta property="twitter:card" content="summary_large_image" /> ··· 75 75 <meta property="twitter:title" content={title} /> 76 76 <meta property="twitter:description" content={description} /> 77 77 <meta property="twitter:site" content="claycow" /> 78 - { 78 + <!-- { 79 79 image && ( 80 80 <meta 81 81 property="twitter:image" ··· 86 86 content={description} 87 87 /> 88 88 ) 89 - } 89 + } -->
+1 -2
src/components/organisms/Navigation.astro src/components/Navigation.astro
··· 1 1 --- 2 - import { Link } from "@/components/atoms"; 3 - import { ThemeToggle } from "@/components/molecules"; 2 + import { Link, ThemeToggle } from "@/components"; 4 3 --- 5 4 6 5 <nav>
-5
src/components/organisms/index.ts
··· 1 - export { default as Navigation } from "./Navigation.astro"; 2 - export { default as Head } from "./Head.astro"; 3 - export { default as Footer } from "./Footer.astro"; 4 - export { default as BlogPreviewCard } from "./BlogPreviewCard.astro"; 5 - export { default as BlueskyComments } from "./BlueskyComments.svelte";
+3 -6
src/layouts/BlogPost.astro
··· 5 5 Footer, 6 6 Navigation, 7 7 BlueskyComments, 8 - } from "@/components/organisms"; 8 + BandcampWishlist, 9 + LiberaPayDonate, 10 + } from "@/components"; 9 11 import { formatDate } from "@/utils"; 10 12 import { Image } from "astro:assets"; 11 - import SpeedInsights from "@vercel/speed-insights/astro"; 12 - import Analytics from "@vercel/analytics/astro"; 13 - import { BandcampWishlist, LiberaPayDonate } from "@/components/molecules"; 14 13 15 14 type Props = CollectionEntry<"blog">["data"]; 16 15 ··· 109 108 </article> 110 109 </main> 111 110 <Footer /> 112 - <SpeedInsights /> 113 - <Analytics /> 114 111 </body> 115 112 </html>
-57
src/layouts/GalleryPost.astro
··· 1 - --- 2 - import type { CollectionEntry } from "astro:content"; 3 - import { Head, Footer, Navigation } from "@/components/organisms"; 4 - import { Image } from "astro:assets"; 5 - import SpeedInsights from "@vercel/speed-insights/astro"; 6 - import Analytics from "@vercel/analytics/astro"; 7 - 8 - type Props = CollectionEntry<"gallery">["data"]; 9 - 10 - const { title, alt, image, width, height } = Astro.props; 11 - --- 12 - 13 - <html lang="en"> 14 - <head> 15 - <Head title={title} description={alt} image={image} /> 16 - <style> 17 - article { 18 - display: flex; 19 - flex-direction: column; 20 - width: 100%; 21 - gap: 1rem; 22 - } 23 - 24 - h1 { 25 - margin-bottom: 0; 26 - } 27 - 28 - img { 29 - height: 100%; 30 - width: 100%; 31 - object-fit: contain; 32 - } 33 - </style> 34 - </head> 35 - 36 - <body data-theme="dark"> 37 - <Navigation /> 38 - <main> 39 - <article> 40 - <h1>{title}</h1> 41 - <a href={image.src}> 42 - <Image 43 - src={image} 44 - alt={alt} 45 - width={width} 46 - height={height} 47 - format={"webp"} 48 - /> 49 - </a> 50 - <p>{alt}</p> 51 - </article> 52 - <Footer /> 53 - <SpeedInsights /> 54 - <Analytics /> 55 - </main> 56 - </body> 57 - </html>
+1
src/layouts/index.ts
··· 1 + export { default as BlogPostLayout } from "./BlogPost.astro";
+17 -17
src/pages/blog/[...slug].astro
··· 1 1 --- 2 2 import { type CollectionEntry, getCollection } from "astro:content"; 3 - import BlogPost from "@/layouts/BlogPost.astro"; 3 + import { BlogPostLayout } from "@/layouts"; 4 4 import { render } from "astro:content"; 5 5 6 6 export async function getStaticPaths() { 7 - const posts = await getCollection("blog"); 8 - return posts 9 - .filter( 10 - (post) => 11 - import.meta.env.ENVIRONMENT === "preview" || 12 - Boolean(post.data.isPublished), 13 - ) 14 - .map((post) => ({ 15 - params: { 16 - slug: post.data.title.toLowerCase().replaceAll(" ", "-"), 17 - }, 18 - props: post, 19 - })); 7 + const posts = await getCollection("blog"); 8 + return posts 9 + .filter( 10 + (post) => 11 + import.meta.env.ENVIRONMENT === "preview" || 12 + Boolean(post.data.isPublished) 13 + ) 14 + .map((post) => ({ 15 + params: { 16 + slug: post.data.title.toLowerCase().replaceAll(" ", "-"), 17 + }, 18 + props: post, 19 + })); 20 20 } 21 21 type Props = CollectionEntry<"blog">; 22 22 ··· 24 24 const { Content } = await render(post); 25 25 --- 26 26 27 - <BlogPost {...post.data}> 28 - <Content /> 29 - </BlogPost> 27 + <BlogPostLayout {...post.data}> 28 + <Content /> 29 + </BlogPostLayout>
+49 -58
src/pages/blog/index.astro
··· 1 1 --- 2 - import { 3 - Head, 4 - Footer, 5 - Navigation, 6 - BlogPreviewCard, 7 - } from "@/components/organisms"; 2 + import { Head, Footer, Navigation, BlogPreviewCard } from "@/components"; 8 3 import { SITE_TITLE, SITE_DESCRIPTION } from "@/consts"; 9 4 import { getCollection } from "astro:content"; 10 - import SpeedInsights from "@vercel/speed-insights/astro"; 11 - import Analytics from "@vercel/analytics/astro"; 12 5 13 6 const posts = (await getCollection("blog")) 14 - .filter( 15 - (post) => 16 - import.meta.env.ENVIRONMENT === "preview" || 17 - Boolean(post.data.isPublished), 18 - ) 19 - .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); 7 + .filter( 8 + (post) => 9 + import.meta.env.ENVIRONMENT === "preview" || 10 + Boolean(post.data.isPublished) 11 + ) 12 + .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); 20 13 --- 21 14 22 15 <!doctype html> 23 16 <html lang="en"> 24 - <head> 25 - <Head title={SITE_TITLE} description={SITE_DESCRIPTION} /> 26 - <style> 27 - main > section { 28 - width: 100%; 29 - } 17 + <head> 18 + <Head title={SITE_TITLE} description={SITE_DESCRIPTION} /> 19 + <style> 20 + main > section { 21 + width: 100%; 22 + } 30 23 31 - main > section > ul { 32 - display: flex; 33 - flex-direction: column; 34 - gap: 2rem; 35 - list-style: none; 36 - padding: 0; 37 - } 38 - </style> 39 - </head> 40 - <body data-theme="dark"> 41 - <Navigation /> 42 - <main> 43 - <h1>Blog</h1> 44 - <section> 45 - { 46 - posts.length !== 0 && ( 47 - <ul> 48 - {posts.map((post) => ( 49 - <li> 50 - <BlogPreviewCard 51 - title={post.data.title} 52 - description={post.data.description} 53 - image={post.data.image} 54 - date={post.data.date} 55 - updatedDate={post.data.updatedDate} 56 - /> 57 - </li> 58 - ))} 59 - </ul> 60 - ) 61 - } 62 - {posts.length === 0 && <h2>There are no posts yet! 😢</h2>} 63 - </section> 64 - </main> 65 - <Footer /> 66 - <SpeedInsights /> 67 - <Analytics /> 68 - </body> 24 + main > section > ul { 25 + display: flex; 26 + flex-direction: column; 27 + gap: 2rem; 28 + list-style: none; 29 + padding: 0; 30 + } 31 + </style> 32 + </head> 33 + <body data-theme="dark"> 34 + <Navigation /> 35 + <main> 36 + <h1>Blog</h1> 37 + <section> 38 + { 39 + posts.length !== 0 && ( 40 + <ul> 41 + {posts.map((post) => ( 42 + <li> 43 + <BlogPreviewCard 44 + title={post.data.title} 45 + description={post.data.description} 46 + image={post.data.image} 47 + date={post.data.date} 48 + updatedDate={post.data.updatedDate} 49 + /> 50 + </li> 51 + ))} 52 + </ul> 53 + ) 54 + } 55 + {posts.length === 0 && <h2>There are no posts yet! 😢</h2>} 56 + </section> 57 + </main> 58 + <Footer /> 59 + </body> 69 60 </html>
+31 -44
src/pages/index.astro
··· 1 1 --- 2 - import { Head, Footer, Navigation } from "@/components/organisms"; 2 + import { Head, Footer, Navigation } from "@/components"; 3 3 import { SITE_TITLE, SITE_DESCRIPTION } from "@/consts"; 4 4 import { Image } from "astro:assets"; 5 - import SpeedInsights from "@vercel/speed-insights/astro"; 6 - import Analytics from "@vercel/analytics/astro"; 7 5 import getBlueskyAvatar from "@/utils/getBlueskyAvatar"; 8 6 9 7 const blueskyAvatar = await getBlueskyAvatar(); ··· 11 9 12 10 <!doctype html> 13 11 <html lang="en"> 14 - <head> 15 - <Head 16 - title={SITE_TITLE} 17 - description={SITE_DESCRIPTION} 18 - image={{ 19 - src: blueskyAvatar, 20 - width: 300, 21 - height: 300, 22 - format: "webp", 23 - }} 24 - /> 25 - <style> 26 - body > main > img { 27 - width: clamp(200px, 100vw, 300px); 28 - } 29 - </style> 30 - </head> 31 - <body data-theme="dark"> 32 - <Navigation /> 33 - <main> 34 - <Image 35 - src={blueskyAvatar} 36 - alt="Profile of Clay, a cow fursona." 37 - width={300} 38 - height={300} 39 - quality={"mid"} 40 - loading={"lazy"} 41 - /> 42 - <h1>🌾 Hello, I'm Clay 🐄</h1> 43 - <p> 44 - I'm a cow furry who's also a software engineer, specifically in 45 - frontend development. Some of my main interests include creating 46 - applications, shaders, algorithmic art and discussing current 47 - social issues. This website is a place for me to aggregate and 48 - fully express my viewpoints through blogging. 49 - </p> 50 - </main> 51 - <Footer /> 52 - <SpeedInsights /> 53 - <Analytics /> 54 - </body> 12 + <head> 13 + <Head title={SITE_TITLE} description={SITE_DESCRIPTION} /> 14 + <style> 15 + body > main > img { 16 + width: clamp(200px, 100vw, 300px); 17 + } 18 + </style> 19 + </head> 20 + <body data-theme="dark"> 21 + <Navigation /> 22 + <main> 23 + <Image 24 + src={blueskyAvatar} 25 + alt="Profile of Clay, a cow fursona." 26 + width={300} 27 + height={300} 28 + quality={"mid"} 29 + loading={"lazy"} 30 + /> 31 + <h1>🌾 Hello, I'm Clay 🐄</h1> 32 + <p> 33 + I'm a cow furry who's also a software engineer, specifically in frontend 34 + development. Some of my main interests include creating applications, 35 + shaders, algorithmic art and discussing current social issues. This 36 + website is a place for me to aggregate and fully express my viewpoints 37 + through blogging. 38 + </p> 39 + </main> 40 + <Footer /> 41 + </body> 55 42 </html>
+20 -4
src/utils/getBlogPostComments.ts
··· 32 32 return data.thread.replies.map((reply) => { 33 33 if (!isThreadViewPost(reply)) return null; 34 34 35 + const record = reply.post.record as AppBskyFeedPost.Record; 36 + 37 + const postId = reply.post.uri.split('/').at(-1); 38 + 39 + if (!postId) return null; 40 + 35 41 return { 36 - ...reply.post, 37 - record: { 38 - ...reply.post.record as AppBskyFeedPost.Record 39 - } 42 + likeCount: reply.post.likeCount, 43 + replyCount: reply.post.replyCount, 44 + repostCount: reply.post.repostCount, 45 + displayName: reply.post.author.displayName, 46 + handle: reply.post.author.handle, 47 + createdAt: record.createdAt, 48 + text: record.text, 49 + authorUrl: `https://bsky.app/profile/${reply.post.author.did}`, 50 + postUrl: `https://bsky.app/profile/${reply.post.author.did}/post/${postId}`, 51 + cid: reply.post.cid 52 + // ...reply.post, 53 + // record: { 54 + // ...reply.post.record as AppBskyFeedPost.Record 55 + // } 40 56 }; 41 57 }).filter(reply => reply !== null); 42 58 })