@sonordev/site-kit 7.1.1 → 7.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.
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +68 -3426
- package/README.md +9 -8
- package/agent-manifest.json +12 -4
- package/dist/{AnalyticsProvider-3MI5RCTL.js → AnalyticsProvider-SLFFBRPE.js} +4 -4
- package/dist/{ArticleViewTracker-CMBSJR3N.js → ArticleViewTracker-TM5AMGN2.js} +3 -3
- package/dist/{BlocksPopup-X3OAAK2L.js → BlocksPopup-YJNJIVPO.js} +5 -5
- package/dist/ChatWidget-XKOSH66S.js +17 -0
- package/dist/{FileField-QRSEGO6D.js → FileField-CI4UL5SH.js} +3 -3
- package/dist/{FormSpotlight-XKV7VTN4.js → FormSpotlight-F2AAEGBN.js} +20 -6
- package/dist/{FormStage-C3YGYAUZ.js → FormStage-BI5R5CNC.js} +27 -7
- package/dist/ManagedForm-G6COGHXL.js +16 -0
- package/dist/{ManagedNewsletterForm-LPIKGJF4.js → ManagedNewsletterForm-BO3WCGXX.js} +7 -5
- package/dist/{SignalCore-ICXXZBYI.js → SignalCore-ZA6WHKBR.js} +3 -3
- package/dist/SiteChat-YJADDTKG.js +5 -0
- package/dist/{SiteDesignReporter-A7MS2CIP.js → SiteDesignReporter-5YAW36AQ.js} +5 -5
- package/dist/SitePopups-ST5PMKZE.js +10 -0
- package/dist/SitemapSync-RJTEBINX.js +8 -0
- package/dist/SitemapSync.d.ts +1 -1
- package/dist/_client/booking-widget.js +5 -5
- package/dist/_client/testimonial-section.d.ts +1 -1
- package/dist/affiliates/api.d.ts +1 -1
- package/dist/affiliates/index.js +3 -3
- package/dist/analytics/AnalyticsProvider.d.ts +1 -1
- package/dist/analytics/index.js +4 -4
- package/dist/analytics/send-gate.d.ts +13 -26
- package/dist/articles/Article.d.ts +1 -1
- package/dist/articles/ArticleList.d.ts +1 -1
- package/dist/articles/ClusterLandingPage.d.ts +2 -2
- package/dist/articles/PublicationLayout.d.ts +1 -1
- package/dist/articles/PublicationSidebar.d.ts +1 -1
- package/dist/articles/RelatedPosts.d.ts +1 -1
- package/dist/articles/author-schema.d.ts +1 -1
- package/dist/articles/excerpt.d.ts +3 -4
- package/dist/articles/index.d.ts +2 -2
- package/dist/articles/index.js +2 -2
- package/dist/articles/news-sitemap.d.ts +1 -1
- package/dist/articles/processArticleHtml.d.ts +1 -1
- package/dist/articles/server-core.d.ts +4 -4
- package/dist/articles/server-ui.js +3 -2
- package/dist/articles/server.d.ts +4 -4
- package/dist/articles/server.js +2 -1
- package/dist/articles/types.d.ts +1 -1
- package/dist/{engage → chat}/ChatWidget.d.ts +8 -7
- package/dist/{engage → chat}/EchoUiActions.d.ts +4 -4
- package/dist/chat/SiteChat.d.ts +25 -0
- package/dist/{engage → chat}/brand-color.d.ts +2 -2
- package/dist/{engage → chat}/chat-messages.d.ts +1 -1
- package/dist/{engage → chat}/echo-config.d.ts +1 -1
- package/dist/chat/index.d.ts +10 -6
- package/dist/chat/index.js +14 -8
- package/dist/{engage → chat}/launcher-placement.d.ts +1 -7
- package/dist/{engage → chat}/socket-loader.d.ts +2 -2
- package/dist/chat/types.d.ts +136 -0
- package/dist/chunk-27FISYK4.js +4 -0
- package/dist/{chunk-QPEEKFCA.js → chunk-4NTBQNHA.js} +249 -80
- package/dist/{chunk-RK7GF7PQ.js → chunk-5V2V5C3Y.js} +2 -4
- package/dist/{chunk-ICFRH2ED.js → chunk-5YCEMV2L.js} +1 -1
- package/dist/{chunk-V4HUSHZO.js → chunk-64DKGVEI.js} +41 -7
- package/dist/{chunk-GLHQ3LRO.js → chunk-6U3VLV2C.js} +1 -1
- package/dist/{chunk-ZETJTCMV.js → chunk-BC4Y3S3D.js} +1 -1
- package/dist/{chunk-JSZN6LDZ.js → chunk-BF7TZYC3.js} +38 -31
- package/dist/{chunk-EFPS56JA.js → chunk-BKY2ZNM7.js} +1 -1
- package/dist/{chunk-AQSNPZY4.js → chunk-C2FZBUSS.js} +80 -1
- package/dist/{chunk-H42PZKVC.js → chunk-CF5QNFYU.js} +2 -2
- package/dist/{chunk-JC7KXNNM.js → chunk-D5DCBD4Y.js} +2 -2
- package/dist/{chunk-PXZ7LAOY.js → chunk-D6ZC7XKZ.js} +2 -2
- package/dist/chunk-EJVQJCG5.js +370 -0
- package/dist/{chunk-6OL23QYD.js → chunk-F24FNCTK.js} +1 -1
- package/dist/{chunk-NEKEH4CK.js → chunk-FR6BYFPC.js} +53 -21
- package/dist/{chunk-KOPVJQ2P.js → chunk-GEXT7BTE.js} +1 -1
- package/dist/{chunk-ZAWANWUS.js → chunk-GGORNBSC.js} +18 -6
- package/dist/{chunk-DUQMSNF2.js → chunk-HGFAEDG3.js} +1 -1
- package/dist/chunk-HODO5BX5.js +28 -0
- package/dist/chunk-HW7B43E5.js +14 -0
- package/dist/{chunk-EQBZ24VH.js → chunk-IAM3D3RL.js} +43 -24
- package/dist/{chunk-ODADQM6Z.js → chunk-JJ3DELUL.js} +2 -2
- package/dist/{chunk-542MIHNP.js → chunk-JMLK6MH7.js} +1 -1
- package/dist/{chunk-7FWA3I6A.js → chunk-JWTQS5F5.js} +1 -1
- package/dist/{chunk-OUNRFXDC.js → chunk-K4ZHAURD.js} +1 -1
- package/dist/chunk-NV72HZ4W.js +45 -0
- package/dist/{chunk-4Y5FWDPM.js → chunk-NZ4RXN3G.js} +55 -7
- package/dist/chunk-OWZ7ATLZ.js +187 -0
- package/dist/{chunk-TXBAOEKG.js → chunk-PKGN32AU.js} +5 -4
- package/dist/chunk-Q6E4OPGG.js +40 -0
- package/dist/{chunk-J4D6ZXRW.js → chunk-QL6XNPSD.js} +3 -3
- package/dist/{chunk-HAG4YIZY.js → chunk-R46IQIGK.js} +6 -6
- package/dist/{chunk-R3VKF43H.js → chunk-RPZ7OQGD.js} +1 -1
- package/dist/{chunk-Q3S26EQA.js → chunk-S54ECQGY.js} +1 -1
- package/dist/{chunk-JIL5VY2R.js → chunk-VHKEF5ZB.js} +1 -1
- package/dist/{chunk-ILAW3XKD.js → chunk-VO3JYRT5.js} +43 -29
- package/dist/chunk-VWWITFWM.js +300 -0
- package/dist/{chunk-OPRBF7VE.js → chunk-ZTLMPUO6.js} +3 -2
- package/dist/client/index.js +3 -3
- package/dist/cms/CmsPreview.d.ts +2 -2
- package/dist/cms/index.d.ts +3 -3
- package/dist/cms/server-api.d.ts +1 -1
- package/dist/cms/types.d.ts +1 -1
- package/dist/commerce/EventCheckout.d.ts +1 -1
- package/dist/commerce/EventsAgenda.d.ts +3 -3
- package/dist/commerce/api.d.ts +1 -1
- package/dist/commerce/index.js +4 -4
- package/dist/commerce/server.d.ts +3 -4
- package/dist/contracts/color.d.ts +1 -1
- package/dist/contracts/entries.d.ts +1 -1
- package/dist/contracts/error-page.d.ts +183 -0
- package/dist/contracts/fleet.d.ts +9 -9
- package/dist/contracts/forms.d.ts +4 -10
- package/dist/contracts/llms.d.ts +2 -1
- package/dist/contracts/portfolio.d.ts +11 -15
- package/dist/contracts/proposal-sitemap.d.ts +130 -0
- package/dist/contracts/schema-placeholders.d.ts +87 -0
- package/dist/contracts/seo-meta.d.ts +11 -12
- package/dist/contracts/seo-pages.d.ts +12 -18
- package/dist/contracts/site-cache.d.ts +4 -4
- package/dist/contracts/sites-normalize.d.ts +2 -4
- package/dist/contracts/sites.d.ts +3 -5
- package/dist/contracts/slot-content.d.ts +2 -2
- package/dist/contracts/slots.d.ts +6 -6
- package/dist/contracts/voice.d.ts +56 -0
- package/dist/contracts/website.d.ts +2 -2
- package/dist/engage/EngageWidget.d.ts +13 -12
- package/dist/engage/index.d.ts +10 -9
- package/dist/engage/index.js +31 -35
- package/dist/engage/types.d.ts +19 -244
- package/dist/fleet/FleetHeartbeat.d.ts +2 -2
- package/dist/fleet/index.js +4 -4
- package/dist/forms/FormEnhancer.d.ts +25 -12
- package/dist/forms/FormSpotlight.d.ts +2 -2
- package/dist/forms/ManagedForm.d.ts +1 -1
- package/dist/forms/ServerForm.d.ts +11 -10
- package/dist/forms/StaticForm.d.ts +4 -2
- package/dist/forms/field-autocomplete.d.ts +28 -0
- package/dist/forms/field-interactions.d.ts +2 -2
- package/dist/forms/form-dom-values.d.ts +42 -0
- package/dist/forms/form-loaded-at.d.ts +2 -2
- package/dist/forms/formsApi.d.ts +2 -2
- package/dist/forms/index.d.ts +1 -1
- package/dist/forms/index.js +11 -9
- package/dist/forms/server.d.ts +1 -1
- package/dist/forms/server.js +6 -4
- package/dist/forms/static.js +3 -2
- package/dist/forms/submitForm.d.ts +9 -2
- package/dist/forms/types.d.ts +44 -5
- package/dist/forms/useForm.d.ts +21 -4
- package/dist/forms/webmcp.d.ts +38 -0
- package/dist/images/ManagedFavicon.d.ts +1 -1
- package/dist/images/ManagedImage.d.ts +4 -4
- package/dist/images/api.d.ts +1 -1
- package/dist/images/index.d.ts +2 -2
- package/dist/images/index.js +4 -4
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -1
- package/dist/landing/contract.d.ts +3 -4
- package/dist/layout/SiteKitClientProviders.d.ts +4 -4
- package/dist/layout/SiteKitLayout.d.ts +11 -11
- package/dist/layout/client.d.ts +4 -4
- package/dist/layout/client.js +7 -7
- package/dist/layout/index.js +8 -8
- package/dist/layout/types.d.ts +7 -6
- package/dist/llms/SpeakableSchema.d.ts +2 -2
- package/dist/llms/agent-access.d.ts +2 -2
- package/dist/llms/aiRobots.d.ts +4 -4
- package/dist/llms/api.d.ts +2 -2
- package/dist/llms/contract.js +1 -1
- package/dist/llms/generateLLMsTxt.d.ts +1 -1
- package/dist/llms/index.js +6 -6
- package/dist/llms/links.d.ts +1 -1
- package/dist/llms/revalidate.d.ts +1 -1
- package/dist/llms/seo-revalidate.d.ts +2 -3
- package/dist/llms/types.d.ts +6 -6
- package/dist/llms/writeLLMsTxt.d.ts +10 -10
- package/dist/manifest/index.d.ts +8 -8
- package/dist/maps/index.js +3 -3
- package/dist/mcp/WebMcpTools.d.ts +1 -1
- package/dist/mcp/index.js +2 -14
- package/dist/mcp/report.d.ts +2 -2
- package/dist/mcp/serverCard.d.ts +1 -1
- package/dist/mcp/sonor.d.ts +5 -6
- package/dist/mcp/sonor.js +14 -11
- package/dist/mcp/transport.d.ts +1 -2
- package/dist/mcp/types.d.ts +4 -5
- package/dist/motion/engine.d.ts +2 -3
- package/dist/motion/failsafe.d.ts +1 -2
- package/dist/motion/gsap.d.ts +2 -2
- package/dist/motion/three.d.ts +1 -1
- package/dist/og/config.d.ts +4 -4
- package/dist/og/contrast.d.ts +2 -2
- package/dist/og/pages.d.ts +5 -5
- package/dist/og/route-fit.d.ts +2 -2
- package/dist/og/route.d.ts +1 -2
- package/dist/og/template.d.ts +1 -1
- package/dist/proxy/index.d.ts +2 -2
- package/dist/proxy/securityHeaders.d.ts +10 -12
- package/dist/redirects/index.d.ts +7 -7
- package/dist/reputation/TestimonialSection.d.ts +1 -1
- package/dist/reputation/api.d.ts +1 -1
- package/dist/revalidate/index.js +2 -2
- package/dist/robots/index.d.ts +5 -5
- package/dist/robots/indexnow.d.ts +2 -3
- package/dist/seo/ManagedScripts.d.ts +2 -2
- package/dist/seo/client.js +4 -4
- package/dist/seo/getManagedMetadata.d.ts +1 -1
- package/dist/seo/groundingSchema.d.ts +2 -2
- package/dist/seo/index.js +15 -8
- package/dist/seo/llms/contract.js +1 -1
- package/dist/seo/llms.js +6 -6
- package/dist/seo/register-sitemap-cli-impl.d.ts +1 -1
- package/dist/seo/register-sitemap-cli.d.ts +1 -1
- package/dist/seo/register-sitemap-cli.js +2 -2
- package/dist/seo/schema-filter.d.ts +5 -6
- package/dist/seo/server-api.d.ts +1 -1
- package/dist/seo/sitemap.js +4 -4
- package/dist/seo/types.d.ts +4 -4
- package/dist/seo/withManagedMetadata.d.ts +4 -4
- package/dist/server/index.js +2 -2
- package/dist/server/mint-site-token.d.ts +1 -2
- package/dist/server/server-fetch.d.ts +5 -5
- package/dist/shared/build-entries.d.ts +4 -4
- package/dist/shared/dialog.d.ts +27 -0
- package/dist/shared/frame.d.ts +8 -9
- package/dist/shared/fresh-fetch.d.ts +1 -2
- package/dist/shared/identity.d.ts +1 -1
- package/dist/shared/layers.d.ts +7 -0
- package/dist/shared/mid-form.d.ts +53 -0
- package/dist/shared/next-files.d.ts +1 -1
- package/dist/shared/publishCredential.d.ts +1 -1
- package/dist/shared/reporting-gate.d.ts +1 -1
- package/dist/shared/uuid.d.ts +1 -2
- package/dist/shared/version.d.ts +1 -1
- package/dist/shared/visual-viewport-gap.d.ts +3 -3
- package/dist/signal/index.js +2 -2
- package/dist/signal/types.d.ts +1 -1
- package/dist/site-config/index.d.ts +2 -2
- package/dist/sitemap/index.d.ts +13 -13
- package/dist/sitemap/index.js +4 -4
- package/dist/sitemap/shared.d.ts +3 -3
- package/dist/sites/site-param.d.ts +2 -2
- package/dist/slots/ManagedRichText.d.ts +1 -1
- package/dist/slots/ManagedSlot.d.ts +1 -1
- package/dist/slots/index.d.ts +1 -3
- package/dist/{socket-loader-R24ZSRSQ.js → socket-loader-CGIPEG74.js} +1 -1
- package/dist/sync/index.js +5 -5
- package/dist/types.d.ts +4 -2
- package/dist/website/BlocksPopup.d.ts +1 -1
- package/dist/website/PopupBlocks.d.ts +8 -6
- package/dist/website/SitePopups.d.ts +25 -0
- package/dist/website/images.js +4 -4
- package/dist/website/index.js +6 -6
- package/dist/website/popup-rules.d.ts +46 -0
- package/dist/website/popup-types.d.ts +113 -0
- package/dist/website/popups.d.ts +2 -6
- package/dist/website/popups.js +6 -14
- package/dist/{writeLLMsTxt-QR23OQUE.js → writeLLMsTxt-OV24LQVL.js} +3 -3
- package/docs/MIGRATING-TO-7.md +50 -22
- package/docs.json +2 -1
- package/package.json +2 -2
- package/src/admin-auth/README.md +3 -3
- package/src/analytics/README.md +9 -11
- package/src/articles/README.md +10 -7
- package/src/{engage → chat}/README.md +63 -62
- package/src/cta-bar/README.md +11 -11
- package/src/forms/README.md +60 -12
- package/src/layout/README.md +8 -5
- package/src/llms/README.md +3 -3
- package/src/mcp/README.md +7 -7
- package/src/motion/README.md +1 -1
- package/src/og/README.md +26 -27
- package/src/proxy/README.md +14 -14
- package/src/seo/README.md +4 -2
- package/src/slots/README.md +1 -1
- package/src/sync/README.md +10 -0
- package/src/website/README.md +133 -0
- package/dist/ChatWidget-JMFBXJ75.js +0 -15
- package/dist/EngageWidget-A3QKZ3SU.js +0 -11
- package/dist/ManagedForm-2GSSDJPP.js +0 -14
- package/dist/SitemapSync-4U654SEA.js +0 -8
- package/dist/chunk-B73QPZPH.js +0 -837
- package/dist/chunk-NDF4A5JM.js +0 -37
- package/dist/chunk-RTMMHTEQ.js +0 -100
- package/dist/engage/DesignRenderer.d.ts +0 -57
- package/dist/engage/element-rules.d.ts +0 -38
package/dist/commerce/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import '../chunk-P5GB6NEO.js';
|
|
3
|
-
import { useAnalyticsOptional } from '../chunk-
|
|
3
|
+
import { useAnalyticsOptional } from '../chunk-JWTQS5F5.js';
|
|
4
4
|
import '../chunk-LCDVPAMR.js';
|
|
5
5
|
import '../chunk-24QZEO3Q.js';
|
|
6
6
|
import '../chunk-L2V5PUFN.js';
|
|
7
7
|
import '../chunk-GJWI74ZZ.js';
|
|
8
8
|
import '../chunk-43OCZ3JA.js';
|
|
9
9
|
import '../chunk-EKBEOXTH.js';
|
|
10
|
-
import { sonorFetch } from '../chunk-
|
|
11
|
-
import '../chunk-
|
|
12
|
-
import '../chunk-
|
|
10
|
+
import { sonorFetch } from '../chunk-D6ZC7XKZ.js';
|
|
11
|
+
import '../chunk-K4ZHAURD.js';
|
|
12
|
+
import '../chunk-F24FNCTK.js';
|
|
13
13
|
import '../chunk-PKBMQBKP.js';
|
|
14
14
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
15
15
|
import React5, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Helpers for server-side rendering and dynamic routes.
|
|
5
5
|
* Use in Next.js getStaticPaths, getStaticProps, or App Router.
|
|
6
6
|
*
|
|
7
|
-
* All data fetching goes through the
|
|
7
|
+
* All data fetching goes through the Sonor API.
|
|
8
8
|
*/
|
|
9
9
|
import type { CommerceOffering, OfferingType } from './types';
|
|
10
10
|
interface ServerConfig {
|
|
@@ -102,8 +102,7 @@ export declare function getOfferingsResult(config: ServerConfig, options?: {
|
|
|
102
102
|
* Uses POST /api/public/commerce/events — the same endpoint the client fetcher
|
|
103
103
|
* uses — because the GET offerings list does NOT embed commerce_schedules,
|
|
104
104
|
* which made this helper return events with empty `schedules[]` and forced
|
|
105
|
-
* sites to hand-roll their own fetcher for any calendar UI
|
|
106
|
-
* lesson). Falls back to the legacy offerings query if the events endpoint
|
|
105
|
+
* sites to hand-roll their own fetcher for any calendar UI. Falls back to the legacy offerings query if the events endpoint
|
|
107
106
|
* is unavailable (older self-hosted API).
|
|
108
107
|
*/
|
|
109
108
|
export declare function getUpcomingEvents(config: ServerConfig, options?: {
|
|
@@ -154,7 +153,7 @@ export declare function generateOfferingMetadata(offering: CommerceOffering | nu
|
|
|
154
153
|
/**
|
|
155
154
|
* Create a server config for API calls
|
|
156
155
|
*
|
|
157
|
-
* @param apiUrl -
|
|
156
|
+
* @param apiUrl - Sonor API URL (e.g., 'https://api.sonor.io')
|
|
158
157
|
* @param apiKey - Project API key
|
|
159
158
|
* @param projectId - Project ID
|
|
160
159
|
*/
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Single source of truth for turning a CSS colour string into something the
|
|
5
5
|
* kit can reason about. Used at build time by the brand-profile extractor
|
|
6
6
|
* (`src/brand/extract-brand-profile.ts`, theme detection) and in the browser
|
|
7
|
-
* by Echo (`src/
|
|
7
|
+
* by Echo (`src/chat/brand-color.ts`, brand text contrast). Pure, no
|
|
8
8
|
* imports, safe in both.
|
|
9
9
|
*/
|
|
10
10
|
export type Rgb = {
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* tsup config, its package.json check and the boundary test, so a new
|
|
5
5
|
* contract is added in one place.
|
|
6
6
|
*/
|
|
7
|
-
export declare const CONTRACT_ENTRIES: readonly ['forms', 'llms', 'slots', 'slot-content', 'site-cache', 'site-edit', 'website', 'color', 'fleet', 'sites', 'seo-pages', 'seo-meta', 'portfolio'];
|
|
7
|
+
export declare const CONTRACT_ENTRIES: readonly ['forms', 'llms', 'slots', 'slot-content', 'site-cache', 'site-edit', 'website', 'color', 'fleet', 'sites', 'seo-pages', 'seo-meta', 'portfolio', 'voice', 'proposal-sitemap', 'error-page', 'schema-placeholders'];
|
|
8
8
|
export type ContractEntry = (typeof CONTRACT_ENTRIES)[number];
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sonordev/contracts/error-page: telling an error page from a page (v1)
|
|
3
|
+
*
|
|
4
|
+
* Three questions every writer of page content and page copy has to answer the
|
|
5
|
+
* same way, so they live here once:
|
|
6
|
+
*
|
|
7
|
+
* 1. Is this stored text a rendered ERROR PAGE rather than the page's own
|
|
8
|
+
* content? (`looksLikeErrorPage`, `usablePageText`)
|
|
9
|
+
* 2. Does this generated COPY say the page itself is missing or unavailable?
|
|
10
|
+
* (`findErrorPageClaim`, `describesErrorPage`)
|
|
11
|
+
* 3. Does any string in a page's managed copy (a title, a keyword list, a
|
|
12
|
+
* JSON-LD block) say so? (`findPageCopyClaim`: question 2 over every
|
|
13
|
+
* string of a record, naming the field that holds it)
|
|
14
|
+
*
|
|
15
|
+
* Why it exists: a page's stored text is the text a visitor's browser (or a
|
|
16
|
+
* build's crawler) rendered. Visit a URL while it's briefly broken and the
|
|
17
|
+
* error page's words are stored as that page's content. Everything generated
|
|
18
|
+
* from that content then describes a working page as broken, and the result
|
|
19
|
+
* is published as the page's title, meta description, social tags, JSON-LD
|
|
20
|
+
* and AI-crawler notes. The outage ends in a deploy; the poisoned copy
|
|
21
|
+
* doesn't, because nothing downstream asks whether its input was ever the
|
|
22
|
+
* page.
|
|
23
|
+
*
|
|
24
|
+
* How the text check works
|
|
25
|
+
*
|
|
26
|
+
* - It reads the START of the text (300 characters), lowercased, with
|
|
27
|
+
* whitespace collapsed, HTML entities decoded and curly quotes made
|
|
28
|
+
* straight. Error copy leads; articles bury it.
|
|
29
|
+
* - The signatures are generic on purpose. Every site words its 404
|
|
30
|
+
* differently and frameworks ship their own ("This page could not be
|
|
31
|
+
* found.", "Application error: a client-side exception has occurred"), so
|
|
32
|
+
* the shape is what's matched, not any one site's copy. They're grouped by
|
|
33
|
+
* family below: not found, server and client errors, unavailable and
|
|
34
|
+
* maintenance, walls.
|
|
35
|
+
* - Stored text is often the page's elements run together with no separator
|
|
36
|
+
* ("404This page could not be found."), so no signature depends on a word
|
|
37
|
+
* boundary around the text it's matching.
|
|
38
|
+
* - A signature that follows a word that DESCRIBES errors rather than
|
|
39
|
+
* showing one ("if something went wrong", "how to fix a 404 error", a
|
|
40
|
+
* quote mark) doesn't count: a help page that names an error isn't one.
|
|
41
|
+
* Navigation is Title Case and prose isn't, so a describing word counts
|
|
42
|
+
* only as it's written in lower case, with only its first letter
|
|
43
|
+
* capitalized ("How to fix..."), or opening the text or a sentence;
|
|
44
|
+
* "Home Blog How To Something went wrong" is still an error page, and the
|
|
45
|
+
* words a menu uses too ("About", "Monitoring", "Alerts for") describe an
|
|
46
|
+
* error only in lower case. A describing word inside the matched phrase
|
|
47
|
+
* counts too, and after a described match the search resumes one character
|
|
48
|
+
* on, so a real error later in the same stretch is still found.
|
|
49
|
+
* - A not-found phrase that sits in a question ("Page not found? Use our site
|
|
50
|
+
* map", "Product not found in your size? Ask us") is a help line, and so is
|
|
51
|
+
* most copy that merely uses the words ("Rare parts can't be found
|
|
52
|
+
* elsewhere": the subject has to be a page, URL, link, file, site or app).
|
|
53
|
+
* - Up to ERROR_PAGE_MAX_WORDS words every check applies, but the loosest
|
|
54
|
+
* ones apply only to the shortest texts: a 404 glued to its copy and
|
|
55
|
+
* "refresh the page and try again" up to 60 words, a dynamic route's own
|
|
56
|
+
* "<name> not found" state up to 40. From there up to 400 words only the
|
|
57
|
+
* prefix-aware check applies: a text written as "<meta description> —
|
|
58
|
+
* <every string on the page>" carries its header and footer strings along,
|
|
59
|
+
* which pushes a rendered error state past the short-text ceiling, so the
|
|
60
|
+
* check looks at how the text opens after the description and a skip link,
|
|
61
|
+
* and an opener followed by "explained" or "how to" is an article. Beyond
|
|
62
|
+
* 400 words the text is prose.
|
|
63
|
+
*
|
|
64
|
+
* `wordCount` semantics: pass the word count of `text` itself. Pass the whole
|
|
65
|
+
* document's count only when `text` is a truncated excerpt of it. A supplied
|
|
66
|
+
* count can only RAISE the count the check uses (an excerpt of a long document
|
|
67
|
+
* is prose however short it is), never lower it: text that is longer than the
|
|
68
|
+
* count you gave is counted by what it holds. A browser's page-wide count
|
|
69
|
+
* (navigation and footer included) is therefore the wrong number for a short
|
|
70
|
+
* error state it scraped: pass the count of the text you pass.
|
|
71
|
+
*
|
|
72
|
+
* How the copy check works
|
|
73
|
+
*
|
|
74
|
+
* `findErrorPageClaim` looks for the copy saying THE PAGE is missing or
|
|
75
|
+
* unavailable ("Page Not Found | Acme", "This URL currently returns a 404",
|
|
76
|
+
* "This listing has been removed", "The page failed to load"). The subject
|
|
77
|
+
* has to be the page, its URL, link or listing, so copy about something
|
|
78
|
+
* unavailable on a working page ("sunset cruises are not available at this
|
|
79
|
+
* time") is left alone, and so is a qualified one ("unavailable in winter").
|
|
80
|
+
* Whitespace is collapsed and each string is read up to 5,000 characters, so
|
|
81
|
+
* hostile whitespace costs nothing. The page's own text exempts a phrase the
|
|
82
|
+
* page really says; a page that is itself an error page exempts nothing.
|
|
83
|
+
*
|
|
84
|
+
* `findPageCopyClaim(fields, pageText?)` is the same check over a record:
|
|
85
|
+
* each value of `fields` is checked if it's a string, and walked for every
|
|
86
|
+
* string it holds if it's an array or an object (8 levels deep, 2,000
|
|
87
|
+
* leaves at most; numbers, booleans and null are ignored). It returns the
|
|
88
|
+
* first claim as `{ field, phrase }`, `field` being the TOP-LEVEL key (the
|
|
89
|
+
* column or property that holds the copy), or null. Use it for a page row's
|
|
90
|
+
* managed title, description, keywords and JSON-LD in one call.
|
|
91
|
+
*
|
|
92
|
+
* Known limits (a text-only check can't close these)
|
|
93
|
+
*
|
|
94
|
+
* - Any error phrase in the first 300 characters of a text of 120 words or
|
|
95
|
+
* fewer reads as an error page, wherever in those 300 characters it sits, unless
|
|
96
|
+
* something describes it (see above): a short page ABOUT errors ("Page not
|
|
97
|
+
* found: how to design a helpful 404 page. Five examples.", or a web
|
|
98
|
+
* agency's line about its "custom 404 error page") can be misread. Texts of
|
|
99
|
+
* 121 to 400 words are only read for how they open, and longer ones are
|
|
100
|
+
* prose. The loosest wordings (a 404 glued to its copy, "refresh the page
|
|
101
|
+
* and try again", "<name> not found") apply only up to 60, 60 and 40 words.
|
|
102
|
+
* - A business whose name is an error phrase ("404 Brewing Co.", "Page Not
|
|
103
|
+
* Found Records") reads as an error page when its name leads the text.
|
|
104
|
+
* - Chrome in front hides the error state from the wordings anchored to the
|
|
105
|
+
* start of the text ("Not Found", "Be right back.", "Error: ...", "Oops!
|
|
106
|
+
* This page...", a permission wall, a dynamic route's "<name> not found").
|
|
107
|
+
* The wordings that hold anywhere in the first 300 characters (a status with
|
|
108
|
+
* its reason, "could not be found", "something went wrong") are what's
|
|
109
|
+
* left. Prefer the page's main content.
|
|
110
|
+
* - The text alone never proves a page failed. Where a call site observed
|
|
111
|
+
* the HTTP status (a browser reports it), require a status of 400 or more
|
|
112
|
+
* as well, and treat this check as the fallback for the sites and the
|
|
113
|
+
* paths that report none.
|
|
114
|
+
* - It reads English. A 404 in another language isn't matched.
|
|
115
|
+
* - Walls and pages served by the host in front of the site (a CDN block, a
|
|
116
|
+
* bot challenge, a parked domain, a default server page, a browser
|
|
117
|
+
* interstitial) never reach the writers this guards, so they aren't
|
|
118
|
+
* matched here.
|
|
119
|
+
* - Copy that claims the page is missing in words outside the closed list
|
|
120
|
+
* below isn't caught; the real protection is that error text never reaches
|
|
121
|
+
* the generator (`usablePageText`).
|
|
122
|
+
*
|
|
123
|
+
* Pure string logic: no I/O, no Node built-ins, no lookbehind (this ships to
|
|
124
|
+
* browsers, and Safari before 16.4 throws on a lookbehind at parse time).
|
|
125
|
+
*/
|
|
126
|
+
/** Bump on breaking changes to what counts as an error page or an error claim. */
|
|
127
|
+
export declare const ERROR_PAGE_CONTRACT_VERSION: 1;
|
|
128
|
+
/**
|
|
129
|
+
* Above this many words, text is real prose even if it mentions a 404: only
|
|
130
|
+
* the prefix-aware check (up to 400 words, see the header) still looks at it.
|
|
131
|
+
*
|
|
132
|
+
* The gate is what makes the check safe: an article about broken links ("How
|
|
133
|
+
* to fix 404 errors on your site") legitimately holds every phrase below and
|
|
134
|
+
* must not be discarded. A rendered error page is short, and this ceiling
|
|
135
|
+
* still separates the two by an order of magnitude.
|
|
136
|
+
*/
|
|
137
|
+
export declare const ERROR_PAGE_MAX_WORDS = 120;
|
|
138
|
+
/**
|
|
139
|
+
* True when this text is a rendered error page rather than page content.
|
|
140
|
+
*
|
|
141
|
+
* @param text the scraped or extracted body text
|
|
142
|
+
* @param wordCount the word count of `text` itself, or the whole document's
|
|
143
|
+
* when `text` is a truncated excerpt of it. It can only raise
|
|
144
|
+
* the count the check uses, never lower it; derived from
|
|
145
|
+
* `text` when absent.
|
|
146
|
+
*/
|
|
147
|
+
export declare function looksLikeErrorPage(text?: string | null, wordCount?: number | null): boolean;
|
|
148
|
+
/**
|
|
149
|
+
* The page's stored text, or null when it's an error page. What a model may
|
|
150
|
+
* be shown as "the page's content".
|
|
151
|
+
*/
|
|
152
|
+
export declare function usablePageText(text?: string | null, wordCount?: number | null): string | null;
|
|
153
|
+
/**
|
|
154
|
+
* The phrase in `copy` that says the page itself is missing or unavailable, or
|
|
155
|
+
* null when the copy is fine.
|
|
156
|
+
*
|
|
157
|
+
* @param pageText the page's own text. A phrase the page really says (an
|
|
158
|
+
* article about fixing "page not found" errors) is allowed;
|
|
159
|
+
* only copy that claims what the page doesn't is a finding.
|
|
160
|
+
* A pageText that is itself an error page allows nothing: its
|
|
161
|
+
* words are the poison, so the copy that repeats them would
|
|
162
|
+
* otherwise vouch for itself.
|
|
163
|
+
*/
|
|
164
|
+
export declare function findErrorPageClaim(copy?: string | null, pageText?: string | null): string | null;
|
|
165
|
+
/** True when `copy` says the page itself is missing or unavailable (see `findErrorPageClaim`). */
|
|
166
|
+
export declare function describesErrorPage(copy?: string | null, pageText?: string | null): boolean;
|
|
167
|
+
/**
|
|
168
|
+
* The first string anywhere in a record of managed copy that says the page
|
|
169
|
+
* itself is missing or unavailable, or null. `findErrorPageClaim` over every
|
|
170
|
+
* string of `fields`, and the record's own field name with the phrase.
|
|
171
|
+
*
|
|
172
|
+
* Each value is checked if it's a string, and walked for the strings it holds
|
|
173
|
+
* if it's an array or an object (a JSON-LD block, a keyword list), up to 8
|
|
174
|
+
* levels deep, 2,000 strings and 100,000 characters in all; numbers, booleans and
|
|
175
|
+
* null are ignored (and don't count toward those limits).
|
|
176
|
+
* `field` is the TOP-LEVEL key, the column or property that holds the copy.
|
|
177
|
+
* `pageText` exempts what the page really says, as in `findErrorPageClaim`; the
|
|
178
|
+
* page's own text is read once for the whole record.
|
|
179
|
+
*/
|
|
180
|
+
export declare function findPageCopyClaim(fields: object, pageText?: string | null): {
|
|
181
|
+
field: string;
|
|
182
|
+
phrase: string;
|
|
183
|
+
} | null;
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* @sonordev/contracts/fleet — Fleet heartbeat wire contract (v1)
|
|
3
3
|
*
|
|
4
4
|
* Single source of truth for the heartbeat payload shape, shared by site-kit
|
|
5
|
-
* (send side, `@sonordev/site-kit/fleet/contract`) and
|
|
5
|
+
* (send side, `@sonordev/site-kit/fleet/contract`) and the Sonor API (ingest
|
|
6
6
|
* side: its DTO is type-checked against FleetHeartbeatPayload).
|
|
7
7
|
*
|
|
8
|
-
* Why it exists:
|
|
9
|
-
*
|
|
10
|
-
* site running" without
|
|
11
|
-
*
|
|
8
|
+
* Why it exists: sites run whatever site-kit version they last deployed, and
|
|
9
|
+
* Sonor should be able to answer "which version / modules / key state is each
|
|
10
|
+
* site running" without anyone opening each repo. The heartbeat answers that
|
|
11
|
+
* from live traffic. It is fire-and-forget, deferred to idle (never in the
|
|
12
12
|
* hydration path), and carries NO visitor data — only the site's own build
|
|
13
13
|
* fingerprint. Project identity is resolved server-side from the x-api-key
|
|
14
14
|
* header, exactly like every other /api/public/* endpoint; sites never send
|
|
@@ -56,7 +56,7 @@ export interface FleetHeartbeatPayload {
|
|
|
56
56
|
next_version?: string | null;
|
|
57
57
|
/** Client-observable active modules — deduped, validated, canonical order. */
|
|
58
58
|
modules: FleetModule[];
|
|
59
|
-
/** Normalized lowercase host (multi-site sub-dimension
|
|
59
|
+
/** Normalized lowercase host (multi-site sub-dimension). */
|
|
60
60
|
site?: string | null;
|
|
61
61
|
/** 'client' (runtime boot) or 'build' (CI/build step). Default 'client'. */
|
|
62
62
|
source?: FleetHeartbeatSource;
|
|
@@ -77,9 +77,9 @@ export declare function normalizeFleetModules(input: unknown): FleetModule[];
|
|
|
77
77
|
export declare function sanitizeFleetVersion(raw: unknown): string | null;
|
|
78
78
|
/**
|
|
79
79
|
* Light client-side site normalization to the canonical host shape (lowercase,
|
|
80
|
-
* no protocol/path/port).
|
|
81
|
-
*
|
|
82
|
-
*
|
|
80
|
+
* no protocol/path/port). The Sonor API's normalizer is AUTHORITATIVE and
|
|
81
|
+
* re-runs on ingest; this just avoids sending obvious noise. Keep the two
|
|
82
|
+
* shapes aligned.
|
|
83
83
|
*/
|
|
84
84
|
export declare function normalizeFleetSite(raw: unknown): string | null;
|
|
85
85
|
/**
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @sonordev/contracts/forms — managed-form honeypot policy (v1)
|
|
3
3
|
*
|
|
4
|
-
* Single source of truth for the honeypot
|
|
5
|
-
*
|
|
6
|
-
* the server (sonor-api names the trap in the form config it returns AND
|
|
7
|
-
* detects hits on intake). Both import this: site-kit as
|
|
8
|
-
* `@sonordev/site-kit/forms/contract`, sonor-api through
|
|
9
|
-
* `src/modules/forms/honeypot-contract.ts`.
|
|
4
|
+
* Single source of truth for the honeypot field's naming, shared by site-kit
|
|
5
|
+
* (which renders the field, as `@sonordev/site-kit/forms/contract`) and Sonor.
|
|
10
6
|
*
|
|
11
|
-
* The default field name
|
|
12
|
-
*
|
|
13
|
-
* as spam, so generic names are only trusted when they DON'T match a real
|
|
14
|
-
* field of the form (see `resolveHoneypotFieldNames`).
|
|
7
|
+
* The default field name is deliberately unlike a real field's, so it never
|
|
8
|
+
* collides with a form's own fields (see `resolveHoneypotFieldNames`).
|
|
15
9
|
*/
|
|
16
10
|
/** Bump on breaking changes to the honeypot policy. */
|
|
17
11
|
export declare const FORMS_HONEYPOT_CONTRACT_VERSION: 1;
|
package/dist/contracts/llms.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export declare const LLMS_PUBLIC_SUMMARY_MAX_LENGTH = 400;
|
|
|
14
14
|
/** Max length for optional llms.txt blockquote disclaimer line. */
|
|
15
15
|
export declare const LLMS_DISCLAIMER_MAX_LENGTH = 280;
|
|
16
16
|
/**
|
|
17
|
-
* Optional keys we allow inside managed_llm_schema JSON from
|
|
17
|
+
* Optional keys we allow inside managed_llm_schema JSON from Sonor.
|
|
18
18
|
* Values outside this set should not be relied on by consumers; strip unknown keys when emitting JSON-LD.
|
|
19
19
|
*/
|
|
20
20
|
export declare const MANAGED_LLM_SCHEMA_KNOWN_KEYS: Set<string>;
|
|
@@ -28,6 +28,7 @@ export interface LLMsPayloadMeta {
|
|
|
28
28
|
/** Optional single-line, public-safe note in the llms.txt blockquote (e.g. scope / not legal advice). */
|
|
29
29
|
llms_disclaimer?: string | null;
|
|
30
30
|
}
|
|
31
|
+
export declare function clipAtWordBoundary(text: string, max: number): string;
|
|
31
32
|
/**
|
|
32
33
|
* Public-safe summary for llms.txt `[title](url): notes` — never CRM-only or internal copy.
|
|
33
34
|
*/
|
|
@@ -2,13 +2,9 @@
|
|
|
2
2
|
* @sonordev/contracts/portfolio — portfolio proof display policy (v1)
|
|
3
3
|
*
|
|
4
4
|
* Single source of truth for the Lighthouse-KPI collapse policy shared by the
|
|
5
|
-
* content side (
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* from `src/portfolio/curate-proof.ts`, which keeps the render-only
|
|
9
|
-
* primitives that bind to its section types.
|
|
10
|
-
* - signal-api imports it through
|
|
11
|
-
* `src/modules/skills/portfolio/portfolio-kpi-policy.util.ts`.
|
|
5
|
+
* content side (Signal's case-study generation) and the render side
|
|
6
|
+
* (agency-site-kit, which imports it as `@sonordev/site-kit/portfolio/contract`
|
|
7
|
+
* and keeps the render-only primitives that bind to its section types).
|
|
12
8
|
*
|
|
13
9
|
* Why it exists: generated case studies arrived with four "100/100 Lighthouse X"
|
|
14
10
|
* KPIs repeated across hero, results cards, and a Performance section. One
|
|
@@ -17,8 +13,8 @@
|
|
|
17
13
|
*
|
|
18
14
|
* It is also the single source of truth for METRIC PROVENANCE (below): the
|
|
19
15
|
* three sources a portfolio number can have, what a client-reported number
|
|
20
|
-
* must carry, and which numbers may headline.
|
|
21
|
-
*
|
|
16
|
+
* must carry, and which numbers may headline. The Sonor API applies the same
|
|
17
|
+
* rules.
|
|
22
18
|
*/
|
|
23
19
|
/** The message a cross-origin-framed site-kit page posts to its embedder once it has painted. */
|
|
24
20
|
export declare const FRAME_READY_MESSAGE: 'sonor:frame-ready';
|
|
@@ -109,11 +105,11 @@ export declare const METRIC_SOURCES: readonly ['measured', 'reported', 'estimate
|
|
|
109
105
|
export type MetricSource = (typeof METRIC_SOURCES)[number];
|
|
110
106
|
/** Who stands behind a client-reported number. */
|
|
111
107
|
export interface MetricAttribution {
|
|
112
|
-
/** The person, e.g. "
|
|
108
|
+
/** The person, e.g. "Dana Reyes". */
|
|
113
109
|
name: string;
|
|
114
|
-
/** Their title, e.g. "Director of
|
|
110
|
+
/** Their title, e.g. "Director of Operations". */
|
|
115
111
|
role?: string;
|
|
116
|
-
/** Where they work: almost always the client, e.g. "
|
|
112
|
+
/** Where they work: almost always the client, e.g. "Northwind Engineering". */
|
|
117
113
|
organization: string;
|
|
118
114
|
/** The day they reported it, YYYY-MM-DD. */
|
|
119
115
|
date: string;
|
|
@@ -140,12 +136,12 @@ export declare function isHeadlineMetric(metric: SourcedMetric): boolean;
|
|
|
140
136
|
/** The badge a display shows, or null for a missing or unknown source (show nothing rather than guess). */
|
|
141
137
|
export declare function metricSourceLabel(source: unknown): string | null;
|
|
142
138
|
/**
|
|
143
|
-
* The full credit: "Reported by
|
|
144
|
-
*
|
|
139
|
+
* The full credit: "Reported by Dana Reyes, Director of Operations, Northwind
|
|
140
|
+
* Engineering, September 25, 2026". The date is read as a plain day,
|
|
145
141
|
* never through a time zone, so it can't slip to the day before.
|
|
146
142
|
*/
|
|
147
143
|
export declare function formatAttribution(by: MetricAttribution): string;
|
|
148
|
-
/** The credit for a tight space (a card, a carousel tile): "per
|
|
144
|
+
/** The credit for a tight space (a card, a carousel tile): "per Northwind Engineering". */
|
|
149
145
|
export declare function shortAttribution(by: Pick<MetricAttribution, 'organization'>): string;
|
|
150
146
|
/**
|
|
151
147
|
* Carry a person's reported numbers through a regeneration. The generator can
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sonordev/contracts/proposal-sitemap — how a proposal's site plan is counted (v1)
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the numbers a website proposal's site plan
|
|
5
|
+
* (the `SitemapPlan` block) states: how many pages the build has, when the
|
|
6
|
+
* "before → after" comparison may show, which custom labels the block may
|
|
7
|
+
* carry, and whether page counts written elsewhere in the proposal agree with
|
|
8
|
+
* the plan. The proposal generator stamps them, the Sonor API checks them and
|
|
9
|
+
* the dashboard renders them, so the headline, the plan and the price can't
|
|
10
|
+
* each state a different number.
|
|
11
|
+
*
|
|
12
|
+
* Why it exists: the numbers in a plan are what a buyer reads first, and a
|
|
13
|
+
* count written by a language model can't be trusted to add up. A rebuild
|
|
14
|
+
* that keeps every page must never read as a loss ("24 → 0 pages"), one
|
|
15
|
+
* address listed twice must not count twice, and the headline must not state
|
|
16
|
+
* a number the plan doesn't. Counts are computed here, never taken from prose.
|
|
17
|
+
*/
|
|
18
|
+
/** Bump on breaking changes to how a plan is counted. */
|
|
19
|
+
export declare const PROPOSAL_SITEMAP_CONTRACT_VERSION: 1;
|
|
20
|
+
/**
|
|
21
|
+
* A count the plan states: a non-negative whole number, as a number or a
|
|
22
|
+
* numeric string. Anything else is no count at all (undefined), never 0.
|
|
23
|
+
*/
|
|
24
|
+
export declare function sitemapCount(v: unknown): number | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* The address a plan row stands for, in one shape: lowercase, no scheme or
|
|
27
|
+
* host, no query or fragment, a leading slash and no trailing one ("/" for the
|
|
28
|
+
* home page). Undefined for a row without a slug; such a row always counts,
|
|
29
|
+
* since nothing proves it repeats another.
|
|
30
|
+
*/
|
|
31
|
+
export declare function sitemapPageKey(slug: unknown): string | undefined;
|
|
32
|
+
export interface SitemapPlanCounts {
|
|
33
|
+
/** Core pages (Home, About, Contact and the like), each address once. */
|
|
34
|
+
core: number;
|
|
35
|
+
/**
|
|
36
|
+
* The plan's own pages: top-level pages plus the pages under them, minus
|
|
37
|
+
* any address already counted as a core page.
|
|
38
|
+
*/
|
|
39
|
+
architecture: number;
|
|
40
|
+
/** Existing articles re-published with the build (the plan's `blog.count`). */
|
|
41
|
+
articles: number;
|
|
42
|
+
/** Everything the client gets: core + architecture + articles. */
|
|
43
|
+
full: number;
|
|
44
|
+
/**
|
|
45
|
+
* Pages that exist today and carry over (status rebuild, optimize or
|
|
46
|
+
* migrate, plus the re-published articles).
|
|
47
|
+
*/
|
|
48
|
+
existing: number;
|
|
49
|
+
/** Pages the build adds (status new, or no status). */
|
|
50
|
+
added: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Count a site plan's pages, each address once. Core pages are counted
|
|
54
|
+
* first, then each top-level page and the pages under it; a row whose
|
|
55
|
+
* address was already counted is skipped. Rows without a slug always count.
|
|
56
|
+
*/
|
|
57
|
+
export declare function countSitemapPlan(props: unknown): SitemapPlanCounts;
|
|
58
|
+
export interface SitemapTransformationCounts {
|
|
59
|
+
/** Today's count, as the plan states it. */
|
|
60
|
+
before: number;
|
|
61
|
+
/** The build's count. */
|
|
62
|
+
after: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The before → after comparison a plan may show, or null when it must not
|
|
66
|
+
* show. It shows only when the plan states today's count and the build is
|
|
67
|
+
* bigger. A missing after-count falls back to the plan's own pages (then its
|
|
68
|
+
* `totalPages`); an after-count of zero, or one no bigger than today's, hides
|
|
69
|
+
* the comparison. A rebuild that keeps the same pages isn't a before and after.
|
|
70
|
+
*/
|
|
71
|
+
export declare function sitemapTransformation(props: unknown): SitemapTransformationCounts | null;
|
|
72
|
+
/**
|
|
73
|
+
* Words a plan uses for its own pages, for a business whose pages aren't
|
|
74
|
+
* services sold to industries (communities and towns, practice areas,
|
|
75
|
+
* locations). Each replaces one default: `architecture` the heading
|
|
76
|
+
* ("Service Architecture"), `pillar` a top-level page ("Service pillar"),
|
|
77
|
+
* `child` a page under it ("Industry page"). Singular, title case.
|
|
78
|
+
*/
|
|
79
|
+
export interface SitemapPlanLabels {
|
|
80
|
+
architecture?: string;
|
|
81
|
+
pillar?: string;
|
|
82
|
+
child?: string;
|
|
83
|
+
}
|
|
84
|
+
/** The longest a label may be. */
|
|
85
|
+
export declare const SITEMAP_LABEL_MAX = 40;
|
|
86
|
+
/**
|
|
87
|
+
* Keep only usable labels: strings of 1 to {@link SITEMAP_LABEL_MAX}
|
|
88
|
+
* characters on one line, whitespace collapsed, no angle brackets. Undefined
|
|
89
|
+
* when none survive, so a plan without labels reads the defaults.
|
|
90
|
+
*/
|
|
91
|
+
export declare function normalizeSitemapLabels(raw: unknown): SitemapPlanLabels | undefined;
|
|
92
|
+
export type PageCountUnit = 'page' | 'url';
|
|
93
|
+
export interface PageCountClaim {
|
|
94
|
+
/** The number stated. */
|
|
95
|
+
value: number;
|
|
96
|
+
/** Whether it counts pages or URLs. */
|
|
97
|
+
unit: PageCountUnit;
|
|
98
|
+
/** The words that make the claim, e.g. "27 existing URLs". */
|
|
99
|
+
text: string;
|
|
100
|
+
/** Where the claim starts in the text. */
|
|
101
|
+
index: number;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Every page or URL count stated in a piece of text. A number that's part of
|
|
105
|
+
* a name ("Troy 7 Apartments get pages of their own") isn't a count.
|
|
106
|
+
*/
|
|
107
|
+
export declare function findPageCountClaims(text: unknown): PageCountClaim[];
|
|
108
|
+
export interface PageCountIssue {
|
|
109
|
+
/** The block the claim is in, e.g. "GlassHero". */
|
|
110
|
+
section: string;
|
|
111
|
+
/** Where in the block, e.g. "stats[0]" or "tiers[0].features[1]". */
|
|
112
|
+
field: string;
|
|
113
|
+
/** The number stated. */
|
|
114
|
+
claimed: number;
|
|
115
|
+
unit: PageCountUnit;
|
|
116
|
+
/** The words that make the claim. */
|
|
117
|
+
text: string;
|
|
118
|
+
/** The page count the plan adds up to. */
|
|
119
|
+
planned: number;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Page and URL counts a proposal states that match nothing its site plan or
|
|
123
|
+
* its measured evidence adds up to. A stated count passes when it equals any
|
|
124
|
+
* of: the plan's full, core, own, article, carried-over or added page count;
|
|
125
|
+
* the plan's stated before-count; or the measured URL and page counts the
|
|
126
|
+
* evidence blocks carry. Empty when the proposal has no site plan.
|
|
127
|
+
*/
|
|
128
|
+
export declare function proposalPageCountIssues(sections: unknown): PageCountIssue[];
|
|
129
|
+
/** One plain sentence for a person reviewing the proposal before it's sent. */
|
|
130
|
+
export declare function describePageCountIssue(issue: PageCountIssue): string;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-LD template placeholders: found once, dropped wherever sonor-api serves
|
|
3
|
+
* stored schema to a site, and never stored as implemented by register-schema.
|
|
4
|
+
*
|
|
5
|
+
* Stored JSON-LD (seo_schema_markup.schema_json, seo_pages.managed_schema) can
|
|
6
|
+
* carry a template's unfilled slots. Schema extracted from a site's source,
|
|
7
|
+
* where the base URL was a variable, comes back as https://example.com; an AI
|
|
8
|
+
* filling a template leaves "Example", "+1-000-000-0000", "[Resident Name]",
|
|
9
|
+
* "{plan.name}", or an object that is only a "note" saying what goes there.
|
|
10
|
+
* site-kit renders all of it on the live page, where search engines and AI
|
|
11
|
+
* crawlers take it as the business's identity. Observed 2026-10-01: a live
|
|
12
|
+
* site served an Organization named "Example", phone +1-000-000-0000, on its
|
|
13
|
+
* home page, and dozens more of its implemented rows carried example.com URLs
|
|
14
|
+
* and unfilled template slots.
|
|
15
|
+
*
|
|
16
|
+
* Conservative on purpose, because a false positive removes real schema from a
|
|
17
|
+
* live site:
|
|
18
|
+
* - Domains: only the reserved example domains (example.com, example.net,
|
|
19
|
+
* example.org, their subdomains, and the .example TLD), and only when the
|
|
20
|
+
* whole value is a URL, host or email address. A real domain with
|
|
21
|
+
* "example" in its path, or prose that mentions example.com, is fine.
|
|
22
|
+
* - Names: whole-value matches only (trimmed, case-insensitive) on name-like
|
|
23
|
+
* properties. "Example Plumbing Co" is a real name.
|
|
24
|
+
* - Phones (telephone, faxNumber, tel: links): all zeros, 123-456-7890,
|
|
25
|
+
* XXX-XXX-XXXX, and 555-0100 through 555-0199, the North American range
|
|
26
|
+
* reserved for fiction. 555-0200 and 555-1212 are real numbers.
|
|
27
|
+
* - Template slots: a value that is wholly a bracketed slot ("[YYYY-MM-DD]")
|
|
28
|
+
* or a JSX expression ("{plan.name}"), a {{mustache}} slot, a
|
|
29
|
+
* REPLACE_WITH_ marker, a title-case field name in brackets inside text
|
|
30
|
+
* ("Call [Phone Number]"), or a JSX member expression inside a URL. A URI
|
|
31
|
+
* template's {search_term_string} is how schema.org spells a SearchAction
|
|
32
|
+
* and is never a finding.
|
|
33
|
+
* - Annotations: an object that holds nothing but an AI note. A note key on
|
|
34
|
+
* a real node is stripped (it isn't schema.org vocabulary) and the node
|
|
35
|
+
* stays.
|
|
36
|
+
*
|
|
37
|
+
* The unit is the NODE: the innermost object with @type or @id (or a top-level
|
|
38
|
+
* or @graph member) whose own values hold the placeholder. A placeholder node
|
|
39
|
+
* is dropped whole, since its other values came from the same unfilled
|
|
40
|
+
* template. Its real siblings, and a real parent it hangs off, stay: an
|
|
41
|
+
* FAQPage keeps its questions when only its publisher was a placeholder. A
|
|
42
|
+
* node left with nothing but JSON-LD keywords once its placeholder children
|
|
43
|
+
* are gone (a BreadcrumbList whose every item was a placeholder) goes too, so
|
|
44
|
+
* the site falls back to what it generates itself.
|
|
45
|
+
*
|
|
46
|
+
* Single source of truth for "is this stored schema a placeholder", published
|
|
47
|
+
* as `@sonordev/contracts/schema-placeholders`: sonor-api's public schema reads
|
|
48
|
+
* and register-schema, and site-kit's ManagedSchema and LLMSchema (so a site
|
|
49
|
+
* drops a placeholder whichever Sonor source served it) all call it. Add new
|
|
50
|
+
* shapes here, with a test, never in a caller.
|
|
51
|
+
*/
|
|
52
|
+
export type SchemaPlaceholderReason = 'example_domain' | 'placeholder_phone' | 'placeholder_name' | 'template_token' | 'annotation' | 'emptied';
|
|
53
|
+
export interface SchemaPlaceholderEvidence {
|
|
54
|
+
reason: SchemaPlaceholderReason;
|
|
55
|
+
/** The property that held the value. */
|
|
56
|
+
key: string;
|
|
57
|
+
/** The value, cut short for logs. */
|
|
58
|
+
value: string;
|
|
59
|
+
}
|
|
60
|
+
export interface SchemaPlaceholderNode {
|
|
61
|
+
/** Where the node sits: '$' for the root, then .key and [index] steps. */
|
|
62
|
+
path: string;
|
|
63
|
+
/** The node's @type ('A,B' when it has several), or null. */
|
|
64
|
+
type: string | null;
|
|
65
|
+
reasons: SchemaPlaceholderReason[];
|
|
66
|
+
evidence: SchemaPlaceholderEvidence[];
|
|
67
|
+
}
|
|
68
|
+
export interface SchemaPlaceholderScan<T> {
|
|
69
|
+
/** The value without its placeholder nodes: the same reference when nothing was found, null when nothing real is left. */
|
|
70
|
+
value: T | null;
|
|
71
|
+
/** Every node removed, outermost only (a dropped node's children go with it). */
|
|
72
|
+
dropped: SchemaPlaceholderNode[];
|
|
73
|
+
/** AI note keys stripped from nodes that were kept. */
|
|
74
|
+
notes: Array<{
|
|
75
|
+
path: string;
|
|
76
|
+
key: string;
|
|
77
|
+
}>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The value with its placeholder nodes removed, and what was removed. Pure; the
|
|
81
|
+
* input is never mutated.
|
|
82
|
+
*/
|
|
83
|
+
export declare function withoutSchemaPlaceholders<T>(value: T): SchemaPlaceholderScan<T>;
|
|
84
|
+
/** Which nodes of a JSON-LD value are placeholders, and why. */
|
|
85
|
+
export declare function findSchemaPlaceholders(value: unknown): SchemaPlaceholderNode[];
|
|
86
|
+
/** One line a person can act on: 'Organization at $: name "Example" is a placeholder name'. */
|
|
87
|
+
export declare function describeSchemaPlaceholder(node: SchemaPlaceholderNode): string;
|