@getvitops/astro 0.9.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,223 @@
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
+
3
221
  ## 0.9.0
4
222
 
5
223
  ### 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**:
package/dist/index.d.mts CHANGED
@@ -1,7 +1,173 @@
1
- import { Format } from "@getvitops/generator";
1
+ import { LegalOutput, StylesheetFormat } from "@getvitops/generator";
2
2
  import { AstroIntegration } from "astro";
3
3
  export * from "@getvitops/utils";
4
4
 
5
+ //#region src/seo.d.ts
6
+ /**
7
+ * The `<Seo />` resolution layer — site-level defaults merged with per-page props
8
+ * into the exact tags to emit.
9
+ *
10
+ * Kept separate from `Seo.astro` deliberately. The component this replaces
11
+ * (`SEO.astro`, deleted in c949cae) fused resolution with markup, which is why it
12
+ * was never unit-tested and why its title handling drifted: it computed a title
13
+ * for `og:title` but left `<title>` to the layout, so the two could disagree.
14
+ * Everything decidable lives here and is asserted in `seo.test.ts`; the component
15
+ * only renders what this returns.
16
+ */
17
+ /** Twitter card types, per <https://developer.x.com/en/docs/twitter-for-websites/cards>. */
18
+ type SeoTwitterCard = 'summary' | 'summary_large_image' | 'app' | 'player';
19
+ /** Open Graph object types this component knows how to describe. */
20
+ type SeoOgType = 'website' | 'article' | 'profile';
21
+ /**
22
+ * Site-level SEO defaults, passed to `getvitops({ seo })` and baked into the
23
+ * `virtual:getvitops/head` module. Per-page `<Seo />` props override these.
24
+ *
25
+ * The vocabulary mirrors the site-config schema's `seo` block
26
+ * (`@getvitops/generator`, `src/site.ts`) so a `SiteConfig` → these adapter would
27
+ * be a flat field map — but it is not imported from there. This is the
28
+ * integration's own option surface, and it must stay JSON-serialisable: it is
29
+ * `JSON.stringify`d into the virtual module, so no functions, no `URL`s, no dates.
30
+ */
31
+ interface GetvitopsSeoOptions {
32
+ /** `og:site_name`, and the `<title>` when a page passes none. */
33
+ siteName?: string;
34
+ /**
35
+ * Page-title pattern; `%s` is the page's own title. e.g. `'%s · Acme'`.
36
+ *
37
+ * Skipped when a page's title already equals `siteName` — a homepage titled
38
+ * "Acme" emits `Acme`, not `Acme · Acme` — and never applied to `defaultTitle`.
39
+ */
40
+ titleTemplate?: string;
41
+ /** `<title>` for pages that pass none. Used verbatim — the template is not applied. */
42
+ defaultTitle?: string;
43
+ /** `<meta name="description">` for pages that pass none. */
44
+ defaultDescription?: string;
45
+ /** Site-wide robots directive, e.g. `'index,follow,max-image-preview:large'`. */
46
+ robots?: string;
47
+ /** Google Search Console verification token. */
48
+ googleSiteVerification?: string;
49
+ /** Bing Webmaster Tools verification token. */
50
+ bingSiteVerification?: string;
51
+ openGraph?: {
52
+ /** Default `og:type` (default `'website'`). */type?: SeoOgType; /** `og:locale`. Written either way — `en-CA` and `en_CA` both emit `en_CA`. */
53
+ locale?: string; /** Additional locales the same content exists in → `og:locale:alternate`. */
54
+ localeAlternates?: string[]; /** Fallback share image. Relative URLs resolve against `site`. */
55
+ image?: {
56
+ url: string;
57
+ alt?: string;
58
+ width?: number;
59
+ height?: number;
60
+ type?: string;
61
+ };
62
+ };
63
+ twitter?: {
64
+ /** Card type when no image resolves (default `'summary'`). */card?: SeoTwitterCard; /** The site's own handle, `@acme`. */
65
+ site?: string; /** Default author handle, `@ada`. */
66
+ creator?: string;
67
+ };
68
+ }
69
+ /** Per-page `<Seo />` props. Everything is optional; defaults fill the gaps. */
70
+ interface SeoProps {
71
+ /** Page title, before `titleTemplate` is applied. */
72
+ title?: string;
73
+ description?: string;
74
+ /** Overrides the computed canonical. Relative values resolve against `site`. */
75
+ canonical?: string;
76
+ /** Overrides `og:site_name` and the title fallback. */
77
+ siteName?: string;
78
+ /** Share image for this page. Relative URLs resolve against `site`. */
79
+ image?: string;
80
+ imageAlt?: string;
81
+ imageWidth?: number;
82
+ imageHeight?: number;
83
+ /** MIME type of the share image, e.g. `'image/png'`. */
84
+ imageType?: string;
85
+ noindex?: boolean;
86
+ nofollow?: boolean;
87
+ noarchive?: boolean;
88
+ nocache?: boolean;
89
+ /** Extra robots directives, e.g. `'max-snippet:-1'`. Appended to the composed value. */
90
+ robotsExtras?: string;
91
+ /** Full robots override. Wins over the flags above and the site default. */
92
+ robots?: string;
93
+ /** `og:type` for this page (default: the site default, else `'website'`). */
94
+ type?: SeoOgType;
95
+ /** Emitted only when `type` is `'article'`. */
96
+ article?: {
97
+ publishedTime?: string;
98
+ modifiedTime?: string;
99
+ expirationTime?: string;
100
+ authors?: string[];
101
+ section?: string;
102
+ tags?: string[];
103
+ };
104
+ /** `og:locale` for this page. */
105
+ locale?: string;
106
+ /** Twitter author handle for this page, `@ada`. */
107
+ twitterCreator?: string;
108
+ /**
109
+ * Translations of *this* page, as `<link rel="alternate" hreflang>`.
110
+ *
111
+ * Explicit only, and it must include the current page. Nothing is inferred:
112
+ * the deleted component derived alternates from the site's locale list and
113
+ * pointed every one of them at that locale's homepage, so every page on the
114
+ * site claimed the homepage as its translation. Only the page knows its own
115
+ * translated URLs. Pass `hreflang: 'x-default'` for the fallback entry.
116
+ */
117
+ alternates?: {
118
+ hreflang: string;
119
+ href: string;
120
+ }[];
121
+ }
122
+ /** A `<meta property>` tag (Open Graph uses `property`, not `name`). */
123
+ interface SeoPropertyTag {
124
+ property: string;
125
+ content: string;
126
+ }
127
+ /** A `<meta name>` tag. */
128
+ interface SeoNameTag {
129
+ name: string;
130
+ content: string;
131
+ }
132
+ /** Everything `<Seo />` renders, already decided. `null` means "emit nothing". */
133
+ interface ResolvedSeo {
134
+ title: string | null;
135
+ description: string | null;
136
+ canonical: string | null;
137
+ robots: string | null;
138
+ openGraph: SeoPropertyTag[];
139
+ twitter: SeoNameTag[];
140
+ verification: SeoNameTag[];
141
+ alternates: {
142
+ hreflang: string;
143
+ href: string;
144
+ }[];
145
+ }
146
+ /** Render context supplied by the component from `Astro`. */
147
+ interface SeoContext {
148
+ /**
149
+ * The deployed origin — `Astro.site`, or `config.site` via the virtual module.
150
+ *
151
+ * `undefined` when neither is set, and then every absolute-URL tag is dropped
152
+ * rather than being derived from the request. `astro-seo` falls back to
153
+ * `Astro.url` here; on a static build that is `http://localhost:4321`, which
154
+ * would bake a localhost canonical into production HTML.
155
+ */
156
+ site?: URL | undefined;
157
+ /** `Astro.url.pathname` — already includes `base`. */
158
+ pathname: string;
159
+ /** `Astro.url.search`, if the canonical should keep the query string. */
160
+ search?: string;
161
+ }
162
+ /**
163
+ * Merges site defaults with page props into the final tag set.
164
+ *
165
+ * Pure — no `Astro`, no filesystem, no config lookup. Everything it needs comes
166
+ * through its three arguments, which is what makes it testable and what keeps the
167
+ * package free of the consumer-global coupling that sank the previous site model.
168
+ */
169
+ declare function resolveSeo(defaults: GetvitopsSeoOptions, props: SeoProps, ctx: SeoContext): ResolvedSeo;
170
+ //#endregion
5
171
  //#region src/integration.d.ts
6
172
  interface GetvitopsFaviconOptions {
7
173
  /** Source SVG or PNG. */
@@ -16,8 +182,13 @@ interface GetvitopsFaviconOptions {
16
182
  interface GetvitopsCssOptions {
17
183
  /** Path to design-system.json (default 'design-system.json'). */
18
184
  input?: string;
19
- /** Output format (default 'tailwind'). */
20
- format?: Format;
185
+ /**
186
+ * Output format (default 'tailwind'). Narrower than the generator's `Format`
187
+ * on purpose: this option exists to produce a stylesheet to inject, and the
188
+ * `design` format emits only `DESIGN.md`. Run that one via the CLI
189
+ * (`vitops generate --format design --out .`).
190
+ */
191
+ format?: StylesheetFormat;
21
192
  /** Directory the generated CSS is written to (default 'src/styles'). */
22
193
  out?: string;
23
194
  /**
@@ -29,6 +200,79 @@ interface GetvitopsCssOptions {
29
200
  */
30
201
  inject?: boolean;
31
202
  }
203
+ interface GetvitopsLegalOptions {
204
+ /** Path to the site config (JSON) the documents are rendered from. */
205
+ input: string;
206
+ /**
207
+ * Where to write them (default 'src/content/legal'). The default is a content
208
+ * collection: the documents are markdown, and a collection is what lets a page
209
+ * render one through your own layout rather than the integration injecting a
210
+ * route (which nothing in this package does).
211
+ */
212
+ out?: string;
213
+ /** Output format (default 'md'). */
214
+ format?: LegalOutput;
215
+ /** Environment whose A/B variant applies (default 'production'). */
216
+ siteEnv?: string;
217
+ }
218
+ /** `changefreq`, mirroring @astrojs/sitemap's `ChangeFreq`. */
219
+ type GetvitopsSitemapChangeFreq = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
220
+ /** One sitemap entry, mirroring @astrojs/sitemap's `SitemapItem`. */
221
+ interface GetvitopsSitemapEntry {
222
+ url: string;
223
+ lastmod?: string;
224
+ changefreq?: GetvitopsSitemapChangeFreq;
225
+ priority?: number;
226
+ /** Alternate-language variants. `lang` is required, as upstream requires it. */
227
+ links?: {
228
+ lang: string;
229
+ hreflang?: string;
230
+ url: string;
231
+ }[];
232
+ }
233
+ /**
234
+ * Forwarded verbatim to `@astrojs/sitemap`. Deliberately a hand-written subset of
235
+ * its `SitemapOptions` rather than a re-export: aliasing would put an
236
+ * `import … from '@astrojs/sitemap'` — and, transitively via `SitemapItem`, from
237
+ * `sitemap` — into this package's published .d.mts, which cannot resolve for the
238
+ * consumers who don't install the optional peer. Worse, `skipLibCheck` (on by
239
+ * default via astro's base tsconfig) *suppresses* that error, silently degrading
240
+ * the option to `any` instead of failing loudly.
241
+ *
242
+ * Need something not listed here (e.g. `chunks`)? Add `sitemap()` to your own
243
+ * `integrations` array — getvitops detects it and leaves yours in charge.
244
+ */
245
+ interface GetvitopsSitemapOptions {
246
+ /** Keep a page out of the sitemap. Receives the page's full absolute URL. */
247
+ filter?: (page: string) => boolean;
248
+ /** Absolute URLs of pages Astro doesn't build but the site serves. */
249
+ customPages?: string[];
250
+ /** Absolute URLs of externally-generated sitemaps to list in the index. */
251
+ customSitemaps?: string[];
252
+ /** Max entries per file before it splits (default 45000). */
253
+ entryLimit?: number;
254
+ /** Filename prefix (default 'sitemap' → `sitemap-index.xml`). */
255
+ filenameBase?: string;
256
+ changefreq?: GetvitopsSitemapChangeFreq;
257
+ lastmod?: Date;
258
+ priority?: number;
259
+ /** Per-entry rewrite; return `undefined` to drop the entry. */
260
+ serialize?: (item: GetvitopsSitemapEntry) => GetvitopsSitemapEntry | Promise<GetvitopsSitemapEntry | undefined> | undefined;
261
+ /** Emit `<xhtml:link rel="alternate">` for localised routes. */
262
+ i18n?: {
263
+ defaultLocale: string;
264
+ locales: Record<string, string>;
265
+ };
266
+ /** XSL stylesheet to prettify the XML (path relative to `site`, or absolute URL). */
267
+ xslURL?: string;
268
+ /** XML namespaces to exclude (all included by default). */
269
+ namespaces?: {
270
+ news?: boolean;
271
+ xhtml?: boolean;
272
+ image?: boolean;
273
+ video?: boolean;
274
+ };
275
+ }
32
276
  interface GetvitopsOptions {
33
277
  favicon?: GetvitopsFaviconOptions;
34
278
  /** Copy + link the web-component bundles (default true). */
@@ -47,8 +291,44 @@ interface GetvitopsOptions {
47
291
  * dev server, so on a static build that button simply isn't rendered.
48
292
  */
49
293
  editor?: boolean;
294
+ /**
295
+ * Render the site's legal documents (privacy policy, terms, cookie notice)
296
+ * from a site config, and re-render when it changes. Off unless provided.
297
+ *
298
+ * Requires `css`, which is what registers the Vite plugin that runs the
299
+ * generation — without it the documents would be written once and never
300
+ * refresh. Not using `css`? Run `vitops legal` from the CLI instead; it is the
301
+ * same renderer and works in any stack.
302
+ */
303
+ legal?: GetvitopsLegalOptions;
304
+ /**
305
+ * Emit `sitemap-index.xml` + `sitemap-0.xml` via `@astrojs/sitemap`, and link it
306
+ * from `<Head />`. Off unless provided; `true` uses its defaults.
307
+ *
308
+ * Needs `@astrojs/sitemap` (an optional peer — install it yourself) and the
309
+ * `site` astro.config option, since a sitemap lists absolute URLs.
310
+ *
311
+ * Skipped with a warning when `emdash()` is registered — EmDash serves its own
312
+ * database-driven `/sitemap.xml` — and deferred to when you already list
313
+ * `@astrojs/sitemap` yourself. Note it enumerates **prerendered** routes only,
314
+ * so on-demand pages of an `output: 'server'` site are not listed.
315
+ */
316
+ sitemap?: boolean | GetvitopsSitemapOptions;
317
+ /**
318
+ * Site-level defaults for `<Seo />` (`@getvitops/astro/Seo.astro`) — site name,
319
+ * title template, Open Graph + Twitter defaults, verification tokens. Off unless
320
+ * provided; per-page props override every field.
321
+ *
322
+ * Needs the `site` astro.config option for canonical and `og:url`; without it
323
+ * `<Seo />` omits every absolute-URL tag rather than deriving one from the
324
+ * request, and this warns at build time.
325
+ *
326
+ * On an EmDash site prefer `<EmDashHead>`, which covers the same tags from the
327
+ * CMS — using both double-emits them.
328
+ */
329
+ seo?: GetvitopsSeoOptions;
50
330
  }
51
331
  declare function getvitops(opts?: GetvitopsOptions): AstroIntegration;
52
332
  //#endregion
53
- export { type GetvitopsCssOptions, type GetvitopsFaviconOptions, type GetvitopsOptions, getvitops as default };
333
+ export { type GetvitopsCssOptions, type GetvitopsFaviconOptions, type GetvitopsOptions, type GetvitopsSeoOptions, type GetvitopsSitemapChangeFreq, type GetvitopsSitemapEntry, type GetvitopsSitemapOptions, type ResolvedSeo, type SeoContext, type SeoNameTag, type SeoOgType, type SeoPropertyTag, type SeoProps, type SeoTwitterCard, getvitops as default, resolveSeo };
54
334
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/integration.ts"],"mappings":";;;;;UAsBiB,uBAAA;;EAEf,MAAA;EACA,YAAA;EAHe;EAKf,IAAA;;EAEA,UAAA;EACA,eAAA;AAAA;AAAA,UAGe,mBAAA;EAJf;EAMA,KAAA;EALe;EAOf,MAAA,GAAS,MAAM;EAJA;EAMf,GAAA;;;;;;;;EAQA,MAAA;AAAA;AAAA,UAGe,gBAAA;EACf,OAAA,GAAU,uBAAA;;EAEV,aAAA;EAFA;EAIA,GAAA,GAAM,mBAAmB;EAFzB;;;;;AAcM;AACP;;;;;EADC,MAAA;AAAA;AAAA,iBA2BsB,SAAA,CAAU,IAAA,GAAM,gBAAA,GAAwB,gBAAgB"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/seo.ts","../src/integration.ts"],"mappings":"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"}