@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-labs/seo",
3
- "version": "0.2.4",
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
+ )}