Skip to content

Commit 5dc8d8f

Browse files
authored
fix(landing): align author-page UI with the site shell and link bylines to it (#5655)
* fix(landing): align author-page UI with the site shell and link bylines to it Rebuilds ContentAuthorPage/ContentAuthorLoading to match the standard page-shell pattern used by ContentIndexPage/ContentPostPage (max-w-[1460px] header, BackLink, framed post-list rows with hover states) instead of the previous bare max-w-[900px] container with a 2-column image grid. Adds a styled not-found fallback matching the comparisons/integrations pattern. The byline author Link in ContentPostPage now points at the internal author page (/{basePath}/authors/{id}) instead of the author's external X/GitHub profile, giving the author page a real navigation entry point for the first time - previously it was only reachable via the sitemap. Also fixes both author-page route files to match on any of a post's authors (post.authors.some(a => a.id === id)) instead of only the primary author (post.author.id === id), so posts with co-authors show up on every listed author's page, consistent with the sitemap's own author enumeration. * fix(landing): address review findings on author-page UI rebuild - Not-found fallback no longer renders a nested <main id='main-content'> - blog/layout.tsx and library/layout.tsx already provide that landmark, so the fallback now uses <section> like the rest of the component. - Byline author link now encodeURIComponent()s the author id before using it as a route segment. - Loading skeleton's post-list wrapper now matches the real page's mx-auto max-w-[1460px] shell (was mx-20/max-lg:mx-8, which stretches past the header on wide viewports). * fix(landing): make author Person.image an absolute URL in JSON-LD Same class of bug as the earlier article-image fix: author.avatarUrl is a site-relative path, so Person.image was emitting an invalid relative URL for crawlers. Prefixes with SITE_URL, matching buildArticleJsonLd's existing pattern. * fix(landing): align author not-found fallback width with the site convention max-w-[1446px]/px-12 was copied from comparisons/not-found.tsx, which turns out to be the outlier - models/not-found.tsx and integrations/not-found.tsx both use max-w-[1460px]/px-20, matching the rest of the site's shell and this component's own success-state header. * fix(landing): encode author id consistently in every author URL builder The byline link encoded the author id, but buildAuthorMetadata's canonical URL, buildAuthorGraphJsonLd's Person.url/BreadcrumbList item, and sitemap.ts's buildAuthorPages URL still interpolated the raw id - a mismatch could point sitemap/canonical/structured-data URLs at a different route than the actual page. All four now encodeURIComponent() consistently.
1 parent 8b9f593 commit 5dc8d8f

7 files changed

Lines changed: 149 additions & 71 deletions

File tree

apps/sim/app/(landing)/blog/authors/[id]/page.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,20 @@ export async function generateMetadata({
1111
params: Promise<{ id: string }>
1212
}): Promise<Metadata> {
1313
const { id } = await params
14-
const posts = (await getAllPostMeta()).filter((p) => p.author.id === id)
15-
return buildAuthorMetadata(id, posts[0]?.author)
14+
const posts = (await getAllPostMeta()).filter((p) => p.authors.some((a) => a.id === id))
15+
const author = posts[0]?.authors.find((a) => a.id === id)
16+
return buildAuthorMetadata(id, author)
1617
}
1718

1819
export default async function AuthorPage({ params }: { params: Promise<{ id: string }> }) {
1920
const { id } = await params
20-
const posts = (await getAllPostMeta()).filter((p) => p.author.id === id)
21-
const author = posts[0]?.author
21+
const posts = (await getAllPostMeta()).filter((p) => p.authors.some((a) => a.id === id))
22+
const author = posts[0]?.authors.find((a) => a.id === id)
2223

2324
return (
2425
<ContentAuthorPage
2526
basePath={BLOG_SECTION.basePath}
27+
sectionName={BLOG_SECTION.name}
2628
authorName={author?.name}
2729
authorAvatarUrl={author?.avatarUrl}
2830
posts={posts}

apps/sim/app/(landing)/components/content-author-page/content-author-loading.tsx

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,34 @@ const AUTHOR_POST_SKELETON_COUNT = 4
55
/** Shared loading skeleton for a content section's author-profile route. */
66
export function ContentAuthorLoading() {
77
return (
8-
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
9-
<div className='mb-6 flex items-center gap-3'>
10-
<Skeleton className='size-[40px] rounded-full bg-[var(--surface-hover)]' />
11-
<Skeleton className='h-[32px] w-[160px] rounded-[4px] bg-[var(--surface-hover)]' />
8+
<section className='bg-[var(--bg)]'>
9+
<div className='mx-auto w-full max-w-[1460px] px-20 pt-[112px] max-sm:px-5 max-sm:pt-20 max-lg:px-8'>
10+
<Skeleton className='mb-6 h-[16px] w-[100px] rounded-md bg-[var(--surface-hover)]' />
11+
<div className='flex items-center gap-4'>
12+
<Skeleton className='size-[64px] rounded-full bg-[var(--surface-hover)]' />
13+
<Skeleton className='h-[40px] w-[240px] rounded-[4px] bg-[var(--surface-hover)]' />
14+
</div>
1215
</div>
13-
<div className='grid grid-cols-1 gap-8 sm:grid-cols-2'>
14-
{Array.from({ length: AUTHOR_POST_SKELETON_COUNT }).map((_, i) => (
15-
<div key={i} className='overflow-hidden rounded-lg border border-[var(--border)]'>
16-
<Skeleton className='h-[160px] w-full rounded-none bg-[var(--surface-hover)]' />
17-
<div className='p-3'>
18-
<Skeleton className='mb-1 h-[12px] w-[80px] rounded-[4px] bg-[var(--surface-hover)]' />
19-
<Skeleton className='h-[14px] w-[200px] rounded-[4px] bg-[var(--surface-hover)]' />
16+
17+
<div className='mt-8 h-px w-full bg-[var(--border)]' />
18+
19+
<div className='mx-auto w-full max-w-[1460px] px-20 max-sm:px-5 max-lg:px-8'>
20+
<div className='border-[var(--border)] border-x'>
21+
{Array.from({ length: AUTHOR_POST_SKELETON_COUNT }).map((_, i) => (
22+
<div key={i}>
23+
<div className='flex items-center gap-6 p-6'>
24+
<Skeleton className='hidden h-[14px] w-[120px] rounded-[4px] bg-[var(--surface-hover)] md:block' />
25+
<div className='flex min-w-0 flex-1 flex-col gap-1'>
26+
<Skeleton className='h-[18px] w-[70%] rounded-[4px] bg-[var(--surface-hover)]' />
27+
<Skeleton className='h-[14px] w-[90%] rounded-[4px] bg-[var(--surface-hover)]' />
28+
</div>
29+
<Skeleton className='hidden h-[80px] w-[140px] rounded-[5px] bg-[var(--surface-hover)] sm:block' />
30+
</div>
31+
<div className='h-px w-full bg-[var(--border)]' />
2032
</div>
21-
</div>
22-
))}
33+
))}
34+
</div>
2335
</div>
24-
</main>
36+
</section>
2537
)
2638
}
Lines changed: 102 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,137 @@
1+
import { ChipLink } from '@sim/emcn'
12
import Image from 'next/image'
23
import Link from 'next/link'
34
import type { ContentMeta } from '@/lib/content/schema'
5+
import { BackLink } from '@/app/(landing)/components/back-link'
6+
import { Cta } from '@/app/(landing)/components/cta/cta'
47
import { JsonLd } from '@/app/(landing)/components/json-ld'
58

69
interface ContentAuthorPageProps {
710
/** Route base path, e.g. `/blog` or `/library`. */
811
basePath: string
12+
/** Section label used in the not-found fallback, e.g. "Blog" or "Library". */
13+
sectionName: string
914
authorName?: string
1015
authorAvatarUrl?: string
1116
/** Posts already filtered down to this author. */
1217
posts: ContentMeta[]
1318
graphJsonLd?: Record<string, unknown>
1419
}
1520

16-
/** Shared author-profile layout for a content section. */
21+
/**
22+
* Shared author-profile layout for a content section: standard page-shell
23+
* header (matching `ContentIndexPage`/`ContentPostPage`) with avatar + name,
24+
* followed by the author's posts in the same framed-list card style used
25+
* everywhere else on the site.
26+
*/
1727
export function ContentAuthorPage({
1828
basePath,
29+
sectionName,
1930
authorName,
2031
authorAvatarUrl,
2132
posts,
2233
graphJsonLd,
2334
}: ContentAuthorPageProps) {
2435
if (!authorName) {
2536
return (
26-
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
27-
<h1 className='text-[32px] text-[var(--text-primary)]'>Author not found</h1>
28-
</main>
37+
<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'>
38+
<h1 className='text-balance text-[40px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em]'>
39+
Author not found
40+
</h1>
41+
<p className='text-[var(--text-muted)] text-lg'>
42+
The author you&apos;re looking for doesn&apos;t exist or has been moved.
43+
</p>
44+
<ChipLink variant='primary' href={basePath} className='mt-3'>
45+
Browse {sectionName}
46+
</ChipLink>
47+
</section>
2948
)
3049
}
3150

3251
return (
33-
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
34-
{graphJsonLd && <JsonLd data={graphJsonLd} />}
35-
<div className='mb-6 flex items-center gap-3'>
36-
{authorAvatarUrl ? (
37-
<Image
38-
src={authorAvatarUrl}
39-
alt={authorName}
40-
width={40}
41-
height={40}
42-
className='rounded-full'
43-
unoptimized
44-
/>
45-
) : null}
46-
<h1 className='text-[32px] text-[var(--text-primary)] leading-tight'>{authorName}</h1>
47-
</div>
48-
<div className='grid grid-cols-1 gap-8 sm:grid-cols-2'>
49-
{posts.map((p) => (
50-
<Link key={p.slug} href={`${basePath}/${p.slug}`} className='group'>
51-
<div className='overflow-hidden rounded-lg border border-[var(--border)]'>
52+
<>
53+
<section className='bg-[var(--bg)]'>
54+
{graphJsonLd && <JsonLd data={graphJsonLd} />}
55+
56+
<div className='mx-auto w-full max-w-[1460px] px-20 pt-[112px] max-sm:px-5 max-sm:pt-20 max-lg:px-8'>
57+
<div className='mb-6'>
58+
<BackLink href={basePath} label={`Back to ${sectionName}`} />
59+
</div>
60+
61+
<div className='flex items-center gap-4'>
62+
{authorAvatarUrl ? (
5263
<Image
53-
src={p.ogImage}
54-
alt={p.title}
55-
width={600}
56-
height={315}
57-
className='h-[160px] w-full object-cover transition-transform group-hover:scale-[1.02]'
64+
src={authorAvatarUrl}
65+
alt={authorName}
66+
width={64}
67+
height={64}
68+
className='rounded-full'
5869
unoptimized
5970
/>
60-
<div className='p-3'>
61-
<div className='mb-1 text-[var(--text-muted)] text-xs'>
62-
{new Date(p.date).toLocaleDateString('en-US', {
63-
month: 'short',
64-
day: 'numeric',
65-
year: 'numeric',
66-
})}
67-
</div>
68-
<div className='text-[var(--text-primary)] text-sm leading-tight'>{p.title}</div>
71+
) : null}
72+
<h1 className='text-balance text-[28px] text-[var(--text-primary)] leading-[100%] tracking-[-0.02em] lg:text-[40px]'>
73+
{authorName}
74+
</h1>
75+
</div>
76+
</div>
77+
78+
<div className='mt-8 h-px w-full bg-[var(--border)]' />
79+
80+
<div className='mx-auto w-full max-w-[1460px] px-20 max-sm:px-5 max-lg:px-8'>
81+
<div className='border-[var(--border)] border-x'>
82+
{posts.map((p) => (
83+
<div key={p.slug}>
84+
<Link
85+
href={`${basePath}/${p.slug}`}
86+
className='group flex items-start gap-6 p-6 transition-colors hover:bg-[var(--surface-hover)] md:items-center'
87+
>
88+
<span className='hidden w-[120px] shrink-0 pt-1 text-[var(--text-muted)] text-xs uppercase tracking-[0.1em] md:block'>
89+
{new Date(p.date).toLocaleDateString('en-US', {
90+
month: 'short',
91+
day: 'numeric',
92+
year: 'numeric',
93+
})}
94+
</span>
95+
96+
<div className='flex min-w-0 flex-1 flex-col gap-1'>
97+
<span className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em] md:hidden'>
98+
{new Date(p.date).toLocaleDateString('en-US', {
99+
month: 'short',
100+
day: 'numeric',
101+
year: 'numeric',
102+
})}
103+
</span>
104+
<h3 className='text-[var(--text-primary)] text-base leading-tight tracking-[-0.01em] lg:text-lg'>
105+
{p.title}
106+
</h3>
107+
<p className='line-clamp-2 text-[var(--text-muted)] text-sm leading-[150%]'>
108+
{p.description}
109+
</p>
110+
</div>
111+
112+
<div className='relative hidden h-[80px] w-[140px] shrink-0 overflow-hidden rounded-[5px] sm:block'>
113+
<Image
114+
src={p.ogImage}
115+
alt={p.title}
116+
fill
117+
sizes='140px'
118+
className='object-cover'
119+
unoptimized
120+
/>
121+
</div>
122+
</Link>
123+
<div className='h-px w-full bg-[var(--border)]' />
69124
</div>
70-
</div>
71-
</Link>
72-
))}
125+
))}
126+
</div>
127+
</div>
128+
129+
<div className='-mt-px h-px w-full bg-[var(--border)]' />
130+
</section>
131+
132+
<div className='mt-[120px] max-sm:mt-16 max-lg:mt-[88px]'>
133+
<Cta />
73134
</div>
74-
</main>
135+
</>
75136
)
76137
}

apps/sim/app/(landing)/components/content-post-page/content-post-page.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,8 @@ export function ContentPostPage({
9696
</Avatar>
9797
) : null}
9898
<Link
99-
href={a?.url || '#'}
100-
target='_blank'
101-
rel='noopener noreferrer author'
99+
href={`${basePath}/authors/${encodeURIComponent(a?.id ?? '')}`}
100+
rel='author'
102101
className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em] hover:text-[var(--text-primary)]'
103102
itemProp='author'
104103
itemScope

apps/sim/app/(landing)/library/authors/[id]/page.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,20 @@ export async function generateMetadata({
1111
params: Promise<{ id: string }>
1212
}): Promise<Metadata> {
1313
const { id } = await params
14-
const posts = (await getAllPostMeta()).filter((p) => p.author.id === id)
15-
return buildAuthorMetadata(id, posts[0]?.author)
14+
const posts = (await getAllPostMeta()).filter((p) => p.authors.some((a) => a.id === id))
15+
const author = posts[0]?.authors.find((a) => a.id === id)
16+
return buildAuthorMetadata(id, author)
1617
}
1718

1819
export default async function AuthorPage({ params }: { params: Promise<{ id: string }> }) {
1920
const { id } = await params
20-
const posts = (await getAllPostMeta()).filter((p) => p.author.id === id)
21-
const author = posts[0]?.author
21+
const posts = (await getAllPostMeta()).filter((p) => p.authors.some((a) => a.id === id))
22+
const author = posts[0]?.authors.find((a) => a.id === id)
2223

2324
return (
2425
<ContentAuthorPage
2526
basePath={LIBRARY_SECTION.basePath}
27+
sectionName={LIBRARY_SECTION.name}
2628
authorName={author?.name}
2729
authorAvatarUrl={author?.avatarUrl}
2830
posts={posts}

apps/sim/app/sitemap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function buildAuthorPages(posts: ContentMeta[], basePath: string): MetadataRoute
2525
}
2626
}
2727
return [...authorsMap.entries()].map(([id, date]) => ({
28-
url: `${SITE_URL}${basePath}/authors/${id}`,
28+
url: `${SITE_URL}${basePath}/authors/${encodeURIComponent(id)}`,
2929
lastModified: date,
3030
}))
3131
}

apps/sim/lib/content/seo.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ export function buildAuthorMetadata(
285285
author?: Author
286286
): Metadata {
287287
const name = author?.name ?? 'Author'
288-
const canonical = `${SITE_URL}${section.basePath}/authors/${id}`
288+
const canonical = `${SITE_URL}${section.basePath}/authors/${encodeURIComponent(id)}`
289289
const description = `Read articles by ${name} on the Sim ${section.name.toLowerCase()}.`
290290
return {
291291
title: `${name} | Sim ${section.name}`,
@@ -318,9 +318,11 @@ export function buildAuthorGraphJsonLd(section: ContentSection, author: Author)
318318
{
319319
'@type': 'Person',
320320
name: author.name,
321-
url: `${SITE_URL}${section.basePath}/authors/${author.id}`,
321+
url: `${SITE_URL}${section.basePath}/authors/${encodeURIComponent(author.id)}`,
322322
sameAs: author.url ? [author.url] : [],
323-
image: author.avatarUrl,
323+
image: author.avatarUrl?.startsWith('http')
324+
? author.avatarUrl
325+
: author.avatarUrl && `${SITE_URL}${author.avatarUrl}`,
324326
worksFor: {
325327
'@type': 'Organization',
326328
name: 'Sim',
@@ -341,7 +343,7 @@ export function buildAuthorGraphJsonLd(section: ContentSection, author: Author)
341343
'@type': 'ListItem',
342344
position: 3,
343345
name: author.name,
344-
item: `${SITE_URL}${section.basePath}/authors/${author.id}`,
346+
item: `${SITE_URL}${section.basePath}/authors/${encodeURIComponent(author.id)}`,
345347
},
346348
],
347349
},

0 commit comments

Comments
 (0)