Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions apps/sim/app/(landing)/blog/authors/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@ export async function generateMetadata({
params: Promise<{ id: string }>
}): Promise<Metadata> {
const { id } = await params
const posts = (await getAllPostMeta()).filter((p) => p.author.id === id)
return buildAuthorMetadata(id, posts[0]?.author)
const posts = (await getAllPostMeta()).filter((p) => p.authors.some((a) => a.id === id))
const author = posts[0]?.authors.find((a) => a.id === id)
return buildAuthorMetadata(id, author)
}

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

return (
<ContentAuthorPage
basePath={BLOG_SECTION.basePath}
sectionName={BLOG_SECTION.name}
authorName={author?.name}
authorAvatarUrl={author?.avatarUrl}
posts={posts}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,34 @@ const AUTHOR_POST_SKELETON_COUNT = 4
/** Shared loading skeleton for a content section's author-profile route. */
export function ContentAuthorLoading() {
return (
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
<div className='mb-6 flex items-center gap-3'>
<Skeleton className='size-[40px] rounded-full bg-[var(--surface-hover)]' />
<Skeleton className='h-[32px] w-[160px] rounded-[4px] bg-[var(--surface-hover)]' />
<section className='bg-[var(--bg)]'>
<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'>
<Skeleton className='mb-6 h-[16px] w-[100px] rounded-md bg-[var(--surface-hover)]' />
<div className='flex items-center gap-4'>
<Skeleton className='size-[64px] rounded-full bg-[var(--surface-hover)]' />
<Skeleton className='h-[40px] w-[240px] rounded-[4px] bg-[var(--surface-hover)]' />
</div>
</div>
<div className='grid grid-cols-1 gap-8 sm:grid-cols-2'>
{Array.from({ length: AUTHOR_POST_SKELETON_COUNT }).map((_, i) => (
<div key={i} className='overflow-hidden rounded-lg border border-[var(--border)]'>
<Skeleton className='h-[160px] w-full rounded-none bg-[var(--surface-hover)]' />
<div className='p-3'>
<Skeleton className='mb-1 h-[12px] w-[80px] rounded-[4px] bg-[var(--surface-hover)]' />
<Skeleton className='h-[14px] w-[200px] rounded-[4px] bg-[var(--surface-hover)]' />

<div className='mt-8 h-px w-full bg-[var(--border)]' />

<div className='mx-auto w-full max-w-[1460px] px-20 max-sm:px-5 max-lg:px-8'>
<div className='border-[var(--border)] border-x'>
{Array.from({ length: AUTHOR_POST_SKELETON_COUNT }).map((_, i) => (
<div key={i}>
<div className='flex items-center gap-6 p-6'>
<Skeleton className='hidden h-[14px] w-[120px] rounded-[4px] bg-[var(--surface-hover)] md:block' />
<div className='flex min-w-0 flex-1 flex-col gap-1'>
<Skeleton className='h-[18px] w-[70%] rounded-[4px] bg-[var(--surface-hover)]' />
<Skeleton className='h-[14px] w-[90%] rounded-[4px] bg-[var(--surface-hover)]' />
</div>
<Skeleton className='hidden h-[80px] w-[140px] rounded-[5px] bg-[var(--surface-hover)] sm:block' />
</div>
<div className='h-px w-full bg-[var(--border)]' />
</div>
</div>
))}
))}
</div>
</div>
</main>
</section>
)
}
Original file line number Diff line number Diff line change
@@ -1,76 +1,137 @@
import { ChipLink } from '@sim/emcn'
import Image from 'next/image'
import Link from 'next/link'
import type { ContentMeta } from '@/lib/content/schema'
import { BackLink } from '@/app/(landing)/components/back-link'
import { Cta } from '@/app/(landing)/components/cta/cta'
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". */
sectionName: string
authorName?: string
authorAvatarUrl?: string
/** Posts already filtered down to this author. */
posts: ContentMeta[]
graphJsonLd?: Record<string, unknown>
}

/** Shared author-profile layout for a content section. */
/**
* Shared author-profile layout for a content section: standard page-shell
* header (matching `ContentIndexPage`/`ContentPostPage`) with avatar + name,
* followed by the author's posts in the same framed-list card style used
* everywhere else on the site.
*/
export function ContentAuthorPage({
basePath,
sectionName,
authorName,
authorAvatarUrl,
posts,
graphJsonLd,
}: ContentAuthorPageProps) {
if (!authorName) {
return (
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
<h1 className='text-[32px] text-[var(--text-primary)]'>Author not found</h1>
</main>
<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&apos;re looking for doesn&apos;t exist or has been moved.
</p>
<ChipLink variant='primary' href={basePath} className='mt-3'>
Browse {sectionName}
</ChipLink>
</section>
)
}

return (
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
{graphJsonLd && <JsonLd data={graphJsonLd} />}
<div className='mb-6 flex items-center gap-3'>
{authorAvatarUrl ? (
<Image
src={authorAvatarUrl}
alt={authorName}
width={40}
height={40}
className='rounded-full'
unoptimized
/>
) : null}
<h1 className='text-[32px] text-[var(--text-primary)] leading-tight'>{authorName}</h1>
</div>
<div className='grid grid-cols-1 gap-8 sm:grid-cols-2'>
{posts.map((p) => (
<Link key={p.slug} href={`${basePath}/${p.slug}`} className='group'>
Comment thread
waleedlatif1 marked this conversation as resolved.
<div className='overflow-hidden rounded-lg border border-[var(--border)]'>
<>
<section className='bg-[var(--bg)]'>
{graphJsonLd && <JsonLd data={graphJsonLd} />}

<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'>
<div className='mb-6'>
<BackLink href={basePath} label={`Back to ${sectionName}`} />
</div>

<div className='flex items-center gap-4'>
{authorAvatarUrl ? (
<Image
src={p.ogImage}
alt={p.title}
width={600}
height={315}
className='h-[160px] w-full object-cover transition-transform group-hover:scale-[1.02]'
src={authorAvatarUrl}
alt={authorName}
width={64}
height={64}
className='rounded-full'
unoptimized
/>
<div className='p-3'>
<div className='mb-1 text-[var(--text-muted)] text-xs'>
{new Date(p.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</div>
<div className='text-[var(--text-primary)] text-sm leading-tight'>{p.title}</div>
) : null}
<h1 className='text-balance text-[28px] text-[var(--text-primary)] leading-[100%] tracking-[-0.02em] lg:text-[40px]'>
{authorName}
</h1>
</div>
</div>

<div className='mt-8 h-px w-full bg-[var(--border)]' />

<div className='mx-auto w-full max-w-[1460px] px-20 max-sm:px-5 max-lg:px-8'>
<div className='border-[var(--border)] border-x'>
{posts.map((p) => (
<div key={p.slug}>
<Link
href={`${basePath}/${p.slug}`}
className='group flex items-start gap-6 p-6 transition-colors hover:bg-[var(--surface-hover)] md:items-center'
>
<span className='hidden w-[120px] shrink-0 pt-1 text-[var(--text-muted)] text-xs uppercase tracking-[0.1em] md:block'>
{new Date(p.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</span>

<div className='flex min-w-0 flex-1 flex-col gap-1'>
<span className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em] md:hidden'>
{new Date(p.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</span>
<h3 className='text-[var(--text-primary)] text-base leading-tight tracking-[-0.01em] lg:text-lg'>
{p.title}
</h3>
<p className='line-clamp-2 text-[var(--text-muted)] text-sm leading-[150%]'>
{p.description}
</p>
</div>

<div className='relative hidden h-[80px] w-[140px] shrink-0 overflow-hidden rounded-[5px] sm:block'>
<Image
src={p.ogImage}
alt={p.title}
fill
sizes='140px'
className='object-cover'
unoptimized
/>
</div>
</Link>
<div className='h-px w-full bg-[var(--border)]' />
</div>
</div>
</Link>
))}
))}
</div>
</div>

<div className='-mt-px h-px w-full bg-[var(--border)]' />
</section>

<div className='mt-[120px] max-sm:mt-16 max-lg:mt-[88px]'>
<Cta />
</div>
</main>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,8 @@ export function ContentPostPage({
</Avatar>
) : null}
<Link
href={a?.url || '#'}
target='_blank'
rel='noopener noreferrer author'
href={`${basePath}/authors/${encodeURIComponent(a?.id ?? '')}`}
Comment thread
waleedlatif1 marked this conversation as resolved.
rel='author'
className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em] hover:text-[var(--text-primary)]'
itemProp='author'
itemScope
Expand Down
10 changes: 6 additions & 4 deletions apps/sim/app/(landing)/library/authors/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@ export async function generateMetadata({
params: Promise<{ id: string }>
}): Promise<Metadata> {
const { id } = await params
const posts = (await getAllPostMeta()).filter((p) => p.author.id === id)
return buildAuthorMetadata(id, posts[0]?.author)
const posts = (await getAllPostMeta()).filter((p) => p.authors.some((a) => a.id === id))
const author = posts[0]?.authors.find((a) => a.id === id)
return buildAuthorMetadata(id, author)
}

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

return (
<ContentAuthorPage
basePath={LIBRARY_SECTION.basePath}
sectionName={LIBRARY_SECTION.name}
authorName={author?.name}
authorAvatarUrl={author?.avatarUrl}
posts={posts}
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function buildAuthorPages(posts: ContentMeta[], basePath: string): MetadataRoute
}
}
return [...authorsMap.entries()].map(([id, date]) => ({
url: `${SITE_URL}${basePath}/authors/${id}`,
url: `${SITE_URL}${basePath}/authors/${encodeURIComponent(id)}`,
lastModified: date,
}))
}
Expand Down
10 changes: 6 additions & 4 deletions apps/sim/lib/content/seo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ export function buildAuthorMetadata(
author?: Author
): Metadata {
const name = author?.name ?? 'Author'
const canonical = `${SITE_URL}${section.basePath}/authors/${id}`
const canonical = `${SITE_URL}${section.basePath}/authors/${encodeURIComponent(id)}`
const description = `Read articles by ${name} on the Sim ${section.name.toLowerCase()}.`
return {
title: `${name} | Sim ${section.name}`,
Expand Down Expand Up @@ -318,9 +318,11 @@ export function buildAuthorGraphJsonLd(section: ContentSection, author: Author)
{
'@type': 'Person',
name: author.name,
url: `${SITE_URL}${section.basePath}/authors/${author.id}`,
url: `${SITE_URL}${section.basePath}/authors/${encodeURIComponent(author.id)}`,
sameAs: author.url ? [author.url] : [],
image: author.avatarUrl,
image: author.avatarUrl?.startsWith('http')
? author.avatarUrl
: author.avatarUrl && `${SITE_URL}${author.avatarUrl}`,
worksFor: {
'@type': 'Organization',
name: 'Sim',
Expand All @@ -341,7 +343,7 @@ export function buildAuthorGraphJsonLd(section: ContentSection, author: Author)
'@type': 'ListItem',
position: 3,
name: author.name,
item: `${SITE_URL}${section.basePath}/authors/${author.id}`,
item: `${SITE_URL}${section.basePath}/authors/${encodeURIComponent(author.id)}`,
},
],
},
Expand Down
Loading