diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ab99bb..af9e704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ All notable changes to this project are documented here. The format is based on ### Added +- **Markdown page mirrors**: public pages now expose `text/markdown` versions + at `/.md`, `/index.md`, `/blog.md`, and `/blog/.md`. Blog posts are + converted directly from Payload Lexical content, and HTML pages advertise + their Markdown alternate for discovery. - **Blog reading sidebar** — a sticky table of contents that scroll-spies the current section as you read, with smooth-scroll anchor links to every heading. Hierarchy is conveyed through indentation and dimming: `h2` headings are diff --git a/app/(frontend)/blog/[slug]/page.tsx b/app/(frontend)/blog/[slug]/page.tsx index 521a12c..edaad5a 100644 --- a/app/(frontend)/blog/[slug]/page.tsx +++ b/app/(frontend)/blog/[slug]/page.tsx @@ -34,6 +34,11 @@ export async function generateMetadata({ params }: Props): Promise { return { title: `${post.meta?.title || post.title} | Khushal Bhardwaj`, description: post.meta?.description || post.excerpt, + alternates: { + types: { + "text/markdown": `/blog/${slug}.md`, + }, + }, }; } diff --git a/app/(frontend)/blog/page.tsx b/app/(frontend)/blog/page.tsx index 80eb80e..d4af44c 100644 --- a/app/(frontend)/blog/page.tsx +++ b/app/(frontend)/blog/page.tsx @@ -10,6 +10,11 @@ export const dynamic = "force-dynamic"; export const metadata: Metadata = { title: "Blog | Khushal Bhardwaj", description: "Blog posts by Khushal Bhardwaj", + alternates: { + types: { + "text/markdown": "/blog.md", + }, + }, }; export default async function BlogPage() { diff --git a/app/(frontend)/layout.tsx b/app/(frontend)/layout.tsx index f50e9fe..358492e 100644 --- a/app/(frontend)/layout.tsx +++ b/app/(frontend)/layout.tsx @@ -7,6 +7,7 @@ import { GeistSans } from "geist/font/sans"; import { GeistPixelSquare } from "geist/font/pixel"; export const metadata: Metadata = { + metadataBase: new URL("https://celeroncoder.tech"), title: "Khushal Bhardwaj", description: "Full-stack Software Engineer", icons: { diff --git a/app/(frontend)/llm-markdown/[[...path]]/route.ts b/app/(frontend)/llm-markdown/[[...path]]/route.ts new file mode 100644 index 0000000..b72f710 --- /dev/null +++ b/app/(frontend)/llm-markdown/[[...path]]/route.ts @@ -0,0 +1,130 @@ +import type { SerializedEditorState } from "lexical"; +import { getPayload, type Payload } from "payload"; +import { + convertLexicalToMarkdown, + editorConfigFactory, +} from "@payloadcms/richtext-lexical"; +import config from "@payload-config"; +import { + markdownResponse, + renderBlogIndexMarkdown, + renderBlogPostMarkdown, + renderHomeMarkdown, +} from "@/lib/markdown-pages"; + +export const dynamic = "force-dynamic"; + +type RouteContext = { + params: Promise<{ path?: string[] }>; +}; + +function isSerializedEditorState(value: unknown): value is SerializedEditorState { + if (!value || typeof value !== "object" || !("root" in value)) return false; + + const root = value.root; + return Boolean( + root && + typeof root === "object" && + "type" in root && + root.type === "root" && + "children" in root && + Array.isArray(root.children), + ); +} + +function getPostEditorConfig(payload: Payload) { + const postsCollection = payload.config.collections.find( + (collection) => collection.slug === "posts", + ); + const contentField = postsCollection?.fields.find( + (field) => "name" in field && field.name === "content", + ); + + if (!contentField || contentField.type !== "richText") { + throw new Error("The posts.content rich text field is not configured."); + } + + return editorConfigFactory.fromField({ field: contentField }); +} + +function notFoundResponse() { + return markdownResponse( + "# 404\n\nThe requested page does not exist.", + "404.md", + 404, + ); +} + +export async function GET(request: Request, { params }: RouteContext) { + const { path = [] } = await params; + const origin = new URL(request.url).origin; + const isHome = path.length === 0 || (path.length === 1 && path[0] === "index"); + const isBlogIndex = path.length === 1 && path[0] === "blog"; + const isBlogPost = path.length === 2 && path[0] === "blog"; + + if (!isHome && !isBlogIndex && !isBlogPost) return notFoundResponse(); + + try { + const payload = await getPayload({ config }); + + if (isHome) { + const { docs: posts } = await payload.find({ + collection: "posts", + sort: "-publishedAt", + limit: 5, + }); + + return markdownResponse( + renderHomeMarkdown(posts, origin), + "index.md", + ); + } + + if (isBlogIndex) { + const { docs: posts } = await payload.find({ + collection: "posts", + sort: "-publishedAt", + limit: 50, + depth: 1, + }); + + return markdownResponse( + renderBlogIndexMarkdown(posts, origin), + "blog.md", + ); + } + + const slug = path[1]; + const { + docs: [post], + } = await payload.find({ + collection: "posts", + where: { slug: { equals: slug } }, + limit: 1, + depth: 2, + }); + + if (!post) return notFoundResponse(); + + if (!isSerializedEditorState(post.content)) { + throw new Error(`Post "${post.slug}" has invalid rich text content.`); + } + + const body = convertLexicalToMarkdown({ + data: post.content, + editorConfig: getPostEditorConfig(payload), + }); + + return markdownResponse( + renderBlogPostMarkdown(post, body, origin), + `${post.slug}.md`, + ); + } catch (error) { + console.error("Failed to render Markdown page", error); + return markdownResponse( + "# 500\n\nThe Markdown version of this page is temporarily unavailable.", + "500.md", + 500, + ); + } +} diff --git a/app/(frontend)/page.tsx b/app/(frontend)/page.tsx index a249bdf..bf8a890 100644 --- a/app/(frontend)/page.tsx +++ b/app/(frontend)/page.tsx @@ -15,6 +15,11 @@ export const metadata: Metadata = { title: "Khushal Bhardwaj | Full Stack Web Developer", description: "Portfolio of Khushal Bhardwaj, a Full Stack Web Developer based in Jaipur, India, building web apps with React and Next.js.", + alternates: { + types: { + "text/markdown": "/.md", + }, + }, }; export default function Home() { diff --git a/components/about.tsx b/components/about.tsx index 65aa457..320b857 100644 --- a/components/about.tsx +++ b/components/about.tsx @@ -1,20 +1,16 @@ +import { about, profile } from "@/lib/site-content"; + export function About() { return (

About

- Hey, I'm{" "} - Khushal Bhardwaj, a - Full Stack Web Developer based in Jaipur, India. Passionate about - building web apps with React/NextJS. I'm also an undergrad at VIT - Bhopal University. -

-

- Other than programming, I write blogs. I've developed a hobby of - writing blogs, mostly technical. Although I may not be following it - that regularly, you might think, but I do love it. + {about.beforeName} + {profile.name} + {about.afterName}

+

{about.writing}

); diff --git a/components/footer.tsx b/components/footer.tsx index 12ba2d5..11cd6b2 100644 --- a/components/footer.tsx +++ b/components/footer.tsx @@ -1,10 +1,12 @@ import Link from "next/link"; +import { profile, writingLinks } from "@/lib/site-content"; const footerLinks = [ - { name: "GitHub", url: "https://github.com/celeroncoder/celeroncoder.tech" }, - { name: "Dev.to", url: "https://dev.to/celeron" }, - { name: "Hashnode", url: "https://hashnode.com/@celeroncoder" }, - { name: "Medium", url: "https://medium.com/@celeroncoder" }, + { + name: "GitHub", + url: "https://github.com/celeroncoder/celeroncoder.tech", + }, + ...writingLinks, ]; export function Footer() { @@ -12,7 +14,7 @@ export function Footer() {