blog.hangerthem.com/app/(withRecent)/page.tsx
HangerThem 64ae30b545 feat(seo): sitemap, robots, canonical URLs, OG images and JSON-LD
- Add sitemap.ts and robots.ts
- pageMetadata() helper: consistent title, canonical URL, Open Graph,
  Twitter card and feed alternate on every page
- Generated OG images: a site card, and per post either the cover image
  or a branded title card (pre-rendered at build)
- BlogPosting JSON-LD on post pages
- Fail fast with a clear error when SITE_URL is missing

Content loading:
- Validate post frontmatter at build time and normalize dates to ISO
  (fixes invalid <time datetime> values)
- Add draft: true support (drafts only render in development)
- Cache markdown rendering so metadata and page share one render
- Replace the three copy-pasted about/projects/setup pages with a shared
  MarkdownPage component
2026-10-19 14:22:40 +02:00

44 lines
1.4 KiB
TypeScript

import { getAllPostsMeta } from '@/lib/posts'
import { Post } from '@/components/post/Post'
import type { Metadata } from 'next'
export const metadata: Metadata = { alternates: { canonical: '/' } }
export default function Homepage() {
const posts = getAllPostsMeta()
const rest = posts.slice(1)
const rows = []
for (let i = 0; i < rest.length; i += 2) {
rows.push(rest.slice(i, i + 2))
}
return (
<main className="max-w-3xl 2xl:max-w-4xl mx-auto px-4 py-8 mb-8 lg:py-16">
<section>
{posts.length === 0 ? (
<div className="rounded-xl border border-border p-6 text-fg-dim">
No published posts yet.
</div>
) : (
<>
<Post post={posts[0]} isLatest />
<div className="border-t border-border my-6" aria-hidden="true"></div>
<div className="flex flex-col gap-8 mt-4">
{rows.map((row) => {
const rowHasImage = row.some((p) => p.coverImage)
return (
<div key={row[0].slug} className="grid grid-cols-1 gap-4 md:grid-cols-2">
{row.map((post) => (
<Post post={post} key={post.slug} showImageSlot={rowHasImage} />
))}
</div>
)
})}
</div>
</>
)}
</section>
</main>
)
}