@sonordev/site-kit 8.5.2 → 8.7.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 +16 -0
- package/agent-manifest.json +1 -1
- package/dist/{AnalyticsProvider-HSJMHYBA.js → AnalyticsProvider-3UIQMIRW.js} +6 -6
- package/dist/{ArticleViewTracker-XEA6ZY67.js → ArticleViewTracker-KNXP2DXL.js} +5 -5
- package/dist/{BlocksPopup-DUTOENMQ.js → BlocksPopup-DFE32GPX.js} +9 -8
- package/dist/{ChatWidget-PMVQTSEU.js → ChatWidget-L74L4YAD.js} +6 -6
- package/dist/FileField-P3PLEAON.js +237 -0
- package/dist/{FormSpotlight-RTPJPLQ3.js → FormSpotlight-DNBSBRIE.js} +2 -2
- package/dist/{FormStage-UANGD66Z.js → FormStage-I4JGLT57.js} +2 -2
- package/dist/{ManagedForm-4AACRHEY.js → ManagedForm-GJRGBM6W.js} +8 -8
- package/dist/{ManagedNewsletterForm-RWKFWZ72.js → ManagedNewsletterForm-AUZ6YB7G.js} +11 -11
- package/dist/{SignalCore-XULLPKPX.js → SignalCore-UV7K7PT4.js} +5 -5
- package/dist/SiteChat-6UWE545R.js +6 -0
- package/dist/SiteDesignReporter-ZSU76DZJ.js +13 -0
- package/dist/{SitePopups-US6VQN7X.js → SitePopups-XHWU7GNF.js} +6 -6
- package/dist/SitemapSync-ELWV6W7Y.js +9 -0
- package/dist/_client/booking-widget.js +5 -5
- package/dist/affiliates/index.js +3 -3
- package/dist/analytics/content-text.d.ts +17 -0
- package/dist/analytics/index.js +6 -6
- package/dist/{article-styles-VASFBYA3.js → article-styles-D7VCDXMG.js} +2 -1
- package/dist/article-tables-W45BE353.js +3 -0
- package/dist/articles/index.js +7 -20
- package/dist/articles/server-ui.js +29 -55
- package/dist/articles/server.js +6 -5
- package/dist/chat/index.js +9 -9
- package/dist/{chunk-3RDJKIYO.js → chunk-2WDWQI7K.js} +1 -1
- package/dist/{chunk-DLTLXYOV.js → chunk-36ZMNBXZ.js} +4 -9
- package/dist/chunk-3MJNBOUO.js +8 -0
- package/dist/{chunk-IXA7ZB6U.js → chunk-3MLWIBHX.js} +1 -1
- package/dist/{chunk-BF7TZYC3.js → chunk-52KO4LKD.js} +1 -1
- package/dist/{chunk-ZDUTXHM6.js → chunk-5WHBYQJ4.js} +3 -3
- package/dist/{chunk-5D7ZISUP.js → chunk-7DYMM6SS.js} +11 -6
- package/dist/{chunk-HNYECNCU.js → chunk-7YB3QA4P.js} +15 -7
- package/dist/{chunk-N4ZCNYWF.js → chunk-A2MDVP47.js} +1 -1
- package/dist/{chunk-E4PTMORJ.js → chunk-AL5UCWLI.js} +23 -8
- package/dist/{chunk-ANVHRT35.js → chunk-BAJUXQTH.js} +2 -2
- package/dist/{chunk-ITQND6IB.js → chunk-BJYX6SFI.js} +2 -2
- package/dist/{chunk-TYAVHRWH.js → chunk-CVADNHJ4.js} +8 -2
- package/dist/{chunk-5BRHTBPO.js → chunk-DJIGFX33.js} +1 -1
- package/dist/{chunk-ZSU7KTIE.js → chunk-DPYGC7WE.js} +2 -2
- package/dist/{chunk-XA7Q2VQA.js → chunk-EK7P62DT.js} +1 -1
- package/dist/chunk-FXR5PJDL.js +420 -0
- package/dist/{chunk-GLLFIE2R.js → chunk-G5FD7UXK.js} +2 -2
- package/dist/{chunk-OFQ3BU52.js → chunk-HM7CDWFK.js} +1 -1
- package/dist/{chunk-Q4Y3WIHE.js → chunk-HWQPCZOX.js} +1 -1
- package/dist/{chunk-6KU7STM3.js → chunk-HY7ZEU2C.js} +3 -1
- package/dist/chunk-IBHBEUZV.js +27 -0
- package/dist/{chunk-7HRYSLY2.js → chunk-IXZBOOAD.js} +4 -4
- package/dist/chunk-JESLPEST.js +14 -0
- package/dist/{chunk-BC7JMY4Z.js → chunk-JG53YJLM.js} +58 -1
- package/dist/{chunk-FVXCQC7W.js → chunk-JWCCY72G.js} +1 -1
- package/dist/{chunk-RI2N2EBI.js → chunk-JYITKUUB.js} +13 -5
- package/dist/chunk-KIYACQW3.js +1 -0
- package/dist/{chunk-YW3LNSBM.js → chunk-KLIDCNML.js} +1 -1
- package/dist/{chunk-3PT6SO65.js → chunk-L2KSHJS2.js} +5 -5
- package/dist/{chunk-27LYZVPP.js → chunk-LGWN6KCN.js} +2 -2
- package/dist/{chunk-GTVOB5RN.js → chunk-MJW6YIDJ.js} +1 -1
- package/dist/{chunk-CFSIGEUW.js → chunk-MNORERMF.js} +1 -1
- package/dist/{chunk-Y35BWW2C.js → chunk-ODOV76VW.js} +1 -1
- package/dist/{chunk-EQHGPB3J.js → chunk-OHLCE4GC.js} +2 -2
- package/dist/{chunk-OEA7U4HJ.js → chunk-P5PRR2BB.js} +1 -1
- package/dist/{chunk-JJ4JS3LD.js → chunk-PAYCK7FC.js} +1 -1
- package/dist/{chunk-DLM4FFNH.js → chunk-QTBAKOCZ.js} +1 -1
- package/dist/{chunk-2QFIYYMP.js → chunk-RHT7OZNF.js} +1 -1
- package/dist/{chunk-UGZ3B5AK.js → chunk-RWWSOYJO.js} +9 -9
- package/dist/{chunk-T6U6J3QK.js → chunk-T6TWPKBL.js} +1 -1
- package/dist/{chunk-5SQK3D53.js → chunk-TYSIAWC3.js} +1 -0
- package/dist/{chunk-24NM64IV.js → chunk-U4OQJQK6.js} +3 -3
- package/dist/{chunk-GDQKDL3E.js → chunk-UYOE3HA4.js} +2 -2
- package/dist/chunk-VEJD5EJ2.js +344 -0
- package/dist/{chunk-LNSF6BJC.js → chunk-VPZJ7ZTL.js} +2 -2
- package/dist/chunk-XAZC2TUD.js +1119 -0
- package/dist/{chunk-A7RZQI6H.js → chunk-XC2TWYDV.js} +3 -3
- package/dist/{chunk-URJN75ZG.js → chunk-XIDWNXZN.js} +3 -3
- package/dist/chunk-XLAHRN4B.js +1 -0
- package/dist/{chunk-URZTBPZO.js → chunk-XZFOQ2V4.js} +113 -93
- package/dist/{chunk-I4LMZNBN.js → chunk-YYPYLYZM.js} +1 -1
- package/dist/client/index.js +4 -4
- package/dist/contracts/broadcast-layouts.d.ts +133 -0
- package/dist/contracts/business-claims.d.ts +456 -0
- package/dist/contracts/campaign-normalize.d.ts +2 -0
- package/dist/contracts/entries.d.ts +1 -1
- package/dist/contracts/funnels.d.ts +576 -0
- package/dist/contracts/inbox.d.ts +42 -0
- package/dist/contracts/llms.d.ts +200 -0
- package/dist/contracts/outreach.d.ts +267 -0
- package/dist/contracts/page-titles.d.ts +140 -0
- package/dist/contracts/portfolio.d.ts +170 -0
- package/dist/contracts/public-url-host.d.ts +10 -0
- package/dist/contracts/publication-branding.d.ts +151 -0
- package/dist/contracts/publication-charter.d.ts +14 -0
- package/dist/contracts/site-cache.d.ts +2 -0
- package/dist/fleet/index.js +6 -6
- package/dist/forms/FileField.d.ts +3 -1
- package/dist/forms/PrivateFileField.d.ts +3 -0
- package/dist/forms/index.js +11 -11
- package/dist/forms/private-file-rules.d.ts +8 -0
- package/dist/forms/server.js +5 -5
- package/dist/forms/static.js +3 -3
- package/dist/forms/styles.css +17 -1
- package/dist/forms/types.d.ts +4 -0
- package/dist/images/index.js +4 -4
- package/dist/index.js +1 -1
- package/dist/landing/contract.d.ts +2 -13
- package/dist/landing/contract.js +2 -0
- package/dist/landing/index.js +4 -2
- package/dist/landing/managed-server.d.ts +21 -0
- package/dist/landing/managed.d.ts +11 -0
- package/dist/landing/metadata.js +2 -0
- package/dist/landing/server.d.ts +5 -0
- package/dist/landing/server.js +19 -2
- package/dist/layout/client.js +9 -9
- package/dist/layout/index.js +10 -10
- package/dist/llms/contract.js +2 -1
- package/dist/llms/index.js +9 -7
- package/dist/llms/merge.d.ts +29 -0
- package/dist/maps/index.js +3 -3
- package/dist/mcp/sonor.js +12 -11
- package/dist/portfolio/contract.js +1 -1
- package/dist/press-kit/articles/article-body.d.ts +13 -0
- package/dist/press-kit/articles/article-heading-ids.d.ts +7 -0
- package/dist/press-kit/articles/article-headings.d.ts +61 -0
- package/dist/press-kit/articles/html-tags.d.ts +46 -0
- package/dist/press-kit/articles/types.d.ts +2 -0
- package/dist/revalidate/index.js +3 -3
- package/dist/runtime/index.js +3 -3
- package/dist/seo/client.js +6 -6
- package/dist/seo/index.js +14 -13
- package/dist/seo/llms/contract.js +2 -1
- package/dist/seo/llms.js +9 -7
- package/dist/seo/meta/contract.js +2 -1
- package/dist/seo/register-sitemap-cli.js +1 -1
- package/dist/seo/server.js +5 -4
- package/dist/seo/sitemap.js +7 -5
- package/dist/seo-meta/contract.js +2 -1
- package/dist/server/index.js +3 -3
- package/dist/{server-api-EGOKC7Q3.js → server-api-XIKDWSNO.js} +2 -2
- package/dist/shared/build-entries.d.ts +3 -1
- package/dist/shared/markdown.d.ts +7 -6
- package/dist/shared/version.d.ts +1 -1
- package/dist/signal/index.js +2 -2
- package/dist/sitemap/index.js +7 -5
- package/dist/slots/index.js +2 -2
- package/dist/sync/index.js +5 -5
- package/dist/website/contract.js +2 -1
- package/dist/website/images.js +4 -4
- package/dist/website/index.js +10 -9
- package/dist/website/landing/server.js +19 -2
- package/dist/website/landing.js +4 -2
- package/dist/website/popups.js +9 -8
- package/dist/website/slots.js +2 -2
- package/dist/{writeLLMsTxt-BAK5XMRR.js → writeLLMsTxt-QN2HIVT4.js} +6 -4
- package/docs.json +5 -0
- package/package.json +1 -1
- package/src/analytics/README.md +1 -1
- package/src/forms/README.md +22 -0
- package/src/landing/README.md +64 -0
- package/src/llms/README.md +52 -20
- package/dist/FileField-F7BG4XAZ.js +0 -83
- package/dist/SiteChat-P4CRBRJZ.js +0 -6
- package/dist/SiteDesignReporter-QDVP4WMK.js +0 -12
- package/dist/SitemapSync-5FBRDGHG.js +0 -9
- package/dist/article-tables-6XHVULPU.js +0 -2
- package/dist/chunk-EKBEOXTH.js +0 -114
- package/dist/chunk-NZ4RXN3G.js +0 -145
- package/dist/chunk-OSYCOC3K.js +0 -21
- /package/dist/{chunk-7ZWO5HDQ.js → chunk-BTHEQDN4.js} +0 -0
- /package/dist/{chunk-BQR4IPBW.js → chunk-ISRBSTB7.js} +0 -0
- /package/dist/{chunk-IHG36STL.js → chunk-XHYE24HU.js} +0 -0
package/dist/contracts/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;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/** Bumped when a stored shape here (SegmentGrant, the segment key) changes incompatibly. */
|
|
2
|
+
export declare const OUTREACH_CONTRACT_VERSION = 1;
|
|
3
|
+
/** The badge Signal's outreach writer files and acts under. */
|
|
4
|
+
export declare const OUTREACH_WRITER_BADGE = "signal.outreach.writer";
|
|
5
|
+
/** The ledger kind segments belong to, and its key under settings.autonomy.segments. */
|
|
6
|
+
export declare const OUTREACH_EMAIL_KIND = "outreach_email";
|
|
7
|
+
/**
|
|
8
|
+
* Signal's one graduation bar: a track record earns sending without a click
|
|
9
|
+
* per item once at least minMeasured are measured, at least minWinRate of
|
|
10
|
+
* them won, and none degraded without being undone. Single source of truth
|
|
11
|
+
* for both APIs: Signal measures a kind or a segment against it, and an
|
|
12
|
+
* earned segment grant needs minMeasured receipts wherever
|
|
13
|
+
* grantEvidenceRefusal checks one.
|
|
14
|
+
*/
|
|
15
|
+
export declare const GRADUATION_BAR: {
|
|
16
|
+
readonly minMeasured: 5;
|
|
17
|
+
readonly minWinRate: 0.7;
|
|
18
|
+
readonly maxUnrevertedDegradations: 0;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* A segment's standing, one vocabulary everywhere:
|
|
22
|
+
* - ungranted: nobody turned it on (its next email is a first email);
|
|
23
|
+
* - granted: a person turned it on and it's sending without a per-email click;
|
|
24
|
+
* - paused: it stopped itself (a quality loss or a list signal) until a person resumes it;
|
|
25
|
+
* - lapsed: the pitch changed since it was turned on, so its next email is a first email again;
|
|
26
|
+
* - revoked: a person stopped it.
|
|
27
|
+
* "Awaiting first approval" is a drip check's answer, never a stored state.
|
|
28
|
+
*/
|
|
29
|
+
export declare const SEGMENT_STATES: readonly ['ungranted', 'granted', 'paused', 'lapsed', 'revoked'];
|
|
30
|
+
export type SegmentState = (typeof SEGMENT_STATES)[number];
|
|
31
|
+
export type OutreachSegmentSource = 'drip' | 'mcp';
|
|
32
|
+
/** The vertical of a lead with no industry. Never grantable. */
|
|
33
|
+
export declare const UNSPECIFIED_INDUSTRY = "unspecified";
|
|
34
|
+
/**
|
|
35
|
+
* A prospect's industry as written, in the one order every caller uses: the
|
|
36
|
+
* target company's vertical, then its industry, then the lead's
|
|
37
|
+
* company_industry. Null when none is set.
|
|
38
|
+
*/
|
|
39
|
+
export declare function prospectIndustry(prospect: {
|
|
40
|
+
vertical?: unknown;
|
|
41
|
+
industry?: unknown;
|
|
42
|
+
company_industry?: unknown;
|
|
43
|
+
} | null | undefined): string | null;
|
|
44
|
+
/**
|
|
45
|
+
* An industry as it appears in a segment key: NFKC, accents folded, lowercase,
|
|
46
|
+
* "&" read as "and", every run of anything that isn't a-z or 0-9 a single
|
|
47
|
+
* "-", trimmed, at most 48 characters. Empty is 'unspecified'. There's no
|
|
48
|
+
* synonym table: "Roofing" and "Roofers" are two segments.
|
|
49
|
+
*/
|
|
50
|
+
export declare function normalizeIndustry(value: unknown): string;
|
|
51
|
+
/** The fields of an outreach_narratives row that are its pitch. Its name, its switch and its forbidden phrases aren't. */
|
|
52
|
+
export declare const NARRATIVE_PITCH_FIELDS: readonly ['positioning_statement', 'target_icp_description', 'tone_profile', 'hook_types', 'example_angles', 'value_prop_bullets'];
|
|
53
|
+
/** A narrative row, or any object carrying its pitch fields; every other column (name, enabled, forbidden_phrases, ...) is read past. */
|
|
54
|
+
export type NarrativePitch = {
|
|
55
|
+
[K in (typeof NARRATIVE_PITCH_FIELDS)[number]]?: unknown;
|
|
56
|
+
} & Record<string, unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* The narrative's pitch, fingerprinted: FNV-1a of canonical JSON of
|
|
59
|
+
* NARRATIVE_PITCH_FIELDS only. Rewriting the pitch changes it, and that
|
|
60
|
+
* lapses a segment's grant. Renaming the narrative, switching it on or off,
|
|
61
|
+
* or tightening its forbidden phrases or avoid_tone doesn't: those take
|
|
62
|
+
* effect at once through the checker and the prompt.
|
|
63
|
+
*/
|
|
64
|
+
export declare function narrativeFingerprint(narrative: NarrativePitch | null | undefined): string;
|
|
65
|
+
/** target_ref.segment on an outreach_email row: stamped once, at filing. */
|
|
66
|
+
export interface OutreachSegment {
|
|
67
|
+
key: string;
|
|
68
|
+
source: OutreachSegmentSource;
|
|
69
|
+
narrative_id: string | null;
|
|
70
|
+
mailbox_id: string | null;
|
|
71
|
+
vertical: string;
|
|
72
|
+
label: string;
|
|
73
|
+
angle_fp: string | null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A segment: which pitch, from which mailbox, to which kind of business.
|
|
77
|
+
* - drip: `drip:<narrative uuid>@<mailbox uuid>|<vertical>`. Null when either id isn't a UUID.
|
|
78
|
+
* - mcp: `mcp:-|<vertical>`. For metrics only; never granted.
|
|
79
|
+
*/
|
|
80
|
+
export declare function outreachSegment(input: {
|
|
81
|
+
source: OutreachSegmentSource;
|
|
82
|
+
narrativeId?: string | null;
|
|
83
|
+
mailboxId?: string | null;
|
|
84
|
+
industry?: unknown;
|
|
85
|
+
label?: string | null;
|
|
86
|
+
angleFp?: string | null;
|
|
87
|
+
}): OutreachSegment | null;
|
|
88
|
+
/** A segment's label: "<narrative name> to <industry as written>, from <mailbox address>". */
|
|
89
|
+
export declare function outreachSegmentLabel(input: {
|
|
90
|
+
narrativeName?: unknown;
|
|
91
|
+
industry?: unknown;
|
|
92
|
+
mailboxAddress?: unknown;
|
|
93
|
+
}): string;
|
|
94
|
+
export type ParsedOutreachSegmentKey = {
|
|
95
|
+
source: 'drip';
|
|
96
|
+
narrative_id: string;
|
|
97
|
+
mailbox_id: string;
|
|
98
|
+
vertical: string;
|
|
99
|
+
} | {
|
|
100
|
+
source: 'mcp';
|
|
101
|
+
vertical: string;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* A segment key, parsed strictly, or null. Nothing else is ever used as a key
|
|
105
|
+
* in projects.settings, so a caller-chosen key ("__proto__", a long string)
|
|
106
|
+
* is refused before any read or write.
|
|
107
|
+
*/
|
|
108
|
+
export declare function parseOutreachSegmentKey(key: unknown): ParsedOutreachSegmentKey | null;
|
|
109
|
+
/**
|
|
110
|
+
* Whether a segment can ever be turned on: a drip segment with a known
|
|
111
|
+
* industry. An mcp segment, or a lead with no industry, always waits for a
|
|
112
|
+
* person. (A row whose recipient Sonor already knows waits too; that's the
|
|
113
|
+
* row's relationship, checked separately.)
|
|
114
|
+
*/
|
|
115
|
+
export declare function isGrantableSegmentKey(key: unknown): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* A person's standing approval of one segment, at
|
|
118
|
+
* projects.settings.autonomy.segments.outreach_email[key]. Written only by
|
|
119
|
+
* Signal's AutonomyPolicyService.setSegmentGrant, refused on every generic
|
|
120
|
+
* settings writer, and only ever trusted after grantEvidenceRefusal passes.
|
|
121
|
+
*/
|
|
122
|
+
export interface SegmentGrant {
|
|
123
|
+
state: 'granted' | 'revoked';
|
|
124
|
+
/** first_email: a person approved the segment's first email unedited with "Send the rest like this". earned: they accepted Signal's offer once the bar was met. */
|
|
125
|
+
source: 'first_email' | 'earned';
|
|
126
|
+
/** contacts.id of the person who turned it on. */
|
|
127
|
+
by: string;
|
|
128
|
+
/** Where they did it. Never MCP. */
|
|
129
|
+
via: 'dashboard' | 'drafts';
|
|
130
|
+
at: string;
|
|
131
|
+
/** first_email: the ledger id of that email. earned: the offer's dedup key. */
|
|
132
|
+
ref: string;
|
|
133
|
+
/** earned: the ledger ids of the wins that earned it. */
|
|
134
|
+
receipts?: string[];
|
|
135
|
+
/** The mailbox it sends from. A grant never covers another mailbox. */
|
|
136
|
+
mailbox_id: string;
|
|
137
|
+
label: string;
|
|
138
|
+
/** narrativeFingerprint when it was turned on. A rewritten pitch lapses it. */
|
|
139
|
+
angle_fp: string;
|
|
140
|
+
/** Stopped itself. Cleared only by a person's resume. */
|
|
141
|
+
paused?: {
|
|
142
|
+
at: string;
|
|
143
|
+
reason: string;
|
|
144
|
+
loss_ref: string | null;
|
|
145
|
+
};
|
|
146
|
+
resumed?: {
|
|
147
|
+
by: string;
|
|
148
|
+
at: string;
|
|
149
|
+
};
|
|
150
|
+
revoked?: {
|
|
151
|
+
by: string;
|
|
152
|
+
at: string;
|
|
153
|
+
reason: string;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Every well-formed grant, by segment key. Pass projects.settings or its
|
|
158
|
+
* autonomy document. A key that doesn't parse strictly, or a grant that isn't
|
|
159
|
+
* the SegmentGrant shape, is left out: an unreadable grant counts as no grant.
|
|
160
|
+
*/
|
|
161
|
+
export declare function readSegmentGrants(source: unknown): Record<string, SegmentGrant>;
|
|
162
|
+
/** The grant for a segment when it's live (granted, not paused, on a grantable key), else null. Evidence is checked separately. */
|
|
163
|
+
export declare function activeSegmentGrant(source: unknown, key: string): SegmentGrant | null;
|
|
164
|
+
export type SegmentGrantCoverage = {
|
|
165
|
+
covers: true;
|
|
166
|
+
} | {
|
|
167
|
+
covers: false;
|
|
168
|
+
code: 'not_granted' | 'revoked' | 'paused' | 'other_person' | 'granted_after' | 'angle_changed' | 'other_mailbox';
|
|
169
|
+
reason: string;
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Whether a live grant covers one row: refused when there's no grant, it was
|
|
173
|
+
* revoked or paused, the row names another approver, the row was filed
|
|
174
|
+
* before the grant, the pitch changed since (a different fingerprint), or the
|
|
175
|
+
* row sends from another mailbox.
|
|
176
|
+
*/
|
|
177
|
+
export declare function segmentGrantCovers(grant: SegmentGrant | null | undefined, row: {
|
|
178
|
+
by?: string | null;
|
|
179
|
+
createdAt: string | null | undefined;
|
|
180
|
+
angleFp: string | null | undefined;
|
|
181
|
+
mailboxId: string | null | undefined;
|
|
182
|
+
}): SegmentGrantCoverage;
|
|
183
|
+
/** The columns of a signal_proposals row grantEvidenceRefusal reads. */
|
|
184
|
+
export interface GrantEvidenceRow {
|
|
185
|
+
id: string;
|
|
186
|
+
kind: string;
|
|
187
|
+
status: string;
|
|
188
|
+
project_id?: string | null;
|
|
189
|
+
created_by?: string | null;
|
|
190
|
+
reviewed_by?: string | null;
|
|
191
|
+
reviewed_at?: string | null;
|
|
192
|
+
target_ref?: unknown;
|
|
193
|
+
payload?: unknown;
|
|
194
|
+
}
|
|
195
|
+
export interface GrantEvidenceScope {
|
|
196
|
+
/** The segment key the grant is stored under. */
|
|
197
|
+
key: string;
|
|
198
|
+
/** GRADUATION_BAR.minMeasured: how many receipts an earned grant needs. */
|
|
199
|
+
minMeasured: number;
|
|
200
|
+
/** The project the grant is stored on. Rows from another project never count. */
|
|
201
|
+
projectId?: string | null;
|
|
202
|
+
/** contacts.id of reviewers known to be people (not agents). When given, every reviewer must be in it. */
|
|
203
|
+
people?: ReadonlySet<string> | ReadonlyArray<string> | null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Why a grant can't be trusted, or null when the ledger rows it cites stand
|
|
207
|
+
* behind it. A grant that fails counts as no grant, so a grant forged into
|
|
208
|
+
* settings never sends anything.
|
|
209
|
+
*
|
|
210
|
+
* - first_email: grant.ref is an applied outreach_email the writer filed in
|
|
211
|
+
* this project and segment, from the grant's mailbox, approved by grant.by
|
|
212
|
+
* at or before grant.at, in Sonor (via dashboard or drafts, never MCP),
|
|
213
|
+
* unedited, and read back verified.
|
|
214
|
+
* - earned: at least scope.minMeasured distinct receipts, each an applied
|
|
215
|
+
* writer row in this segment and mailbox, for the same pitch, approved by a
|
|
216
|
+
* person in Sonor at or before grant.at, unedited and read back verified.
|
|
217
|
+
*/
|
|
218
|
+
export declare function grantEvidenceRefusal(grant: SegmentGrant | null | undefined, refRows: ReadonlyArray<GrantEvidenceRow> | null | undefined, scope: GrantEvidenceScope): string | null;
|
|
219
|
+
/** The live roles of the person asking, read by the caller. */
|
|
220
|
+
export interface SegmentGranterRoles {
|
|
221
|
+
contact_type?: string | null;
|
|
222
|
+
is_platform_admin?: boolean | null;
|
|
223
|
+
/** Their role in the project's org. */
|
|
224
|
+
org_role?: string | null;
|
|
225
|
+
/** Their role on the project itself. */
|
|
226
|
+
project_role?: string | null;
|
|
227
|
+
/** Their role in the agency that manages the project's org. */
|
|
228
|
+
managing_org_role?: string | null;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Whether this person may turn a segment on (or resume it): never an agent
|
|
232
|
+
* contact; otherwise a platform admin, or an owner or admin of the project,
|
|
233
|
+
* its org, or the agency that manages that org.
|
|
234
|
+
*/
|
|
235
|
+
export declare function segmentGranterEligible(roles: SegmentGranterRoles | null | undefined): boolean;
|
|
236
|
+
/**
|
|
237
|
+
* Domains anyone can sign up for. A matching address says nothing about the
|
|
238
|
+
* company someone works for, so a relationship check never matches on one.
|
|
239
|
+
*/
|
|
240
|
+
export declare const PUBLIC_MAILBOX_DOMAINS: ReadonlyArray<string>;
|
|
241
|
+
/** Whether an address or a domain is a public mailbox provider's. Case and a leading "mailto:" or "@" don't matter. */
|
|
242
|
+
export declare function isPublicMailboxDomain(value: unknown): boolean;
|
|
243
|
+
/**
|
|
244
|
+
* - live: still waiting on a person or a held send. Leave the lead reserved.
|
|
245
|
+
* - sent: the email went out. The lead moves on.
|
|
246
|
+
* - retire: a person said it shouldn't go to this person. Drop the lead.
|
|
247
|
+
* - declined: a person turned this email down for another reason. The lead goes back in line.
|
|
248
|
+
* - refile: nothing was decided about the lead (wrong timing, expired,
|
|
249
|
+
* superseded, the kind switched off, a send that never went out). It may be
|
|
250
|
+
* filed again after its cooldown.
|
|
251
|
+
*/
|
|
252
|
+
export type DripRowDisposition = 'live' | 'sent' | 'retire' | 'declined' | 'refile';
|
|
253
|
+
/** The columns of a signal_proposals row dripRowDisposition reads. */
|
|
254
|
+
export interface DripLedgerRow {
|
|
255
|
+
status: string;
|
|
256
|
+
review_reason?: string | null;
|
|
257
|
+
reviewed_by?: string | null;
|
|
258
|
+
autonomy_decision?: unknown;
|
|
259
|
+
payload?: unknown;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* What a drip's ledger row means for its lead. Signal's filing dedup and the
|
|
263
|
+
* Sonor API's reservation release both read it, so "decided" means one thing.
|
|
264
|
+
* A status it doesn't know reads as live: a lead stays reserved rather than
|
|
265
|
+
* being written to twice.
|
|
266
|
+
*/
|
|
267
|
+
export declare function dripRowDisposition(row: DripLedgerRow | null | undefined): DripRowDisposition;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-titles contract: what a page is called when a list, a sitemap or a
|
|
3
|
+
* case study needs its own name and not its whole <title>. The Sonor APIs and
|
|
4
|
+
* the agency site kit import it as `@sonordev/contracts/page-titles`.
|
|
5
|
+
*
|
|
6
|
+
* Why it exists: three places each turned a stored title or a URL slug into a
|
|
7
|
+
* label with their own logic, and they disagreed. "ac-replacement" became "Ac
|
|
8
|
+
* Replacement", "faq" became "Faq", "smith-and-smith" became "Smith And
|
|
9
|
+
* Smith", and a title with a stray second pipe shipped as "Loans | | Brand".
|
|
10
|
+
* There's one rule now:
|
|
11
|
+
*
|
|
12
|
+
* - `titleHead` is the page's own name: the title without its trailing
|
|
13
|
+
* " | Brand" segment. Prefer it. A page's title is the label it was
|
|
14
|
+
* written with, so it beats anything guessed from a URL.
|
|
15
|
+
* - `slugLabel` is for the rare case a label has to come from a slug, a file
|
|
16
|
+
* name or a path, because the page has no title at all.
|
|
17
|
+
* - `isRoutePlaceholder` says whether a value is a framework route
|
|
18
|
+
* placeholder (":slug", "[slug]", "[...rest]") and not a page's name.
|
|
19
|
+
* - `isPageAddress` says whether a value is a page's address (a path, a
|
|
20
|
+
* URL, a route pattern, or the page's own path without its slashes) and
|
|
21
|
+
* not its name. It's the one "is this title really a path" rule, so the
|
|
22
|
+
* writer of a page list and the reader that prints it can't disagree.
|
|
23
|
+
* - `isPageName` says whether a stored title is a name: something is left
|
|
24
|
+
* once the pipes are set aside, and no piece between them is an address.
|
|
25
|
+
* It's the one rule for a title that gets printed whole, so the writer
|
|
26
|
+
* that stores a name and the reader that prints it can't disagree about
|
|
27
|
+
* a path, a URL or a placeholder behind a pipe.
|
|
28
|
+
*
|
|
29
|
+
* Pure string logic: no I/O, no Node built-ins. The brand split is
|
|
30
|
+
* seo-meta's `stripTitleSuffix`, imported, never copied: only the LAST
|
|
31
|
+
* " | " segment of a title is its brand.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* The page's own name: the title without its trailing " | Brand" segment.
|
|
35
|
+
*
|
|
36
|
+
* Whitespace is collapsed first. Stray pipes are repaired before the brand is
|
|
37
|
+
* cut, so "Loans | | Brand" reads as "Loans | Brand" (a run of pipes with
|
|
38
|
+
* only whitespace between them is one separator) and a leading or trailing
|
|
39
|
+
* pipe is dropped ("Pricing |" is "Pricing"). Then the last " | " segment is
|
|
40
|
+
* the brand and goes; anything before it stays, pipes and all:
|
|
41
|
+
* "1 Bedroom | 1BR 1BA | Example Apartments" is "1 Bedroom | 1BR 1BA".
|
|
42
|
+
*
|
|
43
|
+
* Apply it once to a stored title. A title with two or more separators
|
|
44
|
+
* loses one segment per call, so its own output isn't a fixed point.
|
|
45
|
+
*
|
|
46
|
+
* A title with no separator comes back as written (trimmed): this can't tell
|
|
47
|
+
* a brand from a name, so a title that's only a brand stays that brand.
|
|
48
|
+
* Empty, whitespace-only, non-string, null and undefined input give "".
|
|
49
|
+
*/
|
|
50
|
+
export declare function titleHead(title: string | null | undefined): string;
|
|
51
|
+
/**
|
|
52
|
+
* A readable label for a slug, a file name or a path: "ac-replacement" is
|
|
53
|
+
* "AC Replacement", "/services/hvac-repair/" is "HVAC Repair".
|
|
54
|
+
*
|
|
55
|
+
* Reach for it only when the page has no title (`titleHead` is the page's own
|
|
56
|
+
* name). It reads the last non-empty path segment of the input, after
|
|
57
|
+
* dropping a "https://host" origin, a query or a hash, and decodes %-escapes
|
|
58
|
+
* without ever throwing. A trailing .html, .htm, .php or .aspx is dropped. The
|
|
59
|
+
* words split on "-", "_", "+" and whitespace.
|
|
60
|
+
*
|
|
61
|
+
* Each word is cased by the first rule that fits:
|
|
62
|
+
* - an acronym is upper case ("ac", "ai", "cms", "faq", "hvac", "seo",
|
|
63
|
+
* "llc", "mls", ...), and "faqs" is "FAQs";
|
|
64
|
+
* - a two-letter US state or DC code is upper case ("ky" is "KY"), apart
|
|
65
|
+
* from the codes that are ordinary words as often as not ("in", "or",
|
|
66
|
+
* "me", "hi", "id", "la", "ma", "pa", "al", "de", "co"). "mt" goes by its
|
|
67
|
+
* position: last, it's Montana ("billings-mt" is "Billings MT"); with a
|
|
68
|
+
* word after it, it's Mount ("mt-adams" is "Mt Adams"). "us" isn't a
|
|
69
|
+
* state: "about-us" is "About Us";
|
|
70
|
+
* - a word with a digit keeps its case and only gains a leading capital
|
|
71
|
+
* when it starts with a letter ("c1" is "C1", "075ct" stays);
|
|
72
|
+
* - a small word ("a", "an", "and", "at", "by", "for", "in", "of", "on",
|
|
73
|
+
* "or", "the", "to", "vs", "with") is lower case, unless it's first;
|
|
74
|
+
* - anything else is capitalized with the rest lower case.
|
|
75
|
+
*
|
|
76
|
+
* Nothing left to label ("", "/", "?x=1") gives "". The home page is the
|
|
77
|
+
* caller's to name. A label isn't a title: "index" isn't special-cased,
|
|
78
|
+
* and a brand's own capitalization ("DeWalt") isn't recoverable from a slug.
|
|
79
|
+
*/
|
|
80
|
+
export declare function slugLabel(input: string): string;
|
|
81
|
+
/**
|
|
82
|
+
* True when the value, or any "/"-separated segment of it, is a framework
|
|
83
|
+
* route placeholder: a page's pattern leaked into its name ("/floor-plans/:slug",
|
|
84
|
+
* ":Slug", "[...rest]"). A placeholder is one token with no spaces, so
|
|
85
|
+
* ordinary text that contains a colon or a bracket isn't one ("Update: new
|
|
86
|
+
* hours", "FAQ / [draft] notes"). Nullish, empty and non-string values are
|
|
87
|
+
* false.
|
|
88
|
+
*/
|
|
89
|
+
export declare function isRoutePlaceholder(value: string | null | undefined): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Whether `value` is a page's address and not its name: what a page's title
|
|
92
|
+
* becomes when nothing better was stored (the old sitemap writers saved the
|
|
93
|
+
* path there), or a pattern that leaked out of the router. True for:
|
|
94
|
+
*
|
|
95
|
+
* - a path, or anything starting with a slash ("/services/ac-repair",
|
|
96
|
+
* "//example.com/services");
|
|
97
|
+
* - an absolute URL ("https://example.com/services");
|
|
98
|
+
* - a host and a path with no scheme ("example.com/services");
|
|
99
|
+
* - a route placeholder, as `isRoutePlaceholder` reads it ("/floor-plans/:slug",
|
|
100
|
+
* ":Slug", "[slug]");
|
|
101
|
+
* - the page's own path written without its slashes. Given `path`, the
|
|
102
|
+
* comparison drops leading and trailing slashes from both sides, so for
|
|
103
|
+
* "/services/ac-repair" the values "services/ac-repair" and
|
|
104
|
+
* "services/ac-repair/" are the address, and for "/faq" the value "faq" is.
|
|
105
|
+
* It compares the whole path and it's case-sensitive: "ac-repair" isn't
|
|
106
|
+
* the address of "/services/ac-repair", and "Pricing" for "/pricing" is a
|
|
107
|
+
* name where "pricing" is the path.
|
|
108
|
+
*
|
|
109
|
+
* The value is trimmed first. Empty, whitespace-only, nullish and non-string
|
|
110
|
+
* values give false: they aren't an address, they're nothing, and the caller
|
|
111
|
+
* decides what an empty title means. `path` is optional; without it only the
|
|
112
|
+
* checks that don't need the page's own path run.
|
|
113
|
+
*
|
|
114
|
+
* It judges one string. A title with a " | Brand" tail is `titleHead`'s to
|
|
115
|
+
* cut, and the caller asks about the head: `isPageAddress(titleHead(title), path)`.
|
|
116
|
+
* Pure string logic, linear in the length of the input.
|
|
117
|
+
*/
|
|
118
|
+
export declare function isPageAddress(value: string | null | undefined, path?: string | null): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Whether a stored title is a real name for the page at `path`: something is
|
|
121
|
+
* left once the pipes are set aside, and none of its pieces is a page's
|
|
122
|
+
* address (`isPageAddress`, given the page's path). It's the one rule for a
|
|
123
|
+
* title that gets printed whole, so whoever stores a name and whoever prints
|
|
124
|
+
* it can't disagree.
|
|
125
|
+
*
|
|
126
|
+
* Every piece is judged, the first and the last included, because a title
|
|
127
|
+
* with a pipe in it prints with its pipes: ":slug | Example Apartments" (a
|
|
128
|
+
* placeholder in front of a brand tail) and "Floor Plans | /floor-plans/x" (a
|
|
129
|
+
* path behind the name) are both refused, and so is the unspaced
|
|
130
|
+
* "Floor Plans|/floor-plans/x". A name with a pipe in it stays a name while
|
|
131
|
+
* every piece is text ("1 Bedroom | 1BR 1BA"). A title that's only pipes has
|
|
132
|
+
* no piece and isn't a name. Empty, whitespace-only, nullish and non-string
|
|
133
|
+
* values aren't names either.
|
|
134
|
+
*
|
|
135
|
+
* It judges the title it is given, brand tail and all. A writer cuts the tail
|
|
136
|
+
* first and asks about what it stores: `isPageName(titleHead(title), path)`.
|
|
137
|
+
* `path` is optional, as it is for `isPageAddress`. Pure string logic, linear
|
|
138
|
+
* in the length of the input.
|
|
139
|
+
*/
|
|
140
|
+
export declare function isPageName(title: string | null | undefined, path?: string | null): boolean;
|