@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/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":";;;;;UAuBiB,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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"}
package/dist/index.mjs CHANGED
@@ -3,7 +3,6 @@ import { dirname, join, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { faviconLinks, generateFavicons, writeFaviconManifest } from "@getvitops/utils";
5
5
  import vitops from "@getvitops/vite";
6
- import tailwindcss from "@tailwindcss/vite";
7
6
 
8
7
  export * from "@getvitops/utils"
9
8
 
@@ -39,6 +38,11 @@ function getvitops(opts = {}) {
39
38
  const webComponents = opts.webComponents !== false;
40
39
  const editor = opts.editor === true;
41
40
  if (editor && !opts.css) logger.warn("editor: true needs a `css` config — it reads design-manifest.json.");
41
+ if (opts.legal && !opts.css) logger.warn("legal: needs a `css` config — the generation runs in the Vite plugin that `css` registers. Run `npx vitops legal` instead if you do not use `css`.");
42
+ if (opts.seo) {
43
+ if (!config.site) logger.warn("seo: needs the `site` astro.config option (your deployed URL) — without it <Seo /> omits canonical, og:url and any relative og:image rather than guessing an origin.");
44
+ if (config.integrations.some((i) => i.name === "emdash")) logger.warn("seo: emdash() is registered, and its <EmDashHead> emits the same title/description/canonical/og tags from the CMS. Use one or the other — rendering both duplicates every tag.");
45
+ }
42
46
  const hasManifest = !!(opts.favicon?.name && opts.favicon?.themeColor);
43
47
  let hasSvg = false;
44
48
  if (opts.favicon) {
@@ -74,6 +78,26 @@ function getvitops(opts = {}) {
74
78
  logger.info("copied web-component bundles → public/vitops/");
75
79
  } else logger.warn(`@getvitops/core dist not found (${coreDist}); run build:core. Skipping WC copy.`);
76
80
  }
81
+ let sitemapHref = null;
82
+ if (opts.sitemap) {
83
+ const registered = config.integrations.map((i) => i.name);
84
+ if (registered.includes("@astrojs/sitemap")) logger.info("sitemap: @astrojs/sitemap is already in your integrations — leaving yours in charge.");
85
+ else if (registered.includes("emdash")) logger.warn("sitemap: emdash() already serves /sitemap.xml from the database, which also covers on-demand pages a static sitemap cannot. Skipping — drop the `sitemap` option, or add @astrojs/sitemap to `integrations` yourself to run both.");
86
+ else if (!config.site) logger.warn("sitemap: needs the `site` astro.config option (your deployed URL) — a sitemap lists absolute URLs. Skipping.");
87
+ else {
88
+ let sitemap;
89
+ try {
90
+ ({default: sitemap} = await import("@astrojs/sitemap"));
91
+ } catch {
92
+ throw new Error("[getvitops] sitemap requires `@astrojs/sitemap` in your devDependencies — install it, or drop the `sitemap` option.");
93
+ }
94
+ if (config.output === "server") logger.warn("sitemap: output: 'server' — @astrojs/sitemap lists prerendered routes only, so on-demand pages are left out. Add `export const prerender = true` to the pages you want indexed, or list them in `sitemap.customPages`.");
95
+ const sitemapOpts = opts.sitemap === true ? void 0 : opts.sitemap;
96
+ updateConfig({ integrations: [sitemap(sitemapOpts)] });
97
+ sitemapHref = `/${sitemapOpts?.filenameBase ?? "sitemap"}-index.xml`;
98
+ logger.info(`sitemap registered → ${sitemapHref.slice(1)}`);
99
+ }
100
+ }
77
101
  updateConfig({ vite: { plugins: [virtualHeadPlugin({
78
102
  favicons: !!opts.favicon,
79
103
  faviconLinks: opts.favicon ? faviconLinks({
@@ -83,18 +107,31 @@ function getvitops(opts = {}) {
83
107
  themeColor: opts.favicon?.themeColor ?? null,
84
108
  webComponents,
85
109
  wcBase: "/vitops",
86
- editor
110
+ editor,
111
+ sitemap: sitemapHref,
112
+ site: config.site ?? null,
113
+ seo: opts.seo ?? {}
87
114
  })] } });
88
115
  if (opts.css) {
89
116
  const format = opts.css.format ?? "tailwind";
117
+ if (format === "design") throw new Error("[getvitops] css.format: 'design' emits only DESIGN.md, not a stylesheet. Run `npx vitops generate --format design --out .` for that file, and set css.format to 'tailwind' | 'css' | 'bricks' here.");
90
118
  const out = opts.css.out ?? "src/styles";
91
119
  const plugins = [vitops({
92
120
  input: opts.css.input ?? "design-system.json",
93
121
  format,
94
122
  out,
95
- ...editor ? { editorManifestDir: publicDir } : {}
123
+ ...editor ? { editorManifestDir: publicDir } : {},
124
+ ...opts.legal ? { legal: opts.legal } : {}
96
125
  })];
97
- if (format === "tailwind") plugins.push(...tailwindcss());
126
+ if (format === "tailwind") {
127
+ let tailwindcss;
128
+ try {
129
+ ({default: tailwindcss} = await import("@tailwindcss/vite"));
130
+ } catch {
131
+ throw new Error("[getvitops] css.format: 'tailwind' requires `tailwindcss` and `@tailwindcss/vite` in your devDependencies — install them, or switch to css.format: 'css' for the standalone bundle.");
132
+ }
133
+ plugins.push(...tailwindcss());
134
+ }
98
135
  updateConfig({ vite: { plugins } });
99
136
  const cssFile = format === "tailwind" ? "tailwind.css" : "styles.css";
100
137
  if (opts.css.inject !== false) {
@@ -108,5 +145,124 @@ function getvitops(opts = {}) {
108
145
  }
109
146
 
110
147
  //#endregion
111
- export { getvitops as default };
148
+ //#region src/seo.ts
149
+ /** `en-CA` → `en_CA`; Open Graph wants the underscore form. */
150
+ const ogLocale = (locale) => locale.replace("-", "_");
151
+ /**
152
+ * Absolute URLs pass through; relative ones resolve against `site`, or are
153
+ * dropped when there is no `site` to resolve them against. A relative `og:image`
154
+ * or canonical is worse than none — crawlers resolve it against whatever origin
155
+ * served the page.
156
+ */
157
+ function absolute(url, site) {
158
+ if (!url) return null;
159
+ try {
160
+ return new URL(url).href;
161
+ } catch {
162
+ return site ? new URL(url, site).href : null;
163
+ }
164
+ }
165
+ /** Composes the robots directive, or null when nothing meaningful was asked for. */
166
+ function resolveRobots(props, defaults) {
167
+ if (props.robots) return props.robots;
168
+ const { noindex, nofollow, noarchive, nocache, robotsExtras } = props;
169
+ if (noindex || nofollow || noarchive || nocache || robotsExtras) return [
170
+ noindex ? "noindex" : "index",
171
+ nofollow ? "nofollow" : "follow",
172
+ ...noarchive ? ["noarchive"] : [],
173
+ ...nocache ? ["nocache"] : [],
174
+ ...robotsExtras ? [robotsExtras] : []
175
+ ].join(", ");
176
+ return defaults.robots ?? null;
177
+ }
178
+ /**
179
+ * Merges site defaults with page props into the final tag set.
180
+ *
181
+ * Pure — no `Astro`, no filesystem, no config lookup. Everything it needs comes
182
+ * through its three arguments, which is what makes it testable and what keeps the
183
+ * package free of the consumer-global coupling that sank the previous site model.
184
+ */
185
+ function resolveSeo(defaults, props, ctx) {
186
+ const { site } = ctx;
187
+ const siteName = props.siteName ?? defaults.siteName ?? null;
188
+ const templated = props.title && defaults.titleTemplate && props.title !== siteName;
189
+ const title = props.title ? templated ? String(defaults.titleTemplate).replaceAll("%s", props.title) : props.title : defaults.defaultTitle ?? siteName;
190
+ const description = props.description ?? defaults.defaultDescription ?? null;
191
+ const canonical = props.canonical ? absolute(props.canonical, site) : site ? new URL(ctx.pathname + (ctx.search ?? ""), site).href : null;
192
+ const image = absolute(props.image, site) ?? absolute(defaults.openGraph?.image?.url, site);
193
+ const usingDefaultImage = !props.image;
194
+ const defaultImage = defaults.openGraph?.image;
195
+ const imageAlt = props.imageAlt ?? (usingDefaultImage ? defaultImage?.alt : void 0);
196
+ const imageWidth = props.imageWidth ?? (usingDefaultImage ? defaultImage?.width : void 0);
197
+ const imageHeight = props.imageHeight ?? (usingDefaultImage ? defaultImage?.height : void 0);
198
+ const imageType = props.imageType ?? (usingDefaultImage ? defaultImage?.type : void 0);
199
+ const type = props.type ?? defaults.openGraph?.type ?? "website";
200
+ const locale = props.locale ?? defaults.openGraph?.locale;
201
+ const openGraph = [];
202
+ const og = (property, content) => {
203
+ if (content !== null && content !== void 0 && content !== "") openGraph.push({
204
+ property,
205
+ content: String(content)
206
+ });
207
+ };
208
+ og("og:type", type);
209
+ og("og:title", title);
210
+ og("og:description", description);
211
+ og("og:url", canonical);
212
+ og("og:site_name", siteName);
213
+ og("og:locale", locale ? ogLocale(locale) : null);
214
+ for (const alt of defaults.openGraph?.localeAlternates ?? []) og("og:locale:alternate", ogLocale(alt));
215
+ og("og:image", image);
216
+ if (image) {
217
+ og("og:image:alt", imageAlt);
218
+ og("og:image:width", imageWidth);
219
+ og("og:image:height", imageHeight);
220
+ og("og:image:type", imageType);
221
+ }
222
+ if (type === "article" && props.article) {
223
+ const a = props.article;
224
+ og("article:published_time", a.publishedTime);
225
+ og("article:modified_time", a.modifiedTime);
226
+ og("article:expiration_time", a.expirationTime);
227
+ for (const author of a.authors ?? []) og("article:author", author);
228
+ og("article:section", a.section);
229
+ for (const tag of a.tags ?? []) og("article:tag", tag);
230
+ }
231
+ const twitter = [];
232
+ const tw = (name, content) => {
233
+ if (content) twitter.push({
234
+ name,
235
+ content
236
+ });
237
+ };
238
+ tw("twitter:card", image ? "summary_large_image" : defaults.twitter?.card ?? "summary");
239
+ tw("twitter:site", defaults.twitter?.site);
240
+ tw("twitter:creator", props.twitterCreator ?? defaults.twitter?.creator);
241
+ tw("twitter:title", title);
242
+ tw("twitter:description", description);
243
+ tw("twitter:image", image);
244
+ tw("twitter:image:alt", image ? imageAlt : null);
245
+ const verification = [];
246
+ if (defaults.googleSiteVerification) verification.push({
247
+ name: "google-site-verification",
248
+ content: defaults.googleSiteVerification
249
+ });
250
+ if (defaults.bingSiteVerification) verification.push({
251
+ name: "msvalidate.01",
252
+ content: defaults.bingSiteVerification
253
+ });
254
+ return {
255
+ title,
256
+ description,
257
+ canonical,
258
+ robots: resolveRobots(props, defaults),
259
+ openGraph,
260
+ twitter,
261
+ verification,
262
+ alternates: props.alternates ?? []
263
+ };
264
+ }
265
+
266
+ //#endregion
267
+ export { getvitops as default, resolveSeo };
112
268
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/integration.ts"],"sourcesContent":["/**\n * @getvitops/astro — the Astro integration. One entry wires the design system's\n * `<head>` contributions into a consumer site:\n * - favicons + PWA manifest (generated into public/),\n * - the web-component runtime bundles (copied into public/vitops/),\n * - (opt-in) the design-system CSS, generated + auto-injected (no manual import).\n * The `<Head />` component (shipped alongside) renders the tags, reading resolved\n * config from the `virtual:getvitops/head` module this integration provides.\n */\nimport { cpSync, existsSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { Format } from '@getvitops/generator';\nimport {\n type FaviconLink,\n faviconLinks,\n generateFavicons,\n writeFaviconManifest,\n} from '@getvitops/utils';\nimport vitops from '@getvitops/vite';\nimport tailwindcss from '@tailwindcss/vite';\nimport type { AstroIntegration } from 'astro';\n\nexport interface GetvitopsFaviconOptions {\n /** Source SVG or PNG. */\n source: string;\n lowResSource?: string;\n /** App name → enables `site.webmanifest`. */\n name?: string;\n /** PWA theme color (also `<meta name=\"theme-color\">`). */\n themeColor?: string;\n backgroundColor?: string;\n}\n\nexport interface GetvitopsCssOptions {\n /** Path to design-system.json (default 'design-system.json'). */\n input?: string;\n /** Output format (default 'tailwind'). */\n format?: Format;\n /** Directory the generated CSS is written to (default 'src/styles'). */\n out?: string;\n /**\n * Auto-inject the generated stylesheet into every SSR page (default true).\n * Set false when other integrations add routes that must not inherit the\n * design system (e.g. EmDash's `/_emdash/admin`) — then import the generated\n * file (`<out>/tailwind.css` or `<out>/styles.css`) from your site layout so\n * only your own pages (and previews rendered through them) are styled.\n */\n inject?: boolean;\n}\n\nexport interface GetvitopsOptions {\n favicon?: GetvitopsFaviconOptions;\n /** Copy + link the web-component bundles (default true). */\n webComponents?: boolean;\n /** Generate + auto-inject the design-system CSS. Off unless provided. */\n css?: GetvitopsCssOptions;\n /**\n * Ship the live theme editor (default false).\n *\n * Copies `editor.js` into `public/vitops/`, loads it from `<Head />`, and mirrors\n * the generated `design-manifest.json` next to it so `<wc-theme-editor>` resolves\n * its default `/vitops/design-manifest.json`. Requires `css` (the manifest is a\n * `css`-format output) — place the `<wc-theme-editor>` tag in your layout yourself.\n *\n * Editing is live on any deploy; **saving back** to `design-system.json` needs the\n * dev server, so on a static build that button simply isn't rendered.\n */\n editor?: boolean;\n}\n\ninterface HeadData {\n favicons: boolean;\n faviconLinks: FaviconLink[];\n themeColor: string | null;\n webComponents: boolean;\n wcBase: string;\n editor: boolean;\n}\n\nconst VIRTUAL_ID = 'virtual:getvitops/head';\nconst RESOLVED_ID = `\\0${VIRTUAL_ID}`;\n\nfunction virtualHeadPlugin(data: HeadData) {\n return {\n name: '@getvitops/astro:virtual-head',\n resolveId(id: string) {\n return id === VIRTUAL_ID ? RESOLVED_ID : null;\n },\n load(id: string) {\n return id === RESOLVED_ID ? `export default ${JSON.stringify(data)};` : null;\n },\n };\n}\n\nexport default function getvitops(opts: GetvitopsOptions = {}): AstroIntegration {\n return {\n name: '@getvitops/astro',\n hooks: {\n 'astro:config:setup': async ({ config, updateConfig, injectScript, logger }) => {\n const root = fileURLToPath(config.root);\n const publicDir = fileURLToPath(config.publicDir);\n const webComponents = opts.webComponents !== false;\n const editor = opts.editor === true;\n if (editor && !opts.css)\n logger.warn('editor: true needs a `css` config — it reads design-manifest.json.');\n const hasManifest = !!(opts.favicon?.name && opts.favicon?.themeColor);\n let hasSvg = false;\n\n // 1. Favicons + manifest → public/\n if (opts.favicon) {\n const source = resolve(root, opts.favicon.source);\n hasSvg = source.endsWith('.svg');\n await generateFavicons({\n source,\n outputDir: publicDir,\n ...(opts.favicon.lowResSource\n ? { lowResSource: resolve(root, opts.favicon.lowResSource) }\n : {}),\n });\n if (opts.favicon.name && opts.favicon.themeColor) {\n await writeFaviconManifest(publicDir, {\n name: opts.favicon.name,\n themeColor: opts.favicon.themeColor,\n ...(opts.favicon.backgroundColor\n ? { backgroundColor: opts.favicon.backgroundColor }\n : {}),\n });\n }\n logger.info('generated favicons + manifest → public/');\n }\n\n // 2. Web-component bundles → public/vitops/\n if (webComponents) {\n const corePkg = fileURLToPath(import.meta.resolve('@getvitops/core/package.json'));\n const coreDist = join(dirname(corePkg), 'dist');\n const dest = join(publicDir, 'vitops');\n if (existsSync(coreDist)) {\n const bundles = ['polyfills.js', 'deferred.js', 'elements.js', 'polyfills'];\n if (editor) bundles.push('editor.js');\n for (const f of bundles) {\n const src = join(coreDist, f);\n if (existsSync(src)) cpSync(src, join(dest, f), { recursive: true });\n }\n logger.info('copied web-component bundles → public/vitops/');\n } else {\n logger.warn(\n `@getvitops/core dist not found (${coreDist}); run build:core. Skipping WC copy.`,\n );\n }\n }\n\n // 3. Virtual head module for <Head/>\n updateConfig({\n vite: {\n plugins: [\n virtualHeadPlugin({\n favicons: !!opts.favicon,\n faviconLinks: opts.favicon ? faviconLinks({ hasSvg, manifest: hasManifest }) : [],\n themeColor: opts.favicon?.themeColor ?? null,\n webComponents,\n wcBase: '/vitops',\n editor,\n }),\n ],\n },\n });\n\n // 4. CSS — generate + compile + auto-inject (consumer imports nothing).\n if (opts.css) {\n const format: Format = opts.css.format ?? 'tailwind';\n const out = opts.css.out ?? 'src/styles';\n const plugins = [\n vitops({\n input: opts.css.input ?? 'design-system.json',\n format,\n out,\n // Mirrored inside the generate pass so the served copy can never be\n // missing on a cold start or stale after a config edit.\n ...(editor ? { editorManifestDir: publicDir } : {}),\n }),\n ];\n // tailwindcss() is typed against a newer vite than @getvitops/vite's\n // peer; the Plugin shapes are structurally compatible at runtime\n // (Astro forwards them verbatim), so bridge the two type identities.\n if (format === 'tailwind') plugins.push(...(tailwindcss() as unknown as typeof plugins));\n updateConfig({ vite: { plugins } });\n const cssFile = format === 'tailwind' ? 'tailwind.css' : 'styles.css';\n if (opts.css.inject !== false) {\n const cssPath = resolve(root, out, cssFile);\n injectScript('page-ssr', `import ${JSON.stringify(cssPath)};`);\n logger.info(`design-system CSS (${format}) auto-injected from ${out}/${cssFile}`);\n } else {\n logger.info(\n `design-system CSS (${format}) generated at ${out}/${cssFile} (inject: false — import it from your layout)`,\n );\n }\n }\n },\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/integration.ts","../src/seo.ts"],"sourcesContent":["/**\n * @getvitops/astro — the Astro integration. One entry wires the design system's\n * `<head>` contributions into a consumer site:\n * - favicons + PWA manifest (generated into public/),\n * - the web-component runtime bundles (copied into public/vitops/),\n * - (opt-in) the design-system CSS, generated + auto-injected (no manual import).\n * The `<Head />` component (shipped alongside) renders the tags, reading resolved\n * config from the `virtual:getvitops/head` module this integration provides.\n */\nimport { cpSync, existsSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { LegalOutput, StylesheetFormat } from '@getvitops/generator';\nimport {\n type FaviconLink,\n faviconLinks,\n generateFavicons,\n writeFaviconManifest,\n} from '@getvitops/utils';\nimport vitops from '@getvitops/vite';\nimport type { AstroIntegration } from 'astro';\nimport type { GetvitopsSeoOptions } from './seo.ts';\n\nexport interface GetvitopsFaviconOptions {\n /** Source SVG or PNG. */\n source: string;\n lowResSource?: string;\n /** App name → enables `site.webmanifest`. */\n name?: string;\n /** PWA theme color (also `<meta name=\"theme-color\">`). */\n themeColor?: string;\n backgroundColor?: string;\n}\n\nexport interface GetvitopsCssOptions {\n /** Path to design-system.json (default 'design-system.json'). */\n input?: string;\n /**\n * Output format (default 'tailwind'). Narrower than the generator's `Format`\n * on purpose: this option exists to produce a stylesheet to inject, and the\n * `design` format emits only `DESIGN.md`. Run that one via the CLI\n * (`vitops generate --format design --out .`).\n */\n format?: StylesheetFormat;\n /** Directory the generated CSS is written to (default 'src/styles'). */\n out?: string;\n /**\n * Auto-inject the generated stylesheet into every SSR page (default true).\n * Set false when other integrations add routes that must not inherit the\n * design system (e.g. EmDash's `/_emdash/admin`) — then import the generated\n * file (`<out>/tailwind.css` or `<out>/styles.css`) from your site layout so\n * only your own pages (and previews rendered through them) are styled.\n */\n inject?: boolean;\n}\n\nexport interface GetvitopsLegalOptions {\n /** Path to the site config (JSON) the documents are rendered from. */\n input: string;\n /**\n * Where to write them (default 'src/content/legal'). The default is a content\n * collection: the documents are markdown, and a collection is what lets a page\n * render one through your own layout rather than the integration injecting a\n * route (which nothing in this package does).\n */\n out?: string;\n /** Output format (default 'md'). */\n format?: LegalOutput;\n /** Environment whose A/B variant applies (default 'production'). */\n siteEnv?: string;\n}\n\n/** `changefreq`, mirroring @astrojs/sitemap's `ChangeFreq`. */\nexport type GetvitopsSitemapChangeFreq =\n | 'always'\n | 'hourly'\n | 'daily'\n | 'weekly'\n | 'monthly'\n | 'yearly'\n | 'never';\n\n/** One sitemap entry, mirroring @astrojs/sitemap's `SitemapItem`. */\nexport interface GetvitopsSitemapEntry {\n url: string;\n lastmod?: string;\n changefreq?: GetvitopsSitemapChangeFreq;\n priority?: number;\n /** Alternate-language variants. `lang` is required, as upstream requires it. */\n links?: { lang: string; hreflang?: string; url: string }[];\n}\n\n/**\n * Forwarded verbatim to `@astrojs/sitemap`. Deliberately a hand-written subset of\n * its `SitemapOptions` rather than a re-export: aliasing would put an\n * `import … from '@astrojs/sitemap'` — and, transitively via `SitemapItem`, from\n * `sitemap` — into this package's published .d.mts, which cannot resolve for the\n * consumers who don't install the optional peer. Worse, `skipLibCheck` (on by\n * default via astro's base tsconfig) *suppresses* that error, silently degrading\n * the option to `any` instead of failing loudly.\n *\n * Need something not listed here (e.g. `chunks`)? Add `sitemap()` to your own\n * `integrations` array — getvitops detects it and leaves yours in charge.\n */\nexport interface GetvitopsSitemapOptions {\n /** Keep a page out of the sitemap. Receives the page's full absolute URL. */\n filter?: (page: string) => boolean;\n /** Absolute URLs of pages Astro doesn't build but the site serves. */\n customPages?: string[];\n /** Absolute URLs of externally-generated sitemaps to list in the index. */\n customSitemaps?: string[];\n /** Max entries per file before it splits (default 45000). */\n entryLimit?: number;\n /** Filename prefix (default 'sitemap' → `sitemap-index.xml`). */\n filenameBase?: string;\n changefreq?: GetvitopsSitemapChangeFreq;\n lastmod?: Date;\n priority?: number;\n /** Per-entry rewrite; return `undefined` to drop the entry. */\n serialize?: (\n item: GetvitopsSitemapEntry,\n ) => GetvitopsSitemapEntry | Promise<GetvitopsSitemapEntry | undefined> | undefined;\n /** Emit `<xhtml:link rel=\"alternate\">` for localised routes. */\n i18n?: { defaultLocale: string; locales: Record<string, string> };\n /** XSL stylesheet to prettify the XML (path relative to `site`, or absolute URL). */\n xslURL?: string;\n /** XML namespaces to exclude (all included by default). */\n namespaces?: { news?: boolean; xhtml?: boolean; image?: boolean; video?: boolean };\n}\n\nexport interface GetvitopsOptions {\n favicon?: GetvitopsFaviconOptions;\n /** Copy + link the web-component bundles (default true). */\n webComponents?: boolean;\n /** Generate + auto-inject the design-system CSS. Off unless provided. */\n css?: GetvitopsCssOptions;\n /**\n * Ship the live theme editor (default false).\n *\n * Copies `editor.js` into `public/vitops/`, loads it from `<Head />`, and mirrors\n * the generated `design-manifest.json` next to it so `<wc-theme-editor>` resolves\n * its default `/vitops/design-manifest.json`. Requires `css` (the manifest is a\n * `css`-format output) — place the `<wc-theme-editor>` tag in your layout yourself.\n *\n * Editing is live on any deploy; **saving back** to `design-system.json` needs the\n * dev server, so on a static build that button simply isn't rendered.\n */\n editor?: boolean;\n /**\n * Render the site's legal documents (privacy policy, terms, cookie notice)\n * from a site config, and re-render when it changes. Off unless provided.\n *\n * Requires `css`, which is what registers the Vite plugin that runs the\n * generation — without it the documents would be written once and never\n * refresh. Not using `css`? Run `vitops legal` from the CLI instead; it is the\n * same renderer and works in any stack.\n */\n legal?: GetvitopsLegalOptions;\n /**\n * Emit `sitemap-index.xml` + `sitemap-0.xml` via `@astrojs/sitemap`, and link it\n * from `<Head />`. Off unless provided; `true` uses its defaults.\n *\n * Needs `@astrojs/sitemap` (an optional peer — install it yourself) and the\n * `site` astro.config option, since a sitemap lists absolute URLs.\n *\n * Skipped with a warning when `emdash()` is registered — EmDash serves its own\n * database-driven `/sitemap.xml` — and deferred to when you already list\n * `@astrojs/sitemap` yourself. Note it enumerates **prerendered** routes only,\n * so on-demand pages of an `output: 'server'` site are not listed.\n */\n sitemap?: boolean | GetvitopsSitemapOptions;\n /**\n * Site-level defaults for `<Seo />` (`@getvitops/astro/Seo.astro`) — site name,\n * title template, Open Graph + Twitter defaults, verification tokens. Off unless\n * provided; per-page props override every field.\n *\n * Needs the `site` astro.config option for canonical and `og:url`; without it\n * `<Seo />` omits every absolute-URL tag rather than deriving one from the\n * request, and this warns at build time.\n *\n * On an EmDash site prefer `<EmDashHead>`, which covers the same tags from the\n * CMS — using both double-emits them.\n */\n seo?: GetvitopsSeoOptions;\n}\n\n/**\n * Serialised into the virtual module with `JSON.stringify`, so every field here\n * must be JSON-representable — no functions. (That is why the sitemap's *href*\n * lives here and its options, which carry `filter`/`serialize`, do not.)\n */\ninterface HeadData {\n favicons: boolean;\n faviconLinks: FaviconLink[];\n themeColor: string | null;\n webComponents: boolean;\n wcBase: string;\n editor: boolean;\n /** `<link rel=\"sitemap\">` target, or null when no sitemap was registered. */\n sitemap: string | null;\n /** `config.site`, so `<Seo />` has a canonical base even without `Astro.site`. */\n site: string | null;\n /** Site-level `<Seo />` defaults. Function-free, hence serialisable. */\n seo: GetvitopsSeoOptions;\n}\n\nconst VIRTUAL_ID = 'virtual:getvitops/head';\nconst RESOLVED_ID = `\\0${VIRTUAL_ID}`;\n\nfunction virtualHeadPlugin(data: HeadData) {\n return {\n name: '@getvitops/astro:virtual-head',\n resolveId(id: string) {\n return id === VIRTUAL_ID ? RESOLVED_ID : null;\n },\n load(id: string) {\n return id === RESOLVED_ID ? `export default ${JSON.stringify(data)};` : null;\n },\n };\n}\n\nexport default function getvitops(opts: GetvitopsOptions = {}): AstroIntegration {\n return {\n name: '@getvitops/astro',\n hooks: {\n 'astro:config:setup': async ({ config, updateConfig, injectScript, logger }) => {\n const root = fileURLToPath(config.root);\n const publicDir = fileURLToPath(config.publicDir);\n const webComponents = opts.webComponents !== false;\n const editor = opts.editor === true;\n if (editor && !opts.css)\n logger.warn('editor: true needs a `css` config — it reads design-manifest.json.');\n if (opts.legal && !opts.css)\n logger.warn(\n 'legal: needs a `css` config — the generation runs in the Vite plugin that `css` ' +\n 'registers. Run `npx vitops legal` instead if you do not use `css`.',\n );\n if (opts.seo) {\n // Warn once here rather than letting every page silently drop its\n // canonical — a missing absolute URL is invisible in the output.\n if (!config.site)\n logger.warn(\n 'seo: needs the `site` astro.config option (your deployed URL) — without it <Seo /> ' +\n 'omits canonical, og:url and any relative og:image rather than guessing an origin.',\n );\n if (config.integrations.some((i) => i.name === 'emdash'))\n logger.warn(\n 'seo: emdash() is registered, and its <EmDashHead> emits the same title/description/' +\n 'canonical/og tags from the CMS. Use one or the other — rendering both duplicates ' +\n 'every tag.',\n );\n }\n const hasManifest = !!(opts.favicon?.name && opts.favicon?.themeColor);\n let hasSvg = false;\n\n // 1. Favicons + manifest → public/\n if (opts.favicon) {\n const source = resolve(root, opts.favicon.source);\n hasSvg = source.endsWith('.svg');\n await generateFavicons({\n source,\n outputDir: publicDir,\n ...(opts.favicon.lowResSource\n ? { lowResSource: resolve(root, opts.favicon.lowResSource) }\n : {}),\n });\n if (opts.favicon.name && opts.favicon.themeColor) {\n await writeFaviconManifest(publicDir, {\n name: opts.favicon.name,\n themeColor: opts.favicon.themeColor,\n ...(opts.favicon.backgroundColor\n ? { backgroundColor: opts.favicon.backgroundColor }\n : {}),\n });\n }\n logger.info('generated favicons + manifest → public/');\n }\n\n // 2. Web-component bundles → public/vitops/\n if (webComponents) {\n const corePkg = fileURLToPath(import.meta.resolve('@getvitops/core/package.json'));\n const coreDist = join(dirname(corePkg), 'dist');\n const dest = join(publicDir, 'vitops');\n if (existsSync(coreDist)) {\n const bundles = ['polyfills.js', 'deferred.js', 'elements.js', 'polyfills'];\n if (editor) bundles.push('editor.js');\n for (const f of bundles) {\n const src = join(coreDist, f);\n if (existsSync(src)) cpSync(src, join(dest, f), { recursive: true });\n }\n logger.info('copied web-component bundles → public/vitops/');\n } else {\n logger.warn(\n `@getvitops/core dist not found (${coreDist}); run build:core. Skipping WC copy.`,\n );\n }\n }\n\n // 3. Sitemap — opt-in, delegated to the official @astrojs/sitemap.\n //\n // Registered through updateConfig rather than imported at module scope,\n // because the package is an optional peer. Astro core has no\n // addIntegration() (that is Starlight's plugin API), but appending here is\n // supported: runHookConfigSetup re-reads `integrations.length` each\n // iteration and assigns the merged array back onto the settings, and\n // @astrojs/sitemap declares no `astro:config:setup` hook at all — only\n // config:done / routes:resolved / build:done, which all run afterwards.\n //\n // Runs before the virtual head module so <Head /> can link the result.\n let sitemapHref: string | null = null;\n if (opts.sitemap) {\n const registered = config.integrations.map((i) => i.name);\n if (registered.includes('@astrojs/sitemap')) {\n logger.info(\n 'sitemap: @astrojs/sitemap is already in your integrations — leaving yours in charge.',\n );\n } else if (registered.includes('emdash')) {\n logger.warn(\n 'sitemap: emdash() already serves /sitemap.xml from the database, which also covers ' +\n 'on-demand pages a static sitemap cannot. Skipping — drop the `sitemap` option, or ' +\n 'add @astrojs/sitemap to `integrations` yourself to run both.',\n );\n } else if (!config.site) {\n logger.warn(\n 'sitemap: needs the `site` astro.config option (your deployed URL) — a sitemap lists ' +\n 'absolute URLs. Skipping.',\n );\n } else {\n // Optional peer, loaded the same way as @tailwindcss/vite below.\n //\n // The import is cast because the option surface here is this package's\n // narrowed mirror, not @astrojs/sitemap's own `SitemapOptions`: the two\n // disagree on `serialize`'s entry type (upstream's `changefreq` is the\n // `EnumChangefreq` enum, ours the string union a consumer actually\n // wants to write). The shapes are identical at runtime — the value is\n // forwarded verbatim. Cast the module rather than widening the local to\n // `(o?: unknown)`, which is the wrong variance: a target accepting\n // `unknown` promises to accept *any* argument, which upstream doesn't.\n type SitemapFactory = (o?: GetvitopsSitemapOptions) => AstroIntegration;\n let sitemap: SitemapFactory;\n try {\n ({ default: sitemap } = (await import('@astrojs/sitemap')) as unknown as {\n default: SitemapFactory;\n });\n } catch {\n throw new Error(\n '[getvitops] sitemap requires `@astrojs/sitemap` in your devDependencies — ' +\n 'install it, or drop the `sitemap` option.',\n );\n }\n if (config.output === 'server')\n logger.warn(\n \"sitemap: output: 'server' — @astrojs/sitemap lists prerendered routes only, so \" +\n 'on-demand pages are left out. Add `export const prerender = true` to the pages ' +\n 'you want indexed, or list them in `sitemap.customPages`.',\n );\n const sitemapOpts = opts.sitemap === true ? undefined : opts.sitemap;\n updateConfig({ integrations: [sitemap(sitemapOpts)] });\n sitemapHref = `/${sitemapOpts?.filenameBase ?? 'sitemap'}-index.xml`;\n logger.info(`sitemap registered → ${sitemapHref.slice(1)}`);\n }\n }\n\n // 4. Virtual head module for <Head/>\n updateConfig({\n vite: {\n plugins: [\n virtualHeadPlugin({\n favicons: !!opts.favicon,\n faviconLinks: opts.favicon ? faviconLinks({ hasSvg, manifest: hasManifest }) : [],\n themeColor: opts.favicon?.themeColor ?? null,\n webComponents,\n wcBase: '/vitops',\n editor,\n sitemap: sitemapHref,\n site: config.site ?? null,\n seo: opts.seo ?? {},\n }),\n ],\n },\n });\n\n // 5. CSS — generate + compile + auto-inject (consumer imports nothing).\n if (opts.css) {\n const format: StylesheetFormat = opts.css.format ?? 'tailwind';\n // Typed out above, but a plain-JS config reaches here unchecked and\n // would otherwise fail late with an unresolvable `styles.css` import.\n if ((format as string) === 'design')\n throw new Error(\n \"[getvitops] css.format: 'design' emits only DESIGN.md, not a stylesheet. \" +\n 'Run `npx vitops generate --format design --out .` for that file, and set ' +\n \"css.format to 'tailwind' | 'css' | 'bricks' here.\",\n );\n const out = opts.css.out ?? 'src/styles';\n const plugins = [\n vitops({\n input: opts.css.input ?? 'design-system.json',\n format,\n out,\n // Mirrored inside the generate pass so the served copy can never be\n // missing on a cold start or stale after a config edit.\n ...(editor ? { editorManifestDir: publicDir } : {}),\n // Same reasoning: inside the pass, so the documents track the site\n // config in dev instead of going stale after the first build.\n ...(opts.legal ? { legal: opts.legal } : {}),\n }),\n ];\n // Tailwind is an optional peer — only the `tailwind` format needs it,\n // so it's loaded here rather than imported at module scope. (The\n // plugin is typed against a newer vite than @getvitops/vite's peer;\n // the Plugin shapes are structurally compatible at runtime — Astro\n // forwards them verbatim — so bridge the two type identities.)\n if (format === 'tailwind') {\n let tailwindcss: () => unknown[];\n try {\n ({ default: tailwindcss } = await import('@tailwindcss/vite'));\n } catch {\n throw new Error(\n \"[getvitops] css.format: 'tailwind' requires `tailwindcss` and `@tailwindcss/vite` \" +\n 'in your devDependencies — install them, or switch to css.format: ' +\n \"'css' for the standalone bundle.\",\n );\n }\n plugins.push(...(tailwindcss() as unknown as typeof plugins));\n }\n updateConfig({ vite: { plugins } });\n const cssFile = format === 'tailwind' ? 'tailwind.css' : 'styles.css';\n if (opts.css.inject !== false) {\n const cssPath = resolve(root, out, cssFile);\n injectScript('page-ssr', `import ${JSON.stringify(cssPath)};`);\n logger.info(`design-system CSS (${format}) auto-injected from ${out}/${cssFile}`);\n } else {\n logger.info(\n `design-system CSS (${format}) generated at ${out}/${cssFile} (inject: false — import it from your layout)`,\n );\n }\n }\n },\n },\n };\n}\n","/**\n * The `<Seo />` resolution layer — site-level defaults merged with per-page props\n * into the exact tags to emit.\n *\n * Kept separate from `Seo.astro` deliberately. The component this replaces\n * (`SEO.astro`, deleted in c949cae) fused resolution with markup, which is why it\n * was never unit-tested and why its title handling drifted: it computed a title\n * for `og:title` but left `<title>` to the layout, so the two could disagree.\n * Everything decidable lives here and is asserted in `seo.test.ts`; the component\n * only renders what this returns.\n */\n\n/** Twitter card types, per <https://developer.x.com/en/docs/twitter-for-websites/cards>. */\nexport type SeoTwitterCard = 'summary' | 'summary_large_image' | 'app' | 'player';\n\n/** Open Graph object types this component knows how to describe. */\nexport type SeoOgType = 'website' | 'article' | 'profile';\n\n/**\n * Site-level SEO defaults, passed to `getvitops({ seo })` and baked into the\n * `virtual:getvitops/head` module. Per-page `<Seo />` props override these.\n *\n * The vocabulary mirrors the site-config schema's `seo` block\n * (`@getvitops/generator`, `src/site.ts`) so a `SiteConfig` → these adapter would\n * be a flat field map — but it is not imported from there. This is the\n * integration's own option surface, and it must stay JSON-serialisable: it is\n * `JSON.stringify`d into the virtual module, so no functions, no `URL`s, no dates.\n */\nexport interface GetvitopsSeoOptions {\n /** `og:site_name`, and the `<title>` when a page passes none. */\n siteName?: string;\n /**\n * Page-title pattern; `%s` is the page's own title. e.g. `'%s · Acme'`.\n *\n * Skipped when a page's title already equals `siteName` — a homepage titled\n * \"Acme\" emits `Acme`, not `Acme · Acme` — and never applied to `defaultTitle`.\n */\n titleTemplate?: string;\n /** `<title>` for pages that pass none. Used verbatim — the template is not applied. */\n defaultTitle?: string;\n /** `<meta name=\"description\">` for pages that pass none. */\n defaultDescription?: string;\n /** Site-wide robots directive, e.g. `'index,follow,max-image-preview:large'`. */\n robots?: string;\n /** Google Search Console verification token. */\n googleSiteVerification?: string;\n /** Bing Webmaster Tools verification token. */\n bingSiteVerification?: string;\n openGraph?: {\n /** Default `og:type` (default `'website'`). */\n type?: SeoOgType;\n /** `og:locale`. Written either way — `en-CA` and `en_CA` both emit `en_CA`. */\n locale?: string;\n /** Additional locales the same content exists in → `og:locale:alternate`. */\n localeAlternates?: string[];\n /** Fallback share image. Relative URLs resolve against `site`. */\n image?: { url: string; alt?: string; width?: number; height?: number; type?: string };\n };\n twitter?: {\n /** Card type when no image resolves (default `'summary'`). */\n card?: SeoTwitterCard;\n /** The site's own handle, `@acme`. */\n site?: string;\n /** Default author handle, `@ada`. */\n creator?: string;\n };\n}\n\n/** Per-page `<Seo />` props. Everything is optional; defaults fill the gaps. */\nexport interface SeoProps {\n /** Page title, before `titleTemplate` is applied. */\n title?: string;\n description?: string;\n /** Overrides the computed canonical. Relative values resolve against `site`. */\n canonical?: string;\n /** Overrides `og:site_name` and the title fallback. */\n siteName?: string;\n /** Share image for this page. Relative URLs resolve against `site`. */\n image?: string;\n imageAlt?: string;\n imageWidth?: number;\n imageHeight?: number;\n /** MIME type of the share image, e.g. `'image/png'`. */\n imageType?: string;\n noindex?: boolean;\n nofollow?: boolean;\n noarchive?: boolean;\n nocache?: boolean;\n /** Extra robots directives, e.g. `'max-snippet:-1'`. Appended to the composed value. */\n robotsExtras?: string;\n /** Full robots override. Wins over the flags above and the site default. */\n robots?: string;\n /** `og:type` for this page (default: the site default, else `'website'`). */\n type?: SeoOgType;\n /** Emitted only when `type` is `'article'`. */\n article?: {\n publishedTime?: string;\n modifiedTime?: string;\n expirationTime?: string;\n authors?: string[];\n section?: string;\n tags?: string[];\n };\n /** `og:locale` for this page. */\n locale?: string;\n /** Twitter author handle for this page, `@ada`. */\n twitterCreator?: string;\n /**\n * Translations of *this* page, as `<link rel=\"alternate\" hreflang>`.\n *\n * Explicit only, and it must include the current page. Nothing is inferred:\n * the deleted component derived alternates from the site's locale list and\n * pointed every one of them at that locale's homepage, so every page on the\n * site claimed the homepage as its translation. Only the page knows its own\n * translated URLs. Pass `hreflang: 'x-default'` for the fallback entry.\n */\n alternates?: { hreflang: string; href: string }[];\n}\n\n/** A `<meta property>` tag (Open Graph uses `property`, not `name`). */\nexport interface SeoPropertyTag {\n property: string;\n content: string;\n}\n\n/** A `<meta name>` tag. */\nexport interface SeoNameTag {\n name: string;\n content: string;\n}\n\n/** Everything `<Seo />` renders, already decided. `null` means \"emit nothing\". */\nexport interface ResolvedSeo {\n title: string | null;\n description: string | null;\n canonical: string | null;\n robots: string | null;\n openGraph: SeoPropertyTag[];\n twitter: SeoNameTag[];\n verification: SeoNameTag[];\n alternates: { hreflang: string; href: string }[];\n}\n\n/** Render context supplied by the component from `Astro`. */\nexport interface SeoContext {\n /**\n * The deployed origin — `Astro.site`, or `config.site` via the virtual module.\n *\n * `undefined` when neither is set, and then every absolute-URL tag is dropped\n * rather than being derived from the request. `astro-seo` falls back to\n * `Astro.url` here; on a static build that is `http://localhost:4321`, which\n * would bake a localhost canonical into production HTML.\n */\n site?: URL | undefined;\n /** `Astro.url.pathname` — already includes `base`. */\n pathname: string;\n /** `Astro.url.search`, if the canonical should keep the query string. */\n search?: string;\n}\n\n/** `en-CA` → `en_CA`; Open Graph wants the underscore form. */\nconst ogLocale = (locale: string) => locale.replace('-', '_');\n\n/**\n * Absolute URLs pass through; relative ones resolve against `site`, or are\n * dropped when there is no `site` to resolve them against. A relative `og:image`\n * or canonical is worse than none — crawlers resolve it against whatever origin\n * served the page.\n */\nfunction absolute(url: string | undefined, site: URL | undefined): string | null {\n if (!url) return null;\n try {\n return new URL(url).href;\n } catch {\n return site ? new URL(url, site).href : null;\n }\n}\n\n/** Composes the robots directive, or null when nothing meaningful was asked for. */\nfunction resolveRobots(props: SeoProps, defaults: GetvitopsSeoOptions): string | null {\n if (props.robots) return props.robots;\n const { noindex, nofollow, noarchive, nocache, robotsExtras } = props;\n if (noindex || nofollow || noarchive || nocache || robotsExtras) {\n return [\n noindex ? 'noindex' : 'index',\n nofollow ? 'nofollow' : 'follow',\n ...(noarchive ? ['noarchive'] : []),\n ...(nocache ? ['nocache'] : []),\n ...(robotsExtras ? [robotsExtras] : []),\n ].join(', ');\n }\n // No explicit default either: emit nothing. `index, follow` is what crawlers\n // already assume, so stating it adds bytes and no meaning.\n return defaults.robots ?? null;\n}\n\n/**\n * Merges site defaults with page props into the final tag set.\n *\n * Pure — no `Astro`, no filesystem, no config lookup. Everything it needs comes\n * through its three arguments, which is what makes it testable and what keeps the\n * package free of the consumer-global coupling that sank the previous site model.\n */\nexport function resolveSeo(\n defaults: GetvitopsSeoOptions,\n props: SeoProps,\n ctx: SeoContext,\n): ResolvedSeo {\n const { site } = ctx;\n const siteName = props.siteName ?? defaults.siteName ?? null;\n\n // The template applies to a page's own title only, and only when that title\n // isn't already the site name. Two ways to get \"Acme · Acme\" otherwise: running\n // it over `defaultTitle`, and a homepage whose own title happens to equal the\n // site name — which is the common case, not an edge case.\n const templated = props.title && defaults.titleTemplate && props.title !== siteName;\n const title = props.title\n ? templated\n ? String(defaults.titleTemplate).replaceAll('%s', props.title)\n : props.title\n : (defaults.defaultTitle ?? siteName);\n\n const description = props.description ?? defaults.defaultDescription ?? null;\n\n const canonical = props.canonical\n ? absolute(props.canonical, site)\n : site\n ? new URL(ctx.pathname + (ctx.search ?? ''), site).href\n : null;\n\n const image = absolute(props.image, site) ?? absolute(defaults.openGraph?.image?.url, site);\n // Only inherit the default image's metadata when the page didn't bring its own\n // image — otherwise a page's image would carry the default's alt text.\n const usingDefaultImage = !props.image;\n const defaultImage = defaults.openGraph?.image;\n const imageAlt = props.imageAlt ?? (usingDefaultImage ? defaultImage?.alt : undefined);\n const imageWidth = props.imageWidth ?? (usingDefaultImage ? defaultImage?.width : undefined);\n const imageHeight = props.imageHeight ?? (usingDefaultImage ? defaultImage?.height : undefined);\n const imageType = props.imageType ?? (usingDefaultImage ? defaultImage?.type : undefined);\n\n const type = props.type ?? defaults.openGraph?.type ?? 'website';\n const locale = props.locale ?? defaults.openGraph?.locale;\n\n const openGraph: SeoPropertyTag[] = [];\n const og = (property: string, content: string | number | null | undefined) => {\n if (content !== null && content !== undefined && content !== '')\n openGraph.push({ property, content: String(content) });\n };\n\n og('og:type', type);\n og('og:title', title);\n og('og:description', description);\n og('og:url', canonical);\n og('og:site_name', siteName);\n og('og:locale', locale ? ogLocale(locale) : null);\n for (const alt of defaults.openGraph?.localeAlternates ?? [])\n og('og:locale:alternate', ogLocale(alt));\n og('og:image', image);\n if (image) {\n og('og:image:alt', imageAlt);\n og('og:image:width', imageWidth);\n og('og:image:height', imageHeight);\n og('og:image:type', imageType);\n }\n if (type === 'article' && props.article) {\n const a = props.article;\n og('article:published_time', a.publishedTime);\n og('article:modified_time', a.modifiedTime);\n og('article:expiration_time', a.expirationTime);\n for (const author of a.authors ?? []) og('article:author', author);\n og('article:section', a.section);\n for (const tag of a.tags ?? []) og('article:tag', tag);\n }\n\n const twitter: SeoNameTag[] = [];\n const tw = (name: string, content: string | null | undefined) => {\n if (content) twitter.push({ name, content });\n };\n // A card with a large image is only honoured if there *is* an image.\n tw('twitter:card', image ? 'summary_large_image' : (defaults.twitter?.card ?? 'summary'));\n tw('twitter:site', defaults.twitter?.site);\n tw('twitter:creator', props.twitterCreator ?? defaults.twitter?.creator);\n tw('twitter:title', title);\n tw('twitter:description', description);\n tw('twitter:image', image);\n tw('twitter:image:alt', image ? imageAlt : null);\n\n const verification: SeoNameTag[] = [];\n if (defaults.googleSiteVerification)\n verification.push({\n name: 'google-site-verification',\n content: defaults.googleSiteVerification,\n });\n if (defaults.bingSiteVerification)\n verification.push({ name: 'msvalidate.01', content: defaults.bingSiteVerification });\n\n return {\n title,\n description,\n canonical,\n robots: resolveRobots(props, defaults),\n openGraph,\n twitter,\n verification,\n alternates: props.alternates ?? [],\n };\n}\n"],"mappings":"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"}