Skip to content

Commit 75b1ed3

Browse files
committed
improvement(landing, blog): SEO and GEO optimization
1 parent ebc1948 commit 75b1ed3

File tree

15 files changed

+424
-175
lines changed

15 files changed

+424
-175
lines changed

apps/sim/app/(landing)/blog/[slug]/back-link.tsx

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,39 @@
11
'use client'
22

3-
import { useState } from 'react'
4-
import { ArrowLeft, ChevronLeft } from 'lucide-react'
53
import Link from 'next/link'
64

75
export function BackLink() {
8-
const [isHovered, setIsHovered] = useState(false)
9-
106
return (
117
<Link
128
href='/blog'
13-
className='group flex items-center gap-1 text-[var(--landing-text-muted)] text-sm hover:text-[var(--landing-text)]'
14-
onMouseEnter={() => setIsHovered(true)}
15-
onMouseLeave={() => setIsHovered(false)}
9+
className='group/link inline-flex items-center gap-1.5 font-season text-[var(--landing-text-muted)] text-sm tracking-[0.02em] hover:text-[var(--landing-text)]'
1610
>
17-
<span className='group-hover:-translate-x-0.5 inline-flex transition-transform duration-200'>
18-
{isHovered ? (
19-
<ArrowLeft className='h-4 w-4' aria-hidden='true' />
20-
) : (
21-
<ChevronLeft className='h-4 w-4' aria-hidden='true' />
22-
)}
23-
</span>
11+
<svg
12+
className='h-3 w-3 shrink-0'
13+
viewBox='0 0 10 10'
14+
fill='none'
15+
xmlns='http://www.w3.org/2000/svg'
16+
>
17+
<line
18+
x1='1'
19+
y1='5'
20+
x2='10'
21+
y2='5'
22+
stroke='currentColor'
23+
strokeWidth='1.33'
24+
strokeLinecap='square'
25+
className='origin-right scale-x-0 transition-transform duration-200 ease-out [transform-box:fill-box] group-hover/link:scale-x-100'
26+
/>
27+
<path
28+
d='M6.5 2L3.5 5L6.5 8'
29+
stroke='currentColor'
30+
strokeWidth='1.33'
31+
strokeLinecap='square'
32+
strokeLinejoin='miter'
33+
fill='none'
34+
className='group-hover/link:-translate-x-[30%] transition-transform duration-200 ease-out'
35+
/>
36+
</svg>
2437
Back to Blog
2538
</Link>
2639
)

apps/sim/app/(landing)/blog/[slug]/loading.tsx

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,51 @@ import { Skeleton } from '@/components/emcn'
22

33
export default function BlogPostLoading() {
44
return (
5-
<article className='w-full'>
6-
{/* Header area */}
7-
<div className='mx-auto max-w-[1450px] px-6 pt-8 sm:px-8 sm:pt-12 md:px-12 md:pt-16'>
8-
{/* Back link */}
5+
<article className='w-full bg-[var(--landing-bg)]'>
6+
<div className='px-5 pt-[60px] lg:px-16 lg:pt-[100px]'>
97
<div className='mb-6'>
10-
<Skeleton className='h-[16px] w-[60px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
8+
<Skeleton className='h-[16px] w-[100px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
119
</div>
12-
{/* Image + title row */}
1310
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
14-
{/* Image */}
1511
<div className='w-full flex-shrink-0 md:w-[450px]'>
16-
<Skeleton className='aspect-[450/360] w-full rounded-lg bg-[var(--landing-bg-elevated)]' />
12+
<Skeleton className='aspect-[450/360] w-full rounded-[5px] bg-[var(--landing-bg-elevated)]' />
1713
</div>
18-
{/* Title + author */}
1914
<div className='flex flex-1 flex-col justify-between'>
2015
<div>
21-
<Skeleton className='h-[48px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
22-
<Skeleton className='mt-2 h-[48px] w-[80%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
16+
<Skeleton className='h-[44px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
17+
<Skeleton className='mt-2 h-[44px] w-[80%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
18+
<Skeleton className='mt-4 h-[18px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
19+
<Skeleton className='mt-2 h-[18px] w-[70%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
2320
</div>
24-
<div className='mt-4 flex items-center justify-between'>
21+
<div className='mt-6 flex items-center gap-6'>
22+
<Skeleton className='h-[12px] w-[100px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
2523
<div className='flex items-center gap-2'>
26-
<Skeleton className='h-[24px] w-[24px] rounded-full bg-[var(--landing-bg-elevated)]' />
27-
<Skeleton className='h-[16px] w-[100px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
24+
<Skeleton className='h-[20px] w-[20px] rounded-full bg-[var(--landing-bg-elevated)]' />
25+
<Skeleton className='h-[12px] w-[80px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
2826
</div>
29-
<Skeleton className='h-[32px] w-[32px] rounded-[6px] bg-[var(--landing-bg-elevated)]' />
3027
</div>
3128
</div>
3229
</div>
33-
{/* Divider */}
34-
<Skeleton className='mt-8 h-[1px] w-full bg-[var(--landing-bg-elevated)] sm:mt-12' />
35-
{/* Date + description */}
36-
<div className='flex flex-col gap-6 py-8 sm:flex-row sm:items-start sm:justify-between sm:gap-8 sm:py-10'>
37-
<Skeleton className='h-[16px] w-[120px] flex-shrink-0 rounded-[4px] bg-[var(--landing-bg-elevated)]' />
38-
<div className='flex-1 space-y-2'>
39-
<Skeleton className='h-[20px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
40-
<Skeleton className='h-[20px] w-[70%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
41-
</div>
42-
</div>
4330
</div>
44-
{/* Article body */}
45-
<div className='mx-auto max-w-[900px] px-6 pb-20 sm:px-8 md:px-12'>
46-
<div className='space-y-4'>
47-
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
48-
<Skeleton className='h-[16px] w-[95%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
49-
<Skeleton className='h-[16px] w-[88%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
50-
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
51-
<Skeleton className='mt-6 h-[24px] w-[200px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
52-
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
53-
<Skeleton className='h-[16px] w-[92%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
54-
<Skeleton className='h-[16px] w-[85%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
31+
32+
<div className='mt-8 h-px w-full bg-[var(--landing-bg-elevated)]' />
33+
34+
<div className='mx-5 border-[var(--landing-bg-elevated)] border-x lg:mx-16'>
35+
<div className='mx-auto max-w-[900px] px-6 py-16'>
36+
<div className='space-y-4'>
37+
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
38+
<Skeleton className='h-[16px] w-[95%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
39+
<Skeleton className='h-[16px] w-[88%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
40+
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
41+
<Skeleton className='mt-6 h-[24px] w-[200px] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
42+
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[var(--landing-bg-elevated)]' />
43+
<Skeleton className='h-[16px] w-[92%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
44+
<Skeleton className='h-[16px] w-[85%] rounded-[4px] bg-[var(--landing-bg-elevated)]' />
45+
</div>
5546
</div>
5647
</div>
48+
49+
<div className='-mt-px h-px w-full bg-[var(--landing-bg-elevated)]' />
5750
</article>
5851
)
5952
}

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

Lines changed: 89 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Link from 'next/link'
44
import { Avatar, AvatarFallback, AvatarImage } from '@/components/emcn'
55
import { FAQ } from '@/lib/blog/faq'
66
import { getAllPostMeta, getPostBySlug, getRelatedPosts } from '@/lib/blog/registry'
7-
import { buildArticleJsonLd, buildBreadcrumbJsonLd, buildPostMetadata } from '@/lib/blog/seo'
7+
import { buildPostGraphJsonLd, buildPostMetadata } from '@/lib/blog/seo'
88
import { getBaseUrl } from '@/lib/core/utils/urls'
99
import { BackLink } from '@/app/(landing)/blog/[slug]/back-link'
1010
import { ShareButton } from '@/app/(landing)/blog/[slug]/share-button'
@@ -30,27 +30,27 @@ export default async function Page({ params }: { params: Promise<{ slug: string
3030
const { slug } = await params
3131
const post = await getPostBySlug(slug)
3232
const Article = post.Content
33-
const jsonLd = buildArticleJsonLd(post)
34-
const breadcrumbLd = buildBreadcrumbJsonLd(post)
33+
const graphJsonLd = buildPostGraphJsonLd(post)
3534
const related = await getRelatedPosts(slug, 3)
3635

3736
return (
38-
<article className='w-full' itemScope itemType='https://schema.org/BlogPosting'>
37+
<article
38+
className='w-full bg-[var(--landing-bg)]'
39+
itemScope
40+
itemType='https://schema.org/TechArticle'
41+
>
3942
<script
4043
type='application/ld+json'
41-
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
44+
dangerouslySetInnerHTML={{ __html: JSON.stringify(graphJsonLd) }}
4245
/>
43-
<script
44-
type='application/ld+json'
45-
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbLd) }}
46-
/>
47-
<header className='mx-auto max-w-[1450px] px-6 pt-8 sm:px-8 sm:pt-12 md:px-12 md:pt-16'>
46+
<header className='px-5 pt-[60px] lg:px-16 lg:pt-[100px]'>
4847
<div className='mb-6'>
4948
<BackLink />
5049
</div>
50+
5151
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
5252
<div className='w-full flex-shrink-0 md:w-[450px]'>
53-
<div className='relative w-full overflow-hidden rounded-lg'>
53+
<div className='relative w-full overflow-hidden rounded-[5px]'>
5454
<Image
5555
src={post.ogImage}
5656
alt={post.title}
@@ -65,18 +65,35 @@ export default async function Page({ params }: { params: Promise<{ slug: string
6565
</div>
6666
</div>
6767
<div className='flex flex-1 flex-col justify-between'>
68-
<h1
69-
className='text-balance font-[500] text-[36px] text-[var(--landing-text)] leading-tight tracking-tight sm:text-[48px] md:text-[56px] lg:text-[64px]'
70-
itemProp='headline'
71-
>
72-
{post.title}
73-
</h1>
74-
<div className='mt-4 flex items-center justify-between'>
68+
<div>
69+
<h1
70+
className='text-balance font-[430] font-season text-[28px] text-white leading-[110%] tracking-[-0.02em] sm:text-[36px] md:text-[44px] lg:text-[52px]'
71+
itemProp='headline'
72+
>
73+
{post.title}
74+
</h1>
75+
<p className='mt-4 font-[430] font-season text-[var(--landing-text-body)] text-base leading-[150%] tracking-[0.02em] sm:text-lg'>
76+
{post.description}
77+
</p>
78+
</div>
79+
<div className='mt-6 flex items-center gap-6'>
80+
<time
81+
className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'
82+
dateTime={post.date}
83+
itemProp='datePublished'
84+
>
85+
{new Date(post.date).toLocaleDateString('en-US', {
86+
month: 'short',
87+
day: 'numeric',
88+
year: 'numeric',
89+
})}
90+
</time>
91+
<meta itemProp='dateModified' content={post.updated ?? post.date} />
7592
<div className='flex items-center gap-3'>
7693
{(post.authors || [post.author]).map((a, idx) => (
7794
<div key={idx} className='flex items-center gap-2'>
7895
{a?.avatarUrl ? (
79-
<Avatar className='size-6'>
96+
<Avatar className='size-5'>
8097
<AvatarImage src={a.avatarUrl} alt={a.name} />
8198
<AvatarFallback>{a.name.slice(0, 2)}</AvatarFallback>
8299
</Avatar>
@@ -85,7 +102,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
85102
href={a?.url || '#'}
86103
target='_blank'
87104
rel='noopener noreferrer author'
88-
className='text-[var(--landing-text-muted)] text-sm leading-[1.5] hover:text-[var(--landing-text)] sm:text-md'
105+
className='font-martian-mono text-[var(--landing-text-muted)] text-xs uppercase tracking-[0.1em] hover:text-white'
89106
itemProp='author'
90107
itemScope
91108
itemType='https://schema.org/Person'
@@ -95,78 +112,72 @@ export default async function Page({ params }: { params: Promise<{ slug: string
95112
</div>
96113
))}
97114
</div>
98-
<ShareButton url={`${getBaseUrl()}/blog/${slug}`} title={post.title} />
115+
<div className='ml-auto'>
116+
<ShareButton url={`${getBaseUrl()}/blog/${slug}`} title={post.title} />
117+
</div>
99118
</div>
100119
</div>
101120
</div>
102-
<hr className='mt-8 border-[var(--landing-bg-elevated)] border-t sm:mt-12' />
103-
<div className='flex flex-col gap-6 py-8 sm:flex-row sm:items-start sm:justify-between sm:gap-8 sm:py-10'>
104-
<div className='flex flex-shrink-0 items-center gap-4'>
105-
<time
106-
className='block text-[var(--landing-text-muted)] text-sm leading-[1.5] sm:text-md'
107-
dateTime={post.date}
108-
itemProp='datePublished'
109-
>
110-
{new Date(post.date).toLocaleDateString('en-US', {
111-
month: 'short',
112-
day: 'numeric',
113-
year: 'numeric',
114-
})}
115-
</time>
116-
<meta itemProp='dateModified' content={post.updated ?? post.date} />
117-
</div>
118-
<div className='flex-1'>
119-
<p className='m-0 block translate-y-[-4px] font-[400] text-[var(--landing-text-muted)] text-lg leading-[1.5] sm:text-[20px] md:text-[26px]'>
120-
{post.description}
121-
</p>
122-
</div>
123-
</div>
124121
</header>
125122

126-
<div className='mx-auto max-w-[900px] px-6 pb-20 sm:px-8 md:px-12' itemProp='articleBody'>
127-
<div className='prose prose-lg prose-invert max-w-none prose-blockquote:border-[var(--landing-border-strong)] prose-hr:border-[var(--landing-bg-elevated)] prose-a:text-[var(--landing-text)] prose-blockquote:text-[var(--landing-text-muted)] prose-code:text-[var(--landing-text)] prose-headings:text-[var(--landing-text)] prose-li:text-[var(--landing-text-muted)] prose-p:text-[var(--landing-text-muted)] prose-strong:text-[var(--landing-text)]'>
128-
<Article />
129-
{post.faq && post.faq.length > 0 ? <FAQ items={post.faq} /> : null}
123+
<div className='mt-8 h-px w-full bg-[var(--landing-bg-elevated)]' />
124+
125+
<div className='mx-5 border-[var(--landing-bg-elevated)] border-x lg:mx-16'>
126+
<div className='mx-auto max-w-[900px] px-6 py-16' itemProp='articleBody'>
127+
<div className='prose prose-lg prose-invert max-w-none prose-blockquote:border-[var(--landing-border-strong)] prose-hr:border-[var(--landing-bg-elevated)] prose-headings:font-[430] prose-headings:font-season prose-a:text-white prose-blockquote:text-[var(--landing-text-muted)] prose-code:text-white prose-headings:text-white prose-li:text-[var(--landing-text-body)] prose-p:text-[var(--landing-text-body)] prose-strong:text-white prose-headings:tracking-[-0.02em]'>
128+
<Article />
129+
{post.faq && post.faq.length > 0 ? <FAQ items={post.faq} /> : null}
130+
</div>
130131
</div>
131-
</div>
132-
{related.length > 0 && (
133-
<div className='mx-auto max-w-[900px] px-6 pb-24 sm:px-8 md:px-12'>
134-
<h2 className='mb-4 font-[500] text-[24px] text-[var(--landing-text)]'>Related posts</h2>
135-
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3'>
136-
{related.map((p) => (
137-
<Link key={p.slug} href={`/blog/${p.slug}`} className='group'>
138-
<div className='overflow-hidden rounded-lg border border-[var(--landing-bg-elevated)]'>
139-
<Image
140-
src={p.ogImage}
141-
alt={p.title}
142-
width={600}
143-
height={315}
144-
className='h-[160px] w-full object-cover'
145-
sizes='(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw'
146-
loading='lazy'
147-
unoptimized
148-
/>
149-
<div className='p-3'>
150-
<div className='mb-1 text-[var(--landing-text-muted)] text-xs'>
132+
133+
{related.length > 0 && (
134+
<>
135+
<div className='h-px w-full bg-[var(--landing-bg-elevated)]' />
136+
<nav aria-label='Related posts' className='flex'>
137+
{related.map((p) => (
138+
<Link
139+
key={p.slug}
140+
href={`/blog/${p.slug}`}
141+
className='group flex flex-1 flex-col gap-4 border-[var(--landing-bg-elevated)] p-6 transition-colors hover:bg-[var(--landing-bg-elevated)] md:border-l md:first:border-l-0'
142+
>
143+
<div className='relative aspect-video w-full overflow-hidden rounded-[5px]'>
144+
<Image
145+
src={p.ogImage}
146+
alt={p.title}
147+
fill
148+
sizes='(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
149+
className='object-cover'
150+
loading='lazy'
151+
unoptimized
152+
/>
153+
</div>
154+
<div className='flex flex-col gap-2'>
155+
<span className='font-martian-mono text-[var(--landing-text-subtle)] text-xs uppercase tracking-[0.1em]'>
151156
{new Date(p.date).toLocaleDateString('en-US', {
152157
month: 'short',
153-
day: 'numeric',
154-
year: 'numeric',
158+
year: '2-digit',
155159
})}
156-
</div>
157-
<div className='font-[500] text-[var(--landing-text)] text-sm leading-tight'>
160+
</span>
161+
<h3 className='font-[430] font-season text-lg text-white leading-tight tracking-[-0.01em]'>
158162
{p.title}
159-
</div>
163+
</h3>
164+
<p className='line-clamp-2 text-[var(--landing-text-muted)] text-sm leading-[150%]'>
165+
{p.description}
166+
</p>
160167
</div>
161-
</div>
162-
</Link>
163-
))}
164-
</div>
165-
</div>
166-
)}
168+
</Link>
169+
))}
170+
</nav>
171+
</>
172+
)}
173+
</div>
174+
175+
<div className='-mt-px h-px w-full bg-[var(--landing-bg-elevated)]' />
176+
167177
<meta itemProp='publisher' content='Sim' />
168178
<meta itemProp='inLanguage' content='en-US' />
169179
<meta itemProp='keywords' content={post.tags.join(', ')} />
180+
{post.wordCount && <meta itemProp='wordCount' content={String(post.wordCount)} />}
170181
</article>
171182
)
172183
}

0 commit comments

Comments
 (0)