@getvitops/astro 0.8.0 → 1.0.0
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/CHANGELOG.md +286 -0
- package/README.md +96 -2
- package/dist/index.d.mts +284 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +161 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +24 -8
- package/src/components/Cards.astro +16 -14
- package/src/components/Drawer.astro +19 -2
- package/src/components/Head.astro +3 -0
- package/src/components/Popover.astro +17 -2
- package/src/components/Seo.astro +68 -0
- package/src/components/Subgrid.astro +18 -18
- package/src/seo.ts +307 -0
- package/virtual.d.ts +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,291 @@
|
|
|
1
1
|
# @getvitops/astro
|
|
2
2
|
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- fd1a35b: Add `<Seo />` — page metadata for non-EmDash sites: `<title>`, description, canonical, Open Graph,
|
|
8
|
+
Twitter cards, robots, `article:*`, `hreflang` and verification tokens.
|
|
9
|
+
|
|
10
|
+
Site-level defaults go in the integration; pages pass only what differs.
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
// astro.config.mjs
|
|
14
|
+
vitops({
|
|
15
|
+
seo: {
|
|
16
|
+
siteName: 'Acme',
|
|
17
|
+
titleTemplate: '%s · Acme',
|
|
18
|
+
defaultDescription: 'We make the thing.',
|
|
19
|
+
openGraph: {
|
|
20
|
+
locale: 'en_CA',
|
|
21
|
+
image: { url: '/og.png', alt: 'Acme', width: 1200, height: 630 },
|
|
22
|
+
},
|
|
23
|
+
twitter: { site: '@acme' },
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```astro
|
|
29
|
+
---
|
|
30
|
+
import Seo from '@getvitops/astro/Seo.astro';
|
|
31
|
+
---
|
|
32
|
+
<Seo title={title} description={description} image={cover} />
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**`<Seo />` owns `<title>` and `<meta name="description">` — remove them from your layout when you
|
|
36
|
+
adopt it.** It already computes the resolved title for `og:title`/`twitter:title`, and emitting those
|
|
37
|
+
in one place while the layout emits `<title>` in another is how the two drift apart. `<Head />` is
|
|
38
|
+
unaffected and still handles favicons, theme-color and the web-component runtime; use both.
|
|
39
|
+
|
|
40
|
+
Notable behaviour:
|
|
41
|
+
- `titleTemplate` is skipped when a page's title already equals `siteName`, so a homepage titled
|
|
42
|
+
"Acme" emits `Acme` rather than `Acme · Acme`. It never applies to `defaultTitle`.
|
|
43
|
+
- Canonical, `og:url` and relative `og:image` values need the `site` astro.config option. Without it
|
|
44
|
+
they're omitted rather than derived from the request URL — a canonical built from a dev or preview
|
|
45
|
+
origin can de-index you — and the integration warns at build time. Absolute image URLs still work.
|
|
46
|
+
- `robots` is omitted unless it says something; `index, follow` is what crawlers already assume. Use
|
|
47
|
+
`noindex`/`nofollow`/`noarchive`/`nocache`/`robotsExtras` per page, `robots` for a full override, or
|
|
48
|
+
`seo.robots` site-wide.
|
|
49
|
+
- `twitter:card` upgrades to `summary_large_image` whenever an image resolves.
|
|
50
|
+
- `hreflang` alternates are explicit only — pass `alternates`, including the current page. Nothing is
|
|
51
|
+
inferred from a locale list.
|
|
52
|
+
- No JSON-LD. The `./schemas/*` components take entity data and compose alongside it.
|
|
53
|
+
|
|
54
|
+
**On an EmDash site use `<EmDashHead>` instead** — it emits the same tags from the CMS, and rendering
|
|
55
|
+
both duplicates every one of them. The integration warns if `seo` is configured alongside `emdash()`.
|
|
56
|
+
|
|
57
|
+
The merge logic ships as the pure `resolveSeo(defaults, props, ctx)` if you need to drive it yourself.
|
|
58
|
+
|
|
59
|
+
- 20252c2: Add an opt-in `sitemap` option to `getvitops()`, and link the result from `<Head />`.
|
|
60
|
+
|
|
61
|
+
`sitemap: true` registers the official `@astrojs/sitemap` for you; pass an object to configure it
|
|
62
|
+
(`filter`, `customPages`, `changefreq`, `priority`, `i18n`, `entryLimit`, `filenameBase`,
|
|
63
|
+
`serialize`, …). `<Head />` gains a matching `<link rel="sitemap">`.
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
getvitops({ sitemap: true });
|
|
67
|
+
getvitops({ sitemap: { filter: (page) => !page.includes('/draft/') } });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`@astrojs/sitemap` is an **optional peer** — install it yourself (`pnpm add -D @astrojs/sitemap`) and
|
|
71
|
+
the build fails with a message saying so if you don't, rather than silently emitting nothing. The
|
|
72
|
+
option also needs the `site` astro.config option, since a sitemap lists absolute URLs; without it the
|
|
73
|
+
option warns and skips. Note `@astrojs/sitemap` enumerates **prerendered** routes only, so on an
|
|
74
|
+
`output: 'server'` site you'll want `export const prerender = true` on the pages you want indexed, or
|
|
75
|
+
`sitemap.customPages`.
|
|
76
|
+
|
|
77
|
+
**On an EmDash site, leave it off** — EmDash serves its own database-driven `/sitemap.xml`, which also
|
|
78
|
+
covers on-demand pages a static sitemap can't. The option detects `emdash()` and skips with a warning.
|
|
79
|
+
If you want both, add `sitemap()` to your own `integrations` array; getvitops detects that too and
|
|
80
|
+
leaves yours in charge, which is also how you reach the few `@astrojs/sitemap` options this
|
|
81
|
+
integration doesn't mirror.
|
|
82
|
+
|
|
83
|
+
Also fixes the `virtual:getvitops/head` type declaration, which was missing the `editor` field that
|
|
84
|
+
`<Head />` already reads — a type error in consumer projects that don't set `skipLibCheck`.
|
|
85
|
+
|
|
86
|
+
- bb92a14: Add a fourth output format, `design`, that emits `DESIGN.md` — the agent-facing brief in
|
|
87
|
+
[google-labs-code/design.md](https://github.com/google-labs-code/design.md) format.
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
vitops generate --format design --out . # DESIGN.md, and nothing else
|
|
91
|
+
vitops generate --format css,design # compose it with a stylesheet
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The file is YAML front matter carrying the tokens (`colors`, `typography`, `rounded`,
|
|
95
|
+
`spacing`, `components`, cross-referenced with `{group.token}`) followed by a prose body
|
|
96
|
+
carrying the rationale — colour model, fluid scales, layout vocabulary, elevation, shape
|
|
97
|
+
cascade, component tiers, do's and don'ts. Every section is rendered from your config, so
|
|
98
|
+
it cannot describe a system the other formats don't build. Point a coding agent, a Figma
|
|
99
|
+
import, or a designer at this one file when they don't have the toolchain; `vitops docs`
|
|
100
|
+
remains the richer reference for those who do.
|
|
101
|
+
|
|
102
|
+
It is emitted with `--out .` in mind: DESIGN.md conventionally lives at a repo root beside
|
|
103
|
+
`AGENTS.md`, not in a build directory.
|
|
104
|
+
|
|
105
|
+
Three things the spec cannot express, handled the same way every time and explained in the
|
|
106
|
+
emitted prose so the file is self-describing:
|
|
107
|
+
- **Fluid `clamp()` sizes** → the maximum (desktop) value, since a spec `Dimension` is a
|
|
108
|
+
bare number plus px/em/rem.
|
|
109
|
+
- **Dark mode** → light values only, with the automatic functional flip explained. Role
|
|
110
|
+
tokens are emitted as `{colors.<hue>-<step>}` references into the raw ramps rather than
|
|
111
|
+
flattened hexes, so the role → ramp lineage survives the export — flattening them is
|
|
112
|
+
exactly what breaks dark mode downstream.
|
|
113
|
+
- **A `50%` radius** → dropped from `rounded` (it is not a `Dimension`) and named in the
|
|
114
|
+
Shapes prose instead, so nothing is silently lost.
|
|
115
|
+
|
|
116
|
+
**New:** an optional `meta` key in `design-system.json` (`{ name, description }`) supplies
|
|
117
|
+
the brand name and the Overview paragraph. It affects no other format.
|
|
118
|
+
|
|
119
|
+
**New:** `StylesheetFormat` (`Exclude<Format, 'design'>`), exported from
|
|
120
|
+
`@getvitops/generator`. `@getvitops/astro`'s `css.format` now takes that narrower type —
|
|
121
|
+
`design` produces no stylesheet to inject, so passing it there is a type error rather than
|
|
122
|
+
a missing-file failure at build time. `vitops lint --format` is likewise restricted to the
|
|
123
|
+
three CSS formats. No change for anyone already passing `tailwind`, `css` or `bricks`.
|
|
124
|
+
|
|
125
|
+
- bb92a14: Generate legal documents from your site config
|
|
126
|
+
|
|
127
|
+
`vitops legal` renders a privacy policy, terms of service and cookie notice from a site
|
|
128
|
+
config, in markdown, HTML or EmDash Portable Text:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
vitops legal --out ./content # every enabled document, as markdown
|
|
132
|
+
vitops legal --doc privacy --format html # one document, as an HTML fragment
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The documents are **derived from your config**, not filled into a form. The analytics
|
|
136
|
+
provider they name is the one whose ID you set; the personal information they list is what
|
|
137
|
+
your configured forms actually collect; the countries they name come from the providers you
|
|
138
|
+
use. So a provider swap updates the policy on the next build, and the fix for a wrong policy
|
|
139
|
+
is a corrected config — hand-editing the output is overwritten.
|
|
140
|
+
|
|
141
|
+
Enable documents under `legal`, which gains the facts the prose asserts:
|
|
142
|
+
|
|
143
|
+
```jsonc
|
|
144
|
+
{
|
|
145
|
+
"legal": {
|
|
146
|
+
"jurisdiction": "ca", // only 'ca' (PIPEDA) ships today
|
|
147
|
+
"privacyPolicy": {
|
|
148
|
+
"enabled": true,
|
|
149
|
+
"lastUpdated": "2026-08-01",
|
|
150
|
+
"retention": "24 months after our last contact with you",
|
|
151
|
+
// Third parties the config cannot imply. Analytics, Turnstile and your
|
|
152
|
+
// deploy platform are detected automatically — list only the rest.
|
|
153
|
+
"processors": [
|
|
154
|
+
{
|
|
155
|
+
"name": "Stripe",
|
|
156
|
+
"purpose": "payment processing",
|
|
157
|
+
"country": "the United States",
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
"termsOfService": { "enabled": true },
|
|
162
|
+
"cookieConsent": {
|
|
163
|
+
"enabled": true,
|
|
164
|
+
"type": "opt-in",
|
|
165
|
+
"categories": ["Essential", "Analytics"],
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Delivery, by stack:
|
|
172
|
+
- **Any stack** — `vitops legal`. No integration code; prints to stdout without `--out`.
|
|
173
|
+
- **WordPress/Bricks** — `vitops generate --site <path>` also writes `dist/legal/*.html`, and
|
|
174
|
+
the theme loader now registers `[vitops_legal doc="privacy"]` to render one in a page. The
|
|
175
|
+
document updates on the next deploy with no action in WordPress.
|
|
176
|
+
- **Astro** — `getvitops({ legal: { input: 'site.json', out: 'src/content/legal' } })` writes
|
|
177
|
+
markdown into a content collection and re-renders when the site config changes. It needs a
|
|
178
|
+
`css` config (that is what registers the Vite plugin); without one, use the CLI.
|
|
179
|
+
- **EmDash** — `--format portable-text`, pasted into the admin.
|
|
180
|
+
|
|
181
|
+
Also new on the public API: `generateLegal()`, `renderMarkdown()`, `renderNodes()`,
|
|
182
|
+
`derivePolicyVars()`, `parseMarkdown()` / `toHtmlFragment()` / `toPortableText()`, and
|
|
183
|
+
`resolvePrivacyContact()`.
|
|
184
|
+
|
|
185
|
+
Two things to know before you publish anything this produces:
|
|
186
|
+
- **It is not legal advice.** Every document opens with a review banner saying so. The
|
|
187
|
+
bundled terms-of-service prose in particular is generic website boilerplate and
|
|
188
|
+
deliberately does not cover sales, refunds, subscriptions, accounts or user-generated
|
|
189
|
+
content — a site doing any of those needs clauses drafted for it.
|
|
190
|
+
- **It is only as true as your config.** A policy asserting things your site does not do is
|
|
191
|
+
worse than no policy. Check that the config describes reality before you ship the output.
|
|
192
|
+
|
|
193
|
+
`validateSite` now rejects a config that enables a privacy policy without a contact for
|
|
194
|
+
privacy requests or a `domains.canonical`, since both are interpolated into sentences that
|
|
195
|
+
would otherwise render blank.
|
|
196
|
+
|
|
197
|
+
### Patch Changes
|
|
198
|
+
|
|
199
|
+
- bb92a14: Docs: import the Astro integration as `vitops`, and document the fourth output format.
|
|
200
|
+
|
|
201
|
+
Every example now reads `import vitops from '@getvitops/astro'` and calls `vitops({ … })`,
|
|
202
|
+
including the scaffolded `emdash` template. The default export is unchanged, so this is a
|
|
203
|
+
naming convention in the docs rather than an API change — existing configs that bind it as
|
|
204
|
+
`getvitops` keep working.
|
|
205
|
+
|
|
206
|
+
The `@getvitops/generator` and `@getvitops/cli` docs also describe the `design` format, which
|
|
207
|
+
was shipped without a mention in either package's output table: `--format design` writes a
|
|
208
|
+
single `DESIGN.md` and no CSS, so a run that composes it with a stylesheet wants its own
|
|
209
|
+
`--out` (the brief conventionally sits at a repo root, the stylesheet does not).
|
|
210
|
+
|
|
211
|
+
- Updated dependencies [bb92a14]
|
|
212
|
+
- Updated dependencies [bb92a14]
|
|
213
|
+
- Updated dependencies [bb92a14]
|
|
214
|
+
- Updated dependencies [bb92a14]
|
|
215
|
+
- Updated dependencies [eeb059f]
|
|
216
|
+
- @getvitops/generator@1.0.0
|
|
217
|
+
- @getvitops/vite@1.0.0
|
|
218
|
+
- @getvitops/utils@1.0.0
|
|
219
|
+
- @getvitops/core@1.0.0
|
|
220
|
+
|
|
221
|
+
## 0.9.0
|
|
222
|
+
|
|
223
|
+
### Minor Changes
|
|
224
|
+
|
|
225
|
+
- **Fixed: `Subgrid` and `Cards` rendered as unstyled lists, in every format.**
|
|
226
|
+
|
|
227
|
+
`Subgrid.astro` drew its geometry with Tailwind utilities — `grid-rows-subgrid` and
|
|
228
|
+
`row-span-(--row-span)`, the latter Tailwind v4's arbitrary-CSS-variable syntax. No framework CSS
|
|
229
|
+
layer defines those, so under `css.format: 'css'` or `'bricks'` the component had no layout at all.
|
|
230
|
+
|
|
231
|
+
It failed under `'tailwind'` too, which is the part worth internalising: **Tailwind v4 is JIT and
|
|
232
|
+
does not scan `node_modules`**, so a class that only a shipped component references is never
|
|
233
|
+
generated. A consumer had to add the package to Tailwind's `@source` by hand to get a subgrid;
|
|
234
|
+
without that, cards silently laid out at `grid-row: auto` — visually plausible, quietly wrong. The
|
|
235
|
+
same trap applied to `grid` in `Subgrid` and `sr-only` / `not-sr-only` in `Popover` / `Drawer`:
|
|
236
|
+
those are in the generator's `TW_CLASH` list, so the framework strips its own rules for them from
|
|
237
|
+
the tailwind bundle and defers to Tailwind — meaning they resolved only when the consumer's own
|
|
238
|
+
templates happened to use the same class.
|
|
239
|
+
|
|
240
|
+
The components now emit framework classes only, and the `subgrid` pattern owns the layout:
|
|
241
|
+
- `Subgrid` renders `<ul class="subgrid"><li>…` and ships no `<style>` block of its own.
|
|
242
|
+
- `Popover` / `Drawer` use a component-scoped `visually-hidden` instead of `sr-only`. The
|
|
243
|
+
`not-sr-only` on their icons was a no-op and is gone.
|
|
244
|
+
- The `subgrid` pattern absorbed the wrapped-row margin the component used to carry, so
|
|
245
|
+
hand-written `.subgrid` markup gets it too, and resets list markers on `ul`/`ol`.
|
|
246
|
+
|
|
247
|
+
**Also fixed: `Cards` discarded the `card` class and every class you put on a child.** It wrote
|
|
248
|
+
`card` onto the slotted element and then serialised that element's _inner_ HTML, dropping the
|
|
249
|
+
attribute it had just set. Child `class` and `style` now reach the rendered `<li>`.
|
|
250
|
+
|
|
251
|
+
**Breaking — the subgrid custom properties are renamed.** One `--subgrid-*` vocabulary now covers
|
|
252
|
+
both the pattern and the component; the old names are removed, not aliased:
|
|
253
|
+
|
|
254
|
+
| removed | use |
|
|
255
|
+
| ------------------------------- | -------------------- |
|
|
256
|
+
| `--items-per-row`, `--cols` | `--subgrid-cols` |
|
|
257
|
+
| `--rows-per-item`, `--row-span` | `--subgrid-row-span` |
|
|
258
|
+
| `--row-margin` | `--subgrid-row-gap` |
|
|
259
|
+
|
|
260
|
+
`--subgrid-gap` (the grid gap) is unchanged, as are the `.subgrid-cols-*` / `.subgrid-rows-*` /
|
|
261
|
+
`.subgrid-responsive` modifiers. Anything still setting an old name falls back to the pattern
|
|
262
|
+
defaults — 3 columns, span 2 — so grep for them when you upgrade.
|
|
263
|
+
|
|
264
|
+
- **`tailwindcss` and `@tailwindcss/vite` are now optional peer dependencies, not dependencies.**
|
|
265
|
+
|
|
266
|
+
Only `css.format: 'tailwind'` ever used them, but every consumer installed them — including
|
|
267
|
+
`'css'` and `'bricks'` projects that never touch Tailwind. The integration now loads
|
|
268
|
+
`@tailwindcss/vite` lazily inside that branch and throws a directive error if it is missing.
|
|
269
|
+
|
|
270
|
+
**Migration:** if you use `css.format: 'tailwind'` (the default), add both to your
|
|
271
|
+
`devDependencies` — `pnpm add -D tailwindcss @tailwindcss/vite`. Most Tailwind projects already
|
|
272
|
+
have them. Everyone else can drop them.
|
|
273
|
+
|
|
274
|
+
### Patch Changes
|
|
275
|
+
|
|
276
|
+
- Updated dependencies
|
|
277
|
+
- Updated dependencies
|
|
278
|
+
- Updated dependencies
|
|
279
|
+
- Updated dependencies [c949cae]
|
|
280
|
+
- Updated dependencies
|
|
281
|
+
- Updated dependencies
|
|
282
|
+
- Updated dependencies
|
|
283
|
+
- Updated dependencies
|
|
284
|
+
- @getvitops/generator@0.9.0
|
|
285
|
+
- @getvitops/core@0.9.0
|
|
286
|
+
- @getvitops/vite@0.9.0
|
|
287
|
+
- @getvitops/utils@0.9.0
|
|
288
|
+
|
|
3
289
|
## 0.8.0
|
|
4
290
|
|
|
5
291
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -15,11 +15,11 @@ npm i -D @getvitops/astro
|
|
|
15
15
|
```js
|
|
16
16
|
// astro.config.mjs
|
|
17
17
|
import { defineConfig } from 'astro/config';
|
|
18
|
-
import
|
|
18
|
+
import vitops from '@getvitops/astro';
|
|
19
19
|
|
|
20
20
|
export default defineConfig({
|
|
21
21
|
integrations: [
|
|
22
|
-
|
|
22
|
+
vitops({
|
|
23
23
|
css: { input: 'design-system.json', format: 'tailwind', out: 'src/styles' },
|
|
24
24
|
favicon: { source: 'src/assets/logo.svg', name: 'My Site', themeColor: '#0b0b0c' },
|
|
25
25
|
}),
|
|
@@ -56,11 +56,105 @@ Astro's module graph and Astro emits that link itself.
|
|
|
56
56
|
| `css.inject` | `true` | inject the stylesheet into every SSR page |
|
|
57
57
|
| `webComponents` | `true` | copy + link the web-component bundles |
|
|
58
58
|
| `favicon` | off unless given | `source`, `lowResSource`, `name`, `themeColor`, `backgroundColor` |
|
|
59
|
+
| `sitemap` | off unless given | generate `sitemap-index.xml` via `@astrojs/sitemap` |
|
|
60
|
+
| `seo` | off unless given | site-level defaults for `<Seo />` |
|
|
59
61
|
|
|
60
62
|
Set `css.inject: false` when another integration adds routes that must not inherit the design system
|
|
61
63
|
(e.g. EmDash's `/_emdash/admin`) — then import the generated file (`<out>/tailwind.css` or
|
|
62
64
|
`<out>/styles.css`) from your own layout, so only your pages are styled.
|
|
63
65
|
|
|
66
|
+
## Sitemap
|
|
67
|
+
|
|
68
|
+
`sitemap: true` registers the official [`@astrojs/sitemap`](https://docs.astro.build/en/guides/integrations-guide/sitemap/)
|
|
69
|
+
and links the result from `<Head />`. Pass an object to configure it (`filter`, `customPages`,
|
|
70
|
+
`changefreq`, `priority`, `i18n`, `entryLimit`, `filenameBase`, `serialize`, …):
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
vitops({ sitemap: { filter: (page) => !page.includes('/draft/') } });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Three things to know:
|
|
77
|
+
|
|
78
|
+
- **It's an optional peer — install it yourself:** `pnpm add -D @astrojs/sitemap`. Without it the
|
|
79
|
+
build fails with a message telling you so, rather than silently emitting nothing.
|
|
80
|
+
- **It needs the `site` astro.config option**, since a sitemap lists absolute URLs. Without it the
|
|
81
|
+
option warns and skips.
|
|
82
|
+
- **It lists prerendered routes only.** On an `output: 'server'` site, mark the pages you want
|
|
83
|
+
indexed with `export const prerender = true`, or list them in `sitemap.customPages`.
|
|
84
|
+
|
|
85
|
+
**On an EmDash site, leave it off.** EmDash serves its own `/sitemap.xml` from the database, which
|
|
86
|
+
also covers on-demand pages a static sitemap can't; the option detects `emdash()` and skips with a
|
|
87
|
+
warning. The two write different filenames and so don't actually collide — if you want both (DB
|
|
88
|
+
content _and_ hand-authored `.astro` pages), add `sitemap()` to your own `integrations` array.
|
|
89
|
+
vitops detects that too and leaves yours in charge, which is also how you reach the handful of
|
|
90
|
+
`@astrojs/sitemap` options this integration doesn't mirror.
|
|
91
|
+
|
|
92
|
+
## `<Seo />`
|
|
93
|
+
|
|
94
|
+
Page metadata: `<title>`, description, canonical, Open Graph, Twitter cards, robots, `article:*`,
|
|
95
|
+
`hreflang`, verification tokens. Site-level defaults go in the integration; pages pass what differs.
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
// astro.config.mjs
|
|
99
|
+
vitops({
|
|
100
|
+
seo: {
|
|
101
|
+
siteName: 'Acme',
|
|
102
|
+
titleTemplate: '%s · Acme',
|
|
103
|
+
defaultDescription: 'We make the thing.',
|
|
104
|
+
openGraph: {
|
|
105
|
+
locale: 'en_CA',
|
|
106
|
+
image: { url: '/og.png', alt: 'Acme', width: 1200, height: 630 },
|
|
107
|
+
},
|
|
108
|
+
twitter: { site: '@acme' },
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```astro
|
|
114
|
+
---
|
|
115
|
+
import Head from '@getvitops/astro/Head.astro';
|
|
116
|
+
import Seo from '@getvitops/astro/Seo.astro';
|
|
117
|
+
---
|
|
118
|
+
<head>
|
|
119
|
+
<meta charset="utf-8" />
|
|
120
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
121
|
+
<Seo title={title} description={description} image={cover} />
|
|
122
|
+
<Head />
|
|
123
|
+
</head>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**It owns `<title>` and `<meta name="description">` — delete yours when you adopt it.** That isn't
|
|
127
|
+
tidiness: it already computes the resolved title for `og:title`/`twitter:title`, and splitting the
|
|
128
|
+
two is how they drift out of sync. `<Head />` is orthogonal (favicons, theme-color, the
|
|
129
|
+
web-component runtime); use both.
|
|
130
|
+
|
|
131
|
+
Behaviour worth knowing:
|
|
132
|
+
|
|
133
|
+
- **`titleTemplate` is skipped when a page's title already equals `siteName`**, so a homepage titled
|
|
134
|
+
"Acme" emits `Acme`, not `Acme · Acme`. It never applies to `defaultTitle` either.
|
|
135
|
+
- **Canonical needs the `site` astro.config option.** Without it, canonical, `og:url` and any
|
|
136
|
+
_relative_ `og:image` are omitted rather than derived from the request — a canonical built from a
|
|
137
|
+
dev or preview origin can de-index you. The integration warns at build time. Absolute image URLs
|
|
138
|
+
still work.
|
|
139
|
+
- **`base` is handled for you.** `Astro.url.pathname` already includes it; don't prepend it yourself.
|
|
140
|
+
Under the default `trailingSlash: 'ignore'` dev and build can disagree on the trailing slash, so
|
|
141
|
+
pin `'always'` or `'never'` if you need a byte-stable canonical.
|
|
142
|
+
- **`robots` is omitted unless it says something.** `index, follow` is what crawlers already assume.
|
|
143
|
+
Set `noindex`/`nofollow`/`noarchive`/`nocache`/`robotsExtras` per page, `robots` for a full
|
|
144
|
+
override, or `seo.robots` for a site-wide default.
|
|
145
|
+
- **`twitter:card`** becomes `summary_large_image` whenever an image resolves.
|
|
146
|
+
- **`hreflang` alternates are explicit only.** Pass `alternates={[{ hreflang, href }, …]}` including
|
|
147
|
+
the current page; nothing is inferred from a locale list, because only the page knows where its own
|
|
148
|
+
translations live.
|
|
149
|
+
- **No JSON-LD.** The structured-data components in `./schemas/` take _entity_ data, `<Seo />` takes
|
|
150
|
+
_page_ data. Compose them: `<Seo … />` then `<Organization {...org} />`.
|
|
151
|
+
|
|
152
|
+
**On an EmDash site, use `<EmDashHead>` instead** — it emits the same tags from the CMS. Rendering
|
|
153
|
+
both duplicates every one of them; the integration warns if you configure `seo` alongside `emdash()`.
|
|
154
|
+
|
|
155
|
+
The merge logic is a pure function, exported as `resolveSeo(defaults, props, ctx)` if you need to
|
|
156
|
+
drive it yourself.
|
|
157
|
+
|
|
64
158
|
## Components
|
|
65
159
|
|
|
66
160
|
Thin authoring helpers that emit the framework's HTML/CSS patterns — **none require runtime JS**:
|