my blog https://overreacted.io

Convert blog to TypeScript with minimal types (#864)

* Convert blog to TypeScript with minimal types

- Rename all .js files in app/ to .ts/.tsx
- Add minimal type annotations using 'any' where needed
- Add Post interface for basic type safety
- Fix date arithmetic and CSS custom property types
- Cast complex plugin configurations to avoid type conflicts
- Build passes successfully with no type errors

Focuses on catching obvious bugs and improving autocomplete
without heavy type definitions as requested.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Improve TypeScript types by removing unnecessary any types

- Replace any with proper React component prop types
- Add proper MouseEvent typing for Link component
- Export and use Post interface throughout the app
- Use proper Promise<{slug: string}> for Next.js params
- Add LinkProps interface with proper typing
- Keep any only where genuinely needed (complex configs)

Better type safety while maintaining simplicity.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Clean up Link component types

- Remove separate LinkProps interface, define inline instead
- Remove [key: string]: any index signature
- Use React.ComponentProps<typeof NextLink> for proper rest props typing
- Cleaner, more explicit typing without escape hatches

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

authored by danabra.mov Claude and committed by GitHub 371f556a 87cf122a

+1 -1
app/HomeLink.js app/HomeLink.tsx
··· 26 WebkitBackgroundClip: "text", 27 color: "transparent", 28 transition: "--myColor1 0.2s ease-out, --myColor2 0.2s ease-in-out", 29 - }} 30 > 31 overreacted 32 </span>
··· 26 WebkitBackgroundClip: "text", 27 color: "transparent", 28 transition: "--myColor1 0.2s ease-out, --myColor2 0.2s ease-in-out", 29 + } as any} 30 > 31 overreacted 32 </span>
+9 -2
app/Link.js app/Link.tsx
··· 3 import { useTransition } from "react"; 4 import NextLink from "next/link"; 5 import { useRouter } from "next/navigation"; 6 7 - function isModifiedEvent(event) { 8 const eventTarget = event.currentTarget; 9 const target = eventTarget.getAttribute("target"); 10 return ( ··· 24 href, 25 target, 26 ...rest 27 - }) { 28 const router = useRouter(); 29 const [isNavigating, trackNavigation] = useTransition(); 30 if (!target && !href.startsWith("/") && !href.startsWith("#")) {
··· 3 import { useTransition } from "react"; 4 import NextLink from "next/link"; 5 import { useRouter } from "next/navigation"; 6 + import { MouseEvent } from "react"; 7 8 + function isModifiedEvent(event: MouseEvent<HTMLAnchorElement>) { 9 const eventTarget = event.currentTarget; 10 const target = eventTarget.getAttribute("target"); 11 return ( ··· 25 href, 26 target, 27 ...rest 28 + }: { 29 + className?: string; 30 + children: React.ReactNode; 31 + style?: React.CSSProperties; 32 + href: string; 33 + target?: string; 34 + } & React.ComponentProps<typeof NextLink>) { 35 const router = useRouter(); 36 const [isNavigating, trackNavigation] = useTransition(); 37 if (!target && !href.startsWith("/") && !href.startsWith("#")) {
+1 -1
app/[slug]/layout.js app/[slug]/layout.tsx
··· 1 import HomeLink from "../HomeLink"; 2 3 - export default function Layout({ children }) { 4 return ( 5 <> 6 {children}
··· 1 import HomeLink from "../HomeLink"; 2 3 + export default function Layout({ children }: { children: React.ReactNode }) { 4 return ( 5 <> 6 {children}
+4 -4
app/[slug]/mdx.js app/[slug]/mdx.ts
··· 2 import { Parser } from "acorn"; 3 import { visit } from "unist-util-visit"; 4 5 - const parser = Parser.extend(jsx()); 6 7 const lang = new Set(["js", "jsx", "javascript"]); 8 9 export function remarkMdxEvalCodeBlock() { 10 - return (tree) => { 11 - visit(tree, "code", (node, index, parent) => { 12 if (lang.has(node.lang) && node.meta === "eval") { 13 - const program = parser.parse(node.value, { 14 ecmaVersion: 2020, 15 sourceType: "module", 16 });
··· 2 import { Parser } from "acorn"; 3 import { visit } from "unist-util-visit"; 4 5 + const parser = Parser.extend(jsx.default()); 6 7 const lang = new Set(["js", "jsx", "javascript"]); 8 9 export function remarkMdxEvalCodeBlock() { 10 + return (tree: any) => { 11 + visit(tree, "code", (node: any, index: number, parent: any) => { 12 if (lang.has(node.lang) && node.meta === "eval") { 13 + const program: any = parser.parse(node.value, { 14 ecmaVersion: 2020, 15 sourceType: "module", 16 });
+8 -8
app/[slug]/page.js app/[slug]/page.tsx
··· 8 import rehypePrettyCode from "rehype-pretty-code"; 9 import rehypeSlug from "rehype-slug"; 10 import rehypeAutolinkHeadings from "rehype-autolink-headings"; 11 - import { remarkMdxEvalCodeBlock } from "./mdx.js"; 12 import overnight from "overnight/themes/Overnight-Slumber.json"; 13 import "./markdown.css"; 14 import remarkGfm from "remark-gfm"; 15 16 overnight.colors["editor.background"] = "var(--code-bg)"; 17 18 - export default async function PostPage({ params }) { 19 const { slug } = await params; 20 const filename = "./public/" + slug + "/index.md"; 21 const file = await readFile(filename, "utf8"); 22 - let postComponents = {}; 23 try { 24 postComponents = await import("../../public/" + slug + "/components.js"); 25 - } catch (e) { 26 if (!e || e.code !== "MODULE_NOT_FOUND") { 27 throw e; 28 } ··· 96 remarkSmartpants, 97 remarkGfm, 98 [remarkMdxEvalCodeBlock, filename], 99 - ], 100 rehypePlugins: [ 101 [ 102 rehypePrettyCode, ··· 115 }, 116 }, 117 ], 118 - ], 119 - }, 120 }} 121 /> 122 </Wrapper> ··· 160 return dirs.map((dir) => ({ slug: dir })); 161 } 162 163 - export async function generateMetadata({ params }) { 164 const { slug } = await params; 165 const file = await readFile("./public/" + slug + "/index.md", "utf8"); 166 let { data } = matter(file);
··· 8 import rehypePrettyCode from "rehype-pretty-code"; 9 import rehypeSlug from "rehype-slug"; 10 import rehypeAutolinkHeadings from "rehype-autolink-headings"; 11 + import { remarkMdxEvalCodeBlock } from "./mdx"; 12 import overnight from "overnight/themes/Overnight-Slumber.json"; 13 import "./markdown.css"; 14 import remarkGfm from "remark-gfm"; 15 16 overnight.colors["editor.background"] = "var(--code-bg)"; 17 18 + export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) { 19 const { slug } = await params; 20 const filename = "./public/" + slug + "/index.md"; 21 const file = await readFile(filename, "utf8"); 22 + let postComponents: any = {}; 23 try { 24 postComponents = await import("../../public/" + slug + "/components.js"); 25 + } catch (e: any) { 26 if (!e || e.code !== "MODULE_NOT_FOUND") { 27 throw e; 28 } ··· 96 remarkSmartpants, 97 remarkGfm, 98 [remarkMdxEvalCodeBlock, filename], 99 + ] as any, 100 rehypePlugins: [ 101 [ 102 rehypePrettyCode, ··· 115 }, 116 }, 117 ], 118 + ] as any, 119 + } as any, 120 }} 121 /> 122 </Wrapper> ··· 160 return dirs.map((dir) => ({ slug: dir })); 161 } 162 163 + export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) { 164 const { slug } = await params; 165 const file = await readFile("./public/" + slug + "/index.md", "utf8"); 166 let { data } = matter(file);
app/atom.xml/route.js app/atom.xml/route.ts
app/fonts.js app/fonts.ts
+2 -2
app/layout.js app/layout.tsx
··· 8 metadataBase: new URL("https://overreacted.io"), 9 }; 10 11 - const Activity = Symbol.for("react.activity"); 12 13 - export default function RootLayout({ children }) { 14 return ( 15 <html lang="en" className={serif.className}> 16 <body className="mx-auto max-w-2xl bg-[--bg] px-5 py-12 text-[--text]">
··· 8 metadataBase: new URL("https://overreacted.io"), 9 }; 10 11 + const Activity: any = Symbol.for("react.activity"); 12 13 + export default function RootLayout({ children }: { children: React.ReactNode }) { 14 return ( 15 <html lang="en" className={serif.className}> 16 <body className="mx-auto max-w-2xl bg-[--bg] px-5 py-12 text-[--text]">
app/not-found.js app/not-found.tsx
+7 -7
app/page.js app/page.tsx
··· 1 import Link from "./Link"; 2 import Color from "colorjs.io"; 3 - import { metadata, getPosts } from "./posts"; 4 import { sans } from "./fonts"; 5 6 export { metadata }; ··· 26 ); 27 } 28 29 - function PostTitle({ post }) { 30 let lightStart = new Color("lab(63 59.32 -1.47)"); 31 let lightEnd = new Color("lab(33 42.09 -43.19)"); 32 let lightRange = lightStart.range(lightEnd); ··· 34 let darkEnd = new Color("lab(78 19.97 -36.75)"); 35 let darkRange = darkStart.range(darkEnd); 36 let today = new Date(); 37 - let timeSinceFirstPost = (today - new Date(2018, 10, 30)).valueOf(); 38 - let timeSinceThisPost = (today - new Date(post.date)).valueOf(); 39 let staleness = timeSinceThisPost / timeSinceFirstPost; 40 41 return ( ··· 48 style={{ 49 "--lightLink": lightRange(staleness).toString(), 50 "--darkLink": darkRange(staleness).toString(), 51 - }} 52 > 53 {post.title} 54 </h2> 55 ); 56 } 57 58 - function PostMeta({ post }) { 59 return ( 60 <p className="text-[13px] text-gray-700 dark:text-gray-300"> 61 {new Date(post.date).toLocaleDateString("en", { ··· 67 ); 68 } 69 70 - function PostSubtitle({ post }) { 71 return <p className="mt-1">{post.spoiler}</p>; 72 }
··· 1 import Link from "./Link"; 2 import Color from "colorjs.io"; 3 + import { metadata, getPosts, Post } from "./posts"; 4 import { sans } from "./fonts"; 5 6 export { metadata }; ··· 26 ); 27 } 28 29 + function PostTitle({ post }: { post: Post }) { 30 let lightStart = new Color("lab(63 59.32 -1.47)"); 31 let lightEnd = new Color("lab(33 42.09 -43.19)"); 32 let lightRange = lightStart.range(lightEnd); ··· 34 let darkEnd = new Color("lab(78 19.97 -36.75)"); 35 let darkRange = darkStart.range(darkEnd); 36 let today = new Date(); 37 + let timeSinceFirstPost = (today.getTime() - new Date(2018, 10, 30).getTime()); 38 + let timeSinceThisPost = (today.getTime() - new Date(post.date).getTime()); 39 let staleness = timeSinceThisPost / timeSinceFirstPost; 40 41 return ( ··· 48 style={{ 49 "--lightLink": lightRange(staleness).toString(), 50 "--darkLink": darkRange(staleness).toString(), 51 + } as any} 52 > 53 {post.title} 54 </h2> 55 ); 56 } 57 58 + function PostMeta({ post }: { post: Post }) { 59 return ( 60 <p className="text-[13px] text-gray-700 dark:text-gray-300"> 61 {new Date(post.date).toLocaleDateString("en", { ··· 67 ); 68 } 69 70 + function PostSubtitle({ post }: { post: Post }) { 71 return <p className="mt-1">{post.spoiler}</p>; 72 }
+13 -4
app/posts.js app/posts.ts
··· 2 import matter from "gray-matter"; 3 import { Feed } from "feed"; 4 5 export const metadata = { 6 title: "overreacted — A blog by Dan Abramov", 7 description: "A blog by Dan Abramov", ··· 16 }, 17 }; 18 19 - export async function getPosts() { 20 const entries = await readdir("./public/", { withFileTypes: true }); 21 const dirs = entries 22 .filter((entry) => entry.isDirectory()) ··· 29 const { data } = matter(fileContent); 30 return { slug, ...data }; 31 }); 32 - posts.sort((a, b) => { 33 return Date.parse(a.date) < Date.parse(b.date) ? 1 : -1; 34 }); 35 - return posts; 36 } 37 38 export async function generateFeed() { ··· 55 title: metadata.title, 56 }; 57 58 - const feed = new Feed(feedOptions); 59 60 for (const post of posts) { 61 feed.addItem({
··· 2 import matter from "gray-matter"; 3 import { Feed } from "feed"; 4 5 + export interface Post { 6 + slug: string; 7 + title: string; 8 + date: string; 9 + spoiler: string; 10 + youtube?: string; 11 + bluesky?: string; 12 + } 13 + 14 export const metadata = { 15 title: "overreacted — A blog by Dan Abramov", 16 description: "A blog by Dan Abramov", ··· 25 }, 26 }; 27 28 + export async function getPosts(): Promise<Post[]> { 29 const entries = await readdir("./public/", { withFileTypes: true }); 30 const dirs = entries 31 .filter((entry) => entry.isDirectory()) ··· 38 const { data } = matter(fileContent); 39 return { slug, ...data }; 40 }); 41 + posts.sort((a: any, b: any) => { 42 return Date.parse(a.date) < Date.parse(b.date) ? 1 : -1; 43 }); 44 + return posts as Post[]; 45 } 46 47 export async function generateFeed() { ··· 64 title: metadata.title, 65 }; 66 67 + const feed = new Feed(feedOptions as any); 68 69 for (const post of posts) { 70 feed.addItem({
app/rss.xml/route.js app/rss.xml/route.ts