mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(landing): graceful not-found handling for blog and library posts and authors (#5661)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getAllPostMeta, getPostBySlug, getRelatedPosts } from '@/lib/blog/registry'
|
||||
import { BLOG_SECTION, buildPostGraphJsonLd, buildPostMetadata } from '@/lib/blog/seo'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
@@ -18,6 +19,7 @@ export async function generateMetadata({
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await params
|
||||
const post = await getPostBySlug(slug)
|
||||
if (!post) return {}
|
||||
return buildPostMetadata(post)
|
||||
}
|
||||
|
||||
@@ -26,6 +28,7 @@ export const revalidate = 86400
|
||||
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params
|
||||
const post = await getPostBySlug(slug)
|
||||
if (!post) notFound()
|
||||
const related = await getRelatedPosts(slug, 3)
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getAllPostMeta } from '@/lib/blog/registry'
|
||||
import { BLOG_SECTION, buildAuthorGraphJsonLd, buildAuthorMetadata } from '@/lib/blog/seo'
|
||||
import { ContentAuthorPage } from '@/app/(landing)/components'
|
||||
@@ -13,6 +14,7 @@ export async function generateMetadata({
|
||||
const { id } = await params
|
||||
const posts = (await getAllPostMeta()).filter((p) => p.authors.some((a) => a.id === id))
|
||||
const author = posts[0]?.authors.find((a) => a.id === id)
|
||||
if (!author) return {}
|
||||
return buildAuthorMetadata(id, author)
|
||||
}
|
||||
|
||||
@@ -20,15 +22,16 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
|
||||
const { id } = await params
|
||||
const posts = (await getAllPostMeta()).filter((p) => p.authors.some((a) => a.id === id))
|
||||
const author = posts[0]?.authors.find((a) => a.id === id)
|
||||
if (!author) notFound()
|
||||
|
||||
return (
|
||||
<ContentAuthorPage
|
||||
basePath={BLOG_SECTION.basePath}
|
||||
sectionName={BLOG_SECTION.name}
|
||||
authorName={author?.name}
|
||||
authorAvatarUrl={author?.avatarUrl}
|
||||
authorName={author.name}
|
||||
authorAvatarUrl={author.avatarUrl}
|
||||
posts={posts}
|
||||
graphJsonLd={author ? buildAuthorGraphJsonLd(author) : undefined}
|
||||
graphJsonLd={buildAuthorGraphJsonLd(author)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ChipLink } from '@sim/emcn'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Page Not Found',
|
||||
robots: { index: false, follow: true },
|
||||
}
|
||||
|
||||
export default function BlogAuthorNotFound() {
|
||||
return (
|
||||
<main
|
||||
id='main-content'
|
||||
className='mx-auto flex min-h-[60vh] w-full max-w-[1460px] flex-col items-center justify-center gap-3 px-20 py-24 text-center max-sm:px-5 max-lg:px-8'
|
||||
>
|
||||
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
|
||||
Author not found
|
||||
</h1>
|
||||
<p className='text-[var(--text-muted)] text-lg'>
|
||||
The author you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<ChipLink variant='primary' href='/blog' className='mt-3'>
|
||||
Browse blog
|
||||
</ChipLink>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ChipLink } from '@sim/emcn'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Page Not Found',
|
||||
robots: { index: false, follow: true },
|
||||
}
|
||||
|
||||
export default function BlogNotFound() {
|
||||
return (
|
||||
<main
|
||||
id='main-content'
|
||||
className='mx-auto flex min-h-[60vh] w-full max-w-[1460px] flex-col items-center justify-center gap-3 px-20 py-24 text-center max-sm:px-5 max-lg:px-8'
|
||||
>
|
||||
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
|
||||
Post not found
|
||||
</h1>
|
||||
<p className='text-[var(--text-muted)] text-lg'>
|
||||
The post you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<ChipLink variant='primary' href='/blog' className='mt-3'>
|
||||
Browse blog
|
||||
</ChipLink>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ChipLink } from '@sim/emcn'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import type { ContentMeta } from '@/lib/content/schema'
|
||||
@@ -9,9 +8,9 @@ import { JsonLd } from '@/app/(landing)/components/json-ld'
|
||||
interface ContentAuthorPageProps {
|
||||
/** Route base path, e.g. `/blog` or `/library`. */
|
||||
basePath: string
|
||||
/** Section label used in the not-found fallback, e.g. "Blog" or "Library". */
|
||||
/** Section label used in the back link, e.g. "Blog" or "Library". */
|
||||
sectionName: string
|
||||
authorName?: string
|
||||
authorName: string
|
||||
authorAvatarUrl?: string
|
||||
/** Posts already filtered down to this author. */
|
||||
posts: ContentMeta[]
|
||||
@@ -32,22 +31,6 @@ export function ContentAuthorPage({
|
||||
posts,
|
||||
graphJsonLd,
|
||||
}: ContentAuthorPageProps) {
|
||||
if (!authorName) {
|
||||
return (
|
||||
<section className='mx-auto flex min-h-[60vh] w-full max-w-[1460px] flex-col items-center justify-center gap-3 px-20 py-24 text-center max-sm:px-5 max-lg:px-8'>
|
||||
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
|
||||
Author not found
|
||||
</h1>
|
||||
<p className='text-[var(--text-muted)] text-lg'>
|
||||
The author you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<ChipLink variant='primary' href={basePath} className='mt-3'>
|
||||
Browse {sectionName}
|
||||
</ChipLink>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='bg-[var(--bg)]'>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getAllPostMeta, getPostBySlug, getRelatedPosts } from '@/lib/library/registry'
|
||||
import { buildPostGraphJsonLd, buildPostMetadata, LIBRARY_SECTION } from '@/lib/library/seo'
|
||||
@@ -18,6 +19,7 @@ export async function generateMetadata({
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await params
|
||||
const post = await getPostBySlug(slug)
|
||||
if (!post) return {}
|
||||
return buildPostMetadata(post)
|
||||
}
|
||||
|
||||
@@ -26,6 +28,7 @@ export const revalidate = 86400
|
||||
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params
|
||||
const post = await getPostBySlug(slug)
|
||||
if (!post) notFound()
|
||||
const related = await getRelatedPosts(slug, 3)
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getAllPostMeta } from '@/lib/library/registry'
|
||||
import { buildAuthorGraphJsonLd, buildAuthorMetadata, LIBRARY_SECTION } from '@/lib/library/seo'
|
||||
import { ContentAuthorPage } from '@/app/(landing)/components'
|
||||
@@ -13,6 +14,7 @@ export async function generateMetadata({
|
||||
const { id } = await params
|
||||
const posts = (await getAllPostMeta()).filter((p) => p.authors.some((a) => a.id === id))
|
||||
const author = posts[0]?.authors.find((a) => a.id === id)
|
||||
if (!author) return {}
|
||||
return buildAuthorMetadata(id, author)
|
||||
}
|
||||
|
||||
@@ -20,15 +22,16 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
|
||||
const { id } = await params
|
||||
const posts = (await getAllPostMeta()).filter((p) => p.authors.some((a) => a.id === id))
|
||||
const author = posts[0]?.authors.find((a) => a.id === id)
|
||||
if (!author) notFound()
|
||||
|
||||
return (
|
||||
<ContentAuthorPage
|
||||
basePath={LIBRARY_SECTION.basePath}
|
||||
sectionName={LIBRARY_SECTION.name}
|
||||
authorName={author?.name}
|
||||
authorAvatarUrl={author?.avatarUrl}
|
||||
authorName={author.name}
|
||||
authorAvatarUrl={author.avatarUrl}
|
||||
posts={posts}
|
||||
graphJsonLd={author ? buildAuthorGraphJsonLd(author) : undefined}
|
||||
graphJsonLd={buildAuthorGraphJsonLd(author)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ChipLink } from '@sim/emcn'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Page Not Found',
|
||||
robots: { index: false, follow: true },
|
||||
}
|
||||
|
||||
export default function LibraryAuthorNotFound() {
|
||||
return (
|
||||
<main
|
||||
id='main-content'
|
||||
className='mx-auto flex min-h-[60vh] w-full max-w-[1460px] flex-col items-center justify-center gap-3 px-20 py-24 text-center max-sm:px-5 max-lg:px-8'
|
||||
>
|
||||
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
|
||||
Author not found
|
||||
</h1>
|
||||
<p className='text-[var(--text-muted)] text-lg'>
|
||||
The author you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<ChipLink variant='primary' href='/library' className='mt-3'>
|
||||
Browse library
|
||||
</ChipLink>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ChipLink } from '@sim/emcn'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Page Not Found',
|
||||
robots: { index: false, follow: true },
|
||||
}
|
||||
|
||||
export default function LibraryNotFound() {
|
||||
return (
|
||||
<main
|
||||
id='main-content'
|
||||
className='mx-auto flex min-h-[60vh] w-full max-w-[1460px] flex-col items-center justify-center gap-3 px-20 py-24 text-center max-sm:px-5 max-lg:px-8'
|
||||
>
|
||||
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
|
||||
Post not found
|
||||
</h1>
|
||||
<p className='text-[var(--text-muted)] text-lg'>
|
||||
The post you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<ChipLink variant='primary' href='/library' className='mt-3'>
|
||||
Browse library
|
||||
</ChipLink>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export interface ContentRegistryConfig {
|
||||
|
||||
export interface ContentRegistry {
|
||||
getAllPostMeta: () => Promise<ContentMeta[]>
|
||||
getPostBySlug: (slug: string) => Promise<ContentPost>
|
||||
getPostBySlug: (slug: string) => Promise<ContentPost | null>
|
||||
getAllTags: () => Promise<TagWithCount[]>
|
||||
getRelatedPosts: (slug: string, limit?: number) => Promise<ContentMeta[]>
|
||||
getNavPosts: () => Promise<Pick<ContentMeta, 'slug' | 'title' | 'ogImage'>[]>
|
||||
@@ -225,10 +225,10 @@ export function createContentRegistry(config: ContentRegistryConfig): ContentReg
|
||||
}
|
||||
}
|
||||
|
||||
async function getPostBySlug(slug: string): Promise<ContentPost> {
|
||||
async function getPostBySlug(slug: string): Promise<ContentPost | null> {
|
||||
const meta = await scanFrontmatters()
|
||||
const found = meta.find((m) => m.slug === slug)
|
||||
if (!found) throw new Error(`Post not found: ${slug}`)
|
||||
if (!found) return null
|
||||
const mdxPath = path.join(contentDir, slug, 'index.mdx')
|
||||
const raw = await fs.readFile(mdxPath, 'utf-8')
|
||||
const { content, data } = matter(raw)
|
||||
|
||||
Reference in New Issue
Block a user