diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..bdf5683 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,18 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:github.com)", + "WebFetch(domain:resume.liukersun.com)", + "Bash(npm run dev)", + "Bash(test:*)", + "Bash(for file in makeblock.png hit.png bytedance.png xiasha.png neau.png)", + "Bash(do if [ ! -f \"$file\" ])", + "Bash(then cp me.png \"$file\")", + "Bash(fi)", + "Bash(done)", + "Bash(ls:*)", + "Bash(npm uninstall:*)", + "Bash(npx knip)" + ] + } +} diff --git a/content-collections.ts b/content-collections.ts deleted file mode 100644 index c112bb6..0000000 --- a/content-collections.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { defineCollection, defineConfig } from "@content-collections/core"; -import { compileMDX } from "@content-collections/mdx"; -import remarkGfm from "remark-gfm"; -import { z } from "zod"; -import { remarkCodeMeta } from "./src/lib/remark-code-meta"; - -const posts = defineCollection({ - name: "posts", - directory: "content", - include: "**/*.mdx", - schema: z.object({ - title: z.string(), - publishedAt: z.string(), - updatedAt: z.string().optional(), - author: z.string().optional(), - summary: z.string(), - image: z.string().optional(), - content: z.string(), - }), - transform: async (document, context) => { - const mdx = await compileMDX(context, document, { - remarkPlugins: [remarkGfm, remarkCodeMeta], - }); - return { - ...document, - mdx, - }; - }, -}); - -export default defineConfig({ - collections: [posts], -}); - diff --git a/next.config.mjs b/next.config.mjs index 0581de9..c97aab7 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,5 +1,3 @@ -import { withContentCollections } from "@content-collections/next"; - /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, @@ -30,5 +28,4 @@ const nextConfig = { }, }; -// withContentCollections must be the outermost plugin -export default withContentCollections(nextConfig); +export default nextConfig; diff --git a/package.json b/package.json index 04f1848..74dacfc 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "lint:fix": "eslint --fix" }, "dependencies": { - "@content-collections/mdx": "^0.2.2", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-icons": "^1.3.2", @@ -23,7 +22,6 @@ "@tailwindcss/postcss": "^4.1.18", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "content-collections": "^0.2.1", "lucide-react": "^0.562.0", "motion": "^12.23.27", "next": "16.1.1", @@ -31,25 +29,19 @@ "react": "^19.2.3", "react-dom": "^19.2.3", "react-markdown": "^10.1.0", - "rehype-pretty-code": "^0.14.1", - "remark-gfm": "^4.0.1", - "shiki": "^3.20.0", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", - "tailwindcss-animate": "^1.0.7", "tw-animate-css": "^1.4.0" }, "devDependencies": { - "@content-collections/core": "^0.13.1", - "@content-collections/next": "^0.2.10", "@tailwindcss/typography": "^0.5.19", "@types/node": "^25.0.3", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "eslint": "^9.39.2", "eslint-config-next": "16.1.1", + "knip": "^5.82.1", "postcss": "^8.5.6", - "typescript": "^5.9.3", - "zod": "^4.3.5" + "typescript": "^5.9.3" } -} \ No newline at end of file +} diff --git a/public/HIT.svg b/public/HIT.svg new file mode 100644 index 0000000..c1d006b --- /dev/null +++ b/public/HIT.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/arch.png b/public/arch.png new file mode 100644 index 0000000..fe06889 Binary files /dev/null and b/public/arch.png differ diff --git a/public/atomic.png b/public/atomic.png deleted file mode 100644 index 92bab46..0000000 Binary files a/public/atomic.png and /dev/null differ diff --git a/public/base.png b/public/base.png new file mode 100644 index 0000000..30c3392 Binary files /dev/null and b/public/base.png differ diff --git a/public/buildspace.jpg b/public/buildspace.jpg deleted file mode 100644 index 1c159be..0000000 Binary files a/public/buildspace.jpg and /dev/null differ diff --git a/public/bytedance.svg b/public/bytedance.svg new file mode 100644 index 0000000..f505790 --- /dev/null +++ b/public/bytedance.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/github.png b/public/github.png new file mode 100644 index 0000000..e03d8dd Binary files /dev/null and b/public/github.png differ diff --git a/public/ib.png b/public/ib.png deleted file mode 100644 index 50e59ba..0000000 Binary files a/public/ib.png and /dev/null differ diff --git a/public/laurier.png b/public/laurier.png deleted file mode 100644 index 6b11df3..0000000 Binary files a/public/laurier.png and /dev/null differ diff --git a/public/lime.svg b/public/lime.svg deleted file mode 100644 index 262b0ed..0000000 --- a/public/lime.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/makeblock.png b/public/makeblock.png new file mode 100644 index 0000000..05c9fab Binary files /dev/null and b/public/makeblock.png differ diff --git a/public/me.png b/public/me.png index 59ec57b..0009dc7 100644 Binary files a/public/me.png and b/public/me.png differ diff --git a/public/mitremedia.png b/public/mitremedia.png deleted file mode 100644 index 4ed04e0..0000000 Binary files a/public/mitremedia.png and /dev/null differ diff --git a/public/neau.svg b/public/neau.svg new file mode 100644 index 0000000..9776d1f --- /dev/null +++ b/public/neau.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/nvidia.png b/public/nvidia.png deleted file mode 100644 index 9552789..0000000 Binary files a/public/nvidia.png and /dev/null differ diff --git a/public/shopify.svg b/public/shopify.svg deleted file mode 100644 index 426386e..0000000 --- a/public/shopify.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/public/splunk.svg b/public/splunk.svg deleted file mode 100644 index b82f86a..0000000 --- a/public/splunk.svg +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/waterloo.png b/public/waterloo.png deleted file mode 100644 index db73737..0000000 Binary files a/public/waterloo.png and /dev/null differ diff --git a/public/yasha.png b/public/yasha.png new file mode 100644 index 0000000..1fdb350 Binary files /dev/null and b/public/yasha.png differ diff --git a/src/app/blog/[slug]/opengraph-image.tsx b/src/app/blog/[slug]/opengraph-image.tsx deleted file mode 100644 index 0e587b7..0000000 --- a/src/app/blog/[slug]/opengraph-image.tsx +++ /dev/null @@ -1,240 +0,0 @@ - - -import { ImageResponse } from "next/og"; -import { allPosts } from "content-collections"; -import { DATA } from "@/data/resume"; - -export const runtime = "edge"; - -export const alt = "Blog Post"; -export const size = { - width: 1200, - height: 630, -}; -export const contentType = "image/png"; - -const getFontData = async () => { - try { - const [cabinetGrotesk, clashDisplay] = await Promise.all([ - fetch( - new URL( - "../../../../public/fonts/CabinetGrotesk-Medium.ttf", - import.meta.url - ) - ).then((res) => res.arrayBuffer()), - fetch( - new URL( - "../../../../public/fonts/ClashDisplay-Semibold.ttf", - import.meta.url - ) - ).then((res) => res.arrayBuffer()), - ]); - return { cabinetGrotesk, clashDisplay }; - } catch (error) { - console.error("Failed to load fonts:", error); - return null; - } -}; - -const styles = { - outerWrapper: { - height: "100%", - width: "100%", - display: "flex", - flexDirection: "column", - backgroundColor: "#ffffff", - position: "relative", - }, - middleWrapper: { - height: "100%", - width: "100%", - display: "flex", - flexDirection: "column", - backgroundColor: "#ffffff", - position: "relative", - padding: "40px", - }, - wrapper: { - height: "100%", - width: "100%", - display: "flex", - flexDirection: "column", - backgroundColor: "#fafafa", - position: "relative", - padding: "40px", - border: "1px solid #e5e5e5", - borderRadius: "12px", - }, - imageSection: { - position: "absolute", - top: "40px", - left: "40px", - display: "flex", - alignItems: "center", - zIndex: "2", - }, - mainContainer: { - display: "flex", - flexDirection: "column", - alignItems: "flex-start", - justifyContent: "flex-end", - height: "100%", - width: "100%", - position: "relative", - zIndex: "1", - }, - image: { - width: "140px", - height: "140px", - borderRadius: "24px", - border: "4px solid #e5e5e5", - objectFit: "cover", - }, - title: { - fontFamily: "Clash Display", - fontSize: "48px", - fontWeight: "600", - lineHeight: "1.1", - textAlign: "left", - color: "#000000", - marginBottom: "16px", - letterSpacing: "-0.02em", - maxWidth: "900px", - }, - description: { - fontSize: "20px", - fontWeight: "400", - lineHeight: "1.5", - textAlign: "left", - maxWidth: "800px", - color: "#404040", - marginBottom: "16px", - textWrap: "balance", - }, - date: { - fontSize: "16px", - fontWeight: "400", - lineHeight: "1.5", - textAlign: "left", - color: "#666666", - marginBottom: "32px", - }, -} as const; - -export default async function Image({ - params, -}: { - params: Promise<{ slug: string }>; -}) { - try { - const fontData = await getFontData(); - const { slug } = await params; - const post = allPosts.find((p) => p._meta.path.replace(/\.mdx$/, "") === slug); - const imageUrl = DATA.avatarUrl - ? new URL(DATA.avatarUrl, DATA.url).toString() - : undefined; - - if (!post) { - return new ImageResponse( - ( -
-
-
- {imageUrl && ( -
- Blog Post -
- )} -
-
Post Not Found
-
-
-
-
- ), - { - ...size, - fonts: fontData - ? [ - { - name: "Clash Display", - data: fontData.clashDisplay, - weight: 600, - style: "normal", - }, - ] - : undefined, - } - ); - } - - const title = post.title; - const description = post.summary || ""; - const publishedDate = post.publishedAt - ? new Date(post.publishedAt).toLocaleDateString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - timeZone: "UTC", - }) - : ""; - - return new ImageResponse( - ( -
-
-
- {imageUrl && ( -
- {title} -
- )} -
-
{title}
- {description && ( -
{description}
- )} - {publishedDate &&
{publishedDate}
} -
-
-
-
- ), - { - ...size, - fonts: fontData - ? [ - { - name: "Cabinet Grotesk", - data: fontData.cabinetGrotesk, - weight: 400, - style: "normal", - }, - { - name: "Cabinet Grotesk", - data: fontData.cabinetGrotesk, - weight: 700, - style: "normal", - }, - { - name: "Clash Display", - data: fontData.clashDisplay, - weight: 600, - style: "normal", - }, - ] - : undefined, - } - ); - } catch (error) { - console.error("Error generating OpenGraph image:", error); - return new Response( - `Failed to generate image: ${error instanceof Error ? error.message : "Unknown error"}`, - { - status: 500, - } - ); - } -} - - diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx deleted file mode 100644 index 7ff8a92..0000000 --- a/src/app/blog/[slug]/page.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { allPosts } from "content-collections"; -import { formatDate } from "@/lib/utils"; -import { DATA } from "@/data/resume"; -import type { Metadata } from "next"; -import { notFound } from "next/navigation"; -import { MDXContent } from "@content-collections/mdx/react"; -import { mdxComponents } from "@/mdx-components"; -import Link from "next/link"; -import { ChevronLeft, ChevronRight } from "lucide-react"; - -function getSortedPosts() { - return [...allPosts].sort((a, b) => { - if (new Date(a.publishedAt) > new Date(b.publishedAt)) { - return -1; - } - return 1; - }); -} - -export async function generateStaticParams() { - return allPosts.map((post) => ({ - slug: post._meta.path.replace(/\.mdx$/, ""), - })); -} - -export async function generateMetadata({ - params, -}: { - params: Promise<{ - slug: string; - }>; -}): Promise { - const { slug } = await params; - const post = allPosts.find((p) => p._meta.path.replace(/\.mdx$/, "") === slug); - - if (!post) { - return undefined; - } - - let { - title, - publishedAt: publishedTime, - summary: description, - image, - } = post; - - return { - title, - description, - openGraph: { - title, - description, - type: "article", - publishedTime, - url: `${DATA.url}/blog/${slug}`, - ...(image && { - images: [ - { - url: `${DATA.url}${image}`, - }, - ], - }), - }, - twitter: { - card: "summary_large_image", - title, - description, - ...(image && { - images: [`${DATA.url}${image}`], - }), - }, - }; -} - -export default async function Blog({ - params, -}: { - params: Promise<{ - slug: string; - }>; -}) { - const { slug } = await params; - const sortedPosts = getSortedPosts(); - const currentIndex = sortedPosts.findIndex( - (p) => p._meta.path.replace(/\.mdx$/, "") === slug - ); - const post = sortedPosts[currentIndex]; - - if (!post) { - notFound(); - } - - const previousPost = currentIndex > 0 ? sortedPosts[currentIndex - 1] : null; - const nextPost = currentIndex < sortedPosts.length - 1 ? sortedPosts[currentIndex + 1] : null; - - const getSlug = (post: (typeof sortedPosts)[0]) => - post._meta.path.replace(/\.mdx$/, ""); - - const jsonLdContent = JSON.stringify({ - "@context": "https://schema.org", - "@type": "BlogPosting", - headline: post.title, - datePublished: post.publishedAt, - dateModified: post.publishedAt, - description: post.summary, - image: post.image - ? `${DATA.url}${post.image}` - : `${DATA.url}/blog/${slug}/opengraph-image`, - url: `${DATA.url}/blog/${slug}`, - author: { - "@type": "Person", - name: DATA.name, - }, - }).replace(/ -