@growth-labs/seo 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/components/AeoHead.astro +14 -2
- package/src/components/SeoHead.astro +131 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growth-labs/seo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"types": "./dist/cron/prune-aeo-r2.d.ts",
|
|
42
42
|
"import": "./dist/cron/prune-aeo-r2.js"
|
|
43
43
|
},
|
|
44
|
-
"./components/AeoHead.astro": "./src/components/AeoHead.astro"
|
|
44
|
+
"./components/AeoHead.astro": "./src/components/AeoHead.astro",
|
|
45
|
+
"./components/SeoHead.astro": "./src/components/SeoHead.astro"
|
|
45
46
|
},
|
|
46
47
|
"files": [
|
|
47
48
|
"dist",
|
|
@@ -17,9 +17,21 @@ export interface Props {
|
|
|
17
17
|
* differs from the request URL (e.g. canonicalizing away trailing slash).
|
|
18
18
|
*/
|
|
19
19
|
canonical?: URL | string
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Emit the `<link rel="alternate" type="text/markdown">` pointer for the
|
|
23
|
+
* current page. Defaults to `true`, matching 0.2.3/0.2.4 behavior.
|
|
24
|
+
*
|
|
25
|
+
* Pass `false` on routes where no `.md` twin exists (homepage, tag
|
|
26
|
+
* indexes, landing pages, search) to avoid a link that 404s. Suppressing
|
|
27
|
+
* the twin link here does NOT suppress other head tags — Apple News
|
|
28
|
+
* discovery still emits, centralized in one place rather than wrapping
|
|
29
|
+
* the whole `<AeoHead />` in a conditional.
|
|
30
|
+
*/
|
|
31
|
+
emitTwinLink?: boolean
|
|
20
32
|
}
|
|
21
33
|
|
|
22
|
-
const { canonical } = Astro.props
|
|
34
|
+
const { canonical, emitTwinLink = true } = Astro.props
|
|
23
35
|
const config = getConfig()
|
|
24
36
|
const aeo = resolveAeoTwins(config.aeoTwins)
|
|
25
37
|
|
|
@@ -46,7 +58,7 @@ function defaultTwinUrl(url: string): string {
|
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
const twinHref =
|
|
49
|
-
aeo && aeo.mode !== 'middleware'
|
|
61
|
+
emitTwinLink && aeo && aeo.mode !== 'middleware'
|
|
50
62
|
? (aeo.twinUrl ?? defaultTwinUrl)(pageUrl)
|
|
51
63
|
: null
|
|
52
64
|
---
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Comprehensive SEO <head> component. Composes <AeoHead /> and emits every
|
|
3
|
+
// SEO-related head tag the package produces:
|
|
4
|
+
// - <meta name="robots"> with max-image-preview:large (default)
|
|
5
|
+
// - OpenGraph + Twitter Card meta (via generateMeta — article / product / website)
|
|
6
|
+
// - <link rel="canonical">
|
|
7
|
+
// - <link rel="alternate" hreflang="..."> for each configured locale
|
|
8
|
+
// - Apple News discovery link (via AeoHead)
|
|
9
|
+
// - Per-page markdown twin link (via AeoHead)
|
|
10
|
+
// - <script type="application/ld+json"> for the item (Article / Product / Website)
|
|
11
|
+
// - news_keywords, apple-news-*, googlebot noarchive (via generateMeta)
|
|
12
|
+
//
|
|
13
|
+
// Design: drop-in for layouts. With no props, emits only global tags suitable
|
|
14
|
+
// for the homepage / tag indexes. Pass `item` + `variant` on content pages for
|
|
15
|
+
// full meta + JSON-LD + AEO discovery. Everything configurable via props;
|
|
16
|
+
// sensible defaults make the zero-config path useful.
|
|
17
|
+
//
|
|
18
|
+
// Why this exists: consumers used to hand-roll <meta name="robots"> in every
|
|
19
|
+
// Base.astro and either forgot `max-image-preview:large` or misspelled it.
|
|
20
|
+
// Centralizing here eliminates that class of bug across ~12 sites.
|
|
21
|
+
//
|
|
22
|
+
// Side-effect import seeds state in both main and Cloudflare prerender Workers.
|
|
23
|
+
import 'virtual:growth-labs/seo/config'
|
|
24
|
+
|
|
25
|
+
import { getConfig, type ContentItem } from '@growth-labs/seo'
|
|
26
|
+
import {
|
|
27
|
+
generateArticleJsonLd,
|
|
28
|
+
generateCanonical,
|
|
29
|
+
generateHreflang,
|
|
30
|
+
generateMeta,
|
|
31
|
+
} from '@growth-labs/seo/utils'
|
|
32
|
+
import AeoHead from './AeoHead.astro'
|
|
33
|
+
|
|
34
|
+
export interface Props {
|
|
35
|
+
/**
|
|
36
|
+
* The page's ContentItem. When omitted, only global head tags are emitted
|
|
37
|
+
* (robots, Apple News discovery, AEO twin for the current URL) — suitable
|
|
38
|
+
* for homepages, tag indexes, search pages, and other non-content routes.
|
|
39
|
+
*/
|
|
40
|
+
item?: ContentItem
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Which OpenGraph variant to emit. Only used when `item` is provided.
|
|
44
|
+
* Defaults to 'website'; pass 'article' for article pages, 'product' for
|
|
45
|
+
* product pages.
|
|
46
|
+
*/
|
|
47
|
+
variant?: 'article' | 'product' | 'website'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `<meta name="robots">` content. Defaults to 'max-image-preview:large'
|
|
51
|
+
* (Google's recommendation for article-heavy sites; tells search engines
|
|
52
|
+
* to show large images in Discover / News results). Search engines default
|
|
53
|
+
* to index,follow, so we don't need to repeat those.
|
|
54
|
+
*
|
|
55
|
+
* Pass 'noindex' to suppress a page from search. Pass null to suppress the
|
|
56
|
+
* tag entirely (e.g. when the consumer's layout emits it separately).
|
|
57
|
+
*/
|
|
58
|
+
robots?: string | null
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Emit a JSON-LD <script> for the item. Defaults to true when `item` and
|
|
62
|
+
* `variant` are both provided. Set false if the consumer emits JSON-LD
|
|
63
|
+
* elsewhere (e.g. their own article layout hand-rolls it).
|
|
64
|
+
*/
|
|
65
|
+
emitJsonLd?: boolean
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Override the canonical URL. Defaults to `item.url` when item is
|
|
69
|
+
* provided, else `Astro.url`.
|
|
70
|
+
*/
|
|
71
|
+
canonical?: URL | string
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Pass-through to <AeoHead />. Emits the per-page markdown twin link.
|
|
75
|
+
* Defaults to true when `item` is provided (article-ish pages usually
|
|
76
|
+
* have a twin), false otherwise (homepage doesn't).
|
|
77
|
+
*/
|
|
78
|
+
emitTwinLink?: boolean
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const {
|
|
82
|
+
item,
|
|
83
|
+
variant = 'website',
|
|
84
|
+
robots = 'max-image-preview:large',
|
|
85
|
+
emitJsonLd,
|
|
86
|
+
canonical,
|
|
87
|
+
emitTwinLink,
|
|
88
|
+
} = Astro.props
|
|
89
|
+
|
|
90
|
+
const config = getConfig()
|
|
91
|
+
|
|
92
|
+
// Canonical: item.url if item provided, else explicit prop, else Astro.url.
|
|
93
|
+
const canonicalHref = (() => {
|
|
94
|
+
if (canonical) return typeof canonical === 'string' ? canonical : canonical.toString()
|
|
95
|
+
if (item) return item.url
|
|
96
|
+
return Astro.url.toString()
|
|
97
|
+
})()
|
|
98
|
+
const canonicalLink = generateCanonical(canonicalHref, config)
|
|
99
|
+
|
|
100
|
+
// Meta tags (OG + Twitter + news_keywords + apple-news-*): item required.
|
|
101
|
+
const metaTags = item ? generateMeta(item, variant, config) : []
|
|
102
|
+
|
|
103
|
+
// Hreflang: item's alternateLocales when present. generateHreflang tolerates
|
|
104
|
+
// empty input; we gate on length anyway to avoid a call when unnecessary.
|
|
105
|
+
const hreflangLinks = item?.alternateLocales?.length
|
|
106
|
+
? generateHreflang(item.alternateLocales, config.defaultLocale ?? 'en')
|
|
107
|
+
: []
|
|
108
|
+
|
|
109
|
+
// JSON-LD emission default: on when item + variant are set.
|
|
110
|
+
const shouldEmitJsonLd = emitJsonLd ?? Boolean(item)
|
|
111
|
+
const jsonLd = shouldEmitJsonLd && item && variant === 'article'
|
|
112
|
+
? generateArticleJsonLd(item, config)
|
|
113
|
+
: null
|
|
114
|
+
|
|
115
|
+
// Twin-link default: on when there's an item (article-ish page), off otherwise.
|
|
116
|
+
const shouldEmitTwinLink = emitTwinLink ?? Boolean(item)
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
{robots !== null && <meta name="robots" content={robots} />}
|
|
120
|
+
|
|
121
|
+
<link {...canonicalLink} />
|
|
122
|
+
|
|
123
|
+
{hreflangLinks.map((l) => <link rel="alternate" hreflang={l.hreflang} href={l.href} />)}
|
|
124
|
+
|
|
125
|
+
{metaTags.map((tag) => <meta {...tag} />)}
|
|
126
|
+
|
|
127
|
+
<AeoHead canonical={canonicalHref} emitTwinLink={shouldEmitTwinLink} />
|
|
128
|
+
|
|
129
|
+
{jsonLd && (
|
|
130
|
+
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
|
|
131
|
+
)}
|