@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 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 getvitops from '@getvitops/astro';
18
+ import vitops from '@getvitops/astro';
19
19
 
20
20
  export default defineConfig({
21
21
  integrations: [
22
- getvitops({
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**: