@getvitops/utils 2.0.0 → 2.1.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,119 @@
1
1
  # @getvitops/utils
2
2
 
3
+ ## 2.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 20e518e: Add `vitops indexing` — tell search engines about a deploy, instead of opening Search Console by hand.
8
+
9
+ Configure it in your site config under `seo.indexing`, then run it after deploying:
10
+
11
+ ```jsonc
12
+ "seo": {
13
+ "indexing": {
14
+ "indexNow": { "key": "…" },
15
+ "searchConsole": { "siteUrl": "sc-domain:acme.ca" },
16
+ "priorityUrls": ["https://acme.ca/", "https://acme.ca/services"]
17
+ }
18
+ }
19
+ ```
20
+
21
+ ```
22
+ vitops indexing --dry # print the plan, make no requests
23
+ vitops indexing # submit
24
+ vitops indexing --check # a day or two later: did Google actually index them?
25
+ ```
26
+
27
+ It reads your sitemap, diffs it against the previous run, and submits only what changed —
28
+ pinging **IndexNow** and re-submitting your sitemap through the **Search Console API**.
29
+ `--check` inspects `priorityUrls` and exits non-zero on a page Google hasn't indexed, so a
30
+ scheduled CI job can catch a page that quietly fell out of the index.
31
+
32
+ **Be clear-eyed about what this can do.** Google exposes no API that requests indexing — the
33
+ button in Search Console isn't available anywhere, URL Inspection is read-only, and the sitemap
34
+ ping endpoint was removed in 2023. IndexNow reaches Bing, Yandex, Naver, Seznam and Yep; Google
35
+ doesn't participate. So this automates every sanctioned step and then verifies the outcome; it
36
+ does not make Google re-index on demand, and nothing can. Google's Indexing API is deliberately
37
+ not wired: it's scoped to job postings and livestreams, and using it for ordinary pages violates
38
+ its terms.
39
+
40
+ Also new, and worth wiring at the same time — **`gitLastmod()` for real sitemap dates**:
41
+
42
+ ```js
43
+ import vitops, { gitLastmod } from '@getvitops/astro';
44
+ vitops({ sitemap: { serialize: await gitLastmod() } });
45
+ ```
46
+
47
+ `@astrojs/sitemap` emits no `<lastmod>` by default, which means a crawler is told your pages
48
+ exist but never that one changed — and it's what lets `vitops indexing` submit a handful of URLs instead
49
+ of all of them. `gitLastmod()` derives each date from the source file's last commit, and leaves a
50
+ page alone rather than guessing (dynamic routes, ambiguous slugs, shallow clones): an inaccurate
51
+ `lastmod` is worse than none, because Google stops trusting the field site-wide.
52
+
53
+ Requires `fetch-depth: 0` in CI — the default shallow clone has no history to read, and it warns
54
+ when that's the case.
55
+
56
+ **Credentials.** IndexNow's key is public by design (it's served at `/<key>.txt` as the ownership
57
+ proof), so it lives in your config, and the Astro integration writes the file into `public/` for
58
+ you — `vitops indexing --new-key` generates one, `--write-key <dir>` writes it for non-Astro stacks.
59
+ Search Console needs a service account in `VITOPS_GSC_SERVICE_ACCOUNT` or
60
+ `GOOGLE_APPLICATION_CREDENTIALS`, added as an owner of the property; it's never read from the
61
+ config file.
62
+
63
+ Persist `.vitops/` between runs (a CI cache) — that's the changed-URL state, and without it every
64
+ run submits everything.
65
+
66
+ An environment whose `robots` policy says `noindex` is refused outright, so pointing this at
67
+ staging can't publish it to a search engine.
68
+
69
+ - 9bf975a: Add `vitops media` — encode raw video into web-ready outputs, instead of hand-rolling an ffmpeg
70
+ script per project.
71
+
72
+ Keep unprocessed video in a `raw/` directory and run:
73
+
74
+ ```
75
+ vitops media --raw raw --out src/assets/processed
76
+ ```
77
+
78
+ Each source becomes three files: **VP9/WebM**, an **H.264/MP4** fallback, and a **JPG poster**.
79
+ Import them like any other asset, so your bundler content-hashes them:
80
+
81
+ ```astro
82
+ ---
83
+ import hero from '../assets/processed/hero.webm';
84
+ import poster from '../assets/processed/hero.jpg';
85
+ ---
86
+ <video poster={poster.src} autoplay muted loop playsinline>
87
+ <source src={hero} type="video/webm" />
88
+ </video>
89
+ ```
90
+
91
+ In Astro it's an integration option — `vitops({ css, media: { raw: 'raw', out: 'src/assets/processed' } })`
92
+ — and it runs in the same pass that generates your CSS. `@getvitops/vite` gains a matching `media`
93
+ option, and `@getvitops/utils/media` exports `processMedia()` for anything else.
94
+
95
+ Defaults: capped at 1920px wide, CRF 32, audio dropped (the common case is a muted autoplay loop),
96
+ poster from frame 0. All of them are flags. `--dry` prints exactly what a run would do.
97
+
98
+ **Runs are cached**, on source content plus encode settings, in `.vitops/media-manifest.json` — a
99
+ 24 MB clip that took 88 seconds the first time takes 0.14 seconds the second. A missing output
100
+ re-encodes; a corrupt manifest re-encodes everything. Neither ever reads as "already done", because
101
+ that failure is silent and no rebuild would fix it.
102
+
103
+ **Commit the outputs and the manifest.** A fresh CI clone has neither and would re-encode from
104
+ scratch; committing both means CI never needs ffmpeg. It also keeps your history clean — ffmpeg
105
+ output isn't byte-reproducible across versions, so a CI re-encode would rewrite every video on any
106
+ toolchain bump. Use `--force` when you mean to re-encode.
107
+
108
+ **`ffmpeg` is an external tool, not an npm dependency** — install it yourself (`brew install
109
+ ffmpeg`, `apt install ffmpeg`, `winget install Gyan.FFmpeg`). The command fails without it rather
110
+ than skipping: a page referencing a video that was never encoded is broken, not degraded.
111
+
112
+ Two defaults worth knowing about. The MP4 exists because older iOS and the in-app webviews inside
113
+ social apps still don't decode VP9, and it's written with `+faststart` — without which a browser
114
+ downloads the whole file before showing frame one. And a poster taken from frame 0 is often black
115
+ on a clip that fades in; that's `--poster-time`, not a bug.
116
+
3
117
  ## 2.0.0
4
118
 
5
119
  ### Minor Changes
package/README.md CHANGED
@@ -25,6 +25,39 @@ await generateFavicons({
25
25
  Also available via the CLI (`vitops favicon -i logo.svg -o public`) and the Vite plugin's
26
26
  `favicon` option.
27
27
 
28
+ ## Video encoding (`@getvitops/utils/media`)
29
+
30
+ Encode a directory of raw video into VP9/WebM + an H.264/MP4 fallback + a JPG poster frame.
31
+
32
+ ```ts
33
+ import { processMedia } from '@getvitops/utils/media';
34
+
35
+ const { written, skipped } = await processMedia({
36
+ raw: 'raw',
37
+ out: 'src/assets/processed',
38
+ config: { maxWidth: 1920, crf: 32 },
39
+ });
40
+ ```
41
+
42
+ Its own subpath, like `./indexing`: it shells out to **`ffmpeg`**, which must be on `PATH`, and
43
+ it throws rather than skipping when it isn't.
44
+
45
+ Runs are cached in `.vitops/media-manifest.json` on source content and encode settings, so a
46
+ second run costs a hash per file. `planMedia` decides all of it and touches no filesystem, which
47
+ is what makes `dry: true` a complete account of a run rather than an approximation — the same
48
+ split `./indexing` makes.
49
+
50
+ Also available via the CLI (`vitops media`), the Vite plugin's `media` option, and
51
+ `getvitops({ media })` in Astro. See the [CLI readme](https://www.npmjs.com/package/@getvitops/cli)
52
+ for why the outputs are meant to be committed.
53
+
54
+ ## Search-engine notification (`@getvitops/utils/indexing`)
55
+
56
+ The engine behind `vitops indexing`: sitemap parsing, the changed-URL diff against the previous
57
+ run's snapshot, IndexNow submission, and the Search Console API calls (sitemap resubmit + read-only
58
+ URL inspection). `plan()` is pure — no network, no filesystem, no clock — and `indexnow.ts` /
59
+ `gsc.ts` execute a plan without deciding anything.
60
+
28
61
  ## Changelog
29
62
 
30
63
  This package's history: [`CHANGELOG.md`](./CHANGELOG.md) (shipped in the npm tarball, so it
@@ -0,0 +1,277 @@
1
+ //#region src/indexing/types.d.ts
2
+ /**
3
+ * The indexing module's own option surface.
4
+ *
5
+ * Deliberately structural rather than `SiteConfig['seo']['indexing']` imported
6
+ * from `@getvitops/generator`: the generator already depends on this package, so
7
+ * importing back would be a cycle. The vocabulary mirrors that block field for
8
+ * field, so the CLI's adapter is a flat map — the same arrangement, and for the
9
+ * same reason, as `GetvitopsSeoOptions` in `@getvitops/astro`.
10
+ */
11
+ /** IndexNow submission settings. The key is public by design — see `key`. */
12
+ interface IndexNowConfig {
13
+ /**
14
+ * The IndexNow key. **Not a secret**: the engine fetches it back from
15
+ * `keyLocation` to prove you control the host, so it is published either way.
16
+ */
17
+ key: string;
18
+ /** Absolute URL of the key file. Defaults to `<canonical>/<key>.txt`. */
19
+ keyLocation?: string | undefined;
20
+ /** Endpoint (default `https://api.indexnow.org/indexnow`). */
21
+ endpoint?: string | undefined;
22
+ }
23
+ /** Google Search Console property settings. The credential never lives here. */
24
+ interface SearchConsoleConfig {
25
+ /** `sc-domain:acme.ca`, or the URL-prefix form `https://acme.ca/`. */
26
+ siteUrl: string;
27
+ /** Re-submit the sitemap on each run (default true). */
28
+ resubmitSitemap?: boolean | undefined;
29
+ }
30
+ /** Everything `plan()` needs to decide what to do. */
31
+ interface IndexingConfig {
32
+ /** `domains.canonical` — the origin every default is derived from. */
33
+ canonical?: string | undefined;
34
+ /** Overrides the derived `<canonical>/sitemap-index.xml`. */
35
+ sitemapUrl?: string | undefined;
36
+ indexNow?: IndexNowConfig | undefined;
37
+ searchConsole?: SearchConsoleConfig | undefined;
38
+ /** Pages `--check` verifies. Explicit because URL Inspection is quota-bound. */
39
+ priorityUrls?: string[] | undefined;
40
+ /**
41
+ * The resolved environment's robots policy. A value containing `noindex` blocks
42
+ * the entire run — see `plan()`.
43
+ */
44
+ robots?: string | undefined;
45
+ }
46
+ /** One `<url>` (or `<sitemap>`) entry. */
47
+ interface SitemapEntry {
48
+ loc: string;
49
+ lastmod?: string;
50
+ }
51
+ /** The previous run's sitemap state, as written by `writeSnapshot`. */
52
+ interface Snapshot {
53
+ version: 1;
54
+ sitemapUrl: string;
55
+ takenAt: string;
56
+ /** `loc` → `lastmod`, or `''` when the entry carried none. */
57
+ entries: Record<string, string>;
58
+ }
59
+ //#endregion
60
+ //#region src/indexing/plan.d.ts
61
+ declare const INDEXNOW_ENDPOINT = "https://api.indexnow.org/indexnow";
62
+ /**
63
+ * IndexNow's own cap per submission. Batches larger than this are split rather
64
+ * than truncated — a silently dropped tail is the failure mode this command is
65
+ * meant to eliminate, not reproduce.
66
+ */
67
+ declare const INDEXNOW_BATCH = 10000;
68
+ /** Why the URL set is what it is. */
69
+ type UrlReason = 'explicit' | 'all' | 'cold' | 'diff';
70
+ /** A channel's decision: on, or off with a stated reason. */
71
+ interface ChannelPlan {
72
+ enabled: boolean;
73
+ /** Why it's off. Present iff `enabled` is false. */
74
+ skip?: string;
75
+ }
76
+ interface IndexNowPlan extends ChannelPlan {
77
+ endpoint: string;
78
+ keyLocation?: string | undefined;
79
+ key?: string | undefined;
80
+ host?: string | undefined;
81
+ /** URL batches, each within `INDEXNOW_BATCH`. */
82
+ batches: string[][];
83
+ }
84
+ interface SearchConsolePlan extends ChannelPlan {
85
+ siteUrl?: string | undefined;
86
+ }
87
+ interface IndexingPlan {
88
+ /** Set when the whole run is refused. Nothing is submitted. */
89
+ blocked?: string;
90
+ /** Always computed; `undefined` when nothing in the config resolves to one. */
91
+ sitemapUrl: string | undefined;
92
+ /** URLs that changed and should be submitted. */
93
+ urls: string[];
94
+ reason: UrlReason;
95
+ /** No usable previous snapshot — `urls` is everything. */
96
+ cold: boolean;
97
+ indexNow: IndexNowPlan;
98
+ searchConsole: SearchConsolePlan;
99
+ /** URLs `--check` will inspect. */
100
+ check: string[];
101
+ /** Things the operator needs to read. Printed by `--dry` and by a real run. */
102
+ notes: string[];
103
+ }
104
+ interface PlanInput {
105
+ config: IndexingConfig;
106
+ /** The sitemap as it stands now. */
107
+ current: SitemapEntry[];
108
+ /** The previous run's state, if any. */
109
+ previous?: Snapshot | undefined;
110
+ /** `--urls`: submit exactly these, skipping the diff. */
111
+ explicitUrls?: string[] | undefined;
112
+ /** `--all`: submit every URL in the sitemap. */
113
+ all?: boolean | undefined;
114
+ }
115
+ /**
116
+ * Resolve the sitemap URL.
117
+ *
118
+ * `sitemap-index.xml` is @astrojs/sitemap's default filename and is what
119
+ * `vitops({ sitemap })` registers, so it is the right default here. It resolves to
120
+ * a flat `<urlset>` on a small site, which `parseSitemap` handles by reading the
121
+ * root element rather than the name.
122
+ */
123
+ declare function resolveSitemapUrl(config: IndexingConfig): string | undefined;
124
+ /** Resolve the IndexNow key file URL. */
125
+ declare function resolveKeyLocation(config: IndexingConfig): string | undefined;
126
+ /**
127
+ * Decide what this run should do.
128
+ *
129
+ * Ordering of the URL-selection rules matters and is not arbitrary: an explicit
130
+ * `--urls` is an operator overriding the tool, `--all` is an operator overriding
131
+ * the diff, a cold snapshot is the tool having no basis to narrow, and the diff is
132
+ * the normal path. Each is reported in `reason` so `--dry` explains itself.
133
+ */
134
+ declare function plan(input: PlanInput): IndexingPlan;
135
+ /** Render a plan as the human-readable block `--dry` prints. */
136
+ declare function formatPlan(p: IndexingPlan): string;
137
+ //#endregion
138
+ //#region src/indexing/sitemap.d.ts
139
+ /** What a sitemap document turned out to be. */
140
+ interface ParsedSitemap {
141
+ /** `<url>` entries, when this was a `<urlset>`. */
142
+ entries: SitemapEntry[];
143
+ /** Child sitemap URLs, when this was a `<sitemapindex>`. */
144
+ sitemaps: string[];
145
+ }
146
+ /**
147
+ * Parse a sitemap or sitemap index.
148
+ *
149
+ * Which one it is comes from the root element, not from the filename — an
150
+ * `sitemap-index.xml` that is actually a flat `<urlset>` is common enough
151
+ * (@astrojs/sitemap emits an index only past `entryLimit`) that keying on the name
152
+ * would misread the single-file case as "no URLs".
153
+ */
154
+ declare function parseSitemap(xml: string): ParsedSitemap;
155
+ /** Reads a sitemap from a URL or a local path. */
156
+ type SitemapReader = (source: string) => Promise<string>;
157
+ /**
158
+ * Resolve a sitemap to its full entry list, following an index one level down.
159
+ *
160
+ * One level is the whole spec — the sitemaps protocol forbids an index pointing at
161
+ * another index — so recursing further would only chase a malformed document.
162
+ * Child fetches run sequentially: a large site's index can list dozens of files,
163
+ * and hammering the consumer's own origin to save a second is a poor trade.
164
+ */
165
+ declare function collectEntries(source: string, read: SitemapReader): Promise<{
166
+ entries: SitemapEntry[];
167
+ sources: string[];
168
+ }>;
169
+ //#endregion
170
+ //#region src/indexing/snapshot.d.ts
171
+ /** Default location, relative to the consumer's project root. */
172
+ declare const SNAPSHOT_PATH = ".vitops/sitemap-snapshot.json";
173
+ /**
174
+ * Read a snapshot, or `undefined` when there isn't a usable one.
175
+ *
176
+ * A corrupt or wrong-version file reads as absent, not as empty. The distinction
177
+ * matters: "absent" submits everything, whereas an empty entry map would look like
178
+ * a site that legitimately has no URLs and submit nothing — the silent-success
179
+ * failure this whole command exists to remove.
180
+ */
181
+ declare function readSnapshot(path: string): Snapshot | undefined;
182
+ /** Build a snapshot from the entries just read. */
183
+ declare function toSnapshot(sitemapUrl: string, entries: SitemapEntry[], takenAt: string): Snapshot;
184
+ /**
185
+ * Write the snapshot, creating its directory.
186
+ *
187
+ * Call this only after a submission actually succeeded. Writing it eagerly would
188
+ * record URLs as notified that never were, and because the next run diffs against
189
+ * it, those URLs would never be submitted again — a single transient 503 would
190
+ * silently drop a page from every future run.
191
+ */
192
+ declare function writeSnapshot(path: string, snapshot: Snapshot): void;
193
+ //#endregion
194
+ //#region src/indexing/indexnow.d.ts
195
+ /** Generate a key. 32 hex characters, the conventional shape. */
196
+ declare function newKey(): string;
197
+ /** The body of the key file served at `keyLocation`: the key, and nothing else. */
198
+ declare const keyFileContents: (key: string) => string;
199
+ interface SubmitResult {
200
+ ok: boolean;
201
+ status: number;
202
+ urls: number;
203
+ /** The endpoint's own explanation, when it gave one. */
204
+ message?: string;
205
+ }
206
+ /**
207
+ * Verify the key file is live and correct before submitting.
208
+ *
209
+ * Worth a request of its own because of how IndexNow fails: a submission with an
210
+ * unreachable key file returns `403`, but one with a *reachable* key file whose
211
+ * contents are stale is accepted with `202` and then silently discarded. Checking
212
+ * first is the only way to tell "submitted" from "submitted and ignored".
213
+ */
214
+ declare function verifyKeyFile(keyLocation: string, key: string, fetchImpl?: typeof fetch): Promise<{
215
+ ok: boolean;
216
+ reason?: string;
217
+ }>;
218
+ /**
219
+ * Submit one batch. Callers iterate `plan.batches` — the split is the planner's
220
+ * job, so that what gets sent is decided in the pure layer and visible in `--dry`.
221
+ */
222
+ declare function submitBatch(plan: IndexNowPlan, urls: string[], fetchImpl?: typeof fetch): Promise<SubmitResult>;
223
+ //#endregion
224
+ //#region src/indexing/gsc.d.ts
225
+ /** The fields this module needs from a service-account JSON key. */
226
+ interface ServiceAccount {
227
+ client_email: string;
228
+ private_key: string;
229
+ }
230
+ /**
231
+ * Parse a service-account credential from its two supported sources.
232
+ *
233
+ * `VITOPS_GSC_SERVICE_ACCOUNT` holds the JSON inline (what a CI secret store
234
+ * gives you); `GOOGLE_APPLICATION_CREDENTIALS` holds a path (Google's own
235
+ * convention, and what a local `gcloud` setup already has). Supporting both means
236
+ * neither environment has to be reshaped to fit the tool.
237
+ */
238
+ declare function parseServiceAccount(raw: string): ServiceAccount;
239
+ /**
240
+ * Exchange a service-account key for an access token (RS256 JWT bearer flow).
241
+ *
242
+ * `now` is a parameter so this is testable without mocking the clock.
243
+ */
244
+ declare function getAccessToken(account: ServiceAccount, fetchImpl?: typeof fetch, now?: number): Promise<string>;
245
+ /**
246
+ * Re-submit a sitemap — the automated form of the manual resubmit in the UI.
247
+ *
248
+ * A `403` here almost always means `siteUrl` doesn't match the property exactly
249
+ * (`sc-domain:acme.ca` vs `https://acme.ca/`) or the service account isn't an
250
+ * owner of it, rather than that the sitemap is bad.
251
+ */
252
+ declare function submitSitemap(token: string, siteUrl: string, sitemapUrl: string, fetchImpl?: typeof fetch): Promise<{
253
+ ok: boolean;
254
+ status: number;
255
+ message?: string;
256
+ }>;
257
+ /** What the URL Inspection API said about one page. */
258
+ interface InspectionResult {
259
+ url: string;
260
+ /** Google's verdict, e.g. `PASS`, `NEUTRAL`, `FAIL`. */
261
+ verdict?: string;
262
+ /** e.g. `INDEXING_ALLOWED`, `BLOCKED_BY_ROBOTS_TXT`. */
263
+ coverageState?: string;
264
+ lastCrawlTime?: string;
265
+ /** True when Google reports the page as indexed. */
266
+ indexed: boolean;
267
+ error?: string;
268
+ }
269
+ /**
270
+ * Inspect one URL. **Read-only** — this reports what Google did, it cannot ask for
271
+ * anything. Quota is 2000 queries/day per property, which is why the caller works
272
+ * from an explicit `priorityUrls` list rather than the whole sitemap.
273
+ */
274
+ declare function inspectUrl(token: string, siteUrl: string, inspectionUrl: string, fetchImpl?: typeof fetch): Promise<InspectionResult>;
275
+ //#endregion
276
+ export { type ChannelPlan, INDEXNOW_BATCH, INDEXNOW_ENDPOINT, type IndexNowConfig, type IndexNowPlan, type IndexingConfig, type IndexingPlan, type InspectionResult, type ParsedSitemap, type PlanInput, SNAPSHOT_PATH, type SearchConsoleConfig, type SearchConsolePlan, type ServiceAccount, type SitemapEntry, type SitemapReader, type Snapshot, type SubmitResult, type UrlReason, collectEntries, formatPlan, getAccessToken, inspectUrl, keyFileContents, newKey, parseServiceAccount, parseSitemap, plan, readSnapshot, resolveKeyLocation, resolveSitemapUrl, submitBatch, submitSitemap, toSnapshot, verifyKeyFile, writeSnapshot };
277
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/indexing/types.ts","../../src/indexing/plan.ts","../../src/indexing/sitemap.ts","../../src/indexing/snapshot.ts","../../src/indexing/indexnow.ts","../../src/indexing/gsc.ts"],"mappings":"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"}