diff --git a/next.config.ts b/next.config.ts index 5e8d0db81..f65aab645 100644 --- a/next.config.ts +++ b/next.config.ts @@ -101,6 +101,16 @@ const nextConfig: NextConfig = { destination: "/testimonials", permanent: true, }, + /** + * /llms-full.txt used to serve the whole site as one document. It is now + * covered by /llms.txt plus the per-section .md routes, so agents probing + * the conventional URL land on the index instead of a 404. + */ + { + source: "/llms-full.txt", + destination: "/llms.txt", + permanent: true, + }, { source: "/blocks/content", destination: "/blocks/marketing", diff --git a/src/app/(llms)/blog.md/route.ts b/src/app/(llms)/blog.md/route.ts new file mode 100644 index 000000000..259222f24 --- /dev/null +++ b/src/app/(llms)/blog.md/route.ts @@ -0,0 +1,34 @@ +import { format } from "date-fns" + +import { SITE_INFO } from "@/config/site" +import { getBlogPosts } from "@/features/doc/data/documents" + +const allPosts = getBlogPosts() + .slice() + .sort( + (a, b) => + new Date(b.metadata.createdAt).getTime() - + new Date(a.metadata.createdAt).getTime() + ) + +const content = `# Blog + +> Writing about code, design, and everything in between. + +Each link below returns the full post as Markdown. Drop the \`.mdx\` extension for the web page. + +## All posts (${allPosts.length}) + +${allPosts.map((item) => `- [${item.metadata.title}](${SITE_INFO.url}/blog/${item.slug}.mdx) (${format(new Date(item.metadata.createdAt), "yyyy-MM-dd")}): ${item.metadata.description}`).join("\n")} +` + +export const revalidate = false +export const dynamic = "force-static" + +export async function GET() { + return new Response(content, { + headers: { + "Content-Type": "text/markdown;charset=utf-8", + }, + }) +} diff --git a/src/app/(llms)/bookmarks.md/route.ts b/src/app/(llms)/bookmarks.md/route.ts new file mode 100644 index 000000000..725075992 --- /dev/null +++ b/src/app/(llms)/bookmarks.md/route.ts @@ -0,0 +1,46 @@ +import { compareDesc, format } from "date-fns" + +import { SITE_INFO } from "@/config/site" +import { BOOKMARKS } from "@/features/portfolio/data/bookmarks" +import { BookmarkCategory } from "@/features/portfolio/types/bookmarks" + +const categorySections = Object.values(BookmarkCategory) + .map((category) => { + const items = BOOKMARKS.filter((item) => item.category === category).sort( + (a, b) => compareDesc(new Date(a.bookmarkedAt), new Date(b.bookmarkedAt)) + ) + + return { category, items } + }) + .filter(({ items }) => items.length > 0) + +const content = `# Bookmarks + +> Articles, courses, books, references, and tools I keep coming back to. + +${BOOKMARKS.length} bookmarks in total, grouped by category and newest first. They are also listed on ${SITE_INFO.url}/#bookmarks. + +${categorySections + .map( + ({ category, items }) => `## ${category} (${items.length}) + +${items + .map( + (item) => + `- [${item.title}](${item.url})${item.author ? ` by ${item.author}` : ""} (${format(new Date(item.bookmarkedAt), "yyyy-MM-dd")})` + ) + .join("\n")}` + ) + .join("\n\n")} +` + +export const revalidate = false +export const dynamic = "force-static" + +export async function GET() { + return new Response(content, { + headers: { + "Content-Type": "text/markdown;charset=utf-8", + }, + }) +} diff --git a/src/app/(llms)/education.md/route.ts b/src/app/(llms)/education.md/route.ts new file mode 100644 index 000000000..5be3f6417 --- /dev/null +++ b/src/app/(llms)/education.md/route.ts @@ -0,0 +1,26 @@ +import { EDUCATION } from "@/features/portfolio/data/education" + +const content = `# Education + +${EDUCATION.map((item) => { + const heading = + [item.degree, item.fieldOfStudy].filter(Boolean).join(", ") || item.school + const school = heading === item.school ? "" : ` | ${item.school}` + const skills = item.skills?.length + ? `\n\nSkills: ${item.skills.join(", ")}` + : "" + const description = item.description ? `\n\n${item.description.trim()}` : "" + return `## ${heading}${school}\n\nDuration: ${item.period.start} - ${item.period.end || "Present"}${skills}${description}` +}).join("\n\n")} +` + +export const revalidate = false +export const dynamic = "force-static" + +export async function GET() { + return new Response(content, { + headers: { + "Content-Type": "text/markdown;charset=utf-8", + }, + }) +} diff --git a/src/app/(llms)/llms-full.txt/route.ts b/src/app/(llms)/llms-full.txt/route.ts deleted file mode 100644 index 75481bb9f..000000000 --- a/src/app/(llms)/llms-full.txt/route.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { format } from "date-fns" - -import { SITE_INFO } from "@/config/site" -import { getBlogPosts } from "@/features/doc/data/documents" -import { getLLMText } from "@/features/doc/lib/get-llm-text" -import { AWARDS } from "@/features/portfolio/data/awards" -import { CERTIFICATIONS } from "@/features/portfolio/data/certifications" -import { EXPERIENCES } from "@/features/portfolio/data/experiences" -import { PROJECTS } from "@/features/portfolio/data/projects" -import { SOCIAL_LINKS } from "@/features/portfolio/data/social-links" -import { TECH_STACK } from "@/features/portfolio/data/tech-stack" -import { USER } from "@/features/portfolio/data/user" - -const allPosts = getBlogPosts() - -const aboutText = `## About - -${USER.about.trim()} - -### Personal Information - -- First Name: ${USER.firstName} -- Last Name: ${USER.lastName} -- Display Name: ${USER.displayName} -- Location: ${USER.address} -- Website: ${USER.website} - -### Social Links - -${SOCIAL_LINKS.map((item) => `- [${item.title}](${item.href})`).join("\n")} - -### Tech Stack - -${TECH_STACK.map((item) => `- [${item.title}](${item.href})`).join("\n")}\n` - -const experienceText = `## Experience - -${EXPERIENCES.map((item) => - item.positions - .map((position) => { - const skills = position.skills?.map((skill) => skill).join(", ") || "N/A" - return `### ${position.title} | ${item.companyName}\n\nDuration: ${position.employmentPeriod.start} - ${position.employmentPeriod.end || "Present"}\n\nSkills: ${skills}\n\n${position.description?.trim()}` - }) - .join("\n\n") -).join("\n\n")} -` - -const projectsText = `## Projects - -${PROJECTS.map((item) => { - const skills = `\n\nSkills: ${item.skills.join(", ")}` - const description = item.description ? `\n\n${item.description.trim()}` : "" - return `### ${item.title}\n\nProject URL: ${item.link}${skills}${description}` -}).join("\n\n")} -` - -const awardsText = `## Awards - -${AWARDS.map((item) => `### ${item.prize} | ${item.title}\n\n${item.description}`).join("\n\n")} -` - -const certificationsText = `## Certifications - -${CERTIFICATIONS.map((item) => `- [${item.title}](${item.credentialURL})`).join("\n")}` - -async function getBlogContent() { - const text = await Promise.all( - allPosts.map( - async (item) => - `---\ntitle: "${item.metadata.title}"\ndescription: "${item.metadata.description}"\nlast_updated: "${format(new Date(item.metadata.updatedAt), "MMMM d, yyyy")}"\nsource: "${SITE_INFO.url}/blog/${item.slug}"\n---\n\n${await getLLMText(item)}` - ) - ) - return text.join("\n\n") -} - -async function getContent() { - return `This document contains comprehensive information about ${USER.displayName}'s professional profile, portfolio, and blog content. It includes personal details, work experience, projects, achievements, certifications, and all published blog posts. This data is formatted for consumption by Large Language Models (LLMs) to provide accurate and up-to-date information about ${USER.displayName}'s background, skills, and expertise as a Design Engineer. - -# chanhdai.com - -> A pixel-perfect dev portfolio and shadcn registry showcasing my work as a Design Engineer. - -${aboutText} -${experienceText} -${projectsText} -${awardsText} -${certificationsText} - -## Blog - -${await getBlogContent()}` -} - -export const revalidate = false -export const dynamic = "force-static" - -export async function GET() { - return new Response(await getContent(), { - headers: { - "Content-Type": "text/markdown;charset=utf-8", - }, - }) -} diff --git a/src/app/(llms)/llms.txt/route.ts b/src/app/(llms)/llms.txt/route.ts index 5f4279b34..e2503a335 100644 --- a/src/app/(llms)/llms.txt/route.ts +++ b/src/app/(llms)/llms.txt/route.ts @@ -10,9 +10,14 @@ const content = `# chanhdai.com - [About](${SITE_INFO.url}/about.md): A quick intro to me, my tech stack, and how to connect. - [Experience](${SITE_INFO.url}/experience.md): Highlights from my career and key roles I've taken on. +- [Education](${SITE_INFO.url}/education.md): Where I studied, what I focused on, and what I built along the way. - [Projects](${SITE_INFO.url}/projects.md): Selected projects that show my skills and creativity. - [Awards](${SITE_INFO.url}/awards.md): My key awards and honors. - [Certifications](${SITE_INFO.url}/certifications.md): Certifications and credentials I've earned. +- [Components](${SITE_INFO.url}/components.md): Every registry component, with install instructions. +- [Blocks](${SITE_INFO.url}/blocks.md): Every registry block, grouped by category, with install instructions. +- [Blog](${SITE_INFO.url}/blog.md): Every blog post, newest first, with publish dates. +- [Bookmarks](${SITE_INFO.url}/bookmarks.md): Articles, courses, books, references, and tools I recommend. ## Components