@growth-labs/seo 0.1.5 → 0.2.1
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/README.md +142 -54
- package/dist/bindings.d.ts +127 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +11 -0
- package/dist/bindings.js.map +1 -0
- package/dist/cron/prune-aeo-r2.d.ts +36 -0
- package/dist/cron/prune-aeo-r2.d.ts.map +1 -0
- package/dist/cron/prune-aeo-r2.js +94 -0
- package/dist/cron/prune-aeo-r2.js.map +1 -0
- package/dist/durable-objects/aeo-revalidation-coord.d.ts +69 -0
- package/dist/durable-objects/aeo-revalidation-coord.d.ts.map +1 -0
- package/dist/durable-objects/aeo-revalidation-coord.js +177 -0
- package/dist/durable-objects/aeo-revalidation-coord.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +79 -12
- package/dist/index.js.map +1 -1
- package/dist/middleware/seo.d.ts +44 -4
- package/dist/middleware/seo.d.ts.map +1 -1
- package/dist/middleware/seo.js +237 -41
- package/dist/middleware/seo.js.map +1 -1
- package/dist/options.d.ts +1293 -6
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +238 -1
- package/dist/options.js.map +1 -1
- package/dist/routes/aeo-twin.d.ts +5 -0
- package/dist/routes/aeo-twin.d.ts.map +1 -0
- package/dist/routes/aeo-twin.js +108 -0
- package/dist/routes/aeo-twin.js.map +1 -0
- package/dist/routes/apple-news.d.ts +4 -0
- package/dist/routes/apple-news.d.ts.map +1 -0
- package/dist/routes/apple-news.js +28 -0
- package/dist/routes/apple-news.js.map +1 -0
- package/dist/routes/llms-full.d.ts +4 -0
- package/dist/routes/llms-full.d.ts.map +1 -0
- package/dist/routes/llms-full.js +29 -0
- package/dist/routes/llms-full.js.map +1 -0
- package/dist/routes/revalidate.d.ts +16 -0
- package/dist/routes/revalidate.d.ts.map +1 -0
- package/dist/routes/revalidate.js +243 -0
- package/dist/routes/revalidate.js.map +1 -0
- package/dist/routes/rss.d.ts.map +1 -1
- package/dist/routes/rss.js +4 -1
- package/dist/routes/rss.js.map +1 -1
- package/dist/routes/sitemap-markdown.d.ts +4 -0
- package/dist/routes/sitemap-markdown.d.ts.map +1 -0
- package/dist/routes/sitemap-markdown.js +32 -0
- package/dist/routes/sitemap-markdown.js.map +1 -0
- package/dist/types.d.ts +16 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/aeo-summary.d.ts +35 -0
- package/dist/utils/aeo-summary.d.ts.map +1 -0
- package/dist/utils/aeo-summary.js +141 -0
- package/dist/utils/aeo-summary.js.map +1 -0
- package/dist/utils/aeo-twin-emitter.d.ts +79 -0
- package/dist/utils/aeo-twin-emitter.d.ts.map +1 -0
- package/dist/utils/aeo-twin-emitter.js +99 -0
- package/dist/utils/aeo-twin-emitter.js.map +1 -0
- package/dist/utils/aeo.d.ts +62 -12
- package/dist/utils/aeo.d.ts.map +1 -1
- package/dist/utils/aeo.js +187 -26
- package/dist/utils/aeo.js.map +1 -1
- package/dist/utils/apple-news-anf.d.ts +38 -0
- package/dist/utils/apple-news-anf.d.ts.map +1 -0
- package/dist/utils/apple-news-anf.js +120 -0
- package/dist/utils/apple-news-anf.js.map +1 -0
- package/dist/utils/apple-news-rss.d.ts +31 -0
- package/dist/utils/apple-news-rss.d.ts.map +1 -0
- package/dist/utils/apple-news-rss.js +103 -0
- package/dist/utils/apple-news-rss.js.map +1 -0
- package/dist/utils/content-filter.d.ts +52 -0
- package/dist/utils/content-filter.d.ts.map +1 -0
- package/dist/utils/content-filter.js +75 -0
- package/dist/utils/content-filter.js.map +1 -0
- package/dist/utils/crawler-class.d.ts +39 -0
- package/dist/utils/crawler-class.d.ts.map +1 -0
- package/dist/utils/crawler-class.js +127 -0
- package/dist/utils/crawler-class.js.map +1 -0
- package/dist/utils/effective-auth.d.ts +28 -0
- package/dist/utils/effective-auth.d.ts.map +1 -0
- package/dist/utils/effective-auth.js +33 -0
- package/dist/utils/effective-auth.js.map +1 -0
- package/dist/utils/fcrdns.d.ts +73 -0
- package/dist/utils/fcrdns.d.ts.map +1 -0
- package/dist/utils/fcrdns.js +219 -0
- package/dist/utils/fcrdns.js.map +1 -0
- package/dist/utils/fresh-layer.d.ts +53 -0
- package/dist/utils/fresh-layer.d.ts.map +1 -0
- package/dist/utils/fresh-layer.js +147 -0
- package/dist/utils/fresh-layer.js.map +1 -0
- package/dist/utils/index.d.ts +14 -3
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +14 -3
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/json-ld/article.d.ts +13 -1
- package/dist/utils/json-ld/article.d.ts.map +1 -1
- package/dist/utils/json-ld/article.js +37 -8
- package/dist/utils/json-ld/article.js.map +1 -1
- package/dist/utils/llms-full.d.ts +29 -0
- package/dist/utils/llms-full.d.ts.map +1 -0
- package/dist/utils/llms-full.js +67 -0
- package/dist/utils/llms-full.js.map +1 -0
- package/dist/utils/meta.d.ts +4 -1
- package/dist/utils/meta.d.ts.map +1 -1
- package/dist/utils/meta.js +25 -2
- package/dist/utils/meta.js.map +1 -1
- package/dist/utils/sitemap-markdown.d.ts +24 -0
- package/dist/utils/sitemap-markdown.d.ts.map +1 -0
- package/dist/utils/sitemap-markdown.js +57 -0
- package/dist/utils/sitemap-markdown.js.map +1 -0
- package/dist/utils/staleness.d.ts +27 -0
- package/dist/utils/staleness.d.ts.map +1 -0
- package/dist/utils/staleness.js +46 -0
- package/dist/utils/staleness.js.map +1 -0
- package/dist/utils/validation.d.ts +41 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +78 -0
- package/dist/utils/validation.js.map +1 -1
- package/package.json +13 -1
package/README.md
CHANGED
|
@@ -1,80 +1,168 @@
|
|
|
1
1
|
# @growth-labs/seo
|
|
2
2
|
|
|
3
|
-
Astro integration for complete SEO infrastructure on Cloudflare. Handles JSON-LD structured data, meta tags, sitemaps, RSS/podcast feeds, AEO (Answer Engine Optimization), multilingual support, robots.txt, llms.txt, and build-time validation.
|
|
3
|
+
Astro integration for complete SEO infrastructure on Cloudflare. Handles JSON-LD structured data, meta tags, sitemaps, RSS/podcast/Apple News feeds, AEO (Answer Engine Optimization) with crawler-class dispatch, multilingual support, robots.txt, llms.txt / llms-full.txt, and build-time validation.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Quick start — prerendered content site (WarFronts pattern)
|
|
6
|
+
|
|
7
|
+
For immutable-after-publish content (articles, docs, archived posts). Zero bindings required.
|
|
6
8
|
|
|
7
9
|
```typescript
|
|
8
10
|
import seo from '@growth-labs/seo'
|
|
9
11
|
|
|
12
|
+
export default defineConfig({
|
|
13
|
+
integrations: [
|
|
14
|
+
seo({
|
|
15
|
+
site: 'https://warfronts.channel',
|
|
16
|
+
organization: {
|
|
17
|
+
name: 'WarFronts',
|
|
18
|
+
logo: 'https://media.warfronts.channel/logos/header.png',
|
|
19
|
+
},
|
|
20
|
+
aeoTwins: true, // → { mode: 'static' }
|
|
21
|
+
llmsTxt: true,
|
|
22
|
+
rss: true,
|
|
23
|
+
contentProvider: async ({ type, slugs }) => loadContent(type, slugs),
|
|
24
|
+
}),
|
|
25
|
+
],
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`mode: 'static'` emits `.md` twin files at build time to `dist/client/article/<slug>.md`. Cloudflare Assets (or any static host) serves them directly. No Worker hop.
|
|
30
|
+
|
|
31
|
+
## Premium publisher (fronts.co pattern) — SSR, gated content, Flexible Sampling
|
|
32
|
+
|
|
33
|
+
For paid publications where members see the full body and verified Googlebot gets the paywall-marked full body under Google's Flexible Sampling policy.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
10
36
|
seo({
|
|
11
|
-
site: 'https://
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
37
|
+
site: 'https://fronts.co',
|
|
38
|
+
organization: { name: 'Fronts', logo: 'https://fronts.co/logo.png' },
|
|
39
|
+
aeoTwins: {
|
|
40
|
+
mode: 'middleware',
|
|
41
|
+
onDemandRevalidation: true,
|
|
42
|
+
revalidateToken: import.meta.env.SEO_REVALIDATE_TOKEN, // ≥32 random bytes
|
|
43
|
+
freshLayer: { bindingName: 'AEO_TWINS', type: 'r2' },
|
|
17
44
|
},
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
contentSignal: { // Content-Signal HTTP header
|
|
23
|
-
aiTrain: 'no',
|
|
24
|
-
search: 'yes',
|
|
25
|
-
aiInput: 'yes',
|
|
26
|
-
},
|
|
27
|
-
defaults: {
|
|
28
|
-
titleSuffix: ' | WarFronts',
|
|
29
|
-
twitterSite: '@warfronts',
|
|
30
|
-
},
|
|
31
|
-
// Optional:
|
|
32
|
-
commerce: { enabled: true, currency: 'USD', returnPolicy: { ... } },
|
|
33
|
-
podcast: { enabled: true, title: '...', author: '...', ... },
|
|
34
|
-
audioNarration: { enabled: true, narratorName: '...', asPodcastFeed: true },
|
|
35
|
-
locales: [{ lang: 'en', region: 'US', default: true }], // Multilingual
|
|
45
|
+
flexibleSampling: { enabled: true, sampleMode: 'lead-in', leadInParagraphs: 2 },
|
|
46
|
+
llmsTxt: true,
|
|
47
|
+
llmsFullTxt: true,
|
|
48
|
+
contentProvider: async ({ type, slugs }) => loadContent(type, slugs),
|
|
36
49
|
})
|
|
37
50
|
```
|
|
38
51
|
|
|
39
|
-
|
|
52
|
+
Gated articles need `prerender: false` on their route file — enforced at build time via the prerender-gated-content guard.
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
- Adds `Content-Signal` header to responses
|
|
43
|
-
- Adds `Link` alternate header for hreflang
|
|
44
|
-
- Handles `Accept: text/markdown` → returns markdown twin (AEO)
|
|
54
|
+
## wrangler.toml (required for middleware/both modes)
|
|
45
55
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
```toml
|
|
57
|
+
# Fresh-twin storage (R2 preferred, KV acceptable)
|
|
58
|
+
[[r2_buckets]]
|
|
59
|
+
binding = "AEO_TWINS"
|
|
60
|
+
bucket_name = "my-site-aeo-twins"
|
|
61
|
+
|
|
62
|
+
# Revalidation Coordinator — rate limit + per-slug lock + idempotency
|
|
63
|
+
[[durable_objects.bindings]]
|
|
64
|
+
name = "AEO_REVALIDATION_COORD"
|
|
65
|
+
class_name = "AeoRevalidationCoordinator"
|
|
66
|
+
|
|
67
|
+
[[migrations]]
|
|
68
|
+
tag = "v1"
|
|
69
|
+
new_sqlite_classes = ["AeoRevalidationCoordinator"]
|
|
70
|
+
|
|
71
|
+
# Version metadata (R2 key prefixing for rollback safety)
|
|
72
|
+
[version_metadata]
|
|
73
|
+
binding = "CF_VERSION_METADATA"
|
|
55
74
|
|
|
56
|
-
|
|
75
|
+
# Daily prune cron — deletes old-version R2 entries
|
|
76
|
+
[triggers]
|
|
77
|
+
crons = ["0 3 * * *"]
|
|
57
78
|
|
|
58
|
-
|
|
79
|
+
# Assets binding — MUST set not_found_handling to "none" or middleware's
|
|
80
|
+
# env.ASSETS.fetch() for missing twins will return the SPA fallback.
|
|
81
|
+
[assets]
|
|
82
|
+
binding = "ASSETS"
|
|
83
|
+
directory = "./dist/client"
|
|
84
|
+
not_found_handling = "none"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Re-export the DO class + scheduled handler from your Worker entrypoint:
|
|
59
88
|
|
|
60
89
|
```typescript
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
90
|
+
export { AeoRevalidationCoordinator } from '@growth-labs/seo/durable-objects'
|
|
91
|
+
export { pruneAeoR2 } from '@growth-labs/seo/cron'
|
|
92
|
+
|
|
93
|
+
export default {
|
|
94
|
+
async scheduled(event, env, ctx) {
|
|
95
|
+
await pruneAeoR2({ env })
|
|
96
|
+
},
|
|
97
|
+
}
|
|
64
98
|
```
|
|
65
99
|
|
|
66
|
-
|
|
100
|
+
## What it injects
|
|
67
101
|
|
|
68
|
-
**
|
|
102
|
+
**Middleware** (order: `post`):
|
|
103
|
+
- Classifies every request: verified search crawler (BM fast path + FCrDNS fallback), LLM training crawler, user-directed LLM agent, anonymous.
|
|
104
|
+
- Sets `Astro.locals.crawlerClass` + `effectiveAuthSegment` for consumer cache-key builders.
|
|
105
|
+
- 403s LLM training crawlers on `access: 'members'` items.
|
|
106
|
+
- Adds `Content-Signal` header on every response.
|
|
107
|
+
- Adds `Vary: Accept, User-Agent, Cookie, CF-Connecting-IP` on every SSR response.
|
|
108
|
+
- Adds `Link: rel="alternate"; type="text/markdown"` on HTML responses (suppressed for members items).
|
|
109
|
+
- Serves `.md` twins via `Accept: text/markdown` content-negotiation in middleware/both modes (R2 → Assets → 503 stub + background render fallthrough).
|
|
69
110
|
|
|
70
|
-
|
|
111
|
+
**Routes:**
|
|
112
|
+
- `/sitemap-index.xml` + `sitemap-articles.xml`, `sitemap-pages.xml`, `sitemap-videos.xml`, `sitemap-products.xml`
|
|
113
|
+
- `/sitemap-markdown.xml` — twin URL sitemap (static/both modes only)
|
|
114
|
+
- `/robots.txt`
|
|
115
|
+
- `/llms.txt`, `/llms-full.txt`
|
|
116
|
+
- `/feed.xml` (RSS)
|
|
117
|
+
- `/apple-news.xml` (Apple News Publisher RSS, if enabled)
|
|
118
|
+
- `/podcast.xml`, `/listen.xml`
|
|
119
|
+
- `POST /_seo/revalidate` — CMS webhook target (when `onDemandRevalidation: true`)
|
|
120
|
+
|
|
121
|
+
**Build-time:**
|
|
122
|
+
- Emits `.md` twins + summary twins for public items (static/both modes) under `dist/client/`.
|
|
123
|
+
- Validates hreflang reciprocity.
|
|
124
|
+
- Validates no prerendered route serves a members-gated item (when Flexible Sampling is enabled).
|
|
125
|
+
- Per-page HTML validation (title length, meta description, canonical, H1, JSON-LD presence).
|
|
126
|
+
|
|
127
|
+
## Crawler classes
|
|
128
|
+
|
|
129
|
+
| Class | What they see | Notes |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `verifiedSearchCrawler` | Full body (+ paywall JSON-LD on gated items under Flexible Sampling) | FCrDNS-verified Googlebot/Bingbot/Applebot. Cloudflare BM fast path when available. |
|
|
132
|
+
| `llmTrainingCrawler` | 403 on members items, public body on public items | GPTBot, ClaudeBot, CCBot, PerplexityBot, Applebot-Extended, etc. |
|
|
133
|
+
| `userDirectedLlmAgent` | Anonymous body only, regardless of cookies | ChatGPT-User, Claude-User, PerplexityBot-User, Google-NotebookLM. Load-bearing override prevents cookie-based leakage. |
|
|
134
|
+
| `anonymous` | Public body or gate | Everything else. |
|
|
135
|
+
|
|
136
|
+
## Standalone utilities
|
|
137
|
+
|
|
138
|
+
All pure-function utilities are available without the integration:
|
|
71
139
|
|
|
72
|
-
|
|
140
|
+
```typescript
|
|
141
|
+
import { generateArticleJsonLd, generateProductJsonLd } from '@growth-labs/seo/utils'
|
|
142
|
+
import { generateMeta } from '@growth-labs/seo/utils/meta'
|
|
143
|
+
import { generateAppleNewsRss, generateAppleNewsAnf } from '@growth-labs/seo/utils'
|
|
144
|
+
import { classifyRequest, createFcrdnsVerifier } from '@growth-labs/seo/utils'
|
|
145
|
+
import { computeEffectiveAuthSegment } from '@growth-labs/seo/utils'
|
|
146
|
+
```
|
|
73
147
|
|
|
74
|
-
|
|
148
|
+
**JSON-LD generators:** Article, NewsArticle, BlogPosting, FAQPage, VideoObject, AudioObject, Person, HowTo, Product, BreadcrumbList, Organization, WebSite, ItemList, SpeakableSpecification
|
|
149
|
+
|
|
150
|
+
**Feed generators:** RSS, Apple News Publisher RSS, Apple News Format (ANF) JSON, podcast RSS, listen feed, llms.txt, llms-full.txt
|
|
151
|
+
|
|
152
|
+
**Utilities:** OG + Twitter Card meta, sitemap XML, markdown sitemap, hreflang, robots.txt, AEO markdown generator with RAG chunk markers, summary twin generator, content-hash staleness.
|
|
153
|
+
|
|
154
|
+
## Non-Cloudflare hosts
|
|
155
|
+
|
|
156
|
+
`aeoTwins: { mode: 'static' }` works on any host that serves static files (Vercel, Netlify, GitHub Pages, S3+CloudFront). Other modes (`'middleware'`, `'both'`) and `onDemandRevalidation` require Cloudflare Workers + R2/KV + Durable Objects. See [packages-seo-SPEC-v2.md](../../packages-seo-SPEC-v2.md) "Deployment Targets" for details.
|
|
157
|
+
|
|
158
|
+
## Key patterns
|
|
75
159
|
|
|
76
160
|
- Virtual module: `virtual:growth-labs/seo/config`
|
|
77
|
-
- AI crawler blocking
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
-
|
|
161
|
+
- AI crawler blocking: enforced at `robots.txt` AND per-request 403 on members items
|
|
162
|
+
- `.md` twin canonical: emitted with `X-Robots-Tag: noindex` + `Link: <html-url>; rel="canonical"` — prevents Google from clustering the `.md` as a duplicate of the HTML
|
|
163
|
+
- Summary twins: `.summary.md` companion emitted when `summaryTwin: true` (default), with a 4-tier fallback (item.summary → bullets → first-sentence-per-section → description-only)
|
|
164
|
+
- Build-time validation: non-blocking for per-page issues, FAILS the build for gated content on prerendered routes
|
|
165
|
+
|
|
166
|
+
## Full spec
|
|
167
|
+
|
|
168
|
+
See [packages-seo-SPEC-v2.md](../../packages-seo-SPEC-v2.md) for the complete 2,400-line specification including the test matrix, architectural rationale, and worked examples for every code path.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R2 bucket binding for the fresh-twin layer.
|
|
3
|
+
* Subset of the R2Bucket API we actually use.
|
|
4
|
+
*/
|
|
5
|
+
export interface R2BucketLike {
|
|
6
|
+
get(key: string): Promise<R2ObjectLike | null>;
|
|
7
|
+
put(key: string, value: ReadableStream | ArrayBuffer | string, options?: R2PutOptions): Promise<unknown>;
|
|
8
|
+
delete(keys: string | string[]): Promise<void>;
|
|
9
|
+
list(options?: {
|
|
10
|
+
prefix?: string;
|
|
11
|
+
cursor?: string;
|
|
12
|
+
limit?: number;
|
|
13
|
+
}): Promise<R2ListResult>;
|
|
14
|
+
}
|
|
15
|
+
export interface R2ObjectLike {
|
|
16
|
+
readonly key: string;
|
|
17
|
+
readonly size: number;
|
|
18
|
+
readonly uploaded: Date;
|
|
19
|
+
readonly httpMetadata?: R2HTTPMetadata;
|
|
20
|
+
readonly customMetadata?: Record<string, string>;
|
|
21
|
+
text(): Promise<string>;
|
|
22
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
23
|
+
body: ReadableStream | null;
|
|
24
|
+
bodyUsed: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface R2PutOptions {
|
|
27
|
+
httpMetadata?: R2HTTPMetadata;
|
|
28
|
+
customMetadata?: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
export interface R2HTTPMetadata {
|
|
31
|
+
contentType?: string;
|
|
32
|
+
contentLanguage?: string;
|
|
33
|
+
contentDisposition?: string;
|
|
34
|
+
contentEncoding?: string;
|
|
35
|
+
cacheControl?: string;
|
|
36
|
+
cacheExpiry?: Date;
|
|
37
|
+
}
|
|
38
|
+
export interface R2ListResult {
|
|
39
|
+
objects: R2ObjectLike[];
|
|
40
|
+
truncated: boolean;
|
|
41
|
+
cursor?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* KV namespace binding — acceptable alternative to R2 for the fresh-twin layer.
|
|
45
|
+
* Subset of the KVNamespace API we actually use.
|
|
46
|
+
*/
|
|
47
|
+
export interface KVNamespaceLike {
|
|
48
|
+
get(key: string): Promise<string | null>;
|
|
49
|
+
get(key: string, options: {
|
|
50
|
+
type: 'text';
|
|
51
|
+
}): Promise<string | null>;
|
|
52
|
+
get(key: string, options: {
|
|
53
|
+
type: 'arrayBuffer';
|
|
54
|
+
}): Promise<ArrayBuffer | null>;
|
|
55
|
+
put(key: string, value: string | ArrayBuffer, options?: {
|
|
56
|
+
expiration?: number;
|
|
57
|
+
expirationTtl?: number;
|
|
58
|
+
metadata?: unknown;
|
|
59
|
+
}): Promise<void>;
|
|
60
|
+
delete(key: string): Promise<void>;
|
|
61
|
+
list(options?: {
|
|
62
|
+
prefix?: string;
|
|
63
|
+
cursor?: string;
|
|
64
|
+
limit?: number;
|
|
65
|
+
}): Promise<{
|
|
66
|
+
keys: Array<{
|
|
67
|
+
name: string;
|
|
68
|
+
}>;
|
|
69
|
+
list_complete: boolean;
|
|
70
|
+
cursor?: string;
|
|
71
|
+
}>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Assets binding — reads static files from the Worker's deployment bundle without
|
|
75
|
+
* re-entering the Worker's request routing. Use:
|
|
76
|
+
* env.ASSETS.fetch(new Request('https://assets.local/path/to/file.md'))
|
|
77
|
+
* The hostname is ignored by the assets runtime; only pathname matters.
|
|
78
|
+
* Set `assets.not_found_handling = "none"` in wrangler.toml to prevent SPA fallback
|
|
79
|
+
* from serving index.html when the requested path is missing.
|
|
80
|
+
*/
|
|
81
|
+
export interface AssetsBinding {
|
|
82
|
+
fetch(request: Request): Promise<Response>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Durable Object namespace binding for the Revalidation Coordinator. The actual DO
|
|
86
|
+
* class is exported from `@growth-labs/seo/durable-objects` and wired by the consumer
|
|
87
|
+
* in their Worker entrypoint.
|
|
88
|
+
*/
|
|
89
|
+
export interface DurableObjectNamespaceLike {
|
|
90
|
+
idFromName(name: string): DurableObjectIdLike;
|
|
91
|
+
get(id: DurableObjectIdLike): DurableObjectStubLike;
|
|
92
|
+
}
|
|
93
|
+
export interface DurableObjectIdLike {
|
|
94
|
+
toString(): string;
|
|
95
|
+
equals(other: DurableObjectIdLike): boolean;
|
|
96
|
+
}
|
|
97
|
+
export interface DurableObjectStubLike {
|
|
98
|
+
fetch(request: Request): Promise<Response>;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Version metadata binding. Injected into `env` as `CF_VERSION_METADATA`.
|
|
102
|
+
* Used for rollback-safe R2 key prefixing (`twin/<id>/<slug>.md`).
|
|
103
|
+
* `id` is an opaque version UUID that rotates on every new deployment.
|
|
104
|
+
* `tag` is an optional user-supplied annotation via `workers/tag`.
|
|
105
|
+
* `timestamp` is the version's creation time (ISO 8601 string).
|
|
106
|
+
* Ref: https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/
|
|
107
|
+
*/
|
|
108
|
+
export interface VersionMetadata {
|
|
109
|
+
readonly id: string;
|
|
110
|
+
readonly tag?: string;
|
|
111
|
+
readonly timestamp: string;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The minimal Worker env shape the SEO package depends on. Consumers will have many
|
|
115
|
+
* more bindings than this; we only declare what we read.
|
|
116
|
+
*
|
|
117
|
+
* All fields are optional because the package's `mode: 'static'` code path runs without
|
|
118
|
+
* any Worker bindings. Runtime binding resolvers throw structured errors when required
|
|
119
|
+
* bindings are missing.
|
|
120
|
+
*/
|
|
121
|
+
export interface SeoEnv {
|
|
122
|
+
AEO_TWINS?: R2BucketLike | KVNamespaceLike;
|
|
123
|
+
AEO_REVALIDATION_COORD?: DurableObjectNamespaceLike;
|
|
124
|
+
ASSETS?: AssetsBinding;
|
|
125
|
+
CF_VERSION_METADATA?: VersionMetadata;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=bindings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bindings.d.ts","sourceRoot":"","sources":["../src/bindings.ts"],"names":[],"mappings":"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"}
|
package/dist/bindings.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Cloudflare binding declarations used by the SEO integration at request and build time.
|
|
2
|
+
//
|
|
3
|
+
// These types are not Astro-specific. They describe the subset of `env` the package
|
|
4
|
+
// touches when running inside a Cloudflare Worker. Consumers provide the actual bindings
|
|
5
|
+
// in wrangler.toml; this file gives us a narrow structural type so utility and middleware
|
|
6
|
+
// code can be type-checked without a hard dependency on `@cloudflare/workers-types`.
|
|
7
|
+
//
|
|
8
|
+
// If `@cloudflare/workers-types` is in the consumer's project, their stricter types will
|
|
9
|
+
// widen-compatibly overlap these interfaces.
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=bindings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bindings.js","sourceRoot":"","sources":["../src/bindings.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,EAAE;AACF,oFAAoF;AACpF,yFAAyF;AACzF,0FAA0F;AAC1F,qFAAqF;AACrF,EAAE;AACF,yFAAyF;AACzF,6CAA6C"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SeoEnv } from '../bindings.js';
|
|
2
|
+
export interface PruneAeoR2Options {
|
|
3
|
+
env: SeoEnv;
|
|
4
|
+
/** Binding name to read from env. Matches aeoTwins.freshLayer.bindingName. */
|
|
5
|
+
bindingName?: string;
|
|
6
|
+
/** 'r2' (default) or 'kv'. Matches aeoTwins.freshLayer.type. */
|
|
7
|
+
type?: 'r2' | 'kv';
|
|
8
|
+
/** Retention in days. Matches aeoTwins.freshLayer.retentionDays. */
|
|
9
|
+
retentionDays?: number;
|
|
10
|
+
/** Optional structured-log sink. */
|
|
11
|
+
log?: (event: Record<string, unknown>) => void;
|
|
12
|
+
}
|
|
13
|
+
export interface PruneResult {
|
|
14
|
+
deploymentsScanned: number;
|
|
15
|
+
deploymentsPruned: string[];
|
|
16
|
+
keysDeleted: number;
|
|
17
|
+
errors: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Sweep the fresh layer for entries belonging to deployments that are no longer
|
|
21
|
+
* current AND older than `retentionDays`. Keys from the current deployment are
|
|
22
|
+
* untouched regardless of age.
|
|
23
|
+
*
|
|
24
|
+
* Algorithm:
|
|
25
|
+
* 1. Determine the current versionId from env.CF_VERSION_METADATA.id.
|
|
26
|
+
* 2. List all deployment IDs present in the fresh layer.
|
|
27
|
+
* 3. For each non-current deployment, list its keys; if the oldest matches
|
|
28
|
+
* the retention cutoff, delete all keys under that prefix.
|
|
29
|
+
*
|
|
30
|
+
* "Oldest matches retention cutoff" is approximated by R2's `uploaded` timestamp
|
|
31
|
+
* (R2-only). For KV there's no per-key mtime, so the retention check degrades
|
|
32
|
+
* to "delete everything not matching the current deployment" — adjust the
|
|
33
|
+
* binding to R2 if you need retentionDays fidelity.
|
|
34
|
+
*/
|
|
35
|
+
export declare function pruneAeoR2(options: PruneAeoR2Options): Promise<PruneResult>;
|
|
36
|
+
//# sourceMappingURL=prune-aeo-r2.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prune-aeo-r2.d.ts","sourceRoot":"","sources":["../../src/cron/prune-aeo-r2.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAW5C,MAAM,WAAW,iBAAiB;IACjC,GAAG,EAAE,MAAM,CAAA;IACX,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gEAAgE;IAChE,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAA;IAClB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,oCAAoC;IACpC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;CAC9C;AAED,MAAM,WAAW,WAAW;IAC3B,kBAAkB,EAAE,MAAM,CAAA;IAC1B,iBAAiB,EAAE,MAAM,EAAE,CAAA;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,EAAE,CAAA;CAChB;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAkFjF"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { deleteFreshTwin, listDeploymentIds, listKeysByDeployment } from '../utils/fresh-layer.js';
|
|
2
|
+
const DEFAULT_RETENTION_DAYS = 7;
|
|
3
|
+
/**
|
|
4
|
+
* Sweep the fresh layer for entries belonging to deployments that are no longer
|
|
5
|
+
* current AND older than `retentionDays`. Keys from the current deployment are
|
|
6
|
+
* untouched regardless of age.
|
|
7
|
+
*
|
|
8
|
+
* Algorithm:
|
|
9
|
+
* 1. Determine the current versionId from env.CF_VERSION_METADATA.id.
|
|
10
|
+
* 2. List all deployment IDs present in the fresh layer.
|
|
11
|
+
* 3. For each non-current deployment, list its keys; if the oldest matches
|
|
12
|
+
* the retention cutoff, delete all keys under that prefix.
|
|
13
|
+
*
|
|
14
|
+
* "Oldest matches retention cutoff" is approximated by R2's `uploaded` timestamp
|
|
15
|
+
* (R2-only). For KV there's no per-key mtime, so the retention check degrades
|
|
16
|
+
* to "delete everything not matching the current deployment" — adjust the
|
|
17
|
+
* binding to R2 if you need retentionDays fidelity.
|
|
18
|
+
*/
|
|
19
|
+
export async function pruneAeoR2(options) {
|
|
20
|
+
const { env, bindingName = 'AEO_TWINS', type = 'r2', retentionDays = DEFAULT_RETENTION_DAYS, log, } = options;
|
|
21
|
+
const currentVersion = env.CF_VERSION_METADATA?.id;
|
|
22
|
+
const errors = [];
|
|
23
|
+
const deploymentsPruned = [];
|
|
24
|
+
let keysDeleted = 0;
|
|
25
|
+
if (!currentVersion) {
|
|
26
|
+
errors.push('missing_CF_VERSION_METADATA_id');
|
|
27
|
+
log?.({ event: 'prune_skipped', reason: 'missing_CF_VERSION_METADATA_id' });
|
|
28
|
+
return { deploymentsScanned: 0, deploymentsPruned, keysDeleted, errors };
|
|
29
|
+
}
|
|
30
|
+
const binding = env[bindingName];
|
|
31
|
+
if (!binding) {
|
|
32
|
+
errors.push(`missing_binding_${bindingName}`);
|
|
33
|
+
log?.({ event: 'prune_skipped', reason: 'missing_binding', bindingName });
|
|
34
|
+
return { deploymentsScanned: 0, deploymentsPruned, keysDeleted, errors };
|
|
35
|
+
}
|
|
36
|
+
const impl = binding;
|
|
37
|
+
const freshLayer = { type, impl, deploymentId: currentVersion };
|
|
38
|
+
const cutoffMs = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
39
|
+
const allDeployments = await listDeploymentIds(freshLayer);
|
|
40
|
+
const deploymentsScanned = allDeployments.size;
|
|
41
|
+
for (const deploymentId of allDeployments) {
|
|
42
|
+
if (deploymentId === currentVersion)
|
|
43
|
+
continue;
|
|
44
|
+
let deletedForThisDeployment = 0;
|
|
45
|
+
let shouldDelete = true;
|
|
46
|
+
// R2-only: peek the first object's uploaded time to gate on retentionDays.
|
|
47
|
+
// KV falls through to "delete everything non-current" since no mtime is available.
|
|
48
|
+
if (type === 'r2') {
|
|
49
|
+
const r2 = impl;
|
|
50
|
+
const peek = await r2.list({ prefix: `twin/${deploymentId}/`, limit: 1 });
|
|
51
|
+
const oldest = peek.objects[0];
|
|
52
|
+
if (oldest && oldest.uploaded.getTime() > cutoffMs) {
|
|
53
|
+
// Inside retention window — skip.
|
|
54
|
+
shouldDelete = false;
|
|
55
|
+
log?.({
|
|
56
|
+
event: 'prune_skipped_within_retention',
|
|
57
|
+
deploymentId,
|
|
58
|
+
oldestUploaded: oldest.uploaded.toISOString(),
|
|
59
|
+
retentionDays,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!shouldDelete)
|
|
64
|
+
continue;
|
|
65
|
+
try {
|
|
66
|
+
for await (const key of listKeysByDeployment(freshLayer, deploymentId)) {
|
|
67
|
+
await deleteFreshTwin({ ...freshLayer, deploymentId }, keyToPath(deploymentId, key));
|
|
68
|
+
deletedForThisDeployment++;
|
|
69
|
+
}
|
|
70
|
+
keysDeleted += deletedForThisDeployment;
|
|
71
|
+
deploymentsPruned.push(deploymentId);
|
|
72
|
+
log?.({
|
|
73
|
+
event: 'prune_deployment',
|
|
74
|
+
deploymentId,
|
|
75
|
+
keysDeleted: deletedForThisDeployment,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
80
|
+
errors.push(`deployment_${deploymentId}: ${msg}`);
|
|
81
|
+
log?.({ event: 'prune_error', deploymentId, message: msg });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { deploymentsScanned, deploymentsPruned, keysDeleted, errors };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Convert a full R2/KV key back to the URL-path form used by deleteFreshTwin.
|
|
88
|
+
* Inverse of fresh-layer buildKey.
|
|
89
|
+
*/
|
|
90
|
+
function keyToPath(deploymentId, key) {
|
|
91
|
+
const prefix = `twin/${deploymentId}/`;
|
|
92
|
+
return key.startsWith(prefix) ? `/${key.slice(prefix.length)}` : `/${key}`;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=prune-aeo-r2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prune-aeo-r2.js","sourceRoot":"","sources":["../../src/cron/prune-aeo-r2.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export interface DurableObjectStorageLike {
|
|
2
|
+
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
3
|
+
put<T = unknown>(key: string, value: T): Promise<void>;
|
|
4
|
+
delete(key: string): Promise<boolean>;
|
|
5
|
+
list<T = unknown>(options: {
|
|
6
|
+
prefix: string;
|
|
7
|
+
}): Promise<Map<string, T>>;
|
|
8
|
+
setAlarm(scheduledTime: number): Promise<void>;
|
|
9
|
+
getAlarm(): Promise<number | null>;
|
|
10
|
+
}
|
|
11
|
+
export interface DurableObjectStateLike {
|
|
12
|
+
storage: DurableObjectStorageLike;
|
|
13
|
+
}
|
|
14
|
+
export type CoordRequest = {
|
|
15
|
+
action: 'rate-check';
|
|
16
|
+
token: string;
|
|
17
|
+
limitRpm: number;
|
|
18
|
+
} | {
|
|
19
|
+
action: 'acquire-lock';
|
|
20
|
+
slug: string;
|
|
21
|
+
leaseMs?: number;
|
|
22
|
+
} | {
|
|
23
|
+
action: 'release-lock';
|
|
24
|
+
slug: string;
|
|
25
|
+
leaseId: string;
|
|
26
|
+
} | {
|
|
27
|
+
action: 'idempotency-check';
|
|
28
|
+
key: string;
|
|
29
|
+
} | {
|
|
30
|
+
action: 'idempotency-set';
|
|
31
|
+
key: string;
|
|
32
|
+
result: Record<string, unknown>;
|
|
33
|
+
ttlMs?: number;
|
|
34
|
+
};
|
|
35
|
+
export type CoordResponse = {
|
|
36
|
+
ok: true;
|
|
37
|
+
[k: string]: unknown;
|
|
38
|
+
} | {
|
|
39
|
+
ok: false;
|
|
40
|
+
error: string;
|
|
41
|
+
[k: string]: unknown;
|
|
42
|
+
};
|
|
43
|
+
export declare class AeoRevalidationCoordinator {
|
|
44
|
+
readonly ctx: DurableObjectStateLike;
|
|
45
|
+
constructor(ctx: DurableObjectStateLike);
|
|
46
|
+
/**
|
|
47
|
+
* Dispatch a JSON-encoded CoordRequest. Returns a JSON response. Not a
|
|
48
|
+
* public HTTP API — called from the consumer's Worker via the namespace
|
|
49
|
+
* binding: `await stub.fetch(new Request('https://internal/', { method: 'POST', body: JSON.stringify(req) }))`.
|
|
50
|
+
*/
|
|
51
|
+
fetch(request: Request): Promise<Response>;
|
|
52
|
+
/**
|
|
53
|
+
* Alarm handler: runs every 5 minutes, sweeps expired entries across all
|
|
54
|
+
* three namespaces. Self-re-arming.
|
|
55
|
+
*/
|
|
56
|
+
alarm(): Promise<void>;
|
|
57
|
+
private rateCheck;
|
|
58
|
+
private acquireLock;
|
|
59
|
+
private releaseLock;
|
|
60
|
+
private idempotencyCheck;
|
|
61
|
+
private idempotencySet;
|
|
62
|
+
private ensureAlarm;
|
|
63
|
+
}
|
|
64
|
+
export declare const _internals: {
|
|
65
|
+
LOCK_DEFAULT_LEASE_MS: number;
|
|
66
|
+
IDEMPOTENCY_DEFAULT_TTL_MS: number;
|
|
67
|
+
ALARM_INTERVAL_MS: number;
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=aeo-revalidation-coord.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aeo-revalidation-coord.d.ts","sourceRoot":"","sources":["../../src/durable-objects/aeo-revalidation-coord.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,wBAAwB;IACxC,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAA;IACrD,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACrC,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAA;IACvE,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9C,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CAClC;AAED,MAAM,WAAW,sBAAsB;IACtC,OAAO,EAAE,wBAAwB,CAAA;CACjC;AAID,MAAM,MAAM,YAAY,GACrB;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1D;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,MAAM,EAAE,mBAAmB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,MAAM,EAAE,iBAAiB,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9F,MAAM,MAAM,aAAa,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GAClC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAA;AAyBrD,qBAAa,0BAA0B;IAC1B,QAAQ,CAAC,GAAG,EAAE,sBAAsB;gBAA3B,GAAG,EAAE,sBAAsB;IAEhD;;;;OAIG;IACG,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAmChD;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YA6Bd,SAAS;YAuBT,WAAW;YAaX,WAAW;YAaX,gBAAgB;YAShB,cAAc;YAad,WAAW;CAMzB;AAmBD,eAAO,MAAM,UAAU;;;;CAItB,CAAA"}
|