create-ailk 0.26.0 → 0.28.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/README.md +86 -9
- package/dist/add-module-core.js +2 -1
- package/dist/add-module.d.ts +2 -1
- package/dist/add-module.js +9 -4
- package/dist/add-route.js +46 -19
- package/dist/body-heading.d.ts +56 -0
- package/dist/body-heading.js +394 -0
- package/dist/cli.js +28 -1
- package/dist/content-payloads.js +2 -2
- package/dist/derive-architecture.js +8 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/lib/auth-secret-file.d.ts +6 -2
- package/dist/lib/auth-secret-file.js +46 -20
- package/dist/lib/env-catalog.js +20 -0
- package/dist/lib/site-modules.d.ts +8 -0
- package/dist/lib/site-modules.js +34 -0
- package/dist/module-architecture.js +25 -14
- package/dist/paid-sections.d.ts +17 -0
- package/dist/paid-sections.js +39 -0
- package/dist/programmatic.d.ts +16 -0
- package/dist/programmatic.js +73 -4
- package/dist/remove-module.d.ts +98 -0
- package/dist/remove-module.js +620 -0
- package/dist/seed-flows.d.ts +2 -2
- package/dist/seed-flows.js +16 -9
- package/dist/site-roster.js +4 -1
- package/dist/starter-grid.d.ts +26 -0
- package/dist/starter-grid.js +34 -0
- package/dist/surfaces.d.ts +26 -0
- package/dist/surfaces.js +112 -11
- package/dist/sync-routes.d.ts +7 -0
- package/dist/sync-routes.js +53 -6
- package/dist/template-pages.d.ts +36 -0
- package/dist/template-pages.js +129 -0
- package/dist/topology.d.ts +2 -1
- package/dist/topology.js +4 -2
- package/dist/update.d.ts +3 -1
- package/dist/update.js +90 -6
- package/package.json +3 -3
- package/release-scope.json +2 -1
- package/site-templates/aliases/hub.TEMPLATE-MANIFEST.json +115 -0
- package/templates/.env.example +12 -2
- package/templates/CLAUDE.md +20 -20
- package/templates/README.md +19 -19
- package/templates/apps/api/.env.example +16 -5
- package/templates/apps/api/CLAUDE.md +32 -11
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +37 -0
- package/templates/apps/api/src/vercel-handler.ts +8 -1
- package/templates/apps/api/vercel.json +7 -1
- package/templates/apps/mcp/CLAUDE.md +18 -10
- package/templates/apps/web/.env.example +27 -0
- package/templates/apps/web/CLAUDE.md +56 -8
- package/templates/apps/web/__tests__/setup.ts +7 -0
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +7 -7
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/next-path.test.ts +24 -1
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +51 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-in/next-path.ts +13 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +10 -4
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/page.test.tsx +197 -4
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +23 -7
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +61 -2
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +81 -0
- package/templates/apps/web/app/[locale]/(authed)/account/connect/__tests__/page.test.tsx +197 -0
- package/templates/apps/web/app/[locale]/(authed)/account/connect/page.tsx +152 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +76 -21
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +46 -16
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +180 -2
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +122 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/hub-children.test.tsx +65 -0
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/case-studies/page.tsx +23 -11
- package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/consent.actions.ts +44 -6
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +269 -42
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +88 -30
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/courses/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/courses/page.tsx +22 -6
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +15 -2
- package/templates/apps/web/app/[locale]/events/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +8 -2
- package/templates/apps/web/app/[locale]/events/page.tsx +23 -7
- package/templates/apps/web/app/[locale]/features/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/features/page.tsx +23 -25
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +95 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/save.actions.test.ts +193 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/email-moment.ts +92 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +10 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +513 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +122 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.no-commerce.test.tsx +195 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/landing.test.tsx +293 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +397 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/checkout-seam.ts +74 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +264 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/save.actions.ts +75 -0
- package/templates/apps/web/app/[locale]/for/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +5 -1
- package/templates/apps/web/app/[locale]/for/page.tsx +23 -25
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +25 -4
- package/templates/apps/web/app/[locale]/integrations/__tests__/hub-children.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +8 -2
- package/templates/apps/web/app/[locale]/integrations/page.tsx +23 -11
- package/templates/apps/web/app/[locale]/layout.tsx +161 -128
- package/templates/apps/web/app/[locale]/page.tsx +51 -3
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +16 -3
- package/templates/apps/web/app/[locale]/partners/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/partners/page.tsx +22 -6
- package/templates/apps/web/app/[locale]/portfolio/__tests__/hub-children.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/portfolio/page.tsx +25 -24
- package/templates/apps/web/app/[locale]/pricing/[slug]/__tests__/page.test.tsx +321 -0
- package/templates/apps/web/app/[locale]/pricing/[slug]/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/pricing-pages.test.tsx +215 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +56 -341
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +21 -4
- package/templates/apps/web/app/[locale]/products/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +8 -2
- package/templates/apps/web/app/[locale]/products/page.tsx +22 -6
- package/templates/apps/web/app/[locale]/projects/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -0
- package/templates/apps/web/app/[locale]/projects/page.tsx +18 -11
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +25 -4
- package/templates/apps/web/app/[locale]/resources/__tests__/hub-children.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/resources/page.tsx +20 -10
- package/templates/apps/web/app/[locale]/services/[slug]/[child]/page.tsx +68 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +64 -9
- package/templates/apps/web/app/[locale]/services/__tests__/hub-children.test.tsx +158 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +12 -2
- package/templates/apps/web/app/[locale]/services/__tests__/service-detail-sections.test.tsx +223 -0
- package/templates/apps/web/app/[locale]/services/__tests__/service-families.test.tsx +368 -0
- package/templates/apps/web/app/[locale]/services/page.tsx +45 -19
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/team/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/team/page.tsx +25 -24
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +24 -3
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/templates/[slug]/__tests__/page.test.tsx +203 -0
- package/templates/apps/web/app/[locale]/templates/[slug]/page.tsx +81 -0
- package/templates/apps/web/app/[locale]/templates/__tests__/page.test.tsx +236 -0
- package/templates/apps/web/app/[locale]/templates/page.tsx +108 -0
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +24 -3
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +322 -2
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +161 -29
- package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +51 -0
- package/templates/apps/web/app/api/hub-session/sign-in/route.ts +13 -0
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +233 -7
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +92 -41
- package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +77 -39
- package/templates/apps/web/app/llms.txt/route.ts +41 -3
- package/templates/apps/web/app/robots.txt/route.ts +5 -1
- package/templates/apps/web/app/sitemap.xml/route.ts +45 -3
- package/templates/apps/web/auth.ts +102 -50
- package/templates/apps/web/components/signed-in-nav.tsx +8 -1
- package/templates/apps/web/lib/__tests__/bot-check.test.ts +254 -0
- package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +212 -13
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +13 -1
- package/templates/apps/web/lib/__tests__/docs-index.test.ts +93 -0
- package/templates/apps/web/lib/__tests__/docs-path.test.ts +168 -0
- package/templates/apps/web/lib/__tests__/hub-children.fixtures.ts +278 -0
- package/templates/apps/web/lib/__tests__/nav-label-keys.test.ts +138 -0
- package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +178 -2
- package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +183 -0
- package/templates/apps/web/lib/__tests__/page-chrome.test.ts +412 -0
- package/templates/apps/web/lib/__tests__/pricing-page.test.ts +142 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +484 -4
- package/templates/apps/web/lib/__tests__/route-family.test.ts +57 -0
- package/templates/apps/web/lib/__tests__/service-listing.test.ts +111 -0
- package/templates/apps/web/lib/__tests__/services-path.test.ts +96 -0
- package/templates/apps/web/lib/__tests__/site-header.test.ts +151 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +148 -6
- package/templates/apps/web/lib/__tests__/telemetry-scrub.test.ts +84 -0
- package/templates/apps/web/lib/__tests__/template-listing.test.ts +158 -0
- package/templates/apps/web/lib/auth-secret.ts +57 -0
- package/templates/apps/web/lib/bot-check.ts +138 -0
- package/templates/apps/web/lib/consent-visitor.ts +81 -7
- package/templates/apps/web/lib/deck-page.ts +6 -3
- package/templates/apps/web/lib/docs-index.ts +100 -0
- package/templates/apps/web/lib/docs-path.ts +94 -0
- package/templates/apps/web/lib/hub-children.tsx +318 -0
- package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +22 -2
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +11 -8
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/nav-label-check.ts +61 -0
- package/templates/apps/web/lib/nav-label-keys.ts +78 -0
- package/templates/apps/web/lib/newsletter.actions.ts +31 -6
- package/templates/apps/web/lib/offerings-pricing.ts +135 -6
- package/templates/apps/web/lib/page-chrome.ts +367 -0
- package/templates/apps/web/lib/posting-title.ts +26 -4
- package/templates/apps/web/lib/pricing-page.ts +430 -0
- package/templates/apps/web/lib/relay.ts +44 -4
- package/templates/apps/web/lib/route-family.ts +69 -0
- package/templates/apps/web/lib/service-listing.ts +89 -0
- package/templates/apps/web/lib/services-path.ts +97 -0
- package/templates/apps/web/lib/sign-up-agreements.ts +17 -6
- package/templates/apps/web/lib/site-header.ts +94 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +68 -3
- package/templates/apps/web/lib/template-listing.ts +169 -0
- package/templates/apps/web/messages/en.json +144 -3
- package/templates/apps/web/middleware.ts +179 -12
- package/templates/apps/web/next.config.mjs +220 -3
- package/templates/apps/web/package.json +3 -2
- package/templates/apps/web/scripts/assert-content-bundled.ts +28 -6
- package/templates/apps/web/scripts/check-nav-labels.ts +39 -0
- package/templates/apps/web/sentry.server.config.ts +30 -2
- package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -9
- package/templates/apps/web/src/lib/rate-limit.ts +68 -27
- package/templates/apps/web/src/lib/routes.ts +15 -1
- package/versions.json +48 -48
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// services-path.ts — where the services live (#10043 D5).
|
|
2
|
+
//
|
|
3
|
+
// A site sets `services: { path: /capabilities }` in `content/_site.mdx`;
|
|
4
|
+
// `next.config.mjs` parses it at build and bakes it into
|
|
5
|
+
// `NEXT_PUBLIC_AILK_SERVICES_PATH`. The services routes never move — they stay
|
|
6
|
+
// at `app/[locale]/services/` (`SERVICES_ROUTE`) — and the middleware maps the
|
|
7
|
+
// public path onto them and answers the old `/services` URLs with a 308. Every
|
|
8
|
+
// surface that names a services URL reads `SERVICES_PATH` here.
|
|
9
|
+
//
|
|
10
|
+
// Edge-safe and client-safe: this file imports nothing at runtime (the
|
|
11
|
+
// `NavItem` import is type-only), so the middleware and the client signed-in
|
|
12
|
+
// nav can both read it. The shape is `docs-path.ts`'s (#10041).
|
|
13
|
+
import type { NavItem } from "@working-theory/ui";
|
|
14
|
+
|
|
15
|
+
/** The internal services route — the directory under `app/[locale]/`. Always `/services`. */
|
|
16
|
+
export const SERVICES_ROUTE = "/services";
|
|
17
|
+
|
|
18
|
+
/** The services' public base: the site's `services.path`, or `/services` when it sets none. */
|
|
19
|
+
export const SERVICES_PATH: string =
|
|
20
|
+
process.env.NEXT_PUBLIC_AILK_SERVICES_PATH || SERVICES_ROUTE;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `href` at the internal services route — `/services`, or starting
|
|
24
|
+
* `/services/`, `/services?` or `/services#` — with that prefix replaced by
|
|
25
|
+
* `SERVICES_PATH`. Anything else (absolute URLs, `/servicesx`, a
|
|
26
|
+
* locale-prefixed path) is returned unchanged. The identity on a site with no
|
|
27
|
+
* `services.path`.
|
|
28
|
+
*/
|
|
29
|
+
export function toPublicServicesHref(href: string): string {
|
|
30
|
+
if (SERVICES_PATH === SERVICES_ROUTE) return href;
|
|
31
|
+
const rest = href.slice(SERVICES_ROUTE.length);
|
|
32
|
+
if (href.startsWith(SERVICES_ROUTE) && (rest === "" || /^[/?#]/.test(rest))) {
|
|
33
|
+
return SERVICES_PATH + rest;
|
|
34
|
+
}
|
|
35
|
+
return href;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A locale-less pathname at or under `SERVICES_PATH`, mapped onto the internal
|
|
40
|
+
* route (`/services` + the rest); `null` for anything else, and always `null`
|
|
41
|
+
* on a site with no `services.path`.
|
|
42
|
+
*/
|
|
43
|
+
export function toInternalServicesPath(bare: string): string | null {
|
|
44
|
+
if (SERVICES_PATH === SERVICES_ROUTE) return null;
|
|
45
|
+
if (bare === SERVICES_PATH) return SERVICES_ROUTE;
|
|
46
|
+
if (bare.startsWith(`${SERVICES_PATH}/`)) {
|
|
47
|
+
return SERVICES_ROUTE + bare.slice(SERVICES_PATH.length);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type NavFlyout = NonNullable<NavItem["flyout"]>;
|
|
53
|
+
|
|
54
|
+
function mapPath<T extends { path: string }>(entry: T): T {
|
|
55
|
+
return { ...entry, path: toPublicServicesHref(entry.path) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function mapFlyout(flyout: NavFlyout): NavFlyout {
|
|
59
|
+
return {
|
|
60
|
+
...flyout,
|
|
61
|
+
features: flyout.features.map(mapPath),
|
|
62
|
+
...(flyout.groups
|
|
63
|
+
? {
|
|
64
|
+
groups: flyout.groups.map((group) => ({
|
|
65
|
+
...group,
|
|
66
|
+
items: group.items.map(mapPath),
|
|
67
|
+
})),
|
|
68
|
+
}
|
|
69
|
+
: {}),
|
|
70
|
+
actions: flyout.actions.map(mapPath),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Copies of the nav items with every services path mapped onto
|
|
76
|
+
* `SERVICES_PATH` — the item's own `path` and each flyout feature, group item
|
|
77
|
+
* and action. Every other field is carried by reference.
|
|
78
|
+
*/
|
|
79
|
+
export function withPublicServicesPaths(items: NavItem[]): NavItem[] {
|
|
80
|
+
return items.map((item) => ({
|
|
81
|
+
...item,
|
|
82
|
+
...(item.path !== undefined
|
|
83
|
+
? { path: toPublicServicesHref(item.path) }
|
|
84
|
+
: {}),
|
|
85
|
+
...(item.flyout ? { flyout: mapFlyout(item.flyout) } : {}),
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Copies of the footer columns with every item's services path mapped onto `SERVICES_PATH`. */
|
|
90
|
+
export function withPublicServicesColumns<
|
|
91
|
+
C extends { items: { path: string }[] },
|
|
92
|
+
>(columns: C[]): C[] {
|
|
93
|
+
return columns.map((column) => ({
|
|
94
|
+
...column,
|
|
95
|
+
items: column.items.map(mapPath),
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
@@ -517,8 +517,13 @@ type HookContext = Parameters<
|
|
|
517
517
|
/**
|
|
518
518
|
* A refused magic-link verify: the redirect the plugin's own
|
|
519
519
|
* `redirectWithError` builds — the request's `errorCallbackURL`, else its
|
|
520
|
-
* `callbackURL`, else `/`, against the base URL, with `error=<code>`.
|
|
521
|
-
*
|
|
520
|
+
* `callbackURL`, else `/`, against the base URL, with `error=<code>`.
|
|
521
|
+
*
|
|
522
|
+
* The query value is used as the router decoded it, never decoded again
|
|
523
|
+
* (#9896): a second decode turns `/%2F%2Fevil.com` into a URL on another
|
|
524
|
+
* host, and a malformed `%` into a throw. Whatever the plugin's origin check
|
|
525
|
+
* allowed, a value that does not resolve on the base URL's origin — or does
|
|
526
|
+
* not parse — is replaced by `/`.
|
|
522
527
|
*/
|
|
523
528
|
function verifyRefusal(
|
|
524
529
|
context: NonNullable<HookContext>,
|
|
@@ -527,10 +532,16 @@ function verifyRefusal(
|
|
|
527
532
|
const raw =
|
|
528
533
|
bodyField(context.query, "errorCallbackURL") ??
|
|
529
534
|
bodyField(context.query, "callbackURL");
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
535
|
+
const { baseURL } = context.context;
|
|
536
|
+
let url = new URL("/", baseURL);
|
|
537
|
+
if (typeof raw === "string") {
|
|
538
|
+
try {
|
|
539
|
+
const resolved = new URL(raw, baseURL);
|
|
540
|
+
if (resolved.origin === url.origin) url = resolved;
|
|
541
|
+
} catch {
|
|
542
|
+
// Unparseable: stays `/`.
|
|
543
|
+
}
|
|
544
|
+
}
|
|
534
545
|
url.searchParams.set("error", code);
|
|
535
546
|
return context.redirect(url.toString());
|
|
536
547
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The site's header settings (#10046) — `_site.mdx`'s top-level `header`.
|
|
3
|
+
*
|
|
4
|
+
* `header.cta` replaces the signed-out Sign in / Sign up pair with the site's
|
|
5
|
+
* own button; `header.signIn` keeps Sign in beside it as a quieter text link
|
|
6
|
+
* (`link`, the default) or hides it. Signed in, the header is unchanged.
|
|
7
|
+
*
|
|
8
|
+
* A local frontmatter read rather than `@working-theory/site-config/server`,
|
|
9
|
+
* for the same reason as `app/[locale]/contact/site-contact-email.ts`:
|
|
10
|
+
* `architecture.yaml` keeps `node:fs` out of a tree that also carries edge
|
|
11
|
+
* routes (#1567). The value is validated against the canonical schema from
|
|
12
|
+
* the client-safe `/schema` entry.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
SiteConfigSchema,
|
|
20
|
+
type SiteHeader,
|
|
21
|
+
} from "@working-theory/site-config/schema";
|
|
22
|
+
import type { NavAction } from "@working-theory/ui/chrome";
|
|
23
|
+
import { parse as parseYaml } from "yaml";
|
|
24
|
+
|
|
25
|
+
import { resolveContentRoot } from "./site-brand";
|
|
26
|
+
|
|
27
|
+
/** The `labelKey` the CTA action carries; its test id is `nav-action-headerCta`. */
|
|
28
|
+
export const HEADER_CTA_LABEL_KEY = "headerCta";
|
|
29
|
+
|
|
30
|
+
type LinkAction = Extract<NavAction, { path: string }>;
|
|
31
|
+
|
|
32
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
33
|
+
|
|
34
|
+
const HeaderSchema = SiteConfigSchema.shape.header.unwrap();
|
|
35
|
+
|
|
36
|
+
let warned = false;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The validated `header`, else `undefined`. An absent file, frontmatter or
|
|
40
|
+
* key is silent; an invalid `header` warns once per process, naming the field
|
|
41
|
+
* path and never the value.
|
|
42
|
+
*/
|
|
43
|
+
export function readSiteHeader(): SiteHeader | undefined {
|
|
44
|
+
try {
|
|
45
|
+
const raw = readFileSync(join(resolveContentRoot(), "_site.mdx"), "utf8");
|
|
46
|
+
const match = FRONTMATTER_RE.exec(raw);
|
|
47
|
+
if (!match) return undefined;
|
|
48
|
+
const fm: unknown = parseYaml(match[1] ?? "");
|
|
49
|
+
if (!fm || typeof fm !== "object") return undefined;
|
|
50
|
+
|
|
51
|
+
const { header } = fm as { header?: unknown };
|
|
52
|
+
if (header === undefined) return undefined;
|
|
53
|
+
|
|
54
|
+
// #10049 — `style` is page-chrome.ts's to validate and warn about; set it
|
|
55
|
+
// aside so a bad style never drops the button.
|
|
56
|
+
const parsed = HeaderSchema.safeParse(
|
|
57
|
+
header !== null && typeof header === "object" ? { ...header, style: undefined } : header,
|
|
58
|
+
);
|
|
59
|
+
if (parsed.success) return parsed.data;
|
|
60
|
+
if (!warned) {
|
|
61
|
+
warned = true;
|
|
62
|
+
const issue = parsed.error.issues[0];
|
|
63
|
+
console.warn(
|
|
64
|
+
`[site-header] ignoring _site.mdx header: ${["header", ...(issue?.path ?? [])].join(".")} ${issue?.message ?? ""}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
} catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The signed-out header actions. No `cta` → the auth pair as given. A `cta`
|
|
75
|
+
* → Sign in restyled as a text link (unless `signIn: hidden`), then the CTA
|
|
76
|
+
* as the primary button; Sign up is dropped. Never mutates `authActions`.
|
|
77
|
+
*/
|
|
78
|
+
export function signedOutHeaderActions(
|
|
79
|
+
header: SiteHeader | undefined,
|
|
80
|
+
authActions: readonly LinkAction[],
|
|
81
|
+
): LinkAction[] {
|
|
82
|
+
const cta = header?.cta;
|
|
83
|
+
if (!cta) return [...authActions];
|
|
84
|
+
|
|
85
|
+
const signIn = authActions.find((action) => action.labelKey === "signIn");
|
|
86
|
+
const signInPart: LinkAction[] =
|
|
87
|
+
signIn && header.signIn !== "hidden"
|
|
88
|
+
? [{ ...signIn, variant: "ghost" }]
|
|
89
|
+
: [];
|
|
90
|
+
return [
|
|
91
|
+
...signInPart,
|
|
92
|
+
{ labelKey: HEADER_CTA_LABEL_KEY, path: cta.href, variant: "primary" },
|
|
93
|
+
];
|
|
94
|
+
}
|
|
@@ -13,10 +13,45 @@
|
|
|
13
13
|
// carries the submitter's address as `/sign-in?email=…` and the hub's
|
|
14
14
|
// authorize step as `login_hint=…` — personal data GA and Sentry must not get.
|
|
15
15
|
//
|
|
16
|
+
// `ref` joined by #9966 (D2, SEC-9966-7): the hub's consent page,
|
|
17
|
+
// `/account/connect?ref=…`, carries a sealed authorize continuation.
|
|
18
|
+
//
|
|
16
19
|
// Mirrored (duplicated, not shared) in packages/analytics/src/scrub.ts,
|
|
17
20
|
// since apps/* must not import @working-theory/* packages — keep both lists
|
|
18
21
|
// in sync (#5613).
|
|
19
|
-
export const TOKEN_QUERY_PARAMS = [
|
|
22
|
+
export const TOKEN_QUERY_PARAMS = [
|
|
23
|
+
"g",
|
|
24
|
+
"session_id",
|
|
25
|
+
"email",
|
|
26
|
+
"login_hint",
|
|
27
|
+
"ref",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// The same grant id rides a PATH too: `deliverables.openGrant` fetches
|
|
31
|
+
// `<hub>/v1/deliverables/<grantId>` server-side, so a sampled server trace or
|
|
32
|
+
// a fetch breadcrumb would carry it (#9162, PR #9152's security review). The
|
|
33
|
+
// id segment is replaced anywhere in the string, so a span description
|
|
34
|
+
// (`GET <url>`) is redacted too when a caller passes one. `/v1/deliverables/open/<slug>` is the ungated
|
|
35
|
+
// read; a slug is not a credential. apps/web only: the analytics mirror sees
|
|
36
|
+
// page URLs, never this server-side fetch.
|
|
37
|
+
const GRANT_PATH = /(\/v1\/deliverables\/)(?!open(?:[/?#\s]|$))[^/?#\s]+/g;
|
|
38
|
+
const GRANT_PATH_REDACTED = "$1:grantId";
|
|
39
|
+
|
|
40
|
+
// #9969 D9 (#8329) — the Stripe SDK's own request URLs carry a customer id:
|
|
41
|
+
// `customers.retrieve` in the path (`/v1/customers/cus_…`) and
|
|
42
|
+
// `subscriptions.list` in the query (`?customer=cus_…`), and server-side
|
|
43
|
+
// Sentry records those outgoing requests. A changeset once stated this as a
|
|
44
|
+
// precondition for turning server telemetry on; it now lives here, with its
|
|
45
|
+
// enforcement. Server-side only, so not mirrored into packages/analytics:
|
|
46
|
+
// GA sees page URLs, never a Stripe API URL.
|
|
47
|
+
const STRIPE_ID_QUERY_PARAMS = ["customer"];
|
|
48
|
+
const STRIPE_CUSTOMER_SEGMENT = /\/cus_[A-Za-z0-9]+(?=[/?#\s]|$)/g;
|
|
49
|
+
const STRIPE_CUSTOMER_SEGMENT_REDACTED = "/cus_redacted";
|
|
50
|
+
|
|
51
|
+
const SCRUBBED_QUERY_PARAMS = [
|
|
52
|
+
...TOKEN_QUERY_PARAMS,
|
|
53
|
+
...STRIPE_ID_QUERY_PARAMS,
|
|
54
|
+
];
|
|
20
55
|
|
|
21
56
|
// Sentry navigation breadcrumbs (`data.to`/`data.from`) are relative
|
|
22
57
|
// (`path?search`, no origin) rather than the absolute URLs GA and
|
|
@@ -29,7 +64,10 @@ function hasScheme(url: string): boolean {
|
|
|
29
64
|
return /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(url) || url.startsWith("//");
|
|
30
65
|
}
|
|
31
66
|
|
|
32
|
-
export function scrubTelemetryUrl(
|
|
67
|
+
export function scrubTelemetryUrl(input: string): string {
|
|
68
|
+
const url = input
|
|
69
|
+
.replace(GRANT_PATH, GRANT_PATH_REDACTED)
|
|
70
|
+
.replace(STRIPE_CUSTOMER_SEGMENT, STRIPE_CUSTOMER_SEGMENT_REDACTED);
|
|
33
71
|
const relative = !hasScheme(url);
|
|
34
72
|
let parsed: URL;
|
|
35
73
|
try {
|
|
@@ -39,7 +77,7 @@ export function scrubTelemetryUrl(url: string): string {
|
|
|
39
77
|
}
|
|
40
78
|
|
|
41
79
|
let changed = false;
|
|
42
|
-
for (const param of
|
|
80
|
+
for (const param of SCRUBBED_QUERY_PARAMS) {
|
|
43
81
|
if (parsed.searchParams.has(param)) {
|
|
44
82
|
parsed.searchParams.delete(param);
|
|
45
83
|
changed = true;
|
|
@@ -52,3 +90,30 @@ export function scrubTelemetryUrl(url: string): string {
|
|
|
52
90
|
const result = parsed.toString();
|
|
53
91
|
return relative ? result.slice(RELATIVE_URL_BASE.length) : result;
|
|
54
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A bare query string (`?a=1&b=2`, with or without the `?`) with the same
|
|
96
|
+
* params dropped — the SDK records an outgoing request's query under
|
|
97
|
+
* `http.query` apart from its URL (#9969 D9).
|
|
98
|
+
*/
|
|
99
|
+
export function scrubTelemetryQuery(query: string): string {
|
|
100
|
+
const params = new URLSearchParams(query);
|
|
101
|
+
let changed = false;
|
|
102
|
+
for (const param of SCRUBBED_QUERY_PARAMS) {
|
|
103
|
+
if (params.has(param)) {
|
|
104
|
+
params.delete(param);
|
|
105
|
+
changed = true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!changed) return query;
|
|
109
|
+
const rest = params.toString();
|
|
110
|
+
return rest === "" ? "" : `${query.startsWith("?") ? "?" : ""}${rest}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** An HTTP client span's `<METHOD> <url>` with its URL scrubbed (#9969 D9). */
|
|
114
|
+
export function scrubSpanDescription(description: string): string {
|
|
115
|
+
const match = /^([A-Z]+) (\S+)$/.exec(description);
|
|
116
|
+
return match
|
|
117
|
+
? `${match[1]} ${scrubTelemetryUrl(match[2]!)}`
|
|
118
|
+
: scrubTelemetryUrl(description);
|
|
119
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The template-listing mapping (#10051 D6) — a TemplatePage's frontmatter to
|
|
3
|
+
* the /templates hub's card and to the generated sections of its own
|
|
4
|
+
* /templates/<slug> page. Pure, no I/O: the routes load the pages and the
|
|
5
|
+
* translated labels and pass them in, so no user-facing copy lives here
|
|
6
|
+
* (customer-facing-i18n, ADR 0019).
|
|
7
|
+
*
|
|
8
|
+
* Base tree: no paid import, no `oss: false`.
|
|
9
|
+
*/
|
|
10
|
+
import { SITE_CATEGORIES } from "@working-theory/schema/categories";
|
|
11
|
+
import type { PageSection } from "@working-theory/validation";
|
|
12
|
+
|
|
13
|
+
/** The `templates` messages namespace, read by the routes (#10051 D8). */
|
|
14
|
+
export interface TemplateLabels {
|
|
15
|
+
getIt: string;
|
|
16
|
+
startFree: string;
|
|
17
|
+
viewDemo: string;
|
|
18
|
+
viewTemplate: string;
|
|
19
|
+
included: string;
|
|
20
|
+
demoBadge: string;
|
|
21
|
+
/** The tail of a category label cut at {@link MAX_CATEGORY_NAMES}: "+9 more". */
|
|
22
|
+
moreCategories: (count: number) => string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Read the labels from the `templates` namespace's translator. */
|
|
26
|
+
export function templateLabels(
|
|
27
|
+
t: (key: keyof TemplateLabels, values?: { count: number }) => string,
|
|
28
|
+
): TemplateLabels {
|
|
29
|
+
return {
|
|
30
|
+
getIt: t("getIt"),
|
|
31
|
+
startFree: t("startFree"),
|
|
32
|
+
viewDemo: t("viewDemo"),
|
|
33
|
+
viewTemplate: t("viewTemplate"),
|
|
34
|
+
included: t("included"),
|
|
35
|
+
demoBadge: t("demoBadge"),
|
|
36
|
+
moreCategories: (count) => t("moreCategories", { count }),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* At most this many category names show in a card or hero label; the rest
|
|
42
|
+
* collapse into "+N more", so a template listed under twelve categories keeps
|
|
43
|
+
* a one-line label and the card titles in a row stay aligned (design review
|
|
44
|
+
* round 1, B2).
|
|
45
|
+
*/
|
|
46
|
+
export const MAX_CATEGORY_NAMES = 3;
|
|
47
|
+
|
|
48
|
+
/** The TemplatePage frontmatter fields this mapping reads. */
|
|
49
|
+
interface TemplateFrontmatter {
|
|
50
|
+
title?: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
summary?: string;
|
|
53
|
+
thumbnail?: string;
|
|
54
|
+
categories?: readonly string[];
|
|
55
|
+
demoUrl?: string;
|
|
56
|
+
getItUrl?: string;
|
|
57
|
+
price?: number;
|
|
58
|
+
includes?: readonly { title: string; description: string }[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type CardItem = Extract<
|
|
62
|
+
PageSection,
|
|
63
|
+
{ type: "items" }
|
|
64
|
+
>["data"]["items"][number];
|
|
65
|
+
|
|
66
|
+
function fields(frontmatter: unknown): TemplateFrontmatter {
|
|
67
|
+
return (frontmatter ?? {}) as TemplateFrontmatter;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The known category display names, the first {@link MAX_CATEGORY_NAMES}
|
|
72
|
+
* joined and the rest counted; undefined when none is known.
|
|
73
|
+
*/
|
|
74
|
+
function categoryNames(
|
|
75
|
+
categories: readonly string[] | undefined,
|
|
76
|
+
labels: TemplateLabels,
|
|
77
|
+
): string | undefined {
|
|
78
|
+
const names = (categories ?? []).flatMap((slug) => {
|
|
79
|
+
const found = SITE_CATEGORIES.find((category) => category.slug === slug);
|
|
80
|
+
return found === undefined ? [] : [found.name];
|
|
81
|
+
});
|
|
82
|
+
if (names.length === 0) return undefined;
|
|
83
|
+
const shown = names.slice(0, MAX_CATEGORY_NAMES);
|
|
84
|
+
const hidden = names.length - shown.length;
|
|
85
|
+
return hidden > 0
|
|
86
|
+
? [...shown, labels.moreCategories(hidden)].join(" · ")
|
|
87
|
+
: shown.join(" · ");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** One /templates card for a TemplatePage. */
|
|
91
|
+
export function templateCard(
|
|
92
|
+
page: { slug: string; frontmatter: unknown },
|
|
93
|
+
labels: TemplateLabels,
|
|
94
|
+
): CardItem {
|
|
95
|
+
const fm = fields(page.frontmatter);
|
|
96
|
+
const title = fm.title ?? page.slug;
|
|
97
|
+
const eyebrow = categoryNames(fm.categories, labels);
|
|
98
|
+
return {
|
|
99
|
+
title,
|
|
100
|
+
description: fm.summary ?? fm.description ?? "",
|
|
101
|
+
...(fm.thumbnail !== undefined && {
|
|
102
|
+
image: { src: fm.thumbnail, alt: title },
|
|
103
|
+
}),
|
|
104
|
+
...(eyebrow !== undefined && { eyebrow }),
|
|
105
|
+
...(fm.demoUrl !== undefined && { badge: labels.demoBadge }),
|
|
106
|
+
link: { label: labels.viewTemplate, href: `/templates/${page.slug}` },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** "Start free" for a template priced at exactly 0, else "Get it". */
|
|
111
|
+
export function templateActionLabel(
|
|
112
|
+
price: number | undefined,
|
|
113
|
+
labels: TemplateLabels,
|
|
114
|
+
): string {
|
|
115
|
+
return price === 0 ? labels.startFree : labels.getIt;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The generated top of a /templates/<slug> page: a hero carrying the name,
|
|
120
|
+
* summary, thumbnail, categories, the Get it / Start free action and the
|
|
121
|
+
* View demo link, then the "What's included" list.
|
|
122
|
+
*/
|
|
123
|
+
export function templateDetailSections(
|
|
124
|
+
frontmatter: unknown,
|
|
125
|
+
labels: TemplateLabels,
|
|
126
|
+
): [PageSection, PageSection] {
|
|
127
|
+
const fm = fields(frontmatter);
|
|
128
|
+
const title = fm.title ?? "";
|
|
129
|
+
const subheadline = fm.summary ?? fm.description;
|
|
130
|
+
const eyebrow = categoryNames(fm.categories, labels);
|
|
131
|
+
const hero: PageSection = {
|
|
132
|
+
type: "hero",
|
|
133
|
+
id: "template-hero",
|
|
134
|
+
data: {
|
|
135
|
+
headline: title,
|
|
136
|
+
...(subheadline !== undefined && { subheadline }),
|
|
137
|
+
...(eyebrow !== undefined && { eyebrow }),
|
|
138
|
+
...(fm.thumbnail !== undefined && {
|
|
139
|
+
image: { src: fm.thumbnail, alt: title },
|
|
140
|
+
}),
|
|
141
|
+
...(fm.getItUrl !== undefined && {
|
|
142
|
+
primaryCta: {
|
|
143
|
+
label: templateActionLabel(fm.price, labels),
|
|
144
|
+
href: fm.getItUrl,
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
...(fm.demoUrl !== undefined && {
|
|
148
|
+
secondaryCta: {
|
|
149
|
+
label: labels.viewDemo,
|
|
150
|
+
href: fm.demoUrl,
|
|
151
|
+
variant: "outline" as const,
|
|
152
|
+
},
|
|
153
|
+
}),
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
const included: PageSection = {
|
|
157
|
+
type: "items",
|
|
158
|
+
id: "template-included",
|
|
159
|
+
data: {
|
|
160
|
+
headline: labels.included,
|
|
161
|
+
variant: "minimal",
|
|
162
|
+
items: (fm.includes ?? []).map(({ title: rowTitle, description }) => ({
|
|
163
|
+
title: rowTitle,
|
|
164
|
+
description,
|
|
165
|
+
})),
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
return [hero, included];
|
|
169
|
+
}
|
|
@@ -295,7 +295,13 @@
|
|
|
295
295
|
"addonsHeadline": "Add-ons",
|
|
296
296
|
"monthly": "{amount}/mo",
|
|
297
297
|
"annual": "{amount}/yr",
|
|
298
|
-
"oneTime": "{amount} one-time"
|
|
298
|
+
"oneTime": "{amount} one-time",
|
|
299
|
+
"withRequired": "{plan} + {required}",
|
|
300
|
+
"depositNote": "{deposit} deposit due today; the balance is invoiced."
|
|
301
|
+
},
|
|
302
|
+
"pricingPages": {
|
|
303
|
+
"heading": "Pricing by offering",
|
|
304
|
+
"viewPricing": "See pricing"
|
|
299
305
|
},
|
|
300
306
|
"purchasePolicy": {
|
|
301
307
|
"heading": "Purchase Policy",
|
|
@@ -393,7 +399,9 @@
|
|
|
393
399
|
},
|
|
394
400
|
"services": {
|
|
395
401
|
"heading": "Services",
|
|
396
|
-
"emptyState": "Services listing coming soon."
|
|
402
|
+
"emptyState": "Services listing coming soon.",
|
|
403
|
+
"childrenHeading": "Services in this area",
|
|
404
|
+
"viewService": "View service"
|
|
397
405
|
},
|
|
398
406
|
"software": {
|
|
399
407
|
"heading": "Software",
|
|
@@ -425,6 +433,17 @@
|
|
|
425
433
|
"heading": "Alternatives",
|
|
426
434
|
"emptyState": "Comparisons coming soon."
|
|
427
435
|
},
|
|
436
|
+
"templates": {
|
|
437
|
+
"heading": "Templates",
|
|
438
|
+
"emptyState": "Templates coming soon.",
|
|
439
|
+
"getIt": "Get it",
|
|
440
|
+
"startFree": "Start free",
|
|
441
|
+
"viewDemo": "View demo",
|
|
442
|
+
"viewTemplate": "View template",
|
|
443
|
+
"included": "What's included",
|
|
444
|
+
"demoBadge": "Live demo",
|
|
445
|
+
"moreCategories": "+{count} more"
|
|
446
|
+
},
|
|
428
447
|
"reports": {
|
|
429
448
|
"heading": "Reports",
|
|
430
449
|
"emptyState": "Reports coming soon.",
|
|
@@ -1058,7 +1077,8 @@
|
|
|
1058
1077
|
"site": "Site",
|
|
1059
1078
|
"team": "Team",
|
|
1060
1079
|
"magnets": "Magnets",
|
|
1061
|
-
"meetings": "Meetings"
|
|
1080
|
+
"meetings": "Meetings",
|
|
1081
|
+
"integrations": "Integrations"
|
|
1062
1082
|
},
|
|
1063
1083
|
"general": {
|
|
1064
1084
|
"heading": "General",
|
|
@@ -1250,6 +1270,115 @@
|
|
|
1250
1270
|
"privateOffToast": "Private: off for {title}",
|
|
1251
1271
|
"errorToast": "Couldn't update"
|
|
1252
1272
|
}
|
|
1273
|
+
},
|
|
1274
|
+
"integrations": {
|
|
1275
|
+
"heading": "Integrations",
|
|
1276
|
+
"lead": "Connect the services your site works with.",
|
|
1277
|
+
"slack": {
|
|
1278
|
+
"title": "Connection",
|
|
1279
|
+
"connectedBadge": "Connected",
|
|
1280
|
+
"row": {
|
|
1281
|
+
"notConnectedTitle": "Enable team collaboration in Slack",
|
|
1282
|
+
"notConnectedLine": "Connect your Slack workspace so your team can work with this site in Slack.",
|
|
1283
|
+
"connectedTitle": "Site workspace",
|
|
1284
|
+
"connectedLine": "Enable team collaboration in Slack.",
|
|
1285
|
+
"unavailableTitle": "We couldn't check Slack.",
|
|
1286
|
+
"unavailableLine": "The Slack connection didn't load. Try again in a moment."
|
|
1287
|
+
},
|
|
1288
|
+
"connect": "Connect",
|
|
1289
|
+
"tryAgain": "Try again",
|
|
1290
|
+
"moreActions": "More Slack actions",
|
|
1291
|
+
"replace": "Replace…",
|
|
1292
|
+
"disconnect": "Disconnect…",
|
|
1293
|
+
"setup": {
|
|
1294
|
+
"region": "Set up Slack",
|
|
1295
|
+
"tokenStep": "Create a Slack app and paste its bot token",
|
|
1296
|
+
"tokenTip": "The app needs a paid Slack plan and these bot scopes:",
|
|
1297
|
+
"tipLabel": "More about this step",
|
|
1298
|
+
"openSlackApps": "Open Slack apps",
|
|
1299
|
+
"slackAppsHref": "https://api.slack.com/apps",
|
|
1300
|
+
"tokenPlaceholder": "xoxb-…",
|
|
1301
|
+
"tokenSaved": "Bot token saved",
|
|
1302
|
+
"urlStep": "Copy the events URL into Slack",
|
|
1303
|
+
"urlTip": "In your Slack app, open Event Subscriptions, turn on events, paste this as the Request URL, and subscribe to the bot events {events}.",
|
|
1304
|
+
"urlUnavailable": "The API's address isn't configured, so the events URL can't be shown.",
|
|
1305
|
+
"copy": "Copy",
|
|
1306
|
+
"copied": "Copied",
|
|
1307
|
+
"urlCopied": "Events URL copied",
|
|
1308
|
+
"secretStep": "Paste the app’s signing secret",
|
|
1309
|
+
"secretTip": "In your Slack app, under Basic Information → App Credentials.",
|
|
1310
|
+
"secretPlaceholder": "Signing secret",
|
|
1311
|
+
"save": "Save",
|
|
1312
|
+
"saving": "Saving…",
|
|
1313
|
+
"cancel": "Cancel",
|
|
1314
|
+
"done": "Done"
|
|
1315
|
+
},
|
|
1316
|
+
"botTokenInvalid": "A bot token starts with xoxb- and holds only letters, digits and hyphens.",
|
|
1317
|
+
"signingSecretInvalid": "A signing secret is 32 to 128 letters and digits.",
|
|
1318
|
+
"tokenRefused": {
|
|
1319
|
+
"tokenRefused": "Slack refused this token. Check it and try again.",
|
|
1320
|
+
"notBotToken": "This isn't a bot token. Copy the Bot User OAuth Token (xoxb-).",
|
|
1321
|
+
"enterpriseInstall": "Enterprise Grid installs aren't supported yet."
|
|
1322
|
+
},
|
|
1323
|
+
"scopesMissing": "The Slack app is missing these scopes:",
|
|
1324
|
+
"paidPlanRequired": "Slack Connect needs a paid Slack plan.",
|
|
1325
|
+
"teamConnectedElsewhere": "This Slack workspace is already connected to another site.",
|
|
1326
|
+
"slackUnreachable": "Slack couldn't be reached. Try again in a moment.",
|
|
1327
|
+
"connectFailed": "Couldn't connect Slack. Try again.",
|
|
1328
|
+
"disconnectFailed": "Couldn't disconnect Slack. Try again.",
|
|
1329
|
+
"confirmReplace": {
|
|
1330
|
+
"title": "Replace the Slack connection?",
|
|
1331
|
+
"description": "Prospect channels will open in the new Slack workspace.",
|
|
1332
|
+
"action": "Replace"
|
|
1333
|
+
},
|
|
1334
|
+
"confirmDisconnect": {
|
|
1335
|
+
"title": "Disconnect Slack?",
|
|
1336
|
+
"description": "New bookings will stop opening prospect channels until Slack is connected again.",
|
|
1337
|
+
"action": "Disconnect"
|
|
1338
|
+
}
|
|
1339
|
+
},
|
|
1340
|
+
"prospectChannel": {
|
|
1341
|
+
"heading": "Prospect channels",
|
|
1342
|
+
"lead": "Open a shared Slack channel with each prospect who books a meeting.",
|
|
1343
|
+
"switchLabel": "Prospect channels",
|
|
1344
|
+
"showSettings": "Show prospect channel settings",
|
|
1345
|
+
"hideSettings": "Hide prospect channel settings",
|
|
1346
|
+
"members": "Your channel team members",
|
|
1347
|
+
"membersTipLabel": "About members",
|
|
1348
|
+
"membersTip": "Slack member ids of teammates to add to each new channel, separated by commas. The site's owner is always added.",
|
|
1349
|
+
"membersPlaceholder": "U012AB3CD, U045EF6GH",
|
|
1350
|
+
"welcomeTemplate": "Welcome message for prospects",
|
|
1351
|
+
"mergeFieldsLabel": "Merge fields",
|
|
1352
|
+
"mergeFieldsLead": "You can use",
|
|
1353
|
+
"mergeFields": {
|
|
1354
|
+
"firstName": "The prospect's first name",
|
|
1355
|
+
"hostName": "The host's name",
|
|
1356
|
+
"callTime": "The meeting's time",
|
|
1357
|
+
"interest": "What the prospect is interested in",
|
|
1358
|
+
"timeline": "The prospect's timeline",
|
|
1359
|
+
"meetingLink": "The meeting link"
|
|
1360
|
+
},
|
|
1361
|
+
"welcomePlaceholder": "Leave blank to send the default welcome.",
|
|
1362
|
+
"channelPrefix": "Channel prefix",
|
|
1363
|
+
"prefixTipLabel": "About the channel prefix",
|
|
1364
|
+
"prefixTip": "Each booking gets its own new Slack channel. Its name starts with this prefix, then a short random code, like {example}, so all prospect channels sort together in Slack.",
|
|
1365
|
+
"prefixExample": "prospect-k3x9m2qa",
|
|
1366
|
+
"archiveDays": "Archive after (days)",
|
|
1367
|
+
"save": "Save",
|
|
1368
|
+
"saving": "Saving…",
|
|
1369
|
+
"saved": "Saved",
|
|
1370
|
+
"saveError": "Couldn't save. Try again.",
|
|
1371
|
+
"slackNotConnected": "Slack isn't connected, so these settings can't be changed.",
|
|
1372
|
+
"connectSlackLink": "Connect Slack",
|
|
1373
|
+
"errors": {
|
|
1374
|
+
"memberIds": "Enter Slack member ids like U012AB3CD, separated by commas, with no repeats (up to 50).",
|
|
1375
|
+
"welcomeTemplate": "Use only the merge fields listed, in up to 4,000 characters.",
|
|
1376
|
+
"channelPrefix": "Use lowercase letters, digits, - and _, starting with a letter or digit (up to 21).",
|
|
1377
|
+
"archiveDays": "Enter a whole number of days from 1 to 365."
|
|
1378
|
+
},
|
|
1379
|
+
"unavailableHeadline": "Prospect channel settings couldn't be read",
|
|
1380
|
+
"unavailableDescription": "Reload the page to try again, or come back later."
|
|
1381
|
+
}
|
|
1253
1382
|
}
|
|
1254
1383
|
},
|
|
1255
1384
|
"edit": {
|
|
@@ -1370,5 +1499,17 @@
|
|
|
1370
1499
|
"already": "You were already unsubscribed.",
|
|
1371
1500
|
"invalid": "This link isn't valid.",
|
|
1372
1501
|
"unavailable": "Unsubscribing isn't available right now."
|
|
1502
|
+
},
|
|
1503
|
+
"hubConnect": {
|
|
1504
|
+
"metaTitle": "Sign in to a site",
|
|
1505
|
+
"title": "Sign in to {name}?",
|
|
1506
|
+
"lead": "{origin} will see your name and email address, {email}. We ask once for each site.",
|
|
1507
|
+
"continue": "Continue",
|
|
1508
|
+
"cancel": "Cancel",
|
|
1509
|
+
"unavailable": {
|
|
1510
|
+
"title": "This sign-in can't continue",
|
|
1511
|
+
"body": "The link may have expired. Go back to the site you came from and sign in again.",
|
|
1512
|
+
"back": "Back to your account"
|
|
1513
|
+
}
|
|
1373
1514
|
}
|
|
1374
1515
|
}
|