Skip to content

Commit da5fb6c

Browse files
committed
refactor(blog): split AEO/GEO content into a new /library section
Blog was mixing hand-written editorial posts with listicle/comparison/how-to posts optimized for answer engines. Extract a generic content engine (lib/content) shared by both sections, move the 6 AEO/GEO posts into a new /library route tree, and keep /blog editorial-only. - lib/content: generic registry factory, MDX components, SEO builders - lib/blog + lib/library: thin per-section instantiations over the shared engine - app/(landing)/library: mirrors the blog route tree via shared Content*Page components - app/sitemap.ts, app/robots.ts, navbar: updated for the new section - next.config.ts: permanent redirects from moved /blog/<slug> URLs to /library/<slug>
1 parent 7673071 commit da5fb6c

90 files changed

Lines changed: 1923 additions & 1197 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/sim/app/(landing)/CLAUDE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Landing Page - Build & Optimization Instructions
22

3-
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.
3+
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.
4+
5+
`/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.
46

57
## What this is
68

Lines changed: 2 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,5 @@
1-
import { Skeleton } from '@sim/emcn'
1+
import { ContentPostLoading } from '@/app/(landing)/components'
22

33
export default function BlogPostLoading() {
4-
return (
5-
<article className='w-full bg-[var(--bg)]'>
6-
<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'>
7-
<div className='mb-6'>
8-
<Skeleton className='h-[16px] w-[100px] rounded-[4px] bg-[var(--surface-hover)]' />
9-
</div>
10-
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
11-
<div className='w-full flex-shrink-0 md:w-[450px]'>
12-
<Skeleton className='aspect-[450/360] w-full rounded-[5px] bg-[var(--surface-hover)]' />
13-
</div>
14-
<div className='flex flex-1 flex-col justify-between'>
15-
<div>
16-
<Skeleton className='h-[44px] w-full rounded-[4px] bg-[var(--surface-hover)]' />
17-
<Skeleton className='mt-2 h-[44px] w-[80%] rounded-[4px] bg-[var(--surface-hover)]' />
18-
<Skeleton className='mt-4 h-[18px] w-full rounded-[4px] bg-[var(--surface-hover)]' />
19-
<Skeleton className='mt-2 h-[18px] w-[70%] rounded-[4px] bg-[var(--surface-hover)]' />
20-
</div>
21-
<div className='mt-6 flex items-center gap-6'>
22-
<Skeleton className='h-[12px] w-[100px] rounded-[4px] bg-[var(--surface-hover)]' />
23-
<div className='flex items-center gap-2'>
24-
<Skeleton className='size-[20px] rounded-full bg-[var(--surface-hover)]' />
25-
<Skeleton className='h-[12px] w-[80px] rounded-[4px] bg-[var(--surface-hover)]' />
26-
</div>
27-
</div>
28-
</div>
29-
</div>
30-
</div>
31-
32-
<div className='mt-8 h-px w-full bg-[var(--border)]' />
33-
34-
<div className='mx-auto w-full max-w-[1460px]'>
35-
<div className='mx-20 border-[var(--border)] border-x max-sm:mx-5 max-lg:mx-8'>
36-
<div className='mx-auto max-w-[900px] px-6 py-16'>
37-
<div className='space-y-4'>
38-
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--surface-hover)]' />
39-
<Skeleton className='h-[16px] w-[95%] rounded-[4px] bg-[var(--surface-hover)]' />
40-
<Skeleton className='h-[16px] w-[88%] rounded-[4px] bg-[var(--surface-hover)]' />
41-
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--surface-hover)]' />
42-
<Skeleton className='mt-6 h-[24px] w-[200px] rounded-[4px] bg-[var(--surface-hover)]' />
43-
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--surface-hover)]' />
44-
<Skeleton className='h-[16px] w-[92%] rounded-[4px] bg-[var(--surface-hover)]' />
45-
<Skeleton className='h-[16px] w-[85%] rounded-[4px] bg-[var(--surface-hover)]' />
46-
</div>
47-
</div>
48-
</div>
49-
</div>
50-
51-
<div className='-mt-px h-px w-full bg-[var(--border)]' />
52-
</article>
53-
)
4+
return <ContentPostLoading />
545
}
Lines changed: 10 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
1-
import { Avatar, AvatarFallback, AvatarImage } from '@sim/emcn'
21
import type { Metadata } from 'next'
3-
import Image from 'next/image'
4-
import Link from 'next/link'
5-
import { FAQ } from '@/lib/blog/faq'
62
import { getAllPostMeta, getPostBySlug, getRelatedPosts } from '@/lib/blog/registry'
7-
import { buildPostGraphJsonLd, buildPostMetadata } from '@/lib/blog/seo'
3+
import { BLOG_SECTION, buildPostGraphJsonLd, buildPostMetadata } from '@/lib/blog/seo'
84
import { getBaseUrl } from '@/lib/core/utils/urls'
9-
import { ShareButton } from '@/app/(landing)/blog/[slug]/share-button'
10-
import { BackLink } from '@/app/(landing)/components'
11-
import { JsonLd } from '@/app/(landing)/components/json-ld'
5+
import { ContentPostPage } from '@/app/(landing)/components'
126

137
export const dynamicParams = false
148

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

3931
return (
40-
<article className='w-full bg-[var(--bg)]' itemScope itemType='https://schema.org/TechArticle'>
41-
<JsonLd data={graphJsonLd} />
42-
<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'>
43-
<div className='mb-6'>
44-
<BackLink href='/blog' label='Back to Blog' />
45-
</div>
46-
47-
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
48-
<div className='w-full flex-shrink-0 md:w-[450px]'>
49-
<div className='relative w-full overflow-hidden rounded-[5px]'>
50-
<Image
51-
src={post.ogImage}
52-
alt={post.title}
53-
width={450}
54-
height={360}
55-
className='h-auto w-full'
56-
sizes='(max-width: 768px) 100vw, 450px'
57-
priority
58-
itemProp='image'
59-
unoptimized
60-
/>
61-
</div>
62-
</div>
63-
<div className='flex flex-1 flex-col justify-between'>
64-
<div>
65-
<h1
66-
className='text-balance text-[28px] text-[var(--text-primary)] leading-[110%] tracking-[-0.02em] sm:text-[36px] md:text-[44px] lg:text-[52px]'
67-
itemProp='headline'
68-
>
69-
{post.title}
70-
</h1>
71-
<p className='mt-4 text-[var(--text-body)] text-base leading-[150%] tracking-[0.02em] sm:text-lg'>
72-
{post.description}
73-
</p>
74-
</div>
75-
<div className='mt-6 flex items-center gap-6'>
76-
<time
77-
className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em]'
78-
dateTime={post.date}
79-
itemProp='datePublished'
80-
>
81-
{new Date(post.date).toLocaleDateString('en-US', {
82-
month: 'short',
83-
day: 'numeric',
84-
year: 'numeric',
85-
})}
86-
</time>
87-
<meta itemProp='dateModified' content={post.updated ?? post.date} />
88-
<div className='flex items-center gap-3'>
89-
{(post.authors || [post.author]).map((a) => (
90-
<div key={a?.name} className='flex items-center gap-2'>
91-
{a?.avatarUrl ? (
92-
<Avatar className='size-5'>
93-
<AvatarImage src={a.avatarUrl} alt={a.name} />
94-
<AvatarFallback>{a.name.slice(0, 2)}</AvatarFallback>
95-
</Avatar>
96-
) : null}
97-
<Link
98-
href={a?.url || '#'}
99-
target='_blank'
100-
rel='noopener noreferrer author'
101-
className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em] hover:text-[var(--text-primary)]'
102-
itemProp='author'
103-
itemScope
104-
itemType='https://schema.org/Person'
105-
>
106-
<span itemProp='name'>{a?.name}</span>
107-
</Link>
108-
</div>
109-
))}
110-
</div>
111-
<div className='ml-auto'>
112-
<ShareButton url={`${getBaseUrl()}/blog/${slug}`} title={post.title} />
113-
</div>
114-
</div>
115-
</div>
116-
</div>
117-
</header>
118-
119-
<div className='mt-8 h-px w-full bg-[var(--border)]' />
120-
121-
<div className='mx-auto w-full max-w-[1460px] px-20 max-sm:px-5 max-lg:px-8'>
122-
<div className='border-[var(--border)] border-x'>
123-
<div className='mx-auto max-w-[900px] px-6 py-16' itemProp='articleBody'>
124-
<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]'>
125-
<Article />
126-
{post.faq && post.faq.length > 0 ? <FAQ items={post.faq} /> : null}
127-
</div>
128-
</div>
129-
130-
{related.length > 0 && (
131-
<>
132-
<div className='h-px w-full bg-[var(--border)]' />
133-
<nav aria-label='Related posts' className='flex flex-col sm:flex-row'>
134-
{related.map((p) => (
135-
<Link
136-
key={p.slug}
137-
href={`/blog/${p.slug}`}
138-
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'
139-
>
140-
<div className='relative aspect-video w-full overflow-hidden rounded-[5px]'>
141-
<Image
142-
src={p.ogImage}
143-
alt={p.title}
144-
fill
145-
sizes='(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
146-
className='object-cover'
147-
loading='lazy'
148-
unoptimized
149-
/>
150-
</div>
151-
<div className='flex flex-col gap-2'>
152-
<span className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em]'>
153-
{new Date(p.date).toLocaleDateString('en-US', {
154-
month: 'short',
155-
year: '2-digit',
156-
})}
157-
</span>
158-
<h3 className='text-[var(--text-primary)] text-lg leading-tight tracking-[-0.01em]'>
159-
{p.title}
160-
</h3>
161-
<p className='line-clamp-2 text-[var(--text-muted)] text-sm leading-[150%]'>
162-
{p.description}
163-
</p>
164-
</div>
165-
</Link>
166-
))}
167-
</nav>
168-
</>
169-
)}
170-
</div>
171-
</div>
172-
173-
<div className='-mt-px h-px w-full bg-[var(--border)]' />
174-
175-
<meta itemProp='publisher' content='Sim' />
176-
<meta itemProp='inLanguage' content='en-US' />
177-
<meta itemProp='keywords' content={post.tags.join(', ')} />
178-
{post.wordCount && <meta itemProp='wordCount' content={String(post.wordCount)} />}
179-
</article>
32+
<ContentPostPage
33+
basePath={BLOG_SECTION.basePath}
34+
backLabel='Back to Blog'
35+
post={post}
36+
related={related}
37+
graphJsonLd={buildPostGraphJsonLd(post)}
38+
shareUrl={`${getBaseUrl()}${BLOG_SECTION.basePath}/${slug}`}
39+
/>
18040
)
18141
}
Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,5 @@
1-
import { Skeleton } from '@sim/emcn'
2-
3-
const SKELETON_POST_COUNT = 4
1+
import { ContentAuthorLoading } from '@/app/(landing)/components'
42

53
export default function AuthorLoading() {
6-
return (
7-
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
8-
<div className='mb-6 flex items-center gap-3'>
9-
<Skeleton className='size-[40px] rounded-full bg-[var(--surface-hover)]' />
10-
<Skeleton className='h-[32px] w-[160px] rounded-[4px] bg-[var(--surface-hover)]' />
11-
</div>
12-
<div className='grid grid-cols-1 gap-8 sm:grid-cols-2'>
13-
{Array.from({ length: SKELETON_POST_COUNT }).map((_, i) => (
14-
<div key={i} className='overflow-hidden rounded-lg border border-[var(--border)]'>
15-
<Skeleton className='h-[160px] w-full rounded-none bg-[var(--surface-hover)]' />
16-
<div className='p-3'>
17-
<Skeleton className='mb-1 h-[12px] w-[80px] rounded-[4px] bg-[var(--surface-hover)]' />
18-
<Skeleton className='h-[14px] w-[200px] rounded-[4px] bg-[var(--surface-hover)]' />
19-
</div>
20-
</div>
21-
))}
22-
</div>
23-
</main>
24-
)
4+
return <ContentAuthorLoading />
255
}

0 commit comments

Comments
 (0)