@sonordev/contracts 2.12.0 → 2.15.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/llms.d.ts CHANGED
@@ -6,6 +6,13 @@
6
6
  * `@sonordev/site-kit/seo/llms/contract`.
7
7
  *
8
8
  * @see LLM_GEO_CONTRACT.md in this folder
9
+ *
10
+ * It is also where llms.txt's page notes, page titles and page order are
11
+ * decided, so the Sonor API's index, its fallback file and site-kit's local
12
+ * generator can't disagree: `pickLlmsPageDescription` (summary, curated meta,
13
+ * the page's own meta, the schema description, then a cleaned scrape),
14
+ * `llmsPageTitle`, `isLlmsListablePage`, `selectLlmsPages`, `groupLlmsPages`,
15
+ * `llmsPageListNote` and `llmsContactLines`.
9
16
  */
10
17
  /** Increment when public LLMs payload shape changes incompatibly. */
11
18
  export declare const LLM_GEO_CONTRACT_VERSION: 1;
@@ -29,8 +36,27 @@ export interface LLMsPayloadMeta {
29
36
  llms_disclaimer?: string | null;
30
37
  }
31
38
  export declare function clipAtWordBoundary(text: string, max: number): string;
39
+ /**
40
+ * The text with its phone numbers and email addresses taken out. The business's
41
+ * Contact Information block already carries them, and a page's note is no place
42
+ * for either.
43
+ *
44
+ * Works a sentence at a time: a sentence with a detail in it keeps what comes
45
+ * before the detail (and before a "call" or "email us at" in front of it) when
46
+ * that is still a sentence of four words or more, and is dropped otherwise.
47
+ * "Family-owned HVAC in Cincinnati, call 513-555-1234 today." becomes
48
+ * "Family-owned HVAC in Cincinnati."; "Call 513-555-1234 for a quote." goes.
49
+ * Cutting out only the number would leave "for a quote." behind.
50
+ */
51
+ export declare function stripContactDetails(text: string): string;
52
+ export declare function stripPageFiller(note: string): string;
32
53
  /**
33
54
  * Public-safe summary for llms.txt `[title](url): notes` — never CRM-only or internal copy.
55
+ *
56
+ * A summary that mentions a phone number or an email address loses that part
57
+ * (`stripContactDetails`) and is still used. It used to be thrown away whole,
58
+ * and a page with no summary is described by its scrape, so a well-meant
59
+ * "...call 555-1234" summary made the page's note worse, not safer.
34
60
  */
35
61
  export declare function sanitizeLlmsPublicSummary(raw: unknown): string | null;
36
62
  /**
@@ -53,3 +79,177 @@ export declare function sanitizePrimaryLanguageTag(raw: unknown): string | null;
53
79
  export declare function isEmittableReviewNode(node: Record<string, unknown>): boolean;
54
80
  /** Pick JSON-LD-safe subset of managed_llm_schema for script injection. */
55
81
  export declare function pickManagedLlmSchemaForJsonLd(schema: Record<string, unknown> | null | undefined): Record<string, unknown> | null;
82
+ /** The longest note a page line carries. */
83
+ export declare const LLMS_DESCRIPTION_MAX_LENGTH = 500;
84
+ /**
85
+ * A page's scraped text (seo_pages.content_text) made fit to describe it.
86
+ *
87
+ * The DOM scrape joins elements with nothing between them: a heading, its
88
+ * tagline and the first paragraph arrive as "Getting paid for the
89
+ * workIncome, spending, budgets.Track expenses by vendor", a contact page as
90
+ * "First Name*Last Name*Email*PhoneMessage*Send Message". This puts the
91
+ * whitespace back, drops what is not about the page (form labels, menu items,
92
+ * skip links, cookie banners, "Eyebrow · Section" lines) and returns what is
93
+ * left. It may return '' when nothing worth a note is.
94
+ *
95
+ * It does not cut to a length: the caller clips (`clipAtWordBoundary`). It
96
+ * does not strip phone numbers: that is `stripContactDetails`. `knownTerms`
97
+ * are words written with a capital inside them that must stay whole.
98
+ */
99
+ export declare function cleanScrapedPageText(raw: string | null | undefined, options?: {
100
+ knownTerms?: readonly string[];
101
+ }): string;
102
+ /** Where a page's note came from, best source first. */
103
+ export type LlmsDescriptionSource = 'summary' | 'managed_meta' | 'meta' | 'schema' | 'scrape';
104
+ export interface LlmsDescriptionInput {
105
+ /** The page's own name: a note that only repeats it says nothing. */
106
+ title?: string | null;
107
+ /** seo_pages.llms_public_summary: written for this file. */
108
+ llms_public_summary?: string | null;
109
+ /** seo_pages.managed_meta_description: the curated meta description. */
110
+ managed_meta_description?: string | null;
111
+ /** seo_pages.meta_description: the meta description the page serves. */
112
+ meta_description?: string | null;
113
+ /** The `description` of the page's managed LLM schema. */
114
+ llm_schema_description?: string | null;
115
+ /** seo_pages.content_text: the DOM scrape. */
116
+ content_text?: string | null;
117
+ /**
118
+ * True when the page's managed copy says the page itself is missing
119
+ * (describesErrorPage). That copy, the schema and the page's own meta
120
+ * description are then not used.
121
+ */
122
+ copyWithheld?: boolean;
123
+ }
124
+ /**
125
+ * The note a page's line in llms.txt carries, and where it came from.
126
+ *
127
+ * One rule for every llms.txt Sonor builds, best source first:
128
+ *
129
+ * 1. `llms_public_summary`, written for this file, with a phone number or an
130
+ * email address taken out rather than the whole summary thrown away;
131
+ * 2. `managed_meta_description`, the curated meta description;
132
+ * 3. `meta_description`, the one the page serves;
133
+ * 4. the managed schema's description;
134
+ * 5. the cleaned scrape (`cleanScrapedPageText`), last, because it is the
135
+ * least written of the five.
136
+ *
137
+ * Until 2026-10 the scrape was FIRST: llms.txt, the one file handed straight
138
+ * to AI crawlers, described a contact page as "First Name*Last Name*Email*".
139
+ * Every note is cut at a sentence or word boundary at 500 characters. A scrape
140
+ * that is an error page is not used (`usablePageText`). Returns null when
141
+ * nothing says anything about the page.
142
+ */
143
+ export declare function pickLlmsPageDescription(input: LlmsDescriptionInput): {
144
+ text: string;
145
+ source: LlmsDescriptionSource;
146
+ } | null;
147
+ /** `pickLlmsPageDescription`'s text. */
148
+ export declare function resolveLlmsPageDescription(input: LlmsDescriptionInput): string | null;
149
+ /**
150
+ * The note on a page's line, from what the payload carries: its
151
+ * `llms_public_summary`, then its `description`. With `publicSummaryOnly`
152
+ * (a site that wants strict notes) only the summary counts. Single source of
153
+ * truth for the Sonor API's fallback file and site-kit's local generator, which
154
+ * each had their own copy.
155
+ */
156
+ export declare function llmsPageListNote(page: {
157
+ description?: string | null;
158
+ llms_public_summary?: string | null;
159
+ }, publicSummaryOnly?: boolean): string | undefined;
160
+ export interface LlmsListablePage {
161
+ path: string;
162
+ http_status?: number | null;
163
+ has_noindex?: boolean | null;
164
+ robots_meta?: string | null;
165
+ managed_robots_meta?: string | null;
166
+ }
167
+ /**
168
+ * Whether a live seo_pages row belongs in llms.txt. A page the last visit saw
169
+ * answer with an error (404, 5xx), a page that asks search engines to leave it
170
+ * out, and a utility page (a thank-you page, a cart, a login) are not listed:
171
+ * llms.txt is the index of what a site is, and every dead or unwanted link in
172
+ * it is one a model may follow and cite. Retired pages never get here
173
+ * (LIVE_SEO_PAGE_FILTER).
174
+ */
175
+ export declare function isLlmsListablePage(page: LlmsListablePage): boolean;
176
+ /**
177
+ * What a page is called in llms.txt: its curated title, else the title it
178
+ * serves, else its h1, each with the brand suffix off and only when it is a
179
+ * name and not an address; the home page is `homeName`; else the last segment
180
+ * of its path as a label. Never the path itself: "[/features/accounting]" was
181
+ * what a page with no title got.
182
+ */
183
+ export declare function llmsPageTitle(page: {
184
+ path: string;
185
+ managed_title?: string | null;
186
+ title?: string | null;
187
+ h1?: string | null;
188
+ }, homeName?: string | null): string;
189
+ /**
190
+ * Choose which pages survive a page cap.
191
+ *
192
+ * A round-robin across top-level sections, not a truncation: cutting an
193
+ * alphabetical list amputates its tail. On a 191-page site a cap of 50 kept 37
194
+ * `/industries/*` pages and dropped `/work`, `/pricing`, `/services`,
195
+ * `/web-design` and `/resources` entirely. Every section is represented at any
196
+ * cap, and the survivors keep their original order, because the round-robin
197
+ * decides membership, not presentation. Sections are taken most valuable first
198
+ * (the order `groupLlmsPages` lists them in, legal pages last), so a cap smaller
199
+ * than the number of sections drops the least valuable one.
200
+ *
201
+ * Single source of truth: the Sonor API caps its index with it, and site-kit's
202
+ * local generator caps with it instead of `slice(0, max)`.
203
+ */
204
+ export declare function selectLlmsPages<T extends {
205
+ path: string;
206
+ }>(pages: T[], cap: number): T[];
207
+ export interface LlmsPageGroup<T> {
208
+ /** null for the one list of a small site. */
209
+ heading: string | null;
210
+ pages: T[];
211
+ }
212
+ /**
213
+ * The page index in the order and groups an answer engine reads best: the
214
+ * home page, then the site's top-level pages by value (what it does, where,
215
+ * the proof, who, the cost, contact), then one group per section of the site
216
+ * (`/services/*` is "Services"), biggest unlisted sections last, and the legal
217
+ * pages at the very end. Nothing is dropped or renamed; a list of 12 pages or
218
+ * fewer is one ungrouped list in the same order.
219
+ *
220
+ * Single source of truth for the Sonor API's fallback file and site-kit's
221
+ * local generator.
222
+ */
223
+ export declare function groupLlmsPages<T extends {
224
+ path: string;
225
+ }>(pages: readonly T[]): Array<LlmsPageGroup<T>>;
226
+ /**
227
+ * The one-line summary under the business name: its tagline, else the first
228
+ * sentence of its description, else its name. The first sentence comes from
229
+ * `splitSentences`, not `description.split('.')[0]`, which ends "Smith &
230
+ * Sons, Inc. is a roofing company" at "Inc".
231
+ */
232
+ export declare function llmsBlockquoteSummary(business: {
233
+ name?: string | null;
234
+ tagline?: string | null;
235
+ description?: string | null;
236
+ }): string;
237
+ export interface LlmsContactLike {
238
+ phone?: string | null;
239
+ email?: string | null;
240
+ address?: string | null;
241
+ city?: string | null;
242
+ state?: string | null;
243
+ postal_code?: string | null;
244
+ country?: string | null;
245
+ hours?: unknown;
246
+ }
247
+ /**
248
+ * The bullets of the Contact Information block, none for a field that is
249
+ * empty. An address reads "1 Main St, Springfield, OH 45501", and a
250
+ * default "US" is not printed. Empty array when there is nothing to show, so a
251
+ * caller leaves the whole section out rather than print a bare heading.
252
+ */
253
+ export declare function llmsContactLines(contact: LlmsContactLike | null | undefined): string[];
254
+ /** A page or service link as an absolute URL on the site, so a crawler never resolves it against llms.txt's own address. */
255
+ export declare function llmsAbsoluteUrl(baseUrl: string | null | undefined, pathOrUrl: string): string;