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
4 changes: 3 additions & 1 deletion apps/sim/app/(landing)/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Landing Page - Build & Optimization Instructions

This route group owns `/` and the entire public marketing surface - the home page, platform/solutions pages, pricing, legal, and the marketing subroutes (`/blog`, `/models`, `/integrations`, `/demo`, `/partners`, `/changelog`). Read this file in full before adding or changing anything here. Positioning and language rules live in `.claude/rules/constitution.md`; SEO/GEO rules in `.claude/rules/landing-seo-geo.md`. Both apply to every file in this directory.
This route group owns `/` and the entire public marketing surface - the home page, platform/solutions pages, pricing, legal, and the marketing subroutes (`/blog`, `/library`, `/models`, `/integrations`, `/demo`, `/partners`, `/changelog`). Read this file in full before adding or changing anything here. Positioning and language rules live in `.claude/rules/constitution.md`; SEO/GEO rules in `.claude/rules/landing-seo-geo.md`. Both apply to every file in this directory.

`/blog` (editorial/company-voice posts) and `/library` (AEO/GEO content - listicles, comparisons, how-tos) are two separate route trees over one shared engine: `apps/sim/lib/content/` (generic registry factory, MDX components, SEO builders) instantiated per-section by the thin `apps/sim/lib/blog/` and `apps/sim/lib/library/` modules, rendered through the shared `Content*Page` components in `components/`. Content lives in `apps/sim/content/blog/` and `apps/sim/content/library/`; both share `apps/sim/content/authors/`. Adding a post to either section, or adding a new content section entirely, must reuse this engine - never hand-roll a divergent registry or page layout. Every new marketing subroute (including any future content section) needs `app/sitemap.ts` and `app/robots.ts` updated, same as `/blog` and `/library` were.

## What this is

Expand Down
53 changes: 2 additions & 51 deletions apps/sim/app/(landing)/blog/[slug]/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,5 @@
import { Skeleton } from '@sim/emcn'
import { ContentPostLoading } from '@/app/(landing)/components'

export default function BlogPostLoading() {
return (
<article className='w-full 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'>
<div className='mb-6'>
<Skeleton className='h-[16px] w-[100px] rounded-[4px] bg-[var(--surface-hover)]' />
</div>
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
<div className='w-full flex-shrink-0 md:w-[450px]'>
<Skeleton className='aspect-[450/360] w-full rounded-[5px] bg-[var(--surface-hover)]' />
</div>
<div className='flex flex-1 flex-col justify-between'>
<div>
<Skeleton className='h-[44px] w-full rounded-[4px] bg-[var(--surface-hover)]' />
<Skeleton className='mt-2 h-[44px] w-[80%] rounded-[4px] bg-[var(--surface-hover)]' />
<Skeleton className='mt-4 h-[18px] w-full rounded-[4px] bg-[var(--surface-hover)]' />
<Skeleton className='mt-2 h-[18px] w-[70%] rounded-[4px] bg-[var(--surface-hover)]' />
</div>
<div className='mt-6 flex items-center gap-6'>
<Skeleton className='h-[12px] w-[100px] rounded-[4px] bg-[var(--surface-hover)]' />
<div className='flex items-center gap-2'>
<Skeleton className='size-[20px] rounded-full bg-[var(--surface-hover)]' />
<Skeleton className='h-[12px] w-[80px] rounded-[4px] bg-[var(--surface-hover)]' />
</div>
</div>
</div>
</div>
</div>

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

<div className='mx-auto w-full max-w-[1460px]'>
<div className='mx-20 border-[var(--border)] border-x max-sm:mx-5 max-lg:mx-8'>
<div className='mx-auto max-w-[900px] px-6 py-16'>
<div className='space-y-4'>
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--surface-hover)]' />
<Skeleton className='h-[16px] w-[95%] rounded-[4px] bg-[var(--surface-hover)]' />
<Skeleton className='h-[16px] w-[88%] rounded-[4px] bg-[var(--surface-hover)]' />
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--surface-hover)]' />
<Skeleton className='mt-6 h-[24px] w-[200px] rounded-[4px] bg-[var(--surface-hover)]' />
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--surface-hover)]' />
<Skeleton className='h-[16px] w-[92%] rounded-[4px] bg-[var(--surface-hover)]' />
<Skeleton className='h-[16px] w-[85%] rounded-[4px] bg-[var(--surface-hover)]' />
</div>
</div>
</div>
</div>

<div className='-mt-px h-px w-full bg-[var(--border)]' />
</article>
)
return <ContentPostLoading />
}
160 changes: 10 additions & 150 deletions apps/sim/app/(landing)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import { Avatar, AvatarFallback, AvatarImage } from '@sim/emcn'
import type { Metadata } from 'next'
import Image from 'next/image'
import Link from 'next/link'
import { FAQ } from '@/lib/blog/faq'
import { getAllPostMeta, getPostBySlug, getRelatedPosts } from '@/lib/blog/registry'
import { buildPostGraphJsonLd, buildPostMetadata } from '@/lib/blog/seo'
import { BLOG_SECTION, buildPostGraphJsonLd, buildPostMetadata } from '@/lib/blog/seo'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { ShareButton } from '@/app/(landing)/blog/[slug]/share-button'
import { BackLink } from '@/app/(landing)/components'
import { JsonLd } from '@/app/(landing)/components/json-ld'
import { ContentPostPage } from '@/app/(landing)/components'

export const dynamicParams = false

Expand All @@ -32,150 +26,16 @@ export const revalidate = 86400
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPostBySlug(slug)
const Article = post.Content
const graphJsonLd = buildPostGraphJsonLd(post)
const related = await getRelatedPosts(slug, 3)

return (
<article className='w-full bg-[var(--bg)]' itemScope itemType='https://schema.org/TechArticle'>
<JsonLd data={graphJsonLd} />
<header 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='/blog' label='Back to Blog' />
</div>

<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
<div className='w-full flex-shrink-0 md:w-[450px]'>
<div className='relative w-full overflow-hidden rounded-[5px]'>
<Image
src={post.ogImage}
alt={post.title}
width={450}
height={360}
className='h-auto w-full'
sizes='(max-width: 768px) 100vw, 450px'
priority
itemProp='image'
unoptimized
/>
</div>
</div>
<div className='flex flex-1 flex-col justify-between'>
<div>
<h1
className='text-balance text-[28px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em] sm:text-[36px] md:text-[44px] lg:text-[52px]'
itemProp='headline'
>
{post.title}
</h1>
<p className='mt-4 text-[var(--text-body)] text-base leading-[150%] tracking-[0.02em] sm:text-lg'>
{post.description}
</p>
</div>
<div className='mt-6 flex items-center gap-6'>
<time
className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em]'
dateTime={post.date}
itemProp='datePublished'
>
{new Date(post.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</time>
<meta itemProp='dateModified' content={post.updated ?? post.date} />
<div className='flex items-center gap-3'>
{(post.authors || [post.author]).map((a) => (
<div key={a?.name} className='flex items-center gap-2'>
{a?.avatarUrl ? (
<Avatar className='size-5'>
<AvatarImage src={a.avatarUrl} alt={a.name} />
<AvatarFallback>{a.name.slice(0, 2)}</AvatarFallback>
</Avatar>
) : null}
<Link
href={a?.url || '#'}
target='_blank'
rel='noopener noreferrer author'
className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em] hover:text-[var(--text-primary)]'
itemProp='author'
itemScope
itemType='https://schema.org/Person'
>
<span itemProp='name'>{a?.name}</span>
</Link>
</div>
))}
</div>
<div className='ml-auto'>
<ShareButton url={`${getBaseUrl()}/blog/${slug}`} title={post.title} />
</div>
</div>
</div>
</div>
</header>

<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'>
<div className='mx-auto max-w-[900px] px-6 py-16' itemProp='articleBody'>
<div className='prose prose-lg max-w-none prose-blockquote:border-[var(--border-1)] prose-hr:border-[var(--border)] prose-headings:font-season prose-a:text-[var(--text-primary)] prose-blockquote:text-[var(--text-muted)] prose-code:text-[var(--text-primary)] prose-headings:text-[var(--text-primary)] prose-li:text-[var(--text-body)] prose-p:text-[var(--text-body)] prose-strong:text-[var(--text-primary)] prose-headings:tracking-[-0.02em]'>
<Article />
{post.faq && post.faq.length > 0 ? <FAQ items={post.faq} /> : null}
</div>
</div>

{related.length > 0 && (
<>
<div className='h-px w-full bg-[var(--border)]' />
<nav aria-label='Related posts' className='flex flex-col sm:flex-row'>
{related.map((p) => (
<Link
key={p.slug}
href={`/blog/${p.slug}`}
className='group flex flex-1 flex-col gap-4 border-[var(--border)] border-t p-6 transition-colors first:border-t-0 hover:bg-[var(--surface-hover)] sm:border-t-0 sm:border-l sm:first:border-l-0'
>
<div className='relative aspect-video w-full overflow-hidden rounded-[5px]'>
<Image
src={p.ogImage}
alt={p.title}
fill
sizes='(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
className='object-cover'
loading='lazy'
unoptimized
/>
</div>
<div className='flex flex-col gap-2'>
<span className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em]'>
{new Date(p.date).toLocaleDateString('en-US', {
month: 'short',
year: '2-digit',
})}
</span>
<h3 className='text-[var(--text-primary)] text-lg leading-tight tracking-[-0.01em]'>
{p.title}
</h3>
<p className='line-clamp-2 text-[var(--text-muted)] text-sm leading-[150%]'>
{p.description}
</p>
</div>
</Link>
))}
</nav>
</>
)}
</div>
</div>

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

<meta itemProp='publisher' content='Sim' />
<meta itemProp='inLanguage' content='en-US' />
<meta itemProp='keywords' content={post.tags.join(', ')} />
{post.wordCount && <meta itemProp='wordCount' content={String(post.wordCount)} />}
</article>
<ContentPostPage
basePath={BLOG_SECTION.basePath}
backLabel='Back to Blog'
post={post}
related={related}
graphJsonLd={buildPostGraphJsonLd(post)}
shareUrl={`${getBaseUrl()}${BLOG_SECTION.basePath}/${slug}`}
/>
)
}
24 changes: 2 additions & 22 deletions apps/sim/app/(landing)/blog/authors/[id]/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,5 @@
import { Skeleton } from '@sim/emcn'

const SKELETON_POST_COUNT = 4
import { ContentAuthorLoading } from '@/app/(landing)/components'

export default function AuthorLoading() {
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)]' />
</div>
<div className='grid grid-cols-1 gap-8 sm:grid-cols-2'>
{Array.from({ length: SKELETON_POST_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>
</div>
))}
</div>
</main>
)
return <ContentAuthorLoading />
}
Loading
Loading