Skip to content

Commit 5de65c4

Browse files
committed
feat(seo): add context-driven page h1
1 parent 480c160 commit 5de65c4

8 files changed

Lines changed: 76 additions & 36 deletions

File tree

src/lib/components/BlogPost.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
if (!seo) return
3434
seo.title = `${post.title} | ${config.name}`
3535
seo.description = post.description
36+
seo.h1 = undefined
3637
seo.ogSlug = post.ogSlug ?? `blog-${post.slug}`
3738
seo.ogTitle = post.title
3839
}

src/lib/components/BlogPostV2.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
if (!seo) return
5353
seo.title = `${post.title} | ${config.name}`
5454
seo.description = post.description
55+
seo.h1 = undefined
5556
seo.ogSlug = post.ogSlug ?? `blog-${post.slug}`
5657
seo.ogTitle = post.title
5758
}

src/lib/components/CompareIndexV2.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
const namesList = competitors.map((c) => c.name).join(', ')
7474
seo.title = `Compare | ${ours.name} vs ${namesList}`
7575
seo.description = `See how ${ours.npmPackage} compares to ${namesList}. Honest, side-by-side comparisons with feature matrices and verdicts.`
76+
seo.h1 = undefined
7677
seo.ogTitle = `${ours.name} vs Alternatives`
7778
seo.ogTagline = 'Honest, side-by-side comparisons.'
7879
seo.ogFeatures = ogFeatures

src/lib/components/ComparisonPageV2.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
if (!seo) return
7979
seo.title = `${ours.name} vs ${competitor.name} | Compare`
8080
seo.description = competitor.description
81+
seo.h1 = undefined
8182
seo.ogTitle = `vs ${competitor.name}`
8283
seo.ogTagline = competitor.tagline
8384
seo.ogFeatures = ogFeatures

src/lib/components/RootLayout.svelte

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import BreadcrumbContextProvider from './BreadcrumbContextProvider.svelte'
1212
import BreadcrumbJsonLd from './BreadcrumbJsonLd.svelte'
1313
import SeoContextProvider from './SeoContextProvider.svelte'
14+
import SeoH1 from './SeoH1.svelte'
1415
import SeoHead from './SeoHead.svelte'
1516
1617
const {
@@ -34,42 +35,42 @@
3435
const softwareAppJsonLd = $derived(
3536
stars != null
3637
? '<script type="application/ld+json">' +
37-
JSON.stringify({
38-
'@context': 'https://schema.org',
39-
'@type': 'SoftwareApplication',
40-
name: config.name,
41-
description: config.description,
42-
url: config.url,
43-
downloadUrl: npmUrl,
44-
applicationCategory: 'DeveloperApplication',
45-
operatingSystem: 'Any',
46-
softwareRequirements: config.softwareRequirements ?? 'Svelte 5',
47-
license: 'https://opensource.org/licenses/MIT',
48-
keywords: config.keywords,
49-
releaseNotes: `${repoUrl}/releases`,
50-
...(config.programmingLanguages
51-
? { programmingLanguage: config.programmingLanguages }
52-
: {}),
53-
author: {
54-
'@type': 'Organization',
55-
name: 'Humanspeak, Inc.',
56-
url: 'https://humanspeak.com',
57-
sameAs: ['https://github.com/humanspeak', npmUrl, repoUrl]
58-
},
59-
offers: {
60-
'@type': 'Offer',
61-
price: '0',
62-
priceCurrency: 'USD'
63-
},
64-
aggregateRating: {
65-
'@type': 'AggregateRating',
66-
ratingValue: '5',
67-
ratingCount: String(stars),
68-
bestRating: '5'
69-
}
70-
}) +
71-
'<' +
72-
'/script>'
38+
JSON.stringify({
39+
'@context': 'https://schema.org',
40+
'@type': 'SoftwareApplication',
41+
name: config.name,
42+
description: config.description,
43+
url: config.url,
44+
downloadUrl: npmUrl,
45+
applicationCategory: 'DeveloperApplication',
46+
operatingSystem: 'Any',
47+
softwareRequirements: config.softwareRequirements ?? 'Svelte 5',
48+
license: 'https://opensource.org/licenses/MIT',
49+
keywords: config.keywords,
50+
releaseNotes: `${repoUrl}/releases`,
51+
...(config.programmingLanguages
52+
? { programmingLanguage: config.programmingLanguages }
53+
: {}),
54+
author: {
55+
'@type': 'Organization',
56+
name: 'Humanspeak, Inc.',
57+
url: 'https://humanspeak.com',
58+
sameAs: ['https://github.com/humanspeak', npmUrl, repoUrl]
59+
},
60+
offers: {
61+
'@type': 'Offer',
62+
price: '0',
63+
priceCurrency: 'USD'
64+
},
65+
aggregateRating: {
66+
'@type': 'AggregateRating',
67+
ratingValue: '5',
68+
ratingCount: String(stars),
69+
bestRating: '5'
70+
}
71+
}) +
72+
'<' +
73+
'/script>'
7374
: null
7475
)
7576
</script>
@@ -90,5 +91,6 @@
9091
</MotionConfig>
9192
<BreadcrumbJsonLd {config} />
9293
</BreadcrumbContextProvider>
94+
<SeoH1 {seo} />
9395
<SeoHead {seo} {config} {favicon} />
9496
</SeoContextProvider>

src/lib/components/SeoH1.svelte

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<script lang="ts">
2+
import type { SeoContext } from '../contexts/seo.js'
3+
4+
const { seo } = $props<{
5+
seo: SeoContext
6+
}>()
7+
8+
const h1 = $derived(seo.h1)
9+
const isVisible = $derived(h1?.mode === 'visible')
10+
</script>
11+
12+
{#if h1?.title && h1.mode !== 'none'}
13+
<h1 id={h1.id} class={isVisible ? undefined : 'seo-h1-sr-only'}>{h1.title}</h1>
14+
{/if}
15+
16+
<style>
17+
.seo-h1-sr-only {
18+
position: absolute;
19+
width: 1px;
20+
height: 1px;
21+
padding: 0;
22+
margin: -1px;
23+
overflow: hidden;
24+
clip: rect(0, 0, 0, 0);
25+
white-space: nowrap;
26+
border: 0;
27+
}
28+
</style>

src/lib/contexts/seo.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import { getContext, setContext } from 'svelte'
33
export type SeoContext = {
44
title?: string
55
description?: string
6+
h1?: {
7+
title: string
8+
id?: string
9+
mode?: 'sr-only' | 'visible' | 'none'
10+
}
611
ogTitle?: string
712
ogTagline?: string
813
ogFeatures?: string[]

src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export { default as HeaderV2 } from './components/HeaderV2.svelte'
3030
export { default as OG } from './components/OG.svelte'
3131
export { default as RootLayout } from './components/RootLayout.svelte'
3232
export { default as SeoContextProvider } from './components/SeoContextProvider.svelte'
33+
export { default as SeoH1 } from './components/SeoH1.svelte'
3334
export { default as SeoHead } from './components/SeoHead.svelte'
3435
export { default as Sidebar } from './components/Sidebar.svelte'
3536
export { default as SidebarV2 } from './components/SidebarV2.svelte'

0 commit comments

Comments
 (0)