diff --git a/astro.config.mjs b/astro.config.mjs index e27d692..03e1391 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,14 +1,21 @@ import { defineConfig } from "astro/config"; import prefetch from "@astrojs/prefetch"; +import sitemap from "@astrojs/sitemap"; import tailwindcss from "@tailwindcss/vite"; // https://astro.build/config export default defineConfig({ + site: "https://t3.gg", + image: { + // Apply responsive image defaults to Markdown content. + layout: "constrained", + domains: ["t3.gg", "img.youtube.com"], + }, vite: { plugins: [tailwindcss()], }, plugins: [], - integrations: [prefetch()], + integrations: [prefetch(), sitemap()], redirects: { "/links": "/", "/faq": "/", diff --git a/bun.lock b/bun.lock index 56dbc98..c08bae0 100644 --- a/bun.lock +++ b/bun.lock @@ -5,19 +5,21 @@ "": { "name": "astro-t3", "dependencies": { - "@astrojs/prefetch": "latest", - "date-fns": "latest", + "@astrojs/prefetch": "^0.4.1", + "@astrojs/rss": "^4.0.15", + "@astrojs/sitemap": "^3.7.0", + "date-fns": "^4.1.0", }, "devDependencies": { - "@tailwindcss/typography": "latest", - "@tailwindcss/vite": "latest", - "astro": "latest", - "prettier": "latest", - "prettier-plugin-astro": "latest", - "prettier-plugin-tailwindcss": "latest", - "tailwind-merge": "latest", - "tailwindcss": "latest", - "tailwindcss-safe-area": "latest", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.1.18", + "astro": "5.16.9", + "prettier": "^3.7.4", + "prettier-plugin-astro": "0.14.1", + "prettier-plugin-tailwindcss": "0.7.2", + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.1.18", + "tailwindcss-safe-area": "^1.3.0", }, }, }, @@ -32,6 +34,10 @@ "@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="], + "@astrojs/rss": ["@astrojs/rss@4.0.15", "", { "dependencies": { "fast-xml-parser": "^5.3.3", "piccolore": "^0.1.3" } }, "sha512-uXO/k6AhRkIDXmRoc6xQpoPZrimQNUmS43X4+60yunfuMNHtSRN5e/FiSi7NApcZqmugSMc5+cJi8ovqgO+qIg=="], + + "@astrojs/sitemap": ["@astrojs/sitemap@3.7.0", "", { "dependencies": { "sitemap": "^8.0.2", "stream-replace-string": "^2.0.0", "zod": "^3.25.76" } }, "sha512-+qxjUrz6Jcgh+D5VE1gKUJTA3pSthuPHe6Ao5JCxok794Lewx8hBFaWHtOnN0ntb2lfOf7gvOi9TefUswQ/ZVA=="], + "@astrojs/telemetry": ["@astrojs/telemetry@3.3.0", "", { "dependencies": { "ci-info": "^4.2.0", "debug": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^3.0.0", "is-wsl": "^3.1.0", "which-pm-runs": "^1.1.0" } }, "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], @@ -266,6 +272,8 @@ "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], @@ -280,6 +288,8 @@ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], @@ -392,6 +402,8 @@ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fast-xml-parser": ["fast-xml-parser@5.3.4", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="], @@ -686,18 +698,24 @@ "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "sitemap": ["sitemap@8.0.2", "", { "dependencies": { "@types/node": "^17.0.5", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.4.1" }, "bin": { "sitemap": "dist/cli.js" } }, "sha512-LwktpJcyZDoa0IL6KT++lQ53pbSrx2c9ge41/SeLTyqy2XUNA6uR4+P9u5IVo5lPeL2arAcOKn1aZAxoYbCKlQ=="], + "smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + "suf-log": ["suf-log@2.5.3", "", { "dependencies": { "s.color": "0.0.15" } }, "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow=="], "svgo": ["svgo@4.0.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.4.1" }, "bin": "./bin/svgo.js" }, "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw=="], @@ -824,6 +842,8 @@ "hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], + "sitemap/@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="], + "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], diff --git a/package.json b/package.json index 11020c0..183c488 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ }, "dependencies": { "@astrojs/prefetch": "^0.4.1", + "@astrojs/rss": "^4.0.15", + "@astrojs/sitemap": "^3.7.0", "date-fns": "^4.1.0" } } diff --git a/src/components/BlogImage.astro b/src/components/BlogImage.astro new file mode 100644 index 0000000..07a8631 --- /dev/null +++ b/src/components/BlogImage.astro @@ -0,0 +1,49 @@ +--- +import { Picture } from "astro:assets"; +import type { ImageMetadata } from "astro"; + +interface Props { + src: ImageMetadata | string; + alt: string; + class?: string; + sizes?: string; + widths?: number[]; + priority?: boolean; +} + +const { + src, + alt, + class: className, + sizes = "(min-width: 768px) 768px, 100vw", + widths = [480, 768, 1024, 1280, 1600], + priority = false, +} = Astro.props; + +const isLocalImage = typeof src !== "string"; +const loading = priority ? "eager" : "lazy"; +--- + +{ + isLocalImage ? ( + + ) : ( + {alt} + ) +} + diff --git a/src/components/BlogPostHeader.astro b/src/components/BlogPostHeader.astro index 047bdf1..b29752b 100644 --- a/src/components/BlogPostHeader.astro +++ b/src/components/BlogPostHeader.astro @@ -1,19 +1,38 @@ --- +import type { CollectionEntry } from "astro:content"; +import { format } from "date-fns"; +import BlogImage from "./BlogImage.astro"; + export interface Props { - post: { title: string; date: string }; + post: CollectionEntry<"posts">; } -import { parseISO, format } from "date-fns"; - const { post } = Astro.props; -const date = parseISO(post.date); +const date = post.data.date; const formatted = format(date, "LLLL d, yyyy"); +const coverImage = post.data.cover ?? post.data.imageURL; +const coverAlt = post.data.coverAlt ?? post.data.title; ---

- {post.title} + {post.data.title}

+ {post.data.archived ? ( +
+ This post was archived manually by Theo. It is probably out of date or something. +
+ ) : null} + {coverImage ? ( +
+ +
+ ) : null}
diff --git a/src/components/BlogPostPreview.astro b/src/components/BlogPostPreview.astro index 18e2636..023965c 100644 --- a/src/components/BlogPostPreview.astro +++ b/src/components/BlogPostPreview.astro @@ -1,15 +1,15 @@ --- +import type { CollectionEntry } from "astro:content"; +import { format } from "date-fns"; + export interface Props { - post: any; + post: CollectionEntry<"posts">; } -import { getSlugFromPath } from "../utils/get-slug-from-path"; -import { parseISO, format } from "date-fns"; - const { post } = Astro.props; -const url = "/blog/post/" + getSlugFromPath(post.file); +const url = `/blog/post/${post.id}`; -const date = parseISO(post.frontmatter.date); +const date = post.data.date; const formattedDate = format(date, "MMM d, yyyy"); --- @@ -18,7 +18,7 @@ const formattedDate = format(date, "MMM d, yyyy"); class="group relative flex items-baseline justify-between gap-4 py-3" > - {post.frontmatter.title} + {post.data.title} {formattedDate} { + // Prefer an explicit, canonical slug so folders can move freely + // without breaking old links. + const explicitSlug = + typeof data.slug === "string" && data.slug.trim().length > 0 + ? data.slug.trim() + : undefined; + if (explicitSlug) return explicitSlug; + + // Fall back to the entry path (minus the index file). + return entry.replace(/\/index\.md$/, ""); + }, + }), + schema: ({ image }) => + z.object({ + title: z.string(), + description: z.string().optional(), + date: z.coerce.date(), + updated: z.coerce.date().optional(), + tags: z.array(z.string()).optional(), + // Canonical slug used for stable URLs. + slug: z.string().optional(), + // Optional legacy slugs that should redirect to the canonical slug. + slugAliases: z.array(z.string()).optional(), + draft: z.boolean().default(false), + hidden: z.boolean().default(false), + archived: z.boolean().default(false), + readMore: z.boolean().optional(), + cover: image().optional(), + coverAlt: z.string().optional(), + imageURL: z.string().url().optional(), + canonicalUrl: z.string().url().optional(), + }), +}); + +export const collections = { + posts, +}; diff --git a/src/content/posts/README.md b/src/content/posts/README.md new file mode 100644 index 0000000..8d2d6b7 --- /dev/null +++ b/src/content/posts/README.md @@ -0,0 +1,37 @@ +# Blog Post Organization + +This folder is organized for editing ergonomics without breaking links. + +## Structure + +- Active posts: `src/content/posts/active///index.md` +- Archived posts: `src/content/posts/archive///index.md` + +## Canonical Slug (Important) + +Each post should include a `slug` field in frontmatter: + +```md +--- +title: "My Post" +slug: "my-post" +date: "2026-02-02" +--- +``` + +The `slug` is the canonical URL segment used at `/blog/post/`. +Because the slug is explicit, you can move posts between folders freely +without breaking old links. + +## Optional Aliases + +If you ever rename a slug, you can keep old URLs working: + +```md +slug: "new-slug" +slugAliases: + - "old-slug" +``` + +Aliases will redirect to the canonical slug. + diff --git a/public/images/airpods-max/miles.jpg b/src/content/posts/active/2020/airpods-max/images/miles.jpg similarity index 100% rename from public/images/airpods-max/miles.jpg rename to src/content/posts/active/2020/airpods-max/images/miles.jpg diff --git a/src/content/posts/airpods-max.md b/src/content/posts/active/2020/airpods-max/index.md similarity index 98% rename from src/content/posts/airpods-max.md rename to src/content/posts/active/2020/airpods-max/index.md index f6b800f..c598f47 100644 --- a/src/content/posts/airpods-max.md +++ b/src/content/posts/active/2020/airpods-max/index.md @@ -1,12 +1,14 @@ --- title: "AirPods Max Review" +slug: "airpods-max" date: "2020-12-22" description: "An Audiophile who happens to love Apple comes to terms with this terrible product" -imageURL: "https://t3.gg/images/airpods-max/miles.jpg" +cover: ./images/miles.jpg +coverAlt: "Cat wearing AirPods Max" readMore: true --- -![Cat wearing airpods max](https://t3.gg/images/airpods-max/miles.jpg) +![Cat wearing airpods max](./images/miles.jpg) #### No Magic Here diff --git a/public/images/lighthouse-scores.png b/src/content/posts/active/2020/welcome/images/lighthouse-scores.png similarity index 100% rename from public/images/lighthouse-scores.png rename to src/content/posts/active/2020/welcome/images/lighthouse-scores.png diff --git a/src/content/posts/welcome.md b/src/content/posts/active/2020/welcome/index.md similarity index 82% rename from src/content/posts/welcome.md rename to src/content/posts/active/2020/welcome/index.md index 6541eff..0e2c1dc 100644 --- a/src/content/posts/welcome.md +++ b/src/content/posts/active/2020/welcome/index.md @@ -1,6 +1,10 @@ --- title: "Hello World" +slug: "welcome" date: "2020-12-21" +description: "Kicking off the blog and the stack behind it." +cover: ./images/lighthouse-scores.png +coverAlt: "Lighthouse performance scores" --- This is long, long overdue. @@ -11,7 +15,7 @@ This blog is also built on things I'm nerdy about. My personal site was previous tl;dr on the tech - [Next.js](https://nextjs.org/) rewrite deployed on [Netlify](https://netlify.com) -It's nothing too fancy. Still pumped about the [lighthouse score tho](/images/lighthouse-scores.png) +It's nothing too fancy. Still pumped about the [lighthouse score tho](./images/lighthouse-scores.png) I'll likely write more on the tech itself in the future - but for now, know I'm loving this stack and highly recommend it. diff --git a/src/content/posts/quitting.md b/src/content/posts/active/2021/quitting/index.md similarity index 99% rename from src/content/posts/quitting.md rename to src/content/posts/active/2021/quitting/index.md index 4d1805c..8112371 100644 --- a/src/content/posts/quitting.md +++ b/src/content/posts/active/2021/quitting/index.md @@ -1,5 +1,6 @@ --- title: "Quitting Your Dream Job (Twice)" +slug: "quitting" date: "2021-09-02" description: "Quitting is never easy. Building things can be. I'm leaving TTFM Labs to start a company." readMore: true diff --git a/public/images/next-typesafety/drake.jpeg b/src/content/posts/active/2021/types-and-nextjs/images/drake.jpeg similarity index 100% rename from public/images/next-typesafety/drake.jpeg rename to src/content/posts/active/2021/types-and-nextjs/images/drake.jpeg diff --git a/public/images/next-typesafety/infer-props-fail-1.png b/src/content/posts/active/2021/types-and-nextjs/images/infer-props-fail-1.png similarity index 100% rename from public/images/next-typesafety/infer-props-fail-1.png rename to src/content/posts/active/2021/types-and-nextjs/images/infer-props-fail-1.png diff --git a/public/images/next-typesafety/twitter.png b/src/content/posts/active/2021/types-and-nextjs/images/twitter.png similarity index 100% rename from public/images/next-typesafety/twitter.png rename to src/content/posts/active/2021/types-and-nextjs/images/twitter.png diff --git a/src/content/posts/types-and-nextjs.md b/src/content/posts/active/2021/types-and-nextjs/index.md similarity index 98% rename from src/content/posts/types-and-nextjs.md rename to src/content/posts/active/2021/types-and-nextjs/index.md index a55b38b..a7352f4 100644 --- a/src/content/posts/types-and-nextjs.md +++ b/src/content/posts/active/2021/types-and-nextjs/index.md @@ -1,8 +1,10 @@ --- title: "An Inconsistent Truth: Next.js and Type Safety" +slug: "types-and-nextjs" date: "2021-12-02" description: "Next.js is a great framework. The frontend and backend have never been closer. Can we bridge the remaining gap in a typesafe way?" -imageURL: "https://t3.gg/images/next-typesafety/twitter.png" +cover: ./images/twitter.png +coverAlt: "Next.js type safety graphic" readMore: true --- @@ -49,7 +51,7 @@ _But how??!_ ### Type Inference - +![Type inference drake meme](./images/drake.jpeg) Credits to [Alex for this fantastic meme](https://twitter.com/alexdotjs/status/1465975816370728963) @@ -186,7 +188,7 @@ The goal here is to use the types of your `getServerSideProps` function as a sou As happy I am to know this exists, I've already ran into some painful edges with Next's provided `InferGetServerSidePropsType` -- It [overrides inferable types](https://t3.gg/images/next-typesafety/infer-props-fail-1.png) as `{[key: string]: any}` generic objects if you cast function as `GetServerSideProps` w/o also manually assigning a type +- It [overrides inferable types](./images/infer-props-fail-1.png) as `{[key: string]: any}` generic objects if you cast function as `GetServerSideProps` w/o also manually assigning a type - It infers to `props: never` if you [don't specify input types as it expects](https://github.com/vercel/next.js/issues/15913) To use this correctly, I had to have decent familiarity with Next's internal typings and read through [this GitHub issue thoroughly](https://github.com/vercel/next.js/issues/15913). Even with that prerequisite, I found it shockingly easy to accidentally return a non-implicit `any` type, which _does not throw any errors under the provided Next.js `tsconfig`_ . diff --git a/public/images/announcing-ping/twitter.png b/src/content/posts/active/2022/announcing-ping/images/twitter.png similarity index 100% rename from public/images/announcing-ping/twitter.png rename to src/content/posts/active/2022/announcing-ping/images/twitter.png diff --git a/src/content/posts/announcing-ping.md b/src/content/posts/active/2022/announcing-ping/index.md similarity index 95% rename from src/content/posts/announcing-ping.md rename to src/content/posts/active/2022/announcing-ping/index.md index de31986..a920be6 100644 --- a/src/content/posts/announcing-ping.md +++ b/src/content/posts/active/2022/announcing-ping/index.md @@ -1,8 +1,10 @@ --- title: "Why We're Building Ping" +slug: "announcing-ping" date: "2022-01-11" description: "Streaming with your friends is way too difficult. We're going to fix it. What if Zoom was built for streamers? 🤔" -imageURL: "https://t3.gg/images/announcing-ping/twitter.png" +cover: ./images/twitter.png +coverAlt: "Ping announcement graphic" readMore: true --- @@ -18,7 +20,7 @@ After collective decades of dealing with these problems, we’re building soluti Introducing our first creator-focused tool, [Ping](https://ping.t3.gg). -[Ping brand](https://ping.t3.gg) +[![Ping brand](./images/twitter.png)](https://ping.t3.gg) Have you ever tried streaming with your friends before? If you have, then you know it’s one of the most painful processes in content creation. Interaction is the life blood of live content, and the tools to interact with your friends live are rough. diff --git a/public/images/ping-1.0.png b/src/content/posts/active/2022/dream-job-revisited/images/ping-1.0.png similarity index 100% rename from public/images/ping-1.0.png rename to src/content/posts/active/2022/dream-job-revisited/images/ping-1.0.png diff --git a/src/content/posts/dream-job-revisited.md b/src/content/posts/active/2022/dream-job-revisited/index.md similarity index 96% rename from src/content/posts/dream-job-revisited.md rename to src/content/posts/active/2022/dream-job-revisited/index.md index 19f6518..0dae64d 100644 --- a/src/content/posts/dream-job-revisited.md +++ b/src/content/posts/active/2022/dream-job-revisited/index.md @@ -1,8 +1,10 @@ --- title: "I'm So Happy I Quit My Dream Job" +slug: "dream-job-revisited" date: "2022-06-30" description: "It's been a wild 9 months." -imageURL: "https://t3.gg/images/ping-1.0.png" +cover: ./images/ping-1.0.png +coverAlt: "Ping 1.0 branding" readMore: true --- @@ -73,7 +75,7 @@ We can. ## Ping.gg 1.0 Launches Today, Starting at $16 -
Ping brand
+[![Ping brand](./images/ping-1.0.png)](https://ping.gg) We’re so hyped to announce that Ping has **OFFICIALLY LAUNCHED** as of today. diff --git a/public/images/ssr-expensive.jpg b/src/content/posts/active/2024/ssr-is-not-expensive/images/ssr-expensive.jpg similarity index 100% rename from public/images/ssr-expensive.jpg rename to src/content/posts/active/2024/ssr-is-not-expensive/images/ssr-expensive.jpg diff --git a/src/content/posts/ssr-is-not-expensive.md b/src/content/posts/active/2024/ssr-is-not-expensive/index.md similarity index 97% rename from src/content/posts/ssr-is-not-expensive.md rename to src/content/posts/active/2024/ssr-is-not-expensive/index.md index 5fd71f4..093ad05 100644 --- a/src/content/posts/ssr-is-not-expensive.md +++ b/src/content/posts/active/2024/ssr-is-not-expensive/index.md @@ -1,8 +1,10 @@ --- title: "Debunking the Myth: SSR Isn't Expensive" +slug: "ssr-is-not-expensive" date: "2024-12-10" description: "SSR is not expensive. It's actually a great way to save money and improve the user experience." -imageURL: "https://t3.gg/images/ssr-expensive.jpg" +cover: ./images/ssr-expensive.jpg +coverAlt: "SSR is not expensive cover image" readMore: true --- diff --git a/public/images/vite-vercel/twitter.png b/src/content/posts/archive/2021/vite-vercel/images/twitter.png similarity index 100% rename from public/images/vite-vercel/twitter.png rename to src/content/posts/archive/2021/vite-vercel/images/twitter.png diff --git a/public/images/vite-vercel/vercel-config.png b/src/content/posts/archive/2021/vite-vercel/images/vercel-config.png similarity index 100% rename from public/images/vite-vercel/vercel-config.png rename to src/content/posts/archive/2021/vite-vercel/images/vercel-config.png diff --git a/public/images/vite-vercel/vercel-dev-config.png b/src/content/posts/archive/2021/vite-vercel/images/vercel-dev-config.png similarity index 100% rename from public/images/vite-vercel/vercel-dev-config.png rename to src/content/posts/archive/2021/vite-vercel/images/vercel-dev-config.png diff --git a/src/content/posts/vite-vercel.md b/src/content/posts/archive/2021/vite-vercel/index.md similarity index 95% rename from src/content/posts/vite-vercel.md rename to src/content/posts/archive/2021/vite-vercel/index.md index 9578bf0..973296f 100644 --- a/src/content/posts/vite-vercel.md +++ b/src/content/posts/archive/2021/vite-vercel/index.md @@ -1,12 +1,15 @@ --- title: "Using Vite On Vercel (Semi-Outdated)" +slug: "vite-vercel" date: "2021-06-14" description: "Vite and Vercel are fun new technologies that sadly don't play nice. This is a guide on how to work around that" -imageURL: "https://t3.gg/images/vite-vercel/twitter.png" +cover: ./images/twitter.png +coverAlt: "Vite and Vercel graphic" readMore: true +archived: true --- -![Vite + Vercel](https://t3.gg/images/vite-vercel/twitter.png) +![Vite + Vercel](./images/twitter.png) ### UPDATE (4/17/22) @@ -63,7 +66,7 @@ By default, Vercel tries to resolve all requests to a file at the path. Works gr Once this is added, you can go to [Vercel.com](https://vercel.com) and create a new project. For framework, select "other". For "Output directory", override the default with `dist` -![Vercel Config](/images/vite-vercel/vercel-config.png) +![Vercel Config](./images/vercel-config.png) Click "deploy" and you should be live in no time! @@ -83,7 +86,7 @@ First, we have to modify our Vercel project settings once more to point it at a Toggle "override" for "DEVELOPMENT COMMAND" and set it to `npm run {vercel-special-command-name}`. -![Vercel dev special config](/images/vite-vercel/vercel-dev-config.png) +![Vercel dev special config](./images/vercel-dev-config.png) The following is weird enough that I [stubbed out a commit with all the related changes to make it easier to apply to your project](https://github.com/TheoBr/vercel-vite-demo/commit/8fa15f3b4bfed02019a80fc68845a1e68ef5e196) diff --git a/src/content/posts/2023-infra.md b/src/content/posts/archive/2023/2023-infra/index.md similarity index 98% rename from src/content/posts/2023-infra.md rename to src/content/posts/archive/2023/2023-infra/index.md index 56ec38c..3488dc3 100644 --- a/src/content/posts/2023-infra.md +++ b/src/content/posts/archive/2023/2023-infra/index.md @@ -1,9 +1,11 @@ --- title: "Infra I'm Building On In 2023" +slug: "2023-infra" date: "2023-03-06" description: "You can never be better than your infrastructure" imageURL: "https://img.youtube.com/vi/G3rJC799L2Q/maxresdefault.jpg" readMore: true +archived: true --- The products you build can never be better than the infrastructure you build them on. I have tried a LOT of different infra, and I'm really happy with what I've landed on. diff --git a/src/content/posts/2023-tech.md b/src/content/posts/archive/2023/2023-tech/index.md similarity index 99% rename from src/content/posts/2023-tech.md rename to src/content/posts/archive/2023/2023-tech/index.md index 3e442f1..3aff227 100644 --- a/src/content/posts/2023-tech.md +++ b/src/content/posts/archive/2023/2023-tech/index.md @@ -1,9 +1,11 @@ --- title: "Tech I'm Shipping In 2023" +slug: "2023-tech" date: "2023-03-02" description: "Picking the best tools for the job" imageURL: "https://img.youtube.com/vi/ODJkKJUnKtM/maxresdefault.jpg" readMore: true +archived: true --- The quality of the tools you work with is important. I spend...too much time thinking about this. diff --git a/src/content/posts/2023-tools.md b/src/content/posts/archive/2023/2023-tools/index.md similarity index 96% rename from src/content/posts/2023-tools.md rename to src/content/posts/archive/2023/2023-tools/index.md index cd23cc2..ae19c98 100644 --- a/src/content/posts/2023-tools.md +++ b/src/content/posts/archive/2023/2023-tools/index.md @@ -1,9 +1,11 @@ --- title: "Software I Rely On In 2023" +slug: "2023-tools" date: "2023-03-03" description: "The software you use is important!" imageURL: "https://img.youtube.com/vi/uaCypXEJjes/maxresdefault.jpg" readMore: true +archived: true --- diff --git a/public/images/equinusocio-danger.jpg b/src/content/posts/archive/2025/equinusocio/images/equinusocio-danger.jpg similarity index 100% rename from public/images/equinusocio-danger.jpg rename to src/content/posts/archive/2025/equinusocio/images/equinusocio-danger.jpg diff --git a/src/content/posts/equinusocio.md b/src/content/posts/archive/2025/equinusocio/index.md similarity index 96% rename from src/content/posts/equinusocio.md rename to src/content/posts/archive/2025/equinusocio/index.md index 37604b9..5b2cb87 100644 --- a/src/content/posts/equinusocio.md +++ b/src/content/posts/archive/2025/equinusocio/index.md @@ -1,9 +1,12 @@ --- title: "Mattia Astorino (equinusocio) Is Dangerous" +slug: "equinusocio" date: "2025-02-26" description: "Mattia is the creator of Material Theme for VS Code. He is also insane." -imageURL: "https://t3.gg/images/equinusocio-danger.jpg" +cover: ./images/equinusocio-danger.jpg +coverAlt: "Material Theme warning graphic" readMore: true +archived: true --- Remember the Material Theme for VS Code? It is being managed so poorly that the result is indistinguishable from malware, and Mattia Astorino (aka equinusocio) is to blame. diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index ef576a5..a68529d 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -3,6 +3,8 @@ import { cn } from "../utils/cn"; import "../style/global.css"; import { ClientRouter } from "astro:transitions"; import LeftArrow from "../assets/icons/left-arrow.svg.astro"; +import { getImage } from "astro:assets"; +import type { ImageMetadata } from "astro"; const socialLinks = [ { href: "https://github.com/t3dotgg", label: "gh" }, @@ -14,6 +16,12 @@ const socialLinks = [ interface Props { title: string; description: string; + canonicalUrl?: string; + ogImage?: ImageMetadata | string; + ogImageAlt?: string; + ogType?: "website" | "article"; + publishedTime?: Date; + updatedTime?: Date; classNames?: { html?: string; body?: string; @@ -36,10 +44,40 @@ const defaultBackToLink: Props["backToLink"] = { const { title, description, + canonicalUrl, + ogImage, + ogImageAlt, + ogType = "website", + publishedTime, + updatedTime, classNames, backToLink = defaultBackToLink, } = Astro.props; const isHomepage = Astro.url.pathname === "/"; + +const siteUrl = Astro.site ?? Astro.url; +const canonicalHref = + canonicalUrl ?? new URL(Astro.url.pathname, siteUrl).toString(); + +const defaultOgImagePath = "/images/twitter.png"; +let resolvedOgImageAlt = ogImageAlt ?? title; +let resolvedOgImageHref = new URL(defaultOgImagePath, siteUrl).toString(); + +if (ogImage) { + if (typeof ogImage === "string") { + resolvedOgImageHref = ogImage.startsWith("http") + ? ogImage + : new URL(ogImage, siteUrl).toString(); + } else { + const generatedOgImage = await getImage({ + src: ogImage, + width: 1200, + height: 630, + format: "png", + }); + resolvedOgImageHref = new URL(generatedOgImage.src, siteUrl).toString(); + } +} --- - + + + + - + + + + + { + publishedTime && ( + + ) + } + { + updatedTime && ( + + ) + } {title} - + !post.data.hidden && !post.data.draft && post.data.archived) + .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); + +const backToLink = { + text: "blog", + href: "/blog", +}; +--- + + +
+
+

archived posts

+

+ these posts keep their URLs but no longer show up in the main feed. +

+
+ + {archivedPosts.length === 0 ? ( +

no archived posts yet.

+ ) : ( +
+ {archivedPosts.map((post) => ( + + ))} +
+ )} +
+
+ diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro index cbbf8d4..b62348f 100644 --- a/src/pages/blog/index.astro +++ b/src/pages/blog/index.astro @@ -2,18 +2,15 @@ import "../../style/global.css"; import BlogPostPreview from "../../components/BlogPostPreview.astro"; import BaseLayout from "../../layouts/BaseLayout.astro"; +import { getCollection } from "astro:content"; const siteTitle = "blog"; const description = "Thought dumps and nerdy stuff"; -const allPosts = await Astro.glob("../../content/posts/*.md"); +const allPosts = await getCollection("posts"); const sortedPosts = allPosts - .sort( - (a, b) => - new Date(b.frontmatter.date).valueOf() - - new Date(a.frontmatter.date).valueOf(), - ) - .filter((p) => p.frontmatter.hidden !== true); + .filter((post) => !post.data.hidden && !post.data.draft && !post.data.archived) + .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); --- @@ -23,6 +20,18 @@ const sortedPosts = allPosts

thoughts on tech and content. usually goes to twitter, sometimes here.

+ + rss + + + archive +
diff --git a/src/pages/blog/post/[slug].astro b/src/pages/blog/post/[slug].astro index c2bf082..9200412 100644 --- a/src/pages/blog/post/[slug].astro +++ b/src/pages/blog/post/[slug].astro @@ -1,27 +1,60 @@ --- import BaseLayout from "../../../layouts/BaseLayout.astro"; import BlogPostHeader from "../../../components/BlogPostHeader.astro"; -import { getSlugFromPath } from "../../../utils/get-slug-from-path"; +import { getCollection, render } from "astro:content"; -export async function getStaticPaths({ paginate, rss }) { - const allPosts = await Astro.glob("../../../content/posts/*.md"); +export async function getStaticPaths() { + const posts = await getCollection( + "posts", + ({ data }) => !data.hidden && !data.draft, + ); - const paths = allPosts.map((post) => { - const slug = getSlugFromPath(post.file); - return { - params: { - slug, - }, - props: { post }, - }; - }); + const paths: Array<{ + params: { slug: string }; + props: { post: (typeof posts)[number]; canonicalSlug: string; isAlias: boolean }; + }> = []; + + for (const post of posts) { + const canonicalSlug = post.id; + + paths.push({ + params: { slug: canonicalSlug }, + props: { post, canonicalSlug, isAlias: false }, + }); + + const aliases = post.data.slugAliases ?? []; + for (const alias of aliases) { + if (!alias || alias === canonicalSlug) continue; + paths.push({ + params: { slug: alias }, + props: { post, canonicalSlug, isAlias: true }, + }); + } + } return paths; } -const { post } = Astro.props; -const title = post.frontmatter.title; -const description = post.frontmatter.description; +const { post, canonicalSlug, isAlias } = Astro.props; +const { Content } = await render(post); + +const title = post.data.title; + +const fallbackDescription = post.body + .replace(/```[\s\S]*?```/g, " ") + .replace(/<[^>]+>/g, " ") + .replace(/[#*_>`~\-!\[\]\(\)]/g, " ") + .replace(/\s+/g, " ") + .trim() + .slice(0, 160); + +const description = + post.data.description ?? + fallbackDescription ?? + "Thought dumps and nerdy stuff"; + +const ogImage = post.data.cover ?? post.data.imageURL; +const canonicalPath = `/blog/post/${canonicalSlug}`; const backToLink = { text: "blog", @@ -29,11 +62,45 @@ const backToLink = { }; --- - -
-
- - -
-
-
+{ + isAlias ? ( + +
+

+ Redirecting to the canonical URL.{" "} + Continue +

+ +
+
+ ) : ( + +
+
+ + +
+
+
+ ) +} diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts new file mode 100644 index 0000000..e0294bd --- /dev/null +++ b/src/pages/rss.xml.ts @@ -0,0 +1,36 @@ +import rss from "@astrojs/rss"; +import { getCollection } from "astro:content"; + +const BLOG_DESCRIPTION = "Thought dumps and nerdy stuff"; + +const toExcerpt = (body: string): string => + body + .replace(/```[\s\S]*?```/g, " ") + .replace(/<[^>]+>/g, " ") + .replace(/[#*_>`~\-!\[\]\(\)]/g, " ") + .replace(/\s+/g, " ") + .trim() + .slice(0, 200); + +export async function GET(context: { site: URL | undefined }) { + const posts = await getCollection( + "posts", + ({ data }) => !data.hidden && !data.draft && !data.archived, + ); + + const sortedPosts = posts.sort( + (a, b) => b.data.date.valueOf() - a.data.date.valueOf(), + ); + + return rss({ + title: "Theo's Blog", + description: BLOG_DESCRIPTION, + site: context.site ?? new URL("https://t3.gg"), + items: sortedPosts.map((post) => ({ + title: post.data.title, + description: post.data.description ?? toExcerpt(post.body), + pubDate: post.data.date, + link: `/blog/post/${post.id}`, + })), + }); +} diff --git a/src/style/global.css b/src/style/global.css index 781ee6c..b6c8033 100644 --- a/src/style/global.css +++ b/src/style/global.css @@ -219,6 +219,20 @@ pre[class*="language-"] { font-style: italic; } +/* Blog prose image defaults */ +.prose :where(picture):not(:where([class~="not-prose"] *)) { + display: block; + margin-left: auto; + margin-right: auto; +} + +.prose :where(img):not(:where([class~="not-prose"] *)) { + margin-left: auto; + margin-right: auto; + border-radius: 0.75rem; + border: 1px solid color-mix(in srgb, white 8%, transparent); +} + .token.entity { cursor: help; } diff --git a/src/utils/get-slug-from-path.ts b/src/utils/get-slug-from-path.ts deleted file mode 100644 index e039d01..0000000 --- a/src/utils/get-slug-from-path.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const getSlugFromPath = (path: string): string => { - return path.split("/").at(-1).replace(".md", ""); -};