@growth-labs/seo 0.1.3 → 0.2.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.
Files changed (115) hide show
  1. package/README.md +142 -54
  2. package/dist/bindings.d.ts +127 -0
  3. package/dist/bindings.d.ts.map +1 -0
  4. package/dist/bindings.js +11 -0
  5. package/dist/bindings.js.map +1 -0
  6. package/dist/cron/prune-aeo-r2.d.ts +36 -0
  7. package/dist/cron/prune-aeo-r2.d.ts.map +1 -0
  8. package/dist/cron/prune-aeo-r2.js +94 -0
  9. package/dist/cron/prune-aeo-r2.js.map +1 -0
  10. package/dist/durable-objects/aeo-revalidation-coord.d.ts +69 -0
  11. package/dist/durable-objects/aeo-revalidation-coord.d.ts.map +1 -0
  12. package/dist/durable-objects/aeo-revalidation-coord.js +177 -0
  13. package/dist/durable-objects/aeo-revalidation-coord.js.map +1 -0
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +103 -16
  17. package/dist/index.js.map +1 -1
  18. package/dist/middleware/seo.d.ts +44 -4
  19. package/dist/middleware/seo.d.ts.map +1 -1
  20. package/dist/middleware/seo.js +237 -41
  21. package/dist/middleware/seo.js.map +1 -1
  22. package/dist/options.d.ts +1293 -6
  23. package/dist/options.d.ts.map +1 -1
  24. package/dist/options.js +238 -1
  25. package/dist/options.js.map +1 -1
  26. package/dist/routes/apple-news.d.ts +4 -0
  27. package/dist/routes/apple-news.d.ts.map +1 -0
  28. package/dist/routes/apple-news.js +28 -0
  29. package/dist/routes/apple-news.js.map +1 -0
  30. package/dist/routes/llms-full.d.ts +4 -0
  31. package/dist/routes/llms-full.d.ts.map +1 -0
  32. package/dist/routes/llms-full.js +29 -0
  33. package/dist/routes/llms-full.js.map +1 -0
  34. package/dist/routes/revalidate.d.ts +16 -0
  35. package/dist/routes/revalidate.d.ts.map +1 -0
  36. package/dist/routes/revalidate.js +243 -0
  37. package/dist/routes/revalidate.js.map +1 -0
  38. package/dist/routes/rss.d.ts.map +1 -1
  39. package/dist/routes/rss.js +4 -1
  40. package/dist/routes/rss.js.map +1 -1
  41. package/dist/routes/sitemap-markdown.d.ts +4 -0
  42. package/dist/routes/sitemap-markdown.d.ts.map +1 -0
  43. package/dist/routes/sitemap-markdown.js +32 -0
  44. package/dist/routes/sitemap-markdown.js.map +1 -0
  45. package/dist/types.d.ts +16 -2
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/utils/aeo-summary.d.ts +35 -0
  48. package/dist/utils/aeo-summary.d.ts.map +1 -0
  49. package/dist/utils/aeo-summary.js +141 -0
  50. package/dist/utils/aeo-summary.js.map +1 -0
  51. package/dist/utils/aeo-twin-emitter.d.ts +79 -0
  52. package/dist/utils/aeo-twin-emitter.d.ts.map +1 -0
  53. package/dist/utils/aeo-twin-emitter.js +99 -0
  54. package/dist/utils/aeo-twin-emitter.js.map +1 -0
  55. package/dist/utils/aeo.d.ts +62 -12
  56. package/dist/utils/aeo.d.ts.map +1 -1
  57. package/dist/utils/aeo.js +187 -26
  58. package/dist/utils/aeo.js.map +1 -1
  59. package/dist/utils/apple-news-anf.d.ts +38 -0
  60. package/dist/utils/apple-news-anf.d.ts.map +1 -0
  61. package/dist/utils/apple-news-anf.js +120 -0
  62. package/dist/utils/apple-news-anf.js.map +1 -0
  63. package/dist/utils/apple-news-rss.d.ts +31 -0
  64. package/dist/utils/apple-news-rss.d.ts.map +1 -0
  65. package/dist/utils/apple-news-rss.js +103 -0
  66. package/dist/utils/apple-news-rss.js.map +1 -0
  67. package/dist/utils/content-filter.d.ts +52 -0
  68. package/dist/utils/content-filter.d.ts.map +1 -0
  69. package/dist/utils/content-filter.js +75 -0
  70. package/dist/utils/content-filter.js.map +1 -0
  71. package/dist/utils/crawler-class.d.ts +39 -0
  72. package/dist/utils/crawler-class.d.ts.map +1 -0
  73. package/dist/utils/crawler-class.js +127 -0
  74. package/dist/utils/crawler-class.js.map +1 -0
  75. package/dist/utils/effective-auth.d.ts +28 -0
  76. package/dist/utils/effective-auth.d.ts.map +1 -0
  77. package/dist/utils/effective-auth.js +33 -0
  78. package/dist/utils/effective-auth.js.map +1 -0
  79. package/dist/utils/fcrdns.d.ts +73 -0
  80. package/dist/utils/fcrdns.d.ts.map +1 -0
  81. package/dist/utils/fcrdns.js +219 -0
  82. package/dist/utils/fcrdns.js.map +1 -0
  83. package/dist/utils/fresh-layer.d.ts +53 -0
  84. package/dist/utils/fresh-layer.d.ts.map +1 -0
  85. package/dist/utils/fresh-layer.js +147 -0
  86. package/dist/utils/fresh-layer.js.map +1 -0
  87. package/dist/utils/index.d.ts +14 -3
  88. package/dist/utils/index.d.ts.map +1 -1
  89. package/dist/utils/index.js +14 -3
  90. package/dist/utils/index.js.map +1 -1
  91. package/dist/utils/json-ld/article.d.ts +13 -1
  92. package/dist/utils/json-ld/article.d.ts.map +1 -1
  93. package/dist/utils/json-ld/article.js +37 -8
  94. package/dist/utils/json-ld/article.js.map +1 -1
  95. package/dist/utils/llms-full.d.ts +29 -0
  96. package/dist/utils/llms-full.d.ts.map +1 -0
  97. package/dist/utils/llms-full.js +67 -0
  98. package/dist/utils/llms-full.js.map +1 -0
  99. package/dist/utils/meta.d.ts +4 -1
  100. package/dist/utils/meta.d.ts.map +1 -1
  101. package/dist/utils/meta.js +25 -2
  102. package/dist/utils/meta.js.map +1 -1
  103. package/dist/utils/sitemap-markdown.d.ts +24 -0
  104. package/dist/utils/sitemap-markdown.d.ts.map +1 -0
  105. package/dist/utils/sitemap-markdown.js +57 -0
  106. package/dist/utils/sitemap-markdown.js.map +1 -0
  107. package/dist/utils/staleness.d.ts +27 -0
  108. package/dist/utils/staleness.d.ts.map +1 -0
  109. package/dist/utils/staleness.js +46 -0
  110. package/dist/utils/staleness.js.map +1 -0
  111. package/dist/utils/validation.d.ts +41 -0
  112. package/dist/utils/validation.d.ts.map +1 -1
  113. package/dist/utils/validation.js +78 -0
  114. package/dist/utils/validation.js.map +1 -1
  115. package/package.json +70 -58
@@ -0,0 +1,79 @@
1
+ import type { ContentItem } from '../types.js';
2
+ export type RenderBody = (item: ContentItem) => string | Promise<string>;
3
+ export interface EmitAeoTwinsOptions {
4
+ items: ContentItem[];
5
+ publisherName: string;
6
+ schemaType: string;
7
+ /** Resolver that returns the article body in markdown. Required; the emitter
8
+ * can't synthesize content. Consumers typically produce this from their CMS. */
9
+ renderBody: RenderBody;
10
+ /** Map item.url → primary twin URL. Default: append '.md'. */
11
+ twinUrl?: (articleUrl: string) => string;
12
+ /** Predicate applied after the default members filter. Default: always true. */
13
+ include?: (item: ContentItem) => boolean;
14
+ /** Emit a summary twin alongside the primary. Default true. */
15
+ summaryTwin?: boolean;
16
+ /** Wrap semantic sections in <!-- aeo:section --> markers. Default true. */
17
+ ragChunkMarkers?: boolean;
18
+ /** Stale-hash metadata mode. */
19
+ stalenessCheck?: 'content-hash' | 'dateModified' | 'none';
20
+ /** Called when the summary generator falls back to tier 4. Used for build telemetry. */
21
+ onSummaryMinimalFallback?: (item: ContentItem) => void;
22
+ }
23
+ export interface EmittedTwin {
24
+ /** URL path this file will be served at (used by the writer to derive the filesystem path). */
25
+ urlPath: string;
26
+ /** Full URL (for frontmatter). */
27
+ url: string;
28
+ /** File body. */
29
+ content: string;
30
+ /** Content-type for HTTP response headers when this file is served. */
31
+ contentType: string;
32
+ /** If this emission is the primary twin for an item, the item is populated. */
33
+ item?: ContentItem;
34
+ /** If this emission is a summary twin, the corresponding primary URL. */
35
+ primaryUrl?: string;
36
+ /** Primary twins carry a stable content hash for staleness validation; absent on summaries. */
37
+ contentHash?: string;
38
+ }
39
+ export interface EmitAeoTwinsResult {
40
+ twins: EmittedTwin[];
41
+ /** Map of item.url → contentHash. Persist to disk for staleness validation. */
42
+ contentHashes: Map<string, string>;
43
+ /** Number of items filtered out (either by access rule or consumer predicate). */
44
+ skipped: number;
45
+ }
46
+ /**
47
+ * Compute all twin files for a given contentProvider output, without touching
48
+ * the filesystem. Callers (index.ts at astro:build:done time) write the returned
49
+ * twins to disk and record content hashes for later staleness validation.
50
+ *
51
+ * Filtering:
52
+ * - Members items excluded unconditionally (via forMarkdownTwin).
53
+ * - Consumer-supplied `include` predicate applied after the access filter.
54
+ *
55
+ * For each surviving item we emit:
56
+ * - Primary twin at twinUrl(item.url).
57
+ * - Summary twin at <primary>.summary.md (when summaryTwin: true).
58
+ *
59
+ * Aliases (spec twinAliases) are NOT emitted as static files in v7 — they're
60
+ * middleware-only redirects. Static-mode twins live at the primary URL only.
61
+ */
62
+ export declare function emitAeoTwins(options: EmitAeoTwinsOptions): Promise<EmitAeoTwinsResult>;
63
+ /**
64
+ * Default twinUrl: strip trailing slashes and append '.md'.
65
+ * `/article/midway/` -> `/article/midway.md`
66
+ * `/article/midway` -> `/article/midway.md`
67
+ */
68
+ declare function defaultTwinUrl(articleUrl: string): string;
69
+ /**
70
+ * Extract the URL-path portion of an absolute or relative URL.
71
+ * Used by the caller to derive the filesystem write path under dist/client/.
72
+ */
73
+ declare function urlPath(url: string): string;
74
+ export declare const _internals: {
75
+ defaultTwinUrl: typeof defaultTwinUrl;
76
+ urlPath: typeof urlPath;
77
+ };
78
+ export {};
79
+ //# sourceMappingURL=aeo-twin-emitter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aeo-twin-emitter.d.ts","sourceRoot":"","sources":["../../src/utils/aeo-twin-emitter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAQ9C,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,WAAW,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;AAExE,MAAM,WAAW,mBAAmB;IACnC,KAAK,EAAE,WAAW,EAAE,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB;qFACiF;IACjF,UAAU,EAAE,UAAU,CAAA;IACtB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAA;IACxC,gFAAgF;IAChF,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,OAAO,CAAA;IACxC,+DAA+D;IAC/D,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,4EAA4E;IAC5E,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,gCAAgC;IAChC,cAAc,CAAC,EAAE,cAAc,GAAG,cAAc,GAAG,MAAM,CAAA;IACzD,wFAAwF;IACxF,wBAAwB,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAA;CACtD;AAED,MAAM,WAAW,WAAW;IAC3B,+FAA+F;IAC/F,OAAO,EAAE,MAAM,CAAA;IACf,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAA;IACX,iBAAiB;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAA;IACnB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,+FAA+F;IAC/F,WAAW,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,WAAW,EAAE,CAAA;IACpB,+EAA+E;IAC/E,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,kFAAkF;IAClF,OAAO,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAuE5F;AAID;;;;GAIG;AACH,iBAAS,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;GAGG;AACH,iBAAS,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOpC;AAED,eAAO,MAAM,UAAU;;;CAGtB,CAAA"}
@@ -0,0 +1,99 @@
1
+ import { generateAeoMarkdown } from './aeo.js';
2
+ import { generateSummaryTwin } from './aeo-summary.js';
3
+ import { forMarkdownTwin } from './content-filter.js';
4
+ import { computeContentHash } from './staleness.js';
5
+ /**
6
+ * Compute all twin files for a given contentProvider output, without touching
7
+ * the filesystem. Callers (index.ts at astro:build:done time) write the returned
8
+ * twins to disk and record content hashes for later staleness validation.
9
+ *
10
+ * Filtering:
11
+ * - Members items excluded unconditionally (via forMarkdownTwin).
12
+ * - Consumer-supplied `include` predicate applied after the access filter.
13
+ *
14
+ * For each surviving item we emit:
15
+ * - Primary twin at twinUrl(item.url).
16
+ * - Summary twin at <primary>.summary.md (when summaryTwin: true).
17
+ *
18
+ * Aliases (spec twinAliases) are NOT emitted as static files in v7 — they're
19
+ * middleware-only redirects. Static-mode twins live at the primary URL only.
20
+ */
21
+ export async function emitAeoTwins(options) {
22
+ const { items, publisherName, schemaType, renderBody, twinUrl = defaultTwinUrl, include = () => true, summaryTwin = true, ragChunkMarkers = true, stalenessCheck = 'content-hash', onSummaryMinimalFallback, } = options;
23
+ const filtered = forMarkdownTwin(items).filter(include);
24
+ const skipped = items.length - filtered.length;
25
+ const twins = [];
26
+ const contentHashes = new Map();
27
+ for (const item of filtered) {
28
+ const primaryUrl = twinUrl(item.url);
29
+ const primaryUrlPath = urlPath(primaryUrl);
30
+ const body = await renderBody(item);
31
+ const contentHash = stalenessCheck === 'content-hash' ? await computeContentHash(item, body) : undefined;
32
+ if (contentHash)
33
+ contentHashes.set(item.url, contentHash);
34
+ const summaryUrl = summaryTwin ? `${primaryUrl}.summary.md` : undefined;
35
+ const aeoOpts = {
36
+ publisherName,
37
+ schemaType,
38
+ content: body,
39
+ ragChunkMarkers,
40
+ canonical: item.url,
41
+ twinUrl: primaryUrl,
42
+ summaryUrl,
43
+ contentHash,
44
+ };
45
+ const primaryContent = generateAeoMarkdown(item, aeoOpts);
46
+ twins.push({
47
+ urlPath: primaryUrlPath,
48
+ url: primaryUrl,
49
+ content: primaryContent,
50
+ contentType: 'text/markdown; charset=utf-8',
51
+ item,
52
+ contentHash,
53
+ });
54
+ if (summaryTwin && summaryUrl) {
55
+ const summary = generateSummaryTwin(item, {
56
+ publisherName,
57
+ schemaType,
58
+ content: body,
59
+ fullUrl: primaryUrl,
60
+ onMinimalFallback: onSummaryMinimalFallback,
61
+ });
62
+ twins.push({
63
+ urlPath: urlPath(summaryUrl),
64
+ url: summaryUrl,
65
+ content: summary.markdown,
66
+ contentType: 'text/markdown; charset=utf-8',
67
+ primaryUrl,
68
+ });
69
+ }
70
+ }
71
+ return { twins, contentHashes, skipped };
72
+ }
73
+ // ─── Defaults ───
74
+ /**
75
+ * Default twinUrl: strip trailing slashes and append '.md'.
76
+ * `/article/midway/` -> `/article/midway.md`
77
+ * `/article/midway` -> `/article/midway.md`
78
+ */
79
+ function defaultTwinUrl(articleUrl) {
80
+ return `${articleUrl.replace(/\/+$/, '')}.md`;
81
+ }
82
+ /**
83
+ * Extract the URL-path portion of an absolute or relative URL.
84
+ * Used by the caller to derive the filesystem write path under dist/client/.
85
+ */
86
+ function urlPath(url) {
87
+ try {
88
+ return new URL(url).pathname;
89
+ }
90
+ catch {
91
+ // Relative URL — assume it's already a path.
92
+ return url.startsWith('/') ? url : `/${url}`;
93
+ }
94
+ }
95
+ export const _internals = {
96
+ defaultTwinUrl,
97
+ urlPath,
98
+ };
99
+ //# sourceMappingURL=aeo-twin-emitter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aeo-twin-emitter.js","sourceRoot":"","sources":["../../src/utils/aeo-twin-emitter.ts"],"names":[],"mappings":"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"}
@@ -1,16 +1,66 @@
1
+ import type { ContentItem } from '../types.js';
2
+ export interface GenerateAeoMarkdownOptions {
3
+ /** Publisher organization name for the frontmatter `publisher` field. */
4
+ publisherName: string;
5
+ /** Schema.org type (e.g. 'Article', 'NewsArticle'). */
6
+ schemaType: string;
7
+ /** Rendered article body in clean markdown (no HTML chrome). */
8
+ content: string;
9
+ /** If true, wraps semantic sections in `<!-- aeo:section start/end -->` comments. */
10
+ ragChunkMarkers?: boolean;
11
+ /** Canonical HTML URL to reference from the twin frontmatter. Defaults to item.url. */
12
+ canonical?: string;
13
+ /** The primary twin URL (what this file will be served at). Stored in frontmatter as `url`. */
14
+ twinUrl?: string;
15
+ /** Optional summary-twin URL to cross-link from the full twin frontmatter. */
16
+ summaryUrl?: string;
17
+ /** Pre-computed SHA-256 content hash for staleness validation. */
18
+ contentHash?: string;
19
+ }
1
20
  /**
2
- * AEO (Answer Engine Optimization) utilities
3
- * Generates AI-friendly markdown with YAML frontmatter
21
+ * Cheap token count estimate: 1 token ≈ 4 characters. Used for the `x-markdown-tokens`
22
+ * response header and the `tokens` frontmatter field.
4
23
  */
5
24
  export declare function estimateTokenCount(text: string): number;
6
- export declare function generateAeoMarkdown(item: {
7
- url: string;
8
- title: string;
9
- description?: string;
10
- datePublished?: string;
11
- dateModified?: string;
12
- authors?: Array<{
13
- name: string;
14
- }>;
15
- }, content: string, schemaType: string, publisherName: string): string;
25
+ /**
26
+ * Generate an AEO markdown twin for a ContentItem.
27
+ *
28
+ * Produces: frontmatter (YAML) + body. When `ragChunkMarkers: true`, inserts
29
+ * `<!-- aeo:section start="<slug>" -->` / `<!-- aeo:section end="<slug>" -->` pairs
30
+ * around semantic sections (identified by `## ` headings) to guide RAG retrievers
31
+ * toward boundaries we control.
32
+ *
33
+ * Sanitization:
34
+ * - Slugs are forced to `[a-z0-9-]{1,64}`; empty results fall back to `section-N`.
35
+ * - Any author-supplied `<!--` / `-->` in the body is escaped (`&lt;!--` / `--&gt;`)
36
+ * before wrapping, so forged markers can't close real ones.
37
+ */
38
+ export declare function generateAeoMarkdown(item: ContentItem, options: GenerateAeoMarkdownOptions): string;
39
+ /**
40
+ * Emit a value safely for YAML. Unquoted when possible; quoted (with escapes) when
41
+ * the value would otherwise parse ambiguously.
42
+ */
43
+ declare function yamlValue(v: string): string;
44
+ /**
45
+ * Wrap each `##` section in `<!-- aeo:section start="..." -->` / `<!-- end -->`.
46
+ * Content before the first `##` is wrapped as `lede`; content after the last `##`
47
+ * terminates with an end marker for that section.
48
+ */
49
+ declare function wrapSectionMarkers(body: string): string;
50
+ /** Strict slug sanitization — [a-z0-9-]{1,64}, no leading/trailing/repeated dashes. */
51
+ declare function sanitizeSlug(text: string): string;
52
+ declare function uniqueSlug(base: string, used: Set<string>): string;
53
+ /**
54
+ * Escape any author-supplied `<!--` / `-->` in the body so forged markers can't
55
+ * close real ones. Applied BEFORE wrapping (spec "Marker sanitization").
56
+ */
57
+ declare function escapeHtmlComments(body: string): string;
58
+ export declare const _internals: {
59
+ sanitizeSlug: typeof sanitizeSlug;
60
+ uniqueSlug: typeof uniqueSlug;
61
+ escapeHtmlComments: typeof escapeHtmlComments;
62
+ wrapSectionMarkers: typeof wrapSectionMarkers;
63
+ yamlValue: typeof yamlValue;
64
+ };
65
+ export {};
16
66
  //# sourceMappingURL=aeo.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"aeo.d.ts","sourceRoot":"","sources":["../../src/utils/aeo.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,mBAAmB,CAClC,IAAI,EAAE;IACL,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACjC,EACD,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,GACnB,MAAM,CA8BR"}
1
+ {"version":3,"file":"aeo.d.ts","sourceRoot":"","sources":["../../src/utils/aeo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAI9C,MAAM,WAAW,0BAA0B;IAC1C,yEAAyE;IACzE,aAAa,EAAE,MAAM,CAAA;IACrB,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAA;IAClB,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAA;IACf,qFAAqF;IACrF,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+FAA+F;IAC/F,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAClC,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,0BAA0B,GACjC,MAAM,CAKR;AAqED;;;GAGG;AACH,iBAAS,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAUpC;AAID;;;;GAIG;AACH,iBAAS,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA0ChD;AAED,uFAAuF;AACvF,iBAAS,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQ1C;AAED,iBAAS,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,CAK3D;AAED;;;GAGG;AACH,iBAAS,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAID,eAAO,MAAM,UAAU;;;;;;CAMtB,CAAA"}
package/dist/utils/aeo.js CHANGED
@@ -1,38 +1,199 @@
1
1
  /**
2
- * AEO (Answer Engine Optimization) utilities
3
- * Generates AI-friendly markdown with YAML frontmatter
2
+ * Cheap token count estimate: 1 token ≈ 4 characters. Used for the `x-markdown-tokens`
3
+ * response header and the `tokens` frontmatter field.
4
4
  */
5
5
  export function estimateTokenCount(text) {
6
6
  return Math.ceil(text.length / 4);
7
7
  }
8
- export function generateAeoMarkdown(item, content, schemaType, publisherName) {
9
- const frontmatter = {
10
- title: item.title,
11
- url: item.url,
12
- schemaType,
13
- publisher: publisherName,
14
- };
8
+ /**
9
+ * Generate an AEO markdown twin for a ContentItem.
10
+ *
11
+ * Produces: frontmatter (YAML) + body. When `ragChunkMarkers: true`, inserts
12
+ * `<!-- aeo:section start="<slug>" -->` / `<!-- aeo:section end="<slug>" -->` pairs
13
+ * around semantic sections (identified by `## ` headings) to guide RAG retrievers
14
+ * toward boundaries we control.
15
+ *
16
+ * Sanitization:
17
+ * - Slugs are forced to `[a-z0-9-]{1,64}`; empty results fall back to `section-N`.
18
+ * - Any author-supplied `<!--` / `-->` in the body is escaped (`&lt;!--` / `--&gt;`)
19
+ * before wrapping, so forged markers can't close real ones.
20
+ */
21
+ export function generateAeoMarkdown(item, options) {
22
+ const frontmatter = buildFrontmatter(item, options);
23
+ const safeBody = escapeHtmlComments(options.content);
24
+ const body = options.ragChunkMarkers ? wrapSectionMarkers(safeBody) : safeBody;
25
+ return `---\n${frontmatter}\n---\n\n${body}`;
26
+ }
27
+ // ─── Frontmatter ───
28
+ function buildFrontmatter(item, options) {
29
+ const canonical = options.canonical ?? item.url;
30
+ const twinUrl = options.twinUrl ?? item.url;
31
+ const firstImage = Array.isArray(item.image) ? item.image[0] : item.image;
32
+ const authorEntries = (item.authors ?? []).map((a) => {
33
+ const o = { name: a.name };
34
+ if (a.url)
35
+ o.url = a.url;
36
+ if (a.jobTitle)
37
+ o.jobTitle = a.jobTitle;
38
+ if (a.knowsAbout?.length)
39
+ o.knowsAbout = a.knowsAbout;
40
+ if (a.sameAs?.length)
41
+ o.sameAs = a.sameAs;
42
+ return o;
43
+ });
44
+ const alternateLanguages = (item.alternateLocales ?? []).map((l) => ({
45
+ lang: l.lang,
46
+ url: l.url,
47
+ }));
48
+ const lines = [];
49
+ lines.push(yamlScalar('title', item.title));
15
50
  if (item.description)
16
- frontmatter.description = item.description;
51
+ lines.push(yamlScalar('description', item.description));
52
+ lines.push(yamlScalar('url', twinUrl));
53
+ lines.push(yamlScalar('canonical', canonical));
17
54
  if (item.datePublished)
18
- frontmatter.datePublished = item.datePublished;
55
+ lines.push(yamlScalar('datePublished', item.datePublished));
19
56
  if (item.dateModified)
20
- frontmatter.dateModified = item.dateModified;
21
- if (item.authors?.length) {
22
- frontmatter.authors = item.authors.map((a) => a.name);
57
+ lines.push(yamlScalar('dateModified', item.dateModified));
58
+ if (authorEntries.length) {
59
+ lines.push('author:');
60
+ for (const a of authorEntries) {
61
+ lines.push(` - name: ${yamlValue(a.name)}`);
62
+ if (a.url)
63
+ lines.push(` url: ${yamlValue(a.url)}`);
64
+ if (a.jobTitle)
65
+ lines.push(` jobTitle: ${yamlValue(a.jobTitle)}`);
66
+ if (a.knowsAbout) {
67
+ lines.push(' knowsAbout:');
68
+ for (const k of a.knowsAbout)
69
+ lines.push(` - ${yamlValue(k)}`);
70
+ }
71
+ if (a.sameAs) {
72
+ lines.push(' sameAs:');
73
+ for (const s of a.sameAs)
74
+ lines.push(` - ${yamlValue(s)}`);
75
+ }
76
+ }
23
77
  }
24
- const yamlLines = Object.entries(frontmatter).map(([key, value]) => {
25
- if (Array.isArray(value)) {
26
- return `${key}:\n${value.map((v) => ` - ${v}`).join('\n')}`;
78
+ lines.push(yamlScalar('publisher', options.publisherName));
79
+ if (firstImage)
80
+ lines.push(yamlScalar('image', firstImage));
81
+ lines.push(yamlScalar('type', options.schemaType));
82
+ if (item.locale)
83
+ lines.push(yamlScalar('language', item.locale));
84
+ if (item.audio)
85
+ lines.push(yamlScalar('audio', item.audio.url));
86
+ if (alternateLanguages.length) {
87
+ lines.push('alternateLanguages:');
88
+ for (const l of alternateLanguages) {
89
+ lines.push(` - lang: ${yamlValue(l.lang)}`);
90
+ lines.push(` url: ${yamlValue(l.url)}`);
27
91
  }
28
- // Wrap in quotes only when truly needed for YAML validity
29
- // URLs with :// are fine unquoted; dates with colons need quoting if they'd be ambiguous
30
- const str = String(value);
31
- const isUrl = /^https?:\/\//.test(str);
32
- const isDate = /^\d{4}-\d{2}-\d{2}/.test(str);
33
- const hasYamlSpecial = /[#[\]{},&*?|>'"!%@`]/.test(str) || (!isUrl && !isDate && str.includes(':'));
34
- return `${key}: ${hasYamlSpecial ? `"${str.replace(/"/g, '\\"')}"` : str}`;
35
- });
36
- return `---\n${yamlLines.join('\n')}\n---\n\n${content}`;
92
+ }
93
+ if (options.contentHash)
94
+ lines.push(yamlScalar('contentHash', options.contentHash));
95
+ const tokens = estimateTokenCount(options.content);
96
+ lines.push(`tokens: ${tokens}`);
97
+ if (options.summaryUrl)
98
+ lines.push(yamlScalar('summaryUrl', options.summaryUrl));
99
+ return lines.join('\n');
100
+ }
101
+ function yamlScalar(key, value) {
102
+ return `${key}: ${yamlValue(value)}`;
103
+ }
104
+ /**
105
+ * Emit a value safely for YAML. Unquoted when possible; quoted (with escapes) when
106
+ * the value would otherwise parse ambiguously.
107
+ */
108
+ function yamlValue(v) {
109
+ // Whitelisted forms that can be emitted unquoted even though they contain `:`.
110
+ const isUrl = /^https?:\/\//.test(v);
111
+ const isDate = /^\d{4}-\d{2}-\d{2}/.test(v);
112
+ // Always quote if contains chars with YAML special meaning (excluding `:` which
113
+ // is handled separately below), or if starts with a character that could be misread.
114
+ const needsQuotes = /^[\s-]/.test(v) || /[#{}[\],&*?|>!%@`'"\n\r]/.test(v) || (v.includes(':') && !isUrl && !isDate);
115
+ if (!needsQuotes)
116
+ return v;
117
+ return `"${v.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
118
+ }
119
+ // ─── RAG chunk markers ───
120
+ /**
121
+ * Wrap each `##` section in `<!-- aeo:section start="..." -->` / `<!-- end -->`.
122
+ * Content before the first `##` is wrapped as `lede`; content after the last `##`
123
+ * terminates with an end marker for that section.
124
+ */
125
+ function wrapSectionMarkers(body) {
126
+ const lines = body.split('\n');
127
+ const out = [];
128
+ let currentSlug = null;
129
+ const usedSlugs = new Set();
130
+ let ordinal = 0;
131
+ let sawContent = false;
132
+ // Open with `lede` if we hit body content before any heading.
133
+ const openSection = (slug) => {
134
+ out.push(`<!-- aeo:section start="${slug}" -->`);
135
+ currentSlug = slug;
136
+ };
137
+ const closeSection = () => {
138
+ if (currentSlug) {
139
+ out.push(`<!-- aeo:section end="${currentSlug}" -->`);
140
+ currentSlug = null;
141
+ }
142
+ };
143
+ for (const line of lines) {
144
+ const headingMatch = /^##\s+(.+)$/.exec(line);
145
+ if (headingMatch) {
146
+ closeSection();
147
+ ordinal++;
148
+ const slug = sanitizeSlug(headingMatch[1]) || `section-${ordinal}`;
149
+ const unique = uniqueSlug(slug, usedSlugs);
150
+ usedSlugs.add(unique);
151
+ openSection(unique);
152
+ out.push(line);
153
+ sawContent = true;
154
+ continue;
155
+ }
156
+ if (!currentSlug && line.trim() !== '' && !sawContent) {
157
+ openSection('lede');
158
+ usedSlugs.add('lede');
159
+ sawContent = true;
160
+ }
161
+ out.push(line);
162
+ }
163
+ closeSection();
164
+ return out.join('\n');
165
+ }
166
+ /** Strict slug sanitization — [a-z0-9-]{1,64}, no leading/trailing/repeated dashes. */
167
+ function sanitizeSlug(text) {
168
+ const lowered = text.toLowerCase();
169
+ const ascii = lowered.normalize('NFKD').replace(/[\u0300-\u036f]/g, '');
170
+ const cleaned = ascii
171
+ .replace(/[^a-z0-9]+/g, '-')
172
+ .replace(/^-+|-+$/g, '')
173
+ .slice(0, 64);
174
+ return cleaned;
175
+ }
176
+ function uniqueSlug(base, used) {
177
+ if (!used.has(base))
178
+ return base;
179
+ let n = 2;
180
+ while (used.has(`${base}-${n}`))
181
+ n++;
182
+ return `${base}-${n}`;
183
+ }
184
+ /**
185
+ * Escape any author-supplied `<!--` / `-->` in the body so forged markers can't
186
+ * close real ones. Applied BEFORE wrapping (spec "Marker sanitization").
187
+ */
188
+ function escapeHtmlComments(body) {
189
+ return body.replace(/<!--/g, '&lt;!--').replace(/-->/g, '--&gt;');
37
190
  }
191
+ // ─── Exports for testing ───
192
+ export const _internals = {
193
+ sanitizeSlug,
194
+ uniqueSlug,
195
+ escapeHtmlComments,
196
+ wrapSectionMarkers,
197
+ yamlValue,
198
+ };
38
199
  //# sourceMappingURL=aeo.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"aeo.js","sourceRoot":"","sources":["../../src/utils/aeo.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC9C,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;AAClC,CAAC;AAED,MAAM,UAAU,mBAAmB,CAClC,IAOC,EACD,OAAe,EACf,UAAkB,EAClB,aAAqB;IAErB,MAAM,WAAW,GAAsC;QACtD,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,UAAU;QACV,SAAS,EAAE,aAAa;KACxB,CAAA;IAED,IAAI,IAAI,CAAC,WAAW;QAAE,WAAW,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAA;IAChE,IAAI,IAAI,CAAC,aAAa;QAAE,WAAW,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAA;IACtE,IAAI,IAAI,CAAC,YAAY;QAAE,WAAW,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAA;IACnE,IAAI,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;QAC1B,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IACtD,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;QAClE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAA;QAC7D,CAAC;QACD,0DAA0D;QAC1D,yFAAyF;QACzF,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QACzB,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACtC,MAAM,MAAM,GAAG,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QAC7C,MAAM,cAAc,GACnB,sBAAsB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAA;QAC7E,OAAO,GAAG,GAAG,KAAK,cAAc,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAA;IAC3E,CAAC,CAAC,CAAA;IAEF,OAAO,QAAQ,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,OAAO,EAAE,CAAA;AACzD,CAAC"}
1
+ {"version":3,"file":"aeo.js","sourceRoot":"","sources":["../../src/utils/aeo.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { ContentItem } from '../types.js';
2
+ export interface GenerateAnfOptions {
3
+ channelId: string;
4
+ byline?: string;
5
+ language?: string;
6
+ }
7
+ export interface AnfComponent {
8
+ role: string;
9
+ [key: string]: unknown;
10
+ }
11
+ export type AnfLayoutMap = Record<string, Record<string, unknown>>;
12
+ export type AnfStyleMap = Record<string, Record<string, unknown>>;
13
+ export interface AnfDocument {
14
+ version: string;
15
+ identifier: string;
16
+ title: string;
17
+ subtitle?: string;
18
+ language: string;
19
+ layout: Record<string, unknown>;
20
+ documentStyle?: Record<string, unknown>;
21
+ metadata?: Record<string, unknown>;
22
+ components: AnfComponent[];
23
+ componentLayouts?: AnfLayoutMap;
24
+ componentStyles?: AnfStyleMap;
25
+ }
26
+ /**
27
+ * Generate an Apple News Format document for a ContentItem. The returned value
28
+ * is a JSON-serializable object; consumers submit it as the `article.json` part
29
+ * of a News Publisher multipart POST.
30
+ *
31
+ * The generated document uses a conservative default layout (7-column grid),
32
+ * standard component styles, and a body composed of: title, byline, hero image,
33
+ * then one body component per paragraph-separated block of the article's
34
+ * description. Consumers producing real articles should replace the body
35
+ * components with their rendered content before submitting.
36
+ */
37
+ export declare function generateAppleNewsAnf(item: ContentItem, options: GenerateAnfOptions): AnfDocument;
38
+ //# sourceMappingURL=apple-news-anf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apple-news-anf.d.ts","sourceRoot":"","sources":["../../src/utils/apple-news-anf.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAiB9C,MAAM,WAAW,kBAAkB;IAClC,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;CAGjB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACtB;AAID,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAClE,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAEjE,MAAM,WAAW,WAAW;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,UAAU,EAAE,YAAY,EAAE,CAAA;IAC1B,gBAAgB,CAAC,EAAE,YAAY,CAAA;IAC/B,eAAe,CAAC,EAAE,WAAW,CAAA;CAC7B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAsFhG"}