@aglyn/plugins-ai 1.0.0-beta.270 → 1.0.0-beta.272
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/package.json +17 -17
- package/src/lib/activity/ai-activity-actions.d.ts +4 -3
- package/src/lib/activity/ai-activity-actions.js +4 -0
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/assets/site-styles/beauty.webp +0 -0
- package/src/lib/assets/site-styles/blog.webp +0 -0
- package/src/lib/assets/site-styles/business.webp +0 -0
- package/src/lib/assets/site-styles/coming-soon.webp +0 -0
- package/src/lib/assets/site-styles/education.webp +0 -0
- package/src/lib/assets/site-styles/events.webp +0 -0
- package/src/lib/assets/site-styles/fitness.webp +0 -0
- package/src/lib/assets/site-styles/landing.webp +0 -0
- package/src/lib/assets/site-styles/music.webp +0 -0
- package/src/lib/assets/site-styles/nonprofit.webp +0 -0
- package/src/lib/assets/site-styles/personal.webp +0 -0
- package/src/lib/assets/site-styles/photography.webp +0 -0
- package/src/lib/assets/site-styles/portfolio.webp +0 -0
- package/src/lib/assets/site-styles/professional.webp +0 -0
- package/src/lib/assets/site-styles/realestate.webp +0 -0
- package/src/lib/assets/site-styles/restaurant.webp +0 -0
- package/src/lib/assets/site-styles/store.webp +0 -0
- package/src/lib/assets/site-styles/studio.webp +0 -0
- package/src/lib/assets/site-styles/trades.webp +0 -0
- package/src/lib/assets/site-styles/wellness.webp +0 -0
- package/src/lib/assets/site-styles/yoga.webp +0 -0
- package/src/lib/components/ai-credits-card.component.d.ts +4 -3
- package/src/lib/components/ai-credits-card.component.js +15 -15
- package/src/lib/components/ai-credits-card.component.js.map +1 -1
- package/src/lib/components/ai-job-input-form.component.d.ts +36 -0
- package/src/lib/components/ai-job-input-form.component.js +100 -0
- package/src/lib/components/ai-job-input-form.component.js.map +1 -0
- package/src/lib/components/ai-job-requests.d.ts +4 -0
- package/src/lib/components/ai-job-requests.js +4 -0
- package/src/lib/components/ai-job-requests.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +18 -0
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-import-job.component.d.ts +41 -0
- package/src/lib/components/ai-site-import-job.component.js +707 -0
- package/src/lib/components/ai-site-import-job.component.js.map +1 -0
- package/src/lib/components/ai-site-import-start.component.d.ts +64 -0
- package/src/lib/components/ai-site-import-start.component.js +335 -0
- package/src/lib/components/ai-site-import-start.component.js.map +1 -0
- package/src/lib/components/ai-site-ownership-proof.component.d.ts +42 -0
- package/src/lib/components/ai-site-ownership-proof.component.js +353 -0
- package/src/lib/components/ai-site-ownership-proof.component.js.map +1 -0
- package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
- package/src/lib/components/ai-site-start-card.component.js +261 -119
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-start-steps.component.d.ts +43 -2
- package/src/lib/components/ai-site-start-steps.component.js +651 -15
- package/src/lib/components/ai-site-start-steps.component.js.map +1 -1
- package/src/lib/components/ai-site-start-tracking.d.ts +10 -0
- package/src/lib/components/ai-site-start-tracking.js +14 -0
- package/src/lib/components/ai-site-start-tracking.js.map +1 -1
- package/src/lib/components/ai-site-style-previews.js +26 -22
- package/src/lib/components/ai-site-style-previews.js.map +1 -1
- package/src/lib/components/ai-upsell-dialog.component.d.ts +7 -5
- package/src/lib/components/ai-upsell-dialog.component.js +8 -6
- package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
- package/src/lib/components/assist-build-card.component.js +11 -1
- package/src/lib/components/assist-build-card.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +9 -1
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +8 -4
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/staff-org-ai-card.component.js +4 -7
- package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
- package/src/lib/declarations.d.ts +2 -2
- package/src/lib/declarations.js +9 -5
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.js +3 -1
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-business-profile-prefill.d.ts +1 -1
- package/src/lib/jobs/ai-business-profile-prefill.js +14 -3
- package/src/lib/jobs/ai-business-profile-prefill.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.js +5 -3
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +14 -0
- package/src/lib/jobs/ai-job-drafts.js +24 -0
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-form-step.d.ts +15 -1
- package/src/lib/jobs/ai-job-form-step.js +75 -8
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +136 -0
- package/src/lib/jobs/ai-job-import-step.js +809 -0
- package/src/lib/jobs/ai-job-import-step.js.map +1 -0
- package/src/lib/jobs/ai-job-page-language.d.ts +52 -1
- package/src/lib/jobs/ai-job-page-language.js +99 -5
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +19 -2
- package/src/lib/jobs/ai-job-page-step.js +42 -18
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +25 -2
- package/src/lib/jobs/ai-job-plan-step.js +103 -29
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-settings-step.js +4 -3
- package/src/lib/jobs/ai-job-settings-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +27 -0
- package/src/lib/jobs/ai-job-site-datasets.js +140 -14
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-look.js +18 -5
- package/src/lib/jobs/ai-job-site-look.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +16 -1
- package/src/lib/jobs/ai-job-site-step.js +95 -22
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.d.ts +15 -1
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-job-tracking-step.d.ts +14 -3
- package/src/lib/jobs/ai-job-tracking-step.js +28 -5
- package/src/lib/jobs/ai-job-tracking-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +13 -1
- package/src/lib/jobs/ai-jobs.js +77 -10
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-owner-photos.d.ts +16 -1
- package/src/lib/jobs/ai-layout-owner-photos.js +47 -0
- package/src/lib/jobs/ai-layout-owner-photos.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +27 -2
- package/src/lib/jobs/ai-layout-stock-photos.js +190 -19
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-inspiration-signals.d.ts +98 -0
- package/src/lib/jobs/ai-site-inspiration-signals.js +585 -0
- package/src/lib/jobs/ai-site-inspiration-signals.js.map +1 -0
- package/src/lib/jobs/ai-site-inspiration.d.ts +157 -0
- package/src/lib/jobs/ai-site-inspiration.js +346 -0
- package/src/lib/jobs/ai-site-inspiration.js.map +1 -0
- package/src/lib/jobs/ai-site-publish.d.ts +26 -1
- package/src/lib/jobs/ai-site-publish.js +162 -13
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/ai-theme-brand-inputs.d.ts +77 -3
- package/src/lib/jobs/ai-theme-brand-inputs.js +135 -31
- package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-candle-shop-free-plan-recording.d.ts +97 -0
- package/src/lib/jobs/fixtures/ai-candle-shop-free-plan-recording.js +137 -0
- package/src/lib/jobs/fixtures/ai-candle-shop-free-plan-recording.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-site-inspiration-pages.d.ts +38 -0
- package/src/lib/jobs/fixtures/ai-site-inspiration-pages.js +103 -0
- package/src/lib/jobs/fixtures/ai-site-inspiration-pages.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3988-recordings.d.ts +25 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3988-recordings.js +174 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3988-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +1 -1
- package/src/lib/layout-language/ai-layout-compiler.js +438 -25
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-contact.d.ts +57 -0
- package/src/lib/layout-language/ai-layout-contact.js +137 -0
- package/src/lib/layout-language/ai-layout-contact.js.map +1 -0
- package/src/lib/layout-language/ai-layout-copy.d.ts +12 -0
- package/src/lib/layout-language/ai-layout-copy.js +34 -2
- package/src/lib/layout-language/ai-layout-copy.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +65 -8
- package/src/lib/layout-language/ai-layout-design.js +411 -97
- package/src/lib/layout-language/ai-layout-design.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.js +23 -0
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-labels.d.ts +2 -0
- package/src/lib/layout-language/ai-layout-labels.js +12 -6
- package/src/lib/layout-language/ai-layout-labels.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.d.ts +3 -2
- package/src/lib/layout-language/ai-layout-language.js +4 -3
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/layout-language/ai-layout-links.d.ts +13 -0
- package/src/lib/layout-language/ai-layout-links.js +30 -0
- package/src/lib/layout-language/ai-layout-links.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +49 -6
- package/src/lib/layout-language/ai-layout-pictures.js +145 -22
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +16 -1
- package/src/lib/model/ai-build-job.js +40 -0
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +20 -0
- package/src/lib/model/ai-credit-estimate.js +13 -0
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +2 -2
- package/src/lib/model/ai-job-activity.js +6 -4
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-job-input.d.ts +33 -0
- package/src/lib/model/ai-job-input.js +59 -0
- package/src/lib/model/ai-job-input.js.map +1 -0
- package/src/lib/model/ai-job-notice.d.ts +4 -0
- package/src/lib/model/ai-job-notice.js +28 -4
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +69 -3
- package/src/lib/model/ai-jobs.types.js +2 -1
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-details.d.ts +122 -0
- package/src/lib/model/ai-site-details.js +300 -0
- package/src/lib/model/ai-site-details.js.map +1 -0
- package/src/lib/model/ai-site-entity.d.ts +181 -0
- package/src/lib/model/ai-site-entity.js +390 -0
- package/src/lib/model/ai-site-entity.js.map +1 -0
- package/src/lib/model/ai-site-import-view.d.ts +130 -0
- package/src/lib/model/ai-site-import-view.js +182 -0
- package/src/lib/model/ai-site-import-view.js.map +1 -0
- package/src/lib/model/ai-site-import.d.ts +302 -0
- package/src/lib/model/ai-site-import.js +526 -0
- package/src/lib/model/ai-site-import.js.map +1 -0
- package/src/lib/model/ai-site-inspiration.d.ts +72 -0
- package/src/lib/model/ai-site-inspiration.js +109 -0
- package/src/lib/model/ai-site-inspiration.js.map +1 -0
- package/src/lib/model/ai-site-job.d.ts +13 -1
- package/src/lib/model/ai-site-job.js +15 -2
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-kinds.js +109 -21
- package/src/lib/model/ai-site-kinds.js.map +1 -1
- package/src/lib/model/ai-site-look.d.ts +10 -0
- package/src/lib/model/ai-site-look.js +24 -0
- package/src/lib/model/ai-site-look.js.map +1 -1
- package/src/lib/model/ai-site-start-seo.d.ts +19 -5
- package/src/lib/model/ai-site-start-seo.js +25 -7
- package/src/lib/model/ai-site-start-seo.js.map +1 -1
- package/src/lib/model/ai-site-start-steps.d.ts +9 -3
- package/src/lib/model/ai-site-start-steps.js +67 -5
- package/src/lib/model/ai-site-start-steps.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +81 -4
- package/src/lib/model/ai-site-start.js +207 -13
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/model/ai-upsell-copy.js +4 -1
- package/src/lib/model/ai-upsell-copy.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +4 -3
- package/src/lib/plan-entitlements.js +3 -8
- package/src/lib/plan-entitlements.js.map +1 -1
- package/src/lib/runtime/ai-dataset-generation.d.ts +8 -1
- package/src/lib/runtime/ai-dataset-generation.js +12 -2
- package/src/lib/runtime/ai-dataset-generation.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +26 -33
- package/src/lib/runtime/ai-doctrine-validators.js +54 -44
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +1 -1
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.d.ts +7 -0
- package/src/lib/runtime/ai-node-tree.js +9 -1
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/server/ai-create-route.d.ts +1 -1
- package/src/lib/server/ai-create-route.js +3 -4
- package/src/lib/server/ai-create-route.js.map +1 -1
- package/src/lib/server/ai-jobs-resume.js +27 -3
- package/src/lib/server/ai-jobs-resume.js.map +1 -1
- package/src/lib/server/ai-jobs-route.js +18 -0
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/server/ai-site-import-route.d.ts +89 -0
- package/src/lib/server/ai-site-import-route.js +356 -0
- package/src/lib/server/ai-site-import-route.js.map +1 -0
- package/src/lib/server.js +14 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/site-import/fixtures/site-import-crawl-recordings.d.ts +45 -0
- package/src/lib/site-import/fixtures/site-import-crawl-recordings.js +313 -0
- package/src/lib/site-import/fixtures/site-import-crawl-recordings.js.map +1 -0
- package/src/lib/site-import/fixtures/site-import-wave-recordings.d.ts +50 -0
- package/src/lib/site-import/fixtures/site-import-wave-recordings.js +217 -0
- package/src/lib/site-import/fixtures/site-import-wave-recordings.js.map +1 -0
- package/src/lib/site-import/site-import-crawl.d.ts +163 -0
- package/src/lib/site-import/site-import-crawl.js +467 -0
- package/src/lib/site-import/site-import-crawl.js.map +1 -0
- package/src/lib/site-import/site-import-flag.d.ts +23 -0
- package/src/lib/site-import/site-import-flag.js +23 -0
- package/src/lib/site-import/site-import-flag.js.map +1 -0
- package/src/lib/site-import/site-import-html.d.ts +94 -0
- package/src/lib/site-import/site-import-html.js +462 -0
- package/src/lib/site-import/site-import-html.js.map +1 -0
- package/src/lib/site-import/site-import-images.d.ts +54 -0
- package/src/lib/site-import/site-import-images.js +204 -0
- package/src/lib/site-import/site-import-images.js.map +1 -0
- package/src/lib/site-import/site-import-page.d.ts +48 -0
- package/src/lib/site-import/site-import-page.js +150 -0
- package/src/lib/site-import/site-import-page.js.map +1 -0
- package/src/lib/site-import/site-import-review.d.ts +40 -0
- package/src/lib/site-import/site-import-review.js +149 -0
- package/src/lib/site-import/site-import-review.js.map +1 -0
- package/src/lib/site-import/site-import-robots.d.ts +52 -0
- package/src/lib/site-import/site-import-robots.js +126 -0
- package/src/lib/site-import/site-import-robots.js.map +1 -0
- package/src/lib/usage/assist-credits.d.ts +4 -4
- package/src/lib/usage/assist-credits.js +4 -4
- package/src/lib/usage/assist-credits.js.map +1 -1
- package/src/lib/usage/staff-org-ai-view.js +6 -3
- package/src/lib/usage/staff-org-ai-view.js.map +1 -1
- package/src/lib/usage/staff-org-ai.d.ts +4 -2
- package/src/lib/usage/staff-org-ai.js +7 -7
- package/src/lib/usage/staff-org-ai.js.map +1 -1
- package/src/lib/usage/staff-org-usage-ai.js +2 -2
- package/src/lib/usage/staff-org-usage-ai.js.map +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-details.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AREA_SERVED_MAX,\n PRICE_RANGE_MAX_LENGTH,\n normalizeAreaServed,\n parseOpeningHours,\n type OpeningHoursRow,\n} from '@aglyn/aglyn/app-utils/local-business'\n\n/**\n * The business details a guided start may be given beyond what the site is\n * for (AGL-3979): how to reach it, where it is, when it is open, where it\n * works and the profiles that are its own. Every one is optional. Given,\n * they are what makes a new site read as this business the first time its\n * owner opens it — the phone number in the footer, the email on the contact\n * page — and what its structured data and its `/llms.txt` say about it.\n *\n * They travel as flat strings among the `site` job's scalar inputs, like the\n * answers before them, so a failed start's summary carries them back and Try\n * again reopens the questions filled in. Each one has its own rule here,\n * shared by the questions (which say what is wrong beside the field) and the\n * create door (`parseAiSiteJobInputs`, which refuses what the questions would\n * not have sent).\n *\n * Pure: strings in, strings out.\n */\n\n/** Every detail, by its input key. */\nexport const AI_SITE_DETAIL_KEYS = [\n 'phone',\n 'email',\n 'tagline',\n 'founded',\n 'serviceArea',\n 'priceRange',\n 'languages',\n 'street',\n 'region',\n 'postalCode',\n 'country',\n 'hours',\n 'social',\n] as const\n\nexport type AiSiteDetailKey = (typeof AI_SITE_DETAIL_KEYS)[number]\n\n/** The details as a job's inputs carry them, each `''` where nobody said. */\nexport type AiSiteDetails = Record<AiSiteDetailKey, string>\n\n/** The longest each detail may be, in characters. Together far under the door's 8,000. */\nexport const AI_SITE_DETAIL_MAX_CHARS: Readonly<Record<AiSiteDetailKey, number>> = {\n phone: 32,\n email: 254,\n tagline: 120,\n founded: 4,\n serviceArea: 400,\n priceRange: PRICE_RANGE_MAX_LENGTH,\n languages: 120,\n street: 120,\n region: 80,\n postalCode: 20,\n country: 80,\n hours: 600,\n social: 1_200,\n}\n\n/** The most profile links a start takes. */\nexport const AI_SITE_SOCIAL_MAX = 6\n\n/** No details at all. */\nexport const AI_SITE_NO_DETAILS: Readonly<AiSiteDetails> = Object.fromEntries(\n AI_SITE_DETAIL_KEYS.map((key) => [key, '']),\n) as unknown as AiSiteDetails\n\n/** Why a detail cannot be used, as a closed code the funnel counts and a sentence beside the field. */\nexport interface AiSiteDetailProblem {\n code: 'too_long' | 'invalid'\n message: string\n}\n\nconst PHONE_CHARS = /^\\+?[\\d\\s().-]+$/\n\n/** Whether a phone number reads as one: digits with the usual separators, a `+` only first, 7 to 15 digits. */\nexport function aiSitePhoneValid(value: string): boolean {\n const text = value.trim()\n if (!PHONE_CHARS.test(text)) return false\n const digits = text.replace(/\\D/g, '')\n return digits.length >= 7 && digits.length <= 15\n}\n\n/**\n * The `tel:` address of a phone number, or `''` for one that does not read.\n *\n * Its digits are the number's own, with a `+` only where the person typed one:\n * no country is inferred, so the link dials the number exactly as given, and\n * the layout's own check, which reads a number by its digits, finds the same\n * digits in the brief as in the link.\n */\nexport function aiSitePhoneHref(value: string): string {\n if (!aiSitePhoneValid(value)) return ''\n const text = value.trim()\n return `tel:${text.startsWith('+') ? '+' : ''}${text.replace(/\\D/g, '')}`\n}\n\nconst EMAIL = /^[^\\s@<>()[\\]\\\\,;:\"]+@[^\\s@<>()[\\]\\\\,;:\"]+\\.[^\\s@<>()[\\]\\\\,;:\".]{2,}$/\n\n/** Whether an email address reads as one. */\nexport function aiSiteEmailValid(value: string): boolean {\n const text = value.trim()\n return text.length <= AI_SITE_DETAIL_MAX_CHARS.email && EMAIL.test(text)\n}\n\n/** The `mailto:` address of an email, or `''` for one that does not read. */\nexport function aiSiteEmailHref(value: string): string {\n return aiSiteEmailValid(value) ? `mailto:${value.trim()}` : ''\n}\n\n/** A link typed without its scheme is the https page it names; a plain `http:` one is read as https. */\nfunction asHttps(value: string): string {\n const text = value.trim()\n if (/^https:\\/\\//i.test(text)) return text\n if (/^http:\\/\\//i.test(text)) return `https://${text.slice(7)}`\n if (/^(?:www\\.)?[a-z0-9-]+(?:\\.[a-z0-9-]+)*\\.[a-z]{2,}(?:[/?#]\\S*)?$/i.test(text)) return `https://${text}`\n return text\n}\n\nfunction isHttps(value: string): boolean {\n try {\n const url = new URL(value)\n return url.protocol === 'https:' && url.hostname.includes('.')\n } catch {\n return false\n }\n}\n\n/**\n * The profile links a detail names, one a line (commas and spaces separate\n * them too), each an https page, at most {@link AI_SITE_SOCIAL_MAX}, each\n * once; `null` when one of them is not a link at all.\n */\nexport function aiSiteSocialLinks(value: string): string[] | null {\n const links: string[] = []\n for (const part of value.split(/[\\s,]+/)) {\n if (!part.trim()) continue\n const link = asHttps(part)\n if (!isHttps(link)) return null\n if (!links.some((seen) => seen.toLowerCase() === link.toLowerCase())) links.push(link)\n }\n return links.slice(0, AI_SITE_SOCIAL_MAX)\n}\n\n/** The areas a detail names, one a line or separated by commas, each once, at most twenty. */\nexport function aiSiteServiceAreas(value: string): string[] {\n return normalizeAreaServed(value.split(/\\r?\\n|,|;/)).slice(0, AREA_SERVED_MAX)\n}\n\n/** The opening hours a detail holds, as the lines that read (`Mo-Fr 09:00-17:00`), one a line. */\nexport function aiSiteHoursText(value: string): string {\n return value\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter((line) => line && parseOpeningHours(line).length)\n .join('\\n')\n}\n\nconst DAY_SHORT: Readonly<Record<string, string>> = {\n Monday: 'Mon',\n Tuesday: 'Tue',\n Wednesday: 'Wed',\n Thursday: 'Thu',\n Friday: 'Fri',\n Saturday: 'Sat',\n Sunday: 'Sun',\n}\nconst WEEK = Object.keys(DAY_SHORT)\n\n/** \"09:00\" as \"9am\", \"17:30\" as \"5:30pm\", \"00:00\" as \"12am\". */\nfunction clock(time: string): string {\n const [h = 0, m = 0] = time.split(':').map(Number)\n const suffix = h < 12 ? 'am' : 'pm'\n const hour = h % 12 === 0 ? 12 : h % 12\n return `${hour}${m ? `:${String(m).padStart(2, '0')}` : ''}${suffix}`\n}\n\n/** A row's days as a reader says them: \"Mon–Fri\" for a run, \"Mon, Wed\" otherwise. */\nfunction daysOf(row: OpeningHoursRow): string {\n const at = row.days.map((day) => WEEK.indexOf(day)).sort((a, b) => a - b)\n const run = at.every((index, position) => position === 0 || index === at[position - 1] + 1)\n if (run && at.length > 2) return `${DAY_SHORT[WEEK[at[0]]]}–${DAY_SHORT[WEEK[at[at.length - 1]]]}`\n return at.map((index) => DAY_SHORT[WEEK[index]]).join(', ')\n}\n\n/**\n * The opening hours as a person reads them — \"Mon–Fri 9am–5pm; Sat 10am–2pm\"\n * — which is how the brief states them: the layout's own check admits a\n * footer's hours only where the brief gives hours it can read as hours, and\n * `Mo-Fr 09:00-17:00` is written for a machine.\n */\nexport function aiSiteHoursSentence(value: string): string {\n return parseOpeningHours(value)\n .map((row) => `${daysOf(row)} ${clock(row.opens)}–${clock(row.closes)}`)\n .join('; ')\n}\n\n/**\n * What is wrong with one detail, worded for the person beside its field; `null`\n * when it may be used. An empty detail is always fine: every one is optional.\n */\nexport function aiSiteDetailProblem(\n key: AiSiteDetailKey,\n raw: unknown,\n options: { now?: Date } = {},\n): AiSiteDetailProblem | null {\n if (raw === undefined || raw === null || raw === '') return null\n if (typeof raw !== 'string') return { code: 'invalid', message: 'Use plain text here.' }\n const value = raw.trim()\n if (!value) return null\n const max = AI_SITE_DETAIL_MAX_CHARS[key]\n if (value.length > max) return { code: 'too_long', message: `Keep this under ${max} characters.` }\n switch (key) {\n case 'phone':\n return aiSitePhoneValid(value)\n ? null\n : { code: 'invalid', message: 'Enter the phone number with its area code, like (512) 555-0142 or +44 20 7946 0958.' }\n case 'email':\n return aiSiteEmailValid(value) ? null : { code: 'invalid', message: 'Enter an email address, like hello@yourbusiness.com.' }\n case 'founded': {\n const year = Number(value)\n const latest = (options.now ?? new Date()).getFullYear()\n return /^\\d{4}$/.test(value) && year >= 1000 && year <= latest\n ? null\n : { code: 'invalid', message: `Enter the year as four digits, like ${latest - 10}.` }\n }\n case 'social':\n return aiSiteSocialLinks(value)\n ? null\n : { code: 'invalid', message: 'Paste each profile’s address, one a line, like instagram.com/yourbusiness.' }\n default:\n return null\n }\n}\n\n/**\n * The details a job's inputs carry, each held to its rule and tidied — the\n * profile links as https pages one a line, the areas one a line, only the\n * opening hours that read — or the sentence the create door refuses them\n * with. A key the inputs leave out is `''`.\n */\nexport function parseAiSiteDetails(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n options: { now?: Date } = {},\n): AiSiteDetails | string {\n const details = { ...AI_SITE_NO_DETAILS }\n for (const key of AI_SITE_DETAIL_KEYS) {\n const raw = inputs?.[key]\n const problem = aiSiteDetailProblem(key, raw, options)\n if (problem) return `${key}: ${problem.message}`\n const value = typeof raw === 'string' ? raw.trim() : ''\n if (!value) continue\n details[key] =\n key === 'social'\n ? (aiSiteSocialLinks(value) ?? []).join('\\n')\n : key === 'serviceArea'\n ? aiSiteServiceAreas(value).join('\\n')\n : key === 'hours'\n ? aiSiteHoursText(value)\n : value.replace(/[ \\t]+/g, ' ')\n }\n return details\n}\n\n/** A job's details, read leniently: anything that breaks its rule reads as not given. */\nexport function aiSiteDetailsOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiSiteDetails {\n const details = { ...AI_SITE_NO_DETAILS }\n for (const key of AI_SITE_DETAIL_KEYS) {\n const one = parseAiSiteDetails({ [key]: inputs?.[key] })\n if (typeof one !== 'string') details[key] = one[key]\n }\n return details\n}\n\n/** How a visitor reaches the business, as the pages link it; `null` when neither was given. */\nexport interface AiSiteContact {\n /** The number as the person wrote it, which is how the pages show it. */\n phone: string\n phoneHref: string\n email: string\n emailHref: string\n}\n\n/** The phone and email a job's inputs give, with the links they make; `null` for neither. */\nexport function aiSiteContactOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiSiteContact | null {\n const details = aiSiteDetailsOf(inputs)\n const phoneHref = aiSitePhoneHref(details.phone)\n const emailHref = aiSiteEmailHref(details.email)\n if (!phoneHref && !emailHref) return null\n return {\n phone: phoneHref ? details.phone : '',\n phoneHref,\n email: emailHref ? details.email : '',\n emailHref,\n }\n}\n\n/** The street address the details and the city make, as one line: \"214 Main Street, Austin, TX 78701, US\". */\nexport function aiSiteAddressLine(details: Pick<AiSiteDetails, 'street' | 'region' | 'postalCode' | 'country'>, city = ''): string {\n const regional = [details.region, details.postalCode].map((part) => part.trim()).filter(Boolean).join(' ')\n return [details.street, city, regional, details.country]\n .map((part) => part.trim())\n .filter(Boolean)\n .join(', ')\n}\n\n/**\n * What every unit of a site job is told about the business beyond what it is\n * (AGL-3979), on the job's own turn: its contact details, place and hours,\n * then everything else the person gave, then — where there is a phone or an\n * email — that the pages show them as written and how a link calls or writes.\n * Empty when the person gave no details, so a start without them is told\n * exactly what it was told before.\n */\nexport function aiSiteDetailsBriefLines(details: AiSiteDetails, options: { city?: string } = {}): string[] {\n const lines: string[] = []\n const address = details.street ? aiSiteAddressLine(details, options.city ?? '') : ''\n const hours = aiSiteHoursSentence(details.hours)\n const contact = [\n details.phone ? `phone: ${details.phone}` : '',\n details.email ? `email: ${details.email}` : '',\n address ? `address: ${address}` : '',\n hours ? `hours: ${hours}` : '',\n ].filter(Boolean)\n if (contact.length) lines.push(`Contact — ${contact.join('; ')}.`)\n const areas = aiSiteServiceAreas(details.serviceArea)\n const social = details.social ? details.social.split('\\n').filter(Boolean) : []\n const more = [\n details.tagline ? `tagline: ${details.tagline}` : '',\n details.founded ? `founded: ${details.founded}` : '',\n areas.length ? `serves: ${areas.join(', ')}` : '',\n details.priceRange ? `price range: ${details.priceRange}` : '',\n details.languages ? `languages: ${details.languages}` : '',\n social.length ? `profiles: ${social.join(', ')}` : '',\n ].filter(Boolean)\n if (more.length) lines.push(`About the business — ${more.join('; ')}.`)\n if (details.phone || details.email) {\n const what = details.phone && details.email ? 'phone number and email' : details.phone ? 'phone number' : 'email'\n const links = [details.phone ? 'a link that calls goes to \"phone\"' : '', details.email ? 'a link that writes goes to \"email\"' : '']\n .filter(Boolean)\n .join(' and ')\n lines.push(\n `Show the ${what} exactly as written wherever a visitor looks for a way to get in touch: the contact page, the footer, and the buttons that ask a visitor to call or write; ${links}.`,\n )\n }\n return lines\n}\n"],"names":["AREA_SERVED_MAX","PRICE_RANGE_MAX_LENGTH","normalizeAreaServed","parseOpeningHours","AI_SITE_DETAIL_KEYS","AI_SITE_DETAIL_MAX_CHARS","phone","email","tagline","founded","serviceArea","priceRange","languages","street","region","postalCode","country","hours","social","AI_SITE_SOCIAL_MAX","AI_SITE_NO_DETAILS","Object","fromEntries","map","key","PHONE_CHARS","aiSitePhoneValid","value","text","trim","test","digits","replace","length","aiSitePhoneHref","startsWith","EMAIL","aiSiteEmailValid","aiSiteEmailHref","asHttps","slice","isHttps","url","URL","protocol","hostname","includes","aiSiteSocialLinks","links","part","split","link","some","seen","toLowerCase","push","aiSiteServiceAreas","aiSiteHoursText","line","filter","join","DAY_SHORT","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday","WEEK","keys","clock","time","h","m","Number","suffix","hour","String","padStart","daysOf","row","at","days","day","indexOf","sort","a","b","run","every","index","position","aiSiteHoursSentence","opens","closes","aiSiteDetailProblem","raw","options","undefined","code","message","max","year","latest","now","Date","getFullYear","parseAiSiteDetails","inputs","details","problem","aiSiteDetailsOf","one","aiSiteContactOf","phoneHref","emailHref","aiSiteAddressLine","city","regional","Boolean","aiSiteDetailsBriefLines","lines","address","contact","areas","more","what"],"mappings":"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"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { LocalBusinessType } from '@aglyn/aglyn/app-utils/local-business';
|
|
18
|
+
import { type AiSiteSeoProposal } from './ai-site-start-seo';
|
|
19
|
+
/**
|
|
20
|
+
* Who publishes a site a guided start built, as its structured data and its
|
|
21
|
+
* `/llms.txt` say (AGL-3979).
|
|
22
|
+
*
|
|
23
|
+
* Every tenant page publishes a top-level Organization (or Person) node, and
|
|
24
|
+
* `/llms.txt` carries the site's agent guidance, both read off the host's
|
|
25
|
+
* Setup → SEO → Entity and AI agents fields (`siteEntityJsonLd`,
|
|
26
|
+
* `buildLlmsTxt`). Before this, a guided start wrote none of those fields:
|
|
27
|
+
* every AI-built site published a node holding its name and nothing else — no
|
|
28
|
+
* contact point, no address, no business type — and the guidance was the
|
|
29
|
+
* platform's own default. The answers the person gave are the facts those
|
|
30
|
+
* fields ask for, so the publish writes them.
|
|
31
|
+
*
|
|
32
|
+
* ── Derived, never generated ─────────────────────────────────────────────
|
|
33
|
+
*
|
|
34
|
+
* Each value is the person's own words or arithmetic on them: the schema
|
|
35
|
+
* type from the kind of site they picked (and, for a few kinds, the words
|
|
36
|
+
* they used), the description from the same composition as the site's
|
|
37
|
+
* search listing, the guidance from what they said and gave. No model is
|
|
38
|
+
* asked, and nothing is invented: a field the answers say nothing about is
|
|
39
|
+
* left alone.
|
|
40
|
+
*
|
|
41
|
+
* ── Never over the owner ─────────────────────────────────────────────────
|
|
42
|
+
*
|
|
43
|
+
* {@link aiSiteEntityUpdates} writes only fields that are empty, so a site
|
|
44
|
+
* whose owner already filled its Entity keeps every word of it, and a second
|
|
45
|
+
* publish (Try again) writes nothing it wrote before.
|
|
46
|
+
*
|
|
47
|
+
* ── The site's own listing ───────────────────────────────────────────────
|
|
48
|
+
*
|
|
49
|
+
* `/llms.txt` opens with the site's title and description, and every page
|
|
50
|
+
* without its own title falls back to them. A new site is created with its
|
|
51
|
+
* name and a "Welcome to …" line there, which nobody chose, so
|
|
52
|
+
* {@link aiSiteListingUpdates} puts the start's listing over that seeded
|
|
53
|
+
* default, and only over it: a title or description the owner changed keeps
|
|
54
|
+
* their words, and the SEO section offers the start's beside them.
|
|
55
|
+
*
|
|
56
|
+
* Pure: no Firestore and no React.
|
|
57
|
+
*/
|
|
58
|
+
/** How a kind of site publishes: as a person, or as a business of a `schema.org` LocalBusiness type. */
|
|
59
|
+
interface KindSchema {
|
|
60
|
+
person?: true;
|
|
61
|
+
businessType?: LocalBusinessType;
|
|
62
|
+
/** A narrower type the person's words name, first match wins. */
|
|
63
|
+
refine?: ReadonlyArray<readonly [RegExp, LocalBusinessType]>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Each `AI_SITE_KINDS` entry's `schema.org` type (`ai-site-entity.spec.ts`
|
|
67
|
+
* holds every kind to an entry). A kind with no business type publishes as an
|
|
68
|
+
* Organization — a blog, a band, an event, a course — unless the person gave
|
|
69
|
+
* a street address or opening hours, which make it a place a visitor goes:
|
|
70
|
+
* a LocalBusiness. Only types on the platform's `LOCAL_BUSINESS_TYPES` list
|
|
71
|
+
* are used, since only those publish.
|
|
72
|
+
*/
|
|
73
|
+
export declare const AI_SITE_KIND_SCHEMA: Readonly<Record<string, KindSchema>>;
|
|
74
|
+
/** The schema a site's answers publish under: a Person, or an Organization with a business type or none. */
|
|
75
|
+
export interface AiSiteSchema {
|
|
76
|
+
person: boolean;
|
|
77
|
+
businessType: LocalBusinessType | null;
|
|
78
|
+
}
|
|
79
|
+
/** What a job's inputs say the site publishes as. */
|
|
80
|
+
export declare function aiSiteSchemaOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiSiteSchema;
|
|
81
|
+
/** The Entity and AI-agent values a job's answers give; a field they say nothing about is `''` or `[]`. */
|
|
82
|
+
export interface AiSiteEntityValues {
|
|
83
|
+
person: boolean;
|
|
84
|
+
name: string;
|
|
85
|
+
description: string;
|
|
86
|
+
email: string;
|
|
87
|
+
telephone: string;
|
|
88
|
+
address: {
|
|
89
|
+
streetAddress: string;
|
|
90
|
+
addressLocality: string;
|
|
91
|
+
addressRegion: string;
|
|
92
|
+
postalCode: string;
|
|
93
|
+
addressCountry: string;
|
|
94
|
+
};
|
|
95
|
+
/** The street address as one line, as Setup → Details → Business details holds it; `''` without a street. */
|
|
96
|
+
postalAddress: string;
|
|
97
|
+
businessType: string;
|
|
98
|
+
areaServed: string[];
|
|
99
|
+
openingHours: string;
|
|
100
|
+
priceRange: string;
|
|
101
|
+
sameAs: string[];
|
|
102
|
+
whenToUse: string;
|
|
103
|
+
howToUse: string;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The site's agent guidance from its answers (AGL-3979): when an agent should
|
|
107
|
+
* use the site — what the business is, then what the site answers — and,
|
|
108
|
+
* where the person gave a way to reach it, how to reach a person. Written in
|
|
109
|
+
* English, as the rest of `/llms.txt` is.
|
|
110
|
+
*/
|
|
111
|
+
export declare function aiSiteAgentGuidance(input: {
|
|
112
|
+
name: string;
|
|
113
|
+
description: string;
|
|
114
|
+
areas: readonly string[];
|
|
115
|
+
hours: boolean;
|
|
116
|
+
prices: boolean;
|
|
117
|
+
languages: string;
|
|
118
|
+
phone: string;
|
|
119
|
+
email: string;
|
|
120
|
+
address: string;
|
|
121
|
+
}): {
|
|
122
|
+
whenToUse: string;
|
|
123
|
+
howToUse: string;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* The Entity and AI-agent values a site job's inputs imply, in the language
|
|
127
|
+
* its plan writes the site in. `siteName` is the site's own name: it leads
|
|
128
|
+
* the description and the guidance where the inputs name no business, and is
|
|
129
|
+
* never written as the Entity's name, which falls back to it at render and so
|
|
130
|
+
* follows a rename.
|
|
131
|
+
*/
|
|
132
|
+
export declare function aiSiteEntityValues(inputs: Readonly<Record<string, unknown>> | null | undefined, options?: {
|
|
133
|
+
language?: string | null;
|
|
134
|
+
siteName?: string | null;
|
|
135
|
+
}): AiSiteEntityValues;
|
|
136
|
+
/** The contact a start's phone or email answers, as the Setup form's **Contact is for** says it: its default. */
|
|
137
|
+
export declare const AI_SITE_CONTACT_TYPE = "customer support";
|
|
138
|
+
/**
|
|
139
|
+
* The host updates that put a start's Entity and AI-agent values on its site,
|
|
140
|
+
* as field paths (`seo.entity.telephone`), for the publish's own batch: only
|
|
141
|
+
* fields that are empty on the site, so nothing the owner wrote is replaced.
|
|
142
|
+
* The address is written whole or not at all — a start's street beside the
|
|
143
|
+
* owner's own city would be an address of neither. `{}` when there is
|
|
144
|
+
* nothing to write.
|
|
145
|
+
*
|
|
146
|
+
* Beside the Entity, from what the site already has and the same answers:
|
|
147
|
+
* - the Entity logo is the site's own logo, where it has one and the Entity
|
|
148
|
+
* names none (a Person's Entity picture is their photo, never a logo);
|
|
149
|
+
* - **Contact is for** says what the phone or email this start wrote answers;
|
|
150
|
+
* - Setup → Details → Business details gets the email as the support email
|
|
151
|
+
* (which `{{host.supportEmail}}` and a form that cannot take a message name)
|
|
152
|
+
* and the street address as its one block of text.
|
|
153
|
+
*/
|
|
154
|
+
export declare function aiSiteEntityUpdates(seo: unknown, values: AiSiteEntityValues, site?: {
|
|
155
|
+
logoUrl?: unknown;
|
|
156
|
+
business?: unknown;
|
|
157
|
+
}): Record<string, unknown>;
|
|
158
|
+
/**
|
|
159
|
+
* Which half of a site's own listing is still the one its creation seeded
|
|
160
|
+
* (AGL-3979): `defaultSiteSeo(name)` — the site's name as its title and
|
|
161
|
+
* "Welcome to {name}. See what we do, browse our work and get in touch." as
|
|
162
|
+
* its description. Blank counts as seeded too: nobody chose it.
|
|
163
|
+
*
|
|
164
|
+
* The title is seeded when it is the site's name, or when it still pairs with
|
|
165
|
+
* a seeded description, as it does on a site renamed after it was made. The
|
|
166
|
+
* description is seeded when it is the welcome line for the site's name, for
|
|
167
|
+
* the title's, or for no name. Anything else is the owner's own words.
|
|
168
|
+
*/
|
|
169
|
+
export declare function aiSiteListingSeeded(seo: unknown, displayName: unknown): {
|
|
170
|
+
title: boolean;
|
|
171
|
+
description: boolean;
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* The host updates that put a start's own listing on its site (AGL-3979), as
|
|
175
|
+
* field paths for the publish's batch: each half only where it is still the
|
|
176
|
+
* seeded default and differs from the listing, so the owner's own title or
|
|
177
|
+
* description is never written over, and a second publish writes nothing.
|
|
178
|
+
* `{}` with no listing to write.
|
|
179
|
+
*/
|
|
180
|
+
export declare function aiSiteListingUpdates(seo: unknown, displayName: unknown, listing: AiSiteSeoProposal | null): Record<string, string>;
|
|
181
|
+
export {};
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { defaultHomeDescription, defaultSiteSeo } from "@aglyn/aglyn/app-utils/default-site";
|
|
18
|
+
import { HostEntityType } from "@aglyn/aglyn/foundation/definitions/platform.types";
|
|
19
|
+
import { aiSiteAddressLine, aiSiteDetailsOf, aiSiteHoursSentence, aiSiteServiceAreas } from "./ai-site-details.js";
|
|
20
|
+
import { aiSiteKindOfInputs } from "./ai-site-kinds.js";
|
|
21
|
+
import { aiSiteSeoProposalForInputs, AI_SITE_SEO_FORM_FIELDS } from "./ai-site-start-seo.js";
|
|
22
|
+
/**
|
|
23
|
+
* Each `AI_SITE_KINDS` entry's `schema.org` type (`ai-site-entity.spec.ts`
|
|
24
|
+
* holds every kind to an entry). A kind with no business type publishes as an
|
|
25
|
+
* Organization — a blog, a band, an event, a course — unless the person gave
|
|
26
|
+
* a street address or opening hours, which make it a place a visitor goes:
|
|
27
|
+
* a LocalBusiness. Only types on the platform's `LOCAL_BUSINESS_TYPES` list
|
|
28
|
+
* are used, since only those publish.
|
|
29
|
+
*/ export const AI_SITE_KIND_SCHEMA = {
|
|
30
|
+
business: {
|
|
31
|
+
businessType: 'LocalBusiness',
|
|
32
|
+
refine: [
|
|
33
|
+
[
|
|
34
|
+
/\b(?:marketing|consult\w*|agency|freelanc\w*|staffing|bookkeep\w*|design(?:er)?s?)\b/i,
|
|
35
|
+
'ProfessionalService'
|
|
36
|
+
],
|
|
37
|
+
[
|
|
38
|
+
/\b(?:car wash|detailing|auto repair|mechanic|tire|tyre)\b/i,
|
|
39
|
+
'AutomotiveBusiness'
|
|
40
|
+
]
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
trades: {
|
|
44
|
+
businessType: 'HomeAndConstructionBusiness',
|
|
45
|
+
refine: [
|
|
46
|
+
[
|
|
47
|
+
/\bplumb\w*/i,
|
|
48
|
+
'Plumber'
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
/\belectric\w*/i,
|
|
52
|
+
'Electrician'
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
/\b(?:hvac|heating|air condition\w*|furnace)\b/i,
|
|
56
|
+
'HVACBusiness'
|
|
57
|
+
],
|
|
58
|
+
[
|
|
59
|
+
/\broof\w*/i,
|
|
60
|
+
'RoofingContractor'
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
/\bpaint\w*/i,
|
|
64
|
+
'HousePainter'
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
/\blocksmith\w*/i,
|
|
68
|
+
'Locksmith'
|
|
69
|
+
],
|
|
70
|
+
[
|
|
71
|
+
/\bmov(?:ing|ers?)\b/i,
|
|
72
|
+
'MovingCompany'
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
/\b(?:contractor|remodel\w*|renovat\w*|builders?|construction)\b/i,
|
|
76
|
+
'GeneralContractor'
|
|
77
|
+
]
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
professional: {
|
|
81
|
+
businessType: 'ProfessionalService',
|
|
82
|
+
refine: [
|
|
83
|
+
[
|
|
84
|
+
/\b(?:law|lawyers?|attorneys?|legal|solicitors?)\b/i,
|
|
85
|
+
'LegalService'
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
/\b(?:accounting|accountants?|bookkeep\w*|tax|cpa)\b/i,
|
|
89
|
+
'AccountingService'
|
|
90
|
+
]
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
wellness: {
|
|
94
|
+
businessType: 'HealthAndBeautyBusiness',
|
|
95
|
+
refine: [
|
|
96
|
+
[
|
|
97
|
+
/\b(?:dentist\w*|dental|orthodont\w*)\b/i,
|
|
98
|
+
'Dentist'
|
|
99
|
+
],
|
|
100
|
+
[
|
|
101
|
+
/\b(?:clinic|doctors?|physicians?|medical|therap\w*|chiropract\w*|physio\w*|counsel\w*)\b/i,
|
|
102
|
+
'MedicalBusiness'
|
|
103
|
+
]
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
restaurant: {
|
|
107
|
+
businessType: 'Restaurant',
|
|
108
|
+
refine: [
|
|
109
|
+
[
|
|
110
|
+
/\b(?:caf[eé]s?|coffee|bakery|bakeries|bars?|brewery|food truck|deli)\b/i,
|
|
111
|
+
'FoodEstablishment'
|
|
112
|
+
]
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
store: {
|
|
116
|
+
businessType: 'Store'
|
|
117
|
+
},
|
|
118
|
+
portfolio: {},
|
|
119
|
+
studio: {
|
|
120
|
+
businessType: 'ProfessionalService'
|
|
121
|
+
},
|
|
122
|
+
photography: {
|
|
123
|
+
businessType: 'ProfessionalService'
|
|
124
|
+
},
|
|
125
|
+
blog: {},
|
|
126
|
+
events: {},
|
|
127
|
+
fitness: {
|
|
128
|
+
businessType: 'SportsActivityLocation'
|
|
129
|
+
},
|
|
130
|
+
yoga: {
|
|
131
|
+
businessType: 'SportsActivityLocation'
|
|
132
|
+
},
|
|
133
|
+
beauty: {
|
|
134
|
+
businessType: 'HealthAndBeautyBusiness'
|
|
135
|
+
},
|
|
136
|
+
realestate: {
|
|
137
|
+
businessType: 'RealEstateAgent'
|
|
138
|
+
},
|
|
139
|
+
education: {
|
|
140
|
+
refine: [
|
|
141
|
+
[
|
|
142
|
+
/\b(?:day ?care|child ?care|preschool|nursery)\b/i,
|
|
143
|
+
'ChildCare'
|
|
144
|
+
]
|
|
145
|
+
]
|
|
146
|
+
},
|
|
147
|
+
nonprofit: {
|
|
148
|
+
refine: [
|
|
149
|
+
[
|
|
150
|
+
/\b(?:animal (?:shelter|rescue)|pet rescue)\b/i,
|
|
151
|
+
'AnimalShelter'
|
|
152
|
+
]
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
music: {},
|
|
156
|
+
personal: {
|
|
157
|
+
person: true
|
|
158
|
+
},
|
|
159
|
+
landing: {},
|
|
160
|
+
'coming-soon': {}
|
|
161
|
+
};
|
|
162
|
+
/** What a job's inputs say the site publishes as. */ export function aiSiteSchemaOf(inputs) {
|
|
163
|
+
var _ref;
|
|
164
|
+
var _entry_refine_find, _entry_refine;
|
|
165
|
+
const kind = aiSiteKindOfInputs(inputs);
|
|
166
|
+
const entry = kind && AI_SITE_KIND_SCHEMA[kind.id] || {};
|
|
167
|
+
if (entry.person) return {
|
|
168
|
+
person: true,
|
|
169
|
+
businessType: null
|
|
170
|
+
};
|
|
171
|
+
const words = [
|
|
172
|
+
inputs == null ? void 0 : inputs['businessType'],
|
|
173
|
+
inputs == null ? void 0 : inputs['audience']
|
|
174
|
+
].filter((value)=>typeof value === 'string').join(' ');
|
|
175
|
+
const refined = (_entry_refine = entry.refine) == null ? void 0 : (_entry_refine_find = _entry_refine.find(([pattern])=>pattern.test(words))) == null ? void 0 : _entry_refine_find[1];
|
|
176
|
+
const details = aiSiteDetailsOf(inputs);
|
|
177
|
+
const place = Boolean(details.street || details.hours);
|
|
178
|
+
return {
|
|
179
|
+
person: false,
|
|
180
|
+
businessType: (_ref = refined != null ? refined : entry.businessType) != null ? _ref : place ? 'LocalBusiness' : null
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const text = (value)=>typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
|
|
184
|
+
/** "a, b and c". */ function listed(parts) {
|
|
185
|
+
if (parts.length < 2) return parts.join('');
|
|
186
|
+
return `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The site's agent guidance from its answers (AGL-3979): when an agent should
|
|
190
|
+
* use the site — what the business is, then what the site answers — and,
|
|
191
|
+
* where the person gave a way to reach it, how to reach a person. Written in
|
|
192
|
+
* English, as the rest of `/llms.txt` is.
|
|
193
|
+
*/ export function aiSiteAgentGuidance(input) {
|
|
194
|
+
const who = input.name || 'this business';
|
|
195
|
+
const answers = [
|
|
196
|
+
`what ${who} offers`,
|
|
197
|
+
...input.areas.length ? [
|
|
198
|
+
`the areas it serves (${input.areas.slice(0, 6).join(', ')})`
|
|
199
|
+
] : [],
|
|
200
|
+
...input.hours ? [
|
|
201
|
+
'its opening hours'
|
|
202
|
+
] : [],
|
|
203
|
+
...input.prices ? [
|
|
204
|
+
'its prices'
|
|
205
|
+
] : [],
|
|
206
|
+
'how to get in touch'
|
|
207
|
+
];
|
|
208
|
+
// A site the answers say nothing about gets no guidance: the platform's own
|
|
209
|
+
// derived "When to use this site" says more than a sentence about nobody.
|
|
210
|
+
const said = Boolean(input.name || input.description);
|
|
211
|
+
const when = said ? [
|
|
212
|
+
input.description,
|
|
213
|
+
`Use this site for ${listed(answers)}.`,
|
|
214
|
+
input.languages ? `It serves customers in ${input.languages.replace(/[.\s]+$/, '')}.` : ''
|
|
215
|
+
].filter(Boolean).join(' ') : '';
|
|
216
|
+
const reach = [
|
|
217
|
+
input.phone ? `call ${input.phone}` : '',
|
|
218
|
+
input.email ? `email ${input.email}` : ''
|
|
219
|
+
].filter(Boolean);
|
|
220
|
+
const how = [
|
|
221
|
+
reach.length ? `To reach a person at ${who}, ${reach.join(' or ')}.` : '',
|
|
222
|
+
input.address ? `${input.name || 'The business'} is at ${input.address}.` : ''
|
|
223
|
+
].filter(Boolean).join(' ');
|
|
224
|
+
return {
|
|
225
|
+
whenToUse: when,
|
|
226
|
+
howToUse: how
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The Entity and AI-agent values a site job's inputs imply, in the language
|
|
231
|
+
* its plan writes the site in. `siteName` is the site's own name: it leads
|
|
232
|
+
* the description and the guidance where the inputs name no business, and is
|
|
233
|
+
* never written as the Entity's name, which falls back to it at render and so
|
|
234
|
+
* follows a rename.
|
|
235
|
+
*/ export function aiSiteEntityValues(inputs, options = {}) {
|
|
236
|
+
var _options_language, _options_siteName, _ref, _schema_businessType;
|
|
237
|
+
const details = aiSiteDetailsOf(inputs);
|
|
238
|
+
const schema = aiSiteSchemaOf(inputs);
|
|
239
|
+
const name = text(inputs == null ? void 0 : inputs['businessName']);
|
|
240
|
+
const city = text(inputs == null ? void 0 : inputs['city']);
|
|
241
|
+
const listing = aiSiteSeoProposalForInputs(inputs, (_options_language = options.language) != null ? _options_language : null, (_options_siteName = options.siteName) != null ? _options_siteName : null);
|
|
242
|
+
const description = (_ref = listing == null ? void 0 : listing.values[AI_SITE_SEO_FORM_FIELDS.description]) != null ? _ref : '';
|
|
243
|
+
const areas = aiSiteServiceAreas(details.serviceArea);
|
|
244
|
+
// A business that names no other area serves the city it is in.
|
|
245
|
+
const areaServed = schema.businessType ? areas.length ? areas : city ? [
|
|
246
|
+
city
|
|
247
|
+
] : [] : [];
|
|
248
|
+
const sameAs = details.social ? details.social.split('\n').filter(Boolean) : [];
|
|
249
|
+
const address = details.street ? aiSiteAddressLine(details, city) : '';
|
|
250
|
+
const guidance = aiSiteAgentGuidance({
|
|
251
|
+
name: name || text(options.siteName),
|
|
252
|
+
description,
|
|
253
|
+
areas,
|
|
254
|
+
hours: Boolean(aiSiteHoursSentence(details.hours)),
|
|
255
|
+
prices: Boolean(details.priceRange),
|
|
256
|
+
languages: details.languages,
|
|
257
|
+
phone: details.phone,
|
|
258
|
+
email: details.email,
|
|
259
|
+
address
|
|
260
|
+
});
|
|
261
|
+
return _extends({
|
|
262
|
+
person: schema.person,
|
|
263
|
+
name,
|
|
264
|
+
description,
|
|
265
|
+
email: details.email,
|
|
266
|
+
telephone: details.phone,
|
|
267
|
+
address: {
|
|
268
|
+
streetAddress: details.street,
|
|
269
|
+
// The city is where the business is only where it gave a street to be on.
|
|
270
|
+
addressLocality: details.street || details.postalCode ? city : '',
|
|
271
|
+
addressRegion: details.region,
|
|
272
|
+
postalCode: details.postalCode,
|
|
273
|
+
addressCountry: details.country
|
|
274
|
+
},
|
|
275
|
+
postalAddress: address,
|
|
276
|
+
businessType: (_schema_businessType = schema.businessType) != null ? _schema_businessType : '',
|
|
277
|
+
areaServed,
|
|
278
|
+
openingHours: details.hours,
|
|
279
|
+
priceRange: details.priceRange,
|
|
280
|
+
sameAs
|
|
281
|
+
}, guidance);
|
|
282
|
+
}
|
|
283
|
+
const isBlank = (value)=>value === undefined || value === null || typeof value === 'string' && !value.trim() || Array.isArray(value) && !value.some((entry)=>typeof entry === 'string' && entry.trim());
|
|
284
|
+
const isRecord = (value)=>typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
285
|
+
/** The contact a start's phone or email answers, as the Setup form's **Contact is for** says it: its default. */ export const AI_SITE_CONTACT_TYPE = 'customer support';
|
|
286
|
+
/**
|
|
287
|
+
* The host updates that put a start's Entity and AI-agent values on its site,
|
|
288
|
+
* as field paths (`seo.entity.telephone`), for the publish's own batch: only
|
|
289
|
+
* fields that are empty on the site, so nothing the owner wrote is replaced.
|
|
290
|
+
* The address is written whole or not at all — a start's street beside the
|
|
291
|
+
* owner's own city would be an address of neither. `{}` when there is
|
|
292
|
+
* nothing to write.
|
|
293
|
+
*
|
|
294
|
+
* Beside the Entity, from what the site already has and the same answers:
|
|
295
|
+
* - the Entity logo is the site's own logo, where it has one and the Entity
|
|
296
|
+
* names none (a Person's Entity picture is their photo, never a logo);
|
|
297
|
+
* - **Contact is for** says what the phone or email this start wrote answers;
|
|
298
|
+
* - Setup → Details → Business details gets the email as the support email
|
|
299
|
+
* (which `{{host.supportEmail}}` and a form that cannot take a message name)
|
|
300
|
+
* and the street address as its one block of text.
|
|
301
|
+
*/ export function aiSiteEntityUpdates(seo, values, site = {}) {
|
|
302
|
+
const current = isRecord(seo) ? seo : {};
|
|
303
|
+
const entity = isRecord(current['entity']) ? current['entity'] : {};
|
|
304
|
+
const agent = isRecord(current['agent']) ? current['agent'] : {};
|
|
305
|
+
const business = isRecord(site.business) ? site.business : {};
|
|
306
|
+
const updates = {};
|
|
307
|
+
const fill = (path, now, next)=>{
|
|
308
|
+
if (!isBlank(next) && isBlank(now)) updates[path] = next;
|
|
309
|
+
};
|
|
310
|
+
const person = values.person && isBlank(entity['type']);
|
|
311
|
+
// As the Setup form's select stores it: the enum's value as text.
|
|
312
|
+
if (person) updates['seo.entity.type'] = String(HostEntityType.PERSON);
|
|
313
|
+
fill('seo.entity.name', entity['name'], values.name);
|
|
314
|
+
fill('seo.entity.description', entity['description'], values.description);
|
|
315
|
+
fill('seo.entity.email', entity['email'], values.email);
|
|
316
|
+
fill('seo.entity.telephone', entity['telephone'], values.telephone);
|
|
317
|
+
if (updates['seo.entity.email'] || updates['seo.entity.telephone']) {
|
|
318
|
+
fill('seo.entity.contactType', entity['contactType'], AI_SITE_CONTACT_TYPE);
|
|
319
|
+
}
|
|
320
|
+
const address = isRecord(entity['address']) ? entity['address'] : {};
|
|
321
|
+
if (Object.values(address).every(isBlank)) {
|
|
322
|
+
for (const [field, value] of Object.entries(values.address)){
|
|
323
|
+
if (value) updates[`seo.entity.address.${field}`] = value;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// A Person carries none of a business's fields (`siteEntityJsonLd`).
|
|
327
|
+
const isPerson = person || Number(entity['type']) === HostEntityType.PERSON;
|
|
328
|
+
if (!isPerson) {
|
|
329
|
+
fill('seo.entity.logo', entity['logo'], typeof site.logoUrl === 'string' ? site.logoUrl.trim() : '');
|
|
330
|
+
fill('seo.entity.businessType', entity['businessType'], values.businessType);
|
|
331
|
+
fill('seo.entity.areaServed', entity['areaServed'], values.areaServed);
|
|
332
|
+
fill('seo.entity.openingHours', entity['openingHours'], values.openingHours);
|
|
333
|
+
fill('seo.entity.priceRange', entity['priceRange'], values.priceRange);
|
|
334
|
+
}
|
|
335
|
+
fill('seo.entity.sameAs', entity['sameAs'], values.sameAs);
|
|
336
|
+
fill('seo.agent.whenToUse', agent['whenToUse'], values.whenToUse);
|
|
337
|
+
fill('seo.agent.howToUse', agent['howToUse'], values.howToUse);
|
|
338
|
+
fill('business.supportEmail', business['supportEmail'], values.email);
|
|
339
|
+
fill('business.address', business['address'], values.postalAddress);
|
|
340
|
+
return updates;
|
|
341
|
+
}
|
|
342
|
+
/** Trimmed, or `''` for anything that is not a string: as site creation compares names. */ const trimmed = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
343
|
+
/**
|
|
344
|
+
* Which half of a site's own listing is still the one its creation seeded
|
|
345
|
+
* (AGL-3979): `defaultSiteSeo(name)` — the site's name as its title and
|
|
346
|
+
* "Welcome to {name}. See what we do, browse our work and get in touch." as
|
|
347
|
+
* its description. Blank counts as seeded too: nobody chose it.
|
|
348
|
+
*
|
|
349
|
+
* The title is seeded when it is the site's name, or when it still pairs with
|
|
350
|
+
* a seeded description, as it does on a site renamed after it was made. The
|
|
351
|
+
* description is seeded when it is the welcome line for the site's name, for
|
|
352
|
+
* the title's, or for no name. Anything else is the owner's own words.
|
|
353
|
+
*/ export function aiSiteListingSeeded(seo, displayName) {
|
|
354
|
+
const current = isRecord(seo) ? seo : {};
|
|
355
|
+
const title = trimmed(current['title']);
|
|
356
|
+
const description = trimmed(current['description']);
|
|
357
|
+
const name = trimmed(displayName);
|
|
358
|
+
const welcomes = new Set([
|
|
359
|
+
name,
|
|
360
|
+
title,
|
|
361
|
+
''
|
|
362
|
+
].map((each)=>defaultHomeDescription(each)));
|
|
363
|
+
const seededDescription = !description || welcomes.has(description);
|
|
364
|
+
const seededTitle = !title || title === trimmed(defaultSiteSeo(name)['title']) || Boolean(description) && description === defaultHomeDescription(title);
|
|
365
|
+
return {
|
|
366
|
+
title: seededTitle,
|
|
367
|
+
description: seededDescription
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* The host updates that put a start's own listing on its site (AGL-3979), as
|
|
372
|
+
* field paths for the publish's batch: each half only where it is still the
|
|
373
|
+
* seeded default and differs from the listing, so the owner's own title or
|
|
374
|
+
* description is never written over, and a second publish writes nothing.
|
|
375
|
+
* `{}` with no listing to write.
|
|
376
|
+
*/ export function aiSiteListingUpdates(seo, displayName, listing) {
|
|
377
|
+
if (!listing) return {};
|
|
378
|
+
const current = isRecord(seo) ? seo : {};
|
|
379
|
+
const seeded = aiSiteListingSeeded(current, displayName);
|
|
380
|
+
const updates = {};
|
|
381
|
+
const title = trimmed(listing.values[AI_SITE_SEO_FORM_FIELDS.title]);
|
|
382
|
+
const description = trimmed(listing.values[AI_SITE_SEO_FORM_FIELDS.description]);
|
|
383
|
+
if (title && seeded.title && title !== trimmed(current['title'])) updates[AI_SITE_SEO_FORM_FIELDS.title] = title;
|
|
384
|
+
if (description && seeded.description && description !== trimmed(current['description'])) {
|
|
385
|
+
updates[AI_SITE_SEO_FORM_FIELDS.description] = description;
|
|
386
|
+
}
|
|
387
|
+
return updates;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
//# sourceMappingURL=ai-site-entity.js.map
|