@marktiderman/genesis-sites 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +465 -2
  3. package/dist/assets/form-island.c0f6fac859.js +51 -0
  4. package/dist/assets/islands.77d11f5697.js +1 -0
  5. package/dist/assets/sites-forms.4e6aa42497.css +10 -0
  6. package/dist/assets/sites-forms.css +10 -0
  7. package/dist/assets/sites.css +2 -0
  8. package/dist/assets/sites.e1ffd2708b.css +2 -0
  9. package/dist/blocks.d.ts +13 -0
  10. package/dist/blocks.js +26 -0
  11. package/dist/blocks.js.map +1 -0
  12. package/dist/chunk-3VKTBE7S.js +69 -0
  13. package/dist/chunk-3VKTBE7S.js.map +1 -0
  14. package/dist/chunk-C375ZPU2.js +2241 -0
  15. package/dist/chunk-C375ZPU2.js.map +1 -0
  16. package/dist/chunk-IXMY4B56.js +877 -0
  17. package/dist/chunk-IXMY4B56.js.map +1 -0
  18. package/dist/chunk-VZUDRPFG.js +13 -0
  19. package/dist/chunk-VZUDRPFG.js.map +1 -0
  20. package/dist/chunk-ZGWGQGT2.js +595 -0
  21. package/dist/chunk-ZGWGQGT2.js.map +1 -0
  22. package/dist/import.d.ts +130 -0
  23. package/dist/import.js +330 -0
  24. package/dist/import.js.map +1 -0
  25. package/dist/index.d.ts +273 -0
  26. package/dist/index.js +254 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/registry-D_DCWRTc.d.ts +1030 -0
  29. package/dist/sections.d.ts +802 -0
  30. package/dist/sections.js +6 -0
  31. package/dist/sections.js.map +1 -0
  32. package/dist/store-DkSycDhq.d.ts +443 -0
  33. package/dist/supabase.d.ts +41 -0
  34. package/dist/supabase.js +283 -0
  35. package/dist/supabase.js.map +1 -0
  36. package/dist/web.d.ts +136 -0
  37. package/dist/web.js +479 -0
  38. package/dist/web.js.map +1 -0
  39. package/migrations/0001_genesis_sites.sql +821 -0
  40. package/migrations/0002_genesis_sites_posts.sql +404 -0
  41. package/migrations/tests/0001_genesis_sites.test.sql +349 -0
  42. package/migrations/tests/0002_genesis_sites_posts.test.sql +242 -0
  43. package/package.json +152 -3
@@ -0,0 +1,273 @@
1
+ import { S as SectionNode, P as PageDoc, A as AnySectionDefinition, a as SiteDoc, R as ReleasePage, b as PostSummary, L as LivePost, c as Redirect, d as Link, e as SiteDocInput, f as PageKind } from './registry-D_DCWRTc.js';
2
+ export { B as BUILT_IN_SECTION_TYPES, g as BuiltInSectionType, C as Copy, F as FileRef, h as FontChoice, i as FormsResolver, j as FormsResolverContext, H as Href, I as Image, k as ImportPost, l as ImportPostsResult, m as Line, n as LinkEvent, M as MAX_DOC_BYTES, o as MAX_IMPORT_POSTS, p as MAX_LIVE_POSTS, q as MenuItem, r as PageMeta, s as PagePath, t as PostAuthor, u as PostDoc, v as PostDocInput, w as PostDraft, x as PostMeta, y as PostSlug, z as PostStatus, D as PostSummarySchema, E as PostsContext, G as RegisterSectionOptions, J as Release, K as ReleaseMeta, N as ReleaseSnapshot, O as RenderContext, Q as ResolvedForm, T as RichText, U as SECTION_CATEGORIES, V as SavePostInput, W as SavePostResult, X as SectionCategory, Y as SectionDefinition, Z as SectionDefinitionError, _ as SectionId, $ as SectionManifest, a0 as SectionPlacement, a1 as SectionPreset, a2 as SectionRenderProps, a3 as SectionVariant, a4 as Seo, a5 as SeoDefaults, a6 as Surface, a7 as TokenOverrides, a8 as defineSection, a9 as getSection, aa as isPlainEmail, ab as listSections, ac as mailtoHref, ad as newSectionId, ae as nodeFromPreset, af as opensInNewTab, ag as parseSnapshot, ah as postSummary, ai as registerBuiltInSection, aj as registerSection, ak as richTextPlain, al as sectionJsonSchema, am as sectionManifests, an as unregisterSection, ao as validatePostSlug } from './registry-D_DCWRTc.js';
3
+ import { a as SiteBrand, B as BrandInput } from './store-DkSycDhq.js';
4
+ export { A as ASSET_TYPES, b as Asset, c as AssetMeta, d as BrandSource, C as CheckReleaseOptions, D as DEFAULT_RESERVED_PATHS, F as FONT_CHOICES, e as FontInfo, I as InvalidationInfo, M as MAX_PAGE_PATH_LENGTH, f as MAX_URL_LENGTH, g as MemorySiteStore, h as MemoryStoreOptions, N as NewPage, i as NewSite, P as PAGE_PATH, j as PageDraft, k as PreparedRelease, l as PublishBlockedError, m as PublishOptions, R as ReservedPaths, n as SaveResult, o as SiteConflictError, p as SiteDraft, q as SiteIssue, S as SiteStore, r as SiteTheme, T as ThemePair, s as assetPath, t as brandDisplayName, u as checkBlog, v as checkContrast, w as checkHtmlOutline, x as checkPage, y as checkPost, z as checkRelease, E as checkSiteDoc, G as contrastRatio, H as createMemorySiteStore, J as formCacheTag, K as hexToHslTriplet, L as isConflict, O as isExternalHref, Q as isPublishable, U as isReservedPath, V as isSafeHref, W as isSafeImageSrc, X as isSiteBrand, Y as luminance, Z as mix, _ as normalizeRequestPath, $ as pagePathProblems, a0 as parseHex, a1 as prepareImportPost, a2 as prepareRelease, a3 as siteCacheTag, a4 as siteTheme, a5 as stableHash, a6 as themeCss, a7 as toCanonicalBrand, a8 as validatePagePath, a9 as withCacheInvalidation } from './store-DkSycDhq.js';
5
+ import 'react';
6
+ import 'zod';
7
+ import '@marktiderman/genesis-design-system';
8
+
9
+ /**
10
+ * Props migrations. A section node records the props version it was written
11
+ * at (`v`); its definition declares the current `version` and a pure
12
+ * function per step. `migrateNode` runs `migrations[v + 1 .. version]` and
13
+ * checks the result against the section's schema. The editor and the
14
+ * renderer both read through it, and a publish writes the migrated props
15
+ * into the release, so old drafts keep working after a section changes.
16
+ */
17
+
18
+ interface MigrationIssue {
19
+ severity: "error" | "warning";
20
+ code: "unknown-section" | "newer-version" | "migration-failed" | "invalid-props" | "unknown-variant";
21
+ sectionId: string;
22
+ type: string;
23
+ message: string;
24
+ }
25
+ interface MigratedNode {
26
+ /** The node at the current version with parsed props, or null when it can't be rendered. */
27
+ node: (SectionNode & {
28
+ variant: string;
29
+ }) | null;
30
+ /** Whether anything changed (the version, the props or the variant). */
31
+ changed: boolean;
32
+ issues: MigrationIssue[];
33
+ }
34
+ /** Bring one node up to its section's current version and check its props. */
35
+ declare function migrateNode(node: SectionNode, lookup?: (type: string) => AnySectionDefinition | undefined): MigratedNode;
36
+ interface MigratedPage {
37
+ doc: PageDoc;
38
+ changed: boolean;
39
+ issues: MigrationIssue[];
40
+ }
41
+ /**
42
+ * Migrate every node of a page. Nodes that can't be shown stay in the
43
+ * document unchanged (an editor must not lose them) and are reported.
44
+ */
45
+ declare function migratePage(doc: PageDoc, lookup?: (type: string) => AnySectionDefinition | undefined): MigratedPage;
46
+ declare function migrateSite(site: SiteDoc, lookup?: (type: string) => AnySectionDefinition | undefined): {
47
+ site: SiteDoc;
48
+ changed: boolean;
49
+ issues: MigrationIssue[];
50
+ };
51
+
52
+ /**
53
+ * SEO: a page's meta tags (title, description, canonical, robots, Open Graph,
54
+ * Twitter), sitemap.xml, robots.txt, JSON-LD (Organization, WebSite,
55
+ * BreadcrumbList), and the redirects a page move leaves behind.
56
+ *
57
+ * Canonical and absolute URLs are built from `origin`, the site's primary
58
+ * address, which the host passes in. Nothing here reads a request's Host.
59
+ */
60
+
61
+ interface PageMetaInput {
62
+ site: SiteDoc;
63
+ page: ReleasePage;
64
+ /** The site's primary origin, `https://riverside.example`, no trailing slash. */
65
+ origin: string;
66
+ /** The path prefix the site is served under (`""` at the root). */
67
+ basePath?: string;
68
+ /** A preview: always `noindex`. */
69
+ preview?: boolean;
70
+ /** The page is a blog post: Open Graph says `article`, with its dates. */
71
+ article?: {
72
+ publishedTime: string;
73
+ modifiedTime?: string;
74
+ author?: string;
75
+ section?: string;
76
+ };
77
+ }
78
+ interface PageMeta {
79
+ title: string;
80
+ description?: string;
81
+ canonical: string;
82
+ robots: string;
83
+ openGraph: Record<string, string>;
84
+ twitter: Record<string, string>;
85
+ }
86
+ /** The absolute URL of a site path. */
87
+ declare function absoluteUrl(origin: string, basePath: string, path: string): string;
88
+ /**
89
+ * The page's title with the site's template (`%s · Riverside Gym`), so every
90
+ * tab and search result names the site. An SEO title written for the page
91
+ * (`seo.title`) is used as it is.
92
+ */
93
+ declare function pageTitle(site: SiteDoc, doc: PageDoc, _path: string): string;
94
+ /** Every meta value a page prints in its `<head>`. */
95
+ declare function pageMeta({ site, page, origin, basePath, preview, article }: PageMetaInput): PageMeta;
96
+ declare function organizationJsonLd(site: SiteDoc, origin: string, basePath?: string): Record<string, unknown>;
97
+ declare function websiteJsonLd(site: SiteDoc, origin: string, basePath?: string): Record<string, unknown>;
98
+ /** The trail from the home page to `page`, through every ancestor path that is a page. */
99
+ declare function breadcrumbJsonLd(pages: readonly ReleasePage[], page: ReleasePage, origin: string, basePath?: string): Record<string, unknown> | null;
100
+ /** The URL of a post: `<post page path>/<slug>`. */
101
+ declare function postPath(postsPath: string, slug: string): string;
102
+ /** `Article` JSON-LD for a blog post. */
103
+ declare function articleJsonLd(site: SiteDoc, post: LivePost, url: string, origin: string, basePath?: string): Record<string, unknown>;
104
+ interface RssInput {
105
+ site: SiteDoc;
106
+ /** Published posts, newest first. */
107
+ posts: readonly PostSummary[];
108
+ /** The post page's path (`/blog`). */
109
+ postsPath: string;
110
+ /** Its title, the feed's (default: the site's name). */
111
+ title?: string;
112
+ origin: string;
113
+ basePath?: string;
114
+ /** The most items (default 20). */
115
+ limit?: number;
116
+ }
117
+ /** The blog's RSS 2.0 feed: the newest posts, with their excerpts. */
118
+ declare function rssXml({ site, posts, postsPath, title, origin, basePath, limit }: RssInput): string;
119
+ /** A post's description for a feed or a page with no excerpt: its first words. */
120
+ declare const postDescription: (doc: LivePost["doc"]) => string | undefined;
121
+ /**
122
+ * JSON for a `<script type="application/ld+json">` (or any JSON inside HTML):
123
+ * `<`, `>`, `&` and the line separators are escaped as `\uXXXX`, so stored
124
+ * text can never close the script element or open a comment.
125
+ */
126
+ declare function safeJsonForHtml(value: unknown): string;
127
+ declare function escapeXml(text: string): string;
128
+ interface SitemapInput {
129
+ site: SiteDoc;
130
+ pages: readonly ReleasePage[];
131
+ origin: string;
132
+ basePath?: string;
133
+ /** The release's publish time, as every page's `lastmod`. */
134
+ lastModified?: string | null;
135
+ /** The blog: its post page's path and the published posts (each with its own `lastmod`). */
136
+ posts?: {
137
+ path: string;
138
+ items: readonly PostSummary[];
139
+ };
140
+ }
141
+ /** sitemap.xml: every indexable page (not `noindex`, not one whose canonical points elsewhere). */
142
+ declare function sitemapXml({ site, pages, origin, basePath, lastModified, posts }: SitemapInput): string;
143
+ interface RobotsInput {
144
+ origin: string;
145
+ basePath?: string;
146
+ /** `Disallow: /` for everyone: a site that is `noindex`, or a preview. */
147
+ disallowAll?: boolean;
148
+ }
149
+ declare function robotsTxt({ origin, basePath, disallowAll }: RobotsInput): string;
150
+ /**
151
+ * The redirects after a page moves from `from` to `to`: a permanent redirect
152
+ * from the old path, earlier redirects to the old path pointed straight at
153
+ * the new one (no chains), and any redirect away from the new path dropped
154
+ * (a page lives there now). A move back undoes the earlier redirect.
155
+ */
156
+ declare function redirectsAfterMove(redirects: readonly Redirect[], from: string, to: string): Redirect[];
157
+ /** The redirect for a request path, if any (exact match on the normalised path). */
158
+ declare function matchRedirect(redirects: readonly Redirect[], path: string): Redirect | undefined;
159
+
160
+ /**
161
+ * Templates: whole sites from a little input.
162
+ *
163
+ * - `landingPage`: the one-click landing page from a brand and a message.
164
+ * Nav, hero, features, testimonials, a call to action, a form embed and a
165
+ * footer, on one page.
166
+ * - `onePageSite`: the smallest site a brand with no site can be seeded
167
+ * with: just a name, a logo, a message, and the brand's colours and fonts.
168
+ * - `starterSite`: three pages (home, about, contact) with nav and footer.
169
+ *
170
+ * Each returns documents (`site`, `pages`) for any `SiteStore` to create.
171
+ * Nothing is invented and passed off as real: copy a template can't know
172
+ * (a client's quote, a statistic) is marked `placeholder`, and the publish
173
+ * check refuses it until someone replaces it or hides the section.
174
+ */
175
+
176
+ interface TemplateMessage {
177
+ /** The hero's headline: what you do, for whom. */
178
+ headline: string;
179
+ /** One or two sentences under it. */
180
+ subhead?: string;
181
+ /** The main button. Default: "Get in touch", to the form (or `#contact`). */
182
+ cta?: Link;
183
+ /** What you offer, as the features band. Without it the band is a placeholder to fill in. */
184
+ benefits?: {
185
+ title: string;
186
+ body: string;
187
+ }[];
188
+ }
189
+ interface TemplateInput {
190
+ brand: BrandInput;
191
+ message: TemplateMessage;
192
+ /** A form to embed (its slug, for the host's forms resolver). */
193
+ form?: {
194
+ slug: string;
195
+ heading?: string;
196
+ body?: string;
197
+ };
198
+ /** For the footer. */
199
+ contact?: {
200
+ email?: string;
201
+ phone?: string;
202
+ };
203
+ }
204
+ interface TemplateResult {
205
+ site: SiteDocInput;
206
+ pages: {
207
+ path: string;
208
+ kind: PageKind;
209
+ doc: PageDoc;
210
+ }[];
211
+ }
212
+ /**
213
+ * The one-click landing page: brand plus message, on one page.
214
+ *
215
+ * @stability Beta
216
+ */
217
+ declare function landingPage(input: TemplateInput): TemplateResult;
218
+ interface OnePageSiteInput {
219
+ name: string;
220
+ logo?: {
221
+ src: string;
222
+ alt?: string;
223
+ };
224
+ message: TemplateMessage;
225
+ /** The brand's colours and fonts. */
226
+ colors: SiteBrand["colors"];
227
+ fonts?: SiteBrand["fonts"];
228
+ contact?: TemplateInput["contact"];
229
+ }
230
+ /**
231
+ * The smallest site, for seeding a brand that has none: a header, a hero,
232
+ * a call to action and a footer. `brand` is the minimal brand to store
233
+ * beside it (the host's `BrandSource` returns it).
234
+ *
235
+ * @stability Beta
236
+ */
237
+ declare function onePageSite(input: OnePageSiteInput): TemplateResult & {
238
+ brand: SiteBrand;
239
+ };
240
+ /**
241
+ * A three-page starter site: home, about and contact, with the menu, a
242
+ * footer and redirects-ready paths.
243
+ *
244
+ * @stability Beta
245
+ */
246
+ declare function starterSite(input: TemplateInput): TemplateResult;
247
+
248
+ declare const exampleBrands: Readonly<{
249
+ riverside: {
250
+ name: string;
251
+ colors: {
252
+ primary: string;
253
+ primaryDark: string;
254
+ };
255
+ fonts: {
256
+ heading: "space-grotesk";
257
+ body: "inter";
258
+ };
259
+ };
260
+ hilltop: {
261
+ name: string;
262
+ colors: {
263
+ primary: string;
264
+ primaryDark: string;
265
+ };
266
+ fonts: {
267
+ heading: "playfair-display";
268
+ body: "work-sans";
269
+ };
270
+ };
271
+ }>;
272
+
273
+ export { AnySectionDefinition, BrandInput, Link, LivePost, type MigratedNode, type MigratedPage, type MigrationIssue, type OnePageSiteInput, PageDoc, PageKind, type PageMetaInput, PostSummary, Redirect, ReleasePage, type RobotsInput, type RssInput, SectionNode, type PageMeta as SeoPageMeta, SiteBrand, SiteDoc, SiteDocInput, type SitemapInput, type TemplateInput, type TemplateMessage, type TemplateResult, absoluteUrl, articleJsonLd, breadcrumbJsonLd, escapeXml, exampleBrands, landingPage, matchRedirect, migrateNode, migratePage, migrateSite, onePageSite, organizationJsonLd, pageMeta, pageTitle, postDescription, postPath, redirectsAfterMove, robotsTxt, rssXml, safeJsonForHtml, sitemapXml, starterSite, websiteJsonLd };
package/dist/index.js ADDED
@@ -0,0 +1,254 @@
1
+ export { isPlainEmail, mailtoHref } from './chunk-VZUDRPFG.js';
2
+ import { brandDisplayName } from './chunk-IXMY4B56.js';
3
+ export { ASSET_TYPES, FONT_CHOICES, PublishBlockedError, SiteConflictError, assetPath, brandDisplayName, checkBlog, checkContrast, checkHtmlOutline, checkPage, checkPost, checkRelease, checkSiteDoc, contrastRatio, createMemorySiteStore, formCacheTag, hexToHslTriplet, isConflict, isPublishable, isSiteBrand, luminance, migrateNode, migratePage, migrateSite, mix, parseHex, prepareImportPost, prepareRelease, siteCacheTag, siteTheme, stableHash, themeCss, toCanonicalBrand, withCacheInvalidation } from './chunk-IXMY4B56.js';
4
+ export { BUILT_IN_SECTION_TYPES, Copy, FileRef, FontChoice, Href, Image, Line, Link, LinkEvent, MAX_DOC_BYTES, MAX_IMPORT_POSTS, MAX_LIVE_POSTS, MenuItem, PageDoc, PageKind, PagePath, PostAuthor, PostDoc, PostSlug, PostSummarySchema, Redirect, ReleasePage, ReleaseSnapshot, RichText, SECTION_CATEGORIES, SectionDefinitionError, SectionId, SectionNode, Seo, SeoDefaults, SiteDoc, Surface, TokenOverrides, absoluteUrl, articleJsonLd, breadcrumbJsonLd, defineSection, escapeXml, getSection, listSections, matchRedirect, newSectionId, nodeFromPreset, opensInNewTab, organizationJsonLd, pageMeta, pageTitle, parseSnapshot, postDescription, postPath, postSummary, redirectsAfterMove, registerBuiltInSection, registerSection, richTextPlain, robotsTxt, rssXml, safeJsonForHtml, sectionJsonSchema, sectionManifests, sitemapXml, unregisterSection, validatePostSlug, websiteJsonLd } from './chunk-ZGWGQGT2.js';
5
+ export { DEFAULT_RESERVED_PATHS, MAX_PAGE_PATH_LENGTH, MAX_URL_LENGTH, PAGE_PATH, isExternalHref, isReservedPath, isSafeHref, isSafeImageSrc, normalizeRequestPath, pagePathProblems, validatePagePath } from './chunk-3VKTBE7S.js';
6
+
7
+ // src/core/templates.ts
8
+ var year = () => (/* @__PURE__ */ new Date()).getUTCFullYear();
9
+ function node(id, type, variant, props, extra = {}) {
10
+ return { id, type, v: 1, variant, props, ...extra };
11
+ }
12
+ function footerNode(name, contact, variant = "simple") {
13
+ const links = [];
14
+ if (contact?.email) links.push({ label: contact.email, href: `mailto:${contact.email}` });
15
+ if (contact?.phone) links.push({ label: contact.phone, href: `tel:${contact.phone.replace(/[^+\d]/g, "")}` });
16
+ return node("footer", "footer", variant, {
17
+ ...links.length ? { links } : {},
18
+ copyright: `\xA9 ${year()} ${name}`
19
+ });
20
+ }
21
+ function seo(name, message) {
22
+ return {
23
+ titleTemplate: `%s \xB7 ${name}`,
24
+ ...message.subhead ? { description: message.subhead.slice(0, 200) } : {},
25
+ organization: { name }
26
+ };
27
+ }
28
+ function primaryCta(input) {
29
+ return input.message.cta ?? { label: "Get in touch", href: "#contact" };
30
+ }
31
+ function benefitsNode(message) {
32
+ if (message.benefits?.length) {
33
+ return node("features", "features", "grid", {
34
+ heading: "What you get",
35
+ items: message.benefits.slice(0, 12).map((b, i) => ({ title: b.title, body: b.body, icon: ["check", "star", "heart", "zap", "shield", "clock"][i % 6] }))
36
+ });
37
+ }
38
+ return node(
39
+ "features",
40
+ "features",
41
+ "grid",
42
+ {
43
+ heading: "What you get",
44
+ items: [
45
+ { title: "Your first offer", body: "Say what someone gets and why it matters to them.", icon: "check" },
46
+ { title: "Your second offer", body: "Keep each one to a sentence or two.", icon: "star" },
47
+ { title: "Your third offer", body: "Three is plenty for a landing page.", icon: "heart" }
48
+ ]
49
+ },
50
+ { placeholder: true }
51
+ );
52
+ }
53
+ var testimonialsNode = () => node(
54
+ "testimonials",
55
+ "testimonials",
56
+ "grid",
57
+ {
58
+ heading: "What people say",
59
+ items: [
60
+ { quote: "Add a quote from someone you have helped, in their words and with their permission.", name: "Their name", role: "What they did with you" },
61
+ { quote: "A second quote, from someone different.", name: "Their name" }
62
+ ]
63
+ },
64
+ { placeholder: true }
65
+ );
66
+ function formNode(input) {
67
+ if (!input.form) return null;
68
+ return node("contact", "form-embed", "split", {
69
+ heading: input.form.heading ?? "Get in touch",
70
+ ...input.form.body ? { body: input.form.body } : { body: "Tell us a little about yourself and we'll get back to you." },
71
+ form: input.form.slug,
72
+ anchor: "contact"
73
+ });
74
+ }
75
+ function landingPage(input) {
76
+ const name = brandDisplayName(input.brand);
77
+ const cta = primaryCta(input);
78
+ const form = formNode(input);
79
+ const content = [
80
+ node("hero", "hero", "centered", {
81
+ heading: input.message.headline,
82
+ ...input.message.subhead ? { body: input.message.subhead } : {},
83
+ primaryCta: cta
84
+ }),
85
+ benefitsNode(input.message),
86
+ testimonialsNode(),
87
+ node("cta", "cta", "banner", { heading: "Ready when you are", primaryCta: cta, surface: "accent" })
88
+ ];
89
+ if (form) content.push(form);
90
+ return {
91
+ site: {
92
+ schemaVersion: 1,
93
+ name,
94
+ header: [node("nav", "nav", "minimal", { cta })],
95
+ nav: [],
96
+ footer: [footerNode(name, input.contact, "minimal")],
97
+ seo: seo(name, input.message)
98
+ },
99
+ pages: [{ path: "/", kind: "landing", doc: { schemaVersion: 1, root: { props: { title: input.message.headline.slice(0, 120) } }, content, seo: input.message.subhead ? { description: input.message.subhead.slice(0, 200) } : void 0 } }]
100
+ };
101
+ }
102
+ function onePageSite(input) {
103
+ const brand = { name: input.name, colors: input.colors, ...input.fonts ? { fonts: input.fonts } : {}, ...input.logo ? { logo: input.logo } : {} };
104
+ const cta = input.message.cta ?? (input.contact?.email ? { label: "Email us", href: `mailto:${input.contact.email}` } : { label: "Get in touch", href: "#contact" });
105
+ return {
106
+ brand,
107
+ site: {
108
+ schemaVersion: 1,
109
+ name: input.name,
110
+ header: [node("nav", "nav", "minimal", { cta })],
111
+ nav: [],
112
+ footer: [footerNode(input.name, input.contact)],
113
+ seo: seo(input.name, input.message)
114
+ },
115
+ pages: [
116
+ {
117
+ path: "/",
118
+ kind: "page",
119
+ doc: {
120
+ schemaVersion: 1,
121
+ root: { props: { title: input.message.headline.slice(0, 120) } },
122
+ content: [
123
+ node("hero", "hero", "centered", {
124
+ heading: input.message.headline,
125
+ ...input.message.subhead ? { body: input.message.subhead } : {},
126
+ primaryCta: cta
127
+ }),
128
+ node("contact", "cta", "card", { heading: `Talk to ${input.name}`, primaryCta: cta, anchor: "contact" })
129
+ ],
130
+ ...input.message.subhead ? { seo: { description: input.message.subhead.slice(0, 200) } } : {}
131
+ }
132
+ }
133
+ ]
134
+ };
135
+ }
136
+ function starterSite(input) {
137
+ const name = brandDisplayName(input.brand);
138
+ const cta = input.message.cta ?? { label: "Contact us", href: "/contact" };
139
+ const form = formNode(input);
140
+ const page = (title, content, description) => ({
141
+ schemaVersion: 1,
142
+ root: { props: { title } },
143
+ content,
144
+ ...description ? { seo: { description } } : {}
145
+ });
146
+ return {
147
+ site: {
148
+ schemaVersion: 1,
149
+ name,
150
+ header: [node("nav", "nav", "simple", { cta })],
151
+ nav: [
152
+ { label: "Home", href: "/" },
153
+ { label: "About", href: "/about" },
154
+ { label: "Contact", href: "/contact" }
155
+ ],
156
+ footer: [
157
+ node("footer", "footer", "columns", {
158
+ tagline: input.message.subhead?.slice(0, 200),
159
+ columns: [
160
+ { heading: name, links: [{ label: "About", href: "/about" }, { label: "Contact", href: "/contact" }] },
161
+ ...input.contact?.email || input.contact?.phone ? [
162
+ {
163
+ heading: "Get in touch",
164
+ links: [
165
+ ...input.contact.email ? [{ label: input.contact.email, href: `mailto:${input.contact.email}` }] : [],
166
+ ...input.contact.phone ? [{ label: input.contact.phone, href: `tel:${input.contact.phone.replace(/[^+\d]/g, "")}` }] : []
167
+ ]
168
+ }
169
+ ] : []
170
+ ],
171
+ copyright: `\xA9 ${year()} ${name}`
172
+ })
173
+ ],
174
+ seo: seo(name, input.message)
175
+ },
176
+ pages: [
177
+ {
178
+ path: "/",
179
+ kind: "page",
180
+ doc: page(
181
+ "Home",
182
+ [
183
+ node("hero", "hero", "split", {
184
+ heading: input.message.headline,
185
+ ...input.message.subhead ? { body: input.message.subhead } : {},
186
+ primaryCta: cta,
187
+ secondaryCta: { label: "About us", href: "/about" }
188
+ }),
189
+ benefitsNode(input.message),
190
+ testimonialsNode(),
191
+ node("cta", "cta", "banner", { heading: "Come and see for yourself", primaryCta: cta, surface: "accent" })
192
+ ],
193
+ input.message.subhead?.slice(0, 200)
194
+ )
195
+ },
196
+ {
197
+ path: "/about",
198
+ kind: "page",
199
+ doc: page(
200
+ "About",
201
+ [
202
+ node("hero", "hero", "centered", { eyebrow: "About", heading: `About ${name}` }),
203
+ node(
204
+ "story",
205
+ "rich-text",
206
+ "narrow",
207
+ {
208
+ body: {
209
+ type: "doc",
210
+ content: [
211
+ { type: "heading", attrs: { level: 2 }, content: [{ type: "text", text: "Our story" }] },
212
+ { type: "paragraph", content: [{ type: "text", text: "Tell people how you started, who you help and what you believe." }] }
213
+ ]
214
+ }
215
+ },
216
+ { placeholder: true }
217
+ )
218
+ ],
219
+ `About ${name}.`
220
+ )
221
+ },
222
+ {
223
+ path: "/contact",
224
+ kind: "page",
225
+ doc: page(
226
+ "Contact",
227
+ [
228
+ node("hero", "hero", "centered", { eyebrow: "Contact", heading: "Get in touch", ...input.message.subhead ? { body: input.message.subhead } : {} }),
229
+ ...form ? [form] : [node("contact", "cta", "card", { heading: `Talk to ${name}`, primaryCta: input.contact?.email ? { label: "Email us", href: `mailto:${input.contact.email}` } : cta })]
230
+ ],
231
+ `How to reach ${name}.`
232
+ )
233
+ }
234
+ ]
235
+ };
236
+ }
237
+
238
+ // src/core/examples.ts
239
+ var exampleBrands = Object.freeze({
240
+ riverside: {
241
+ name: "Riverside Gym",
242
+ colors: { primary: "#0f766e", primaryDark: "#2dd4bf" },
243
+ fonts: { heading: "space-grotesk", body: "inter" }
244
+ },
245
+ hilltop: {
246
+ name: "Hilltop Fitness",
247
+ colors: { primary: "#b45309", primaryDark: "#fbbf24" },
248
+ fonts: { heading: "playfair-display", body: "work-sans" }
249
+ }
250
+ });
251
+
252
+ export { exampleBrands, landingPage, onePageSite, starterSite };
253
+ //# sourceMappingURL=index.js.map
254
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/templates.ts","../src/core/examples.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Templates: whole sites from a little input.\n *\n * - `landingPage`: the one-click landing page from a brand and a message.\n * Nav, hero, features, testimonials, a call to action, a form embed and a\n * footer, on one page.\n * - `onePageSite`: the smallest site a brand with no site can be seeded\n * with: just a name, a logo, a message, and the brand's colours and fonts.\n * - `starterSite`: three pages (home, about, contact) with nav and footer.\n *\n * Each returns documents (`site`, `pages`) for any `SiteStore` to create.\n * Nothing is invented and passed off as real: copy a template can't know\n * (a client's quote, a statistic) is marked `placeholder`, and the publish\n * check refuses it until someone replaces it or hides the section.\n */\nimport type { Link, PageDoc, PageKind, SectionNode, SiteDocInput } from \"./schemas\";\nimport { brandDisplayName, type BrandInput, type SiteBrand } from \"./theme\";\n\nexport interface TemplateMessage {\n /** The hero's headline: what you do, for whom. */\n headline: string;\n /** One or two sentences under it. */\n subhead?: string;\n /** The main button. Default: \"Get in touch\", to the form (or `#contact`). */\n cta?: Link;\n /** What you offer, as the features band. Without it the band is a placeholder to fill in. */\n benefits?: { title: string; body: string }[];\n}\n\nexport interface TemplateInput {\n brand: BrandInput;\n message: TemplateMessage;\n /** A form to embed (its slug, for the host's forms resolver). */\n form?: { slug: string; heading?: string; body?: string };\n /** For the footer. */\n contact?: { email?: string; phone?: string };\n}\n\nexport interface TemplateResult {\n site: SiteDocInput;\n pages: { path: string; kind: PageKind; doc: PageDoc }[];\n}\n\nconst year = () => new Date().getUTCFullYear();\n\nfunction node(id: string, type: string, variant: string, props: Record<string, unknown>, extra: Partial<SectionNode> = {}): SectionNode {\n return { id, type, v: 1, variant, props, ...extra };\n}\n\nfunction footerNode(name: string, contact: TemplateInput[\"contact\"], variant = \"simple\"): SectionNode {\n const links: Link[] = [];\n if (contact?.email) links.push({ label: contact.email, href: `mailto:${contact.email}` });\n if (contact?.phone) links.push({ label: contact.phone, href: `tel:${contact.phone.replace(/[^+\\d]/g, \"\")}` });\n return node(\"footer\", \"footer\", variant, {\n ...(links.length ? { links } : {}),\n copyright: `© ${year()} ${name}`,\n });\n}\n\nfunction seo(name: string, message: TemplateMessage) {\n return {\n titleTemplate: `%s · ${name}`,\n ...(message.subhead ? { description: message.subhead.slice(0, 200) } : {}),\n organization: { name },\n };\n}\n\nfunction primaryCta(input: TemplateInput): Link {\n return input.message.cta ?? { label: \"Get in touch\", href: \"#contact\" };\n}\n\nfunction benefitsNode(message: TemplateMessage): SectionNode {\n if (message.benefits?.length) {\n return node(\"features\", \"features\", \"grid\", {\n heading: \"What you get\",\n items: message.benefits.slice(0, 12).map((b, i) => ({ title: b.title, body: b.body, icon: ([\"check\", \"star\", \"heart\", \"zap\", \"shield\", \"clock\"] as const)[i % 6] })),\n });\n }\n return node(\n \"features\",\n \"features\",\n \"grid\",\n {\n heading: \"What you get\",\n items: [\n { title: \"Your first offer\", body: \"Say what someone gets and why it matters to them.\", icon: \"check\" },\n { title: \"Your second offer\", body: \"Keep each one to a sentence or two.\", icon: \"star\" },\n { title: \"Your third offer\", body: \"Three is plenty for a landing page.\", icon: \"heart\" },\n ],\n },\n { placeholder: true },\n );\n}\n\nconst testimonialsNode = () =>\n node(\n \"testimonials\",\n \"testimonials\",\n \"grid\",\n {\n heading: \"What people say\",\n items: [\n { quote: \"Add a quote from someone you have helped, in their words and with their permission.\", name: \"Their name\", role: \"What they did with you\" },\n { quote: \"A second quote, from someone different.\", name: \"Their name\" },\n ],\n },\n { placeholder: true },\n );\n\nfunction formNode(input: TemplateInput): SectionNode | null {\n if (!input.form) return null;\n return node(\"contact\", \"form-embed\", \"split\", {\n heading: input.form.heading ?? \"Get in touch\",\n ...(input.form.body ? { body: input.form.body } : { body: \"Tell us a little about yourself and we'll get back to you.\" }),\n form: input.form.slug,\n anchor: \"contact\",\n });\n}\n\n/**\n * The one-click landing page: brand plus message, on one page.\n *\n * @stability Beta\n */\nexport function landingPage(input: TemplateInput): TemplateResult {\n const name = brandDisplayName(input.brand);\n const cta = primaryCta(input);\n const form = formNode(input);\n const content: SectionNode[] = [\n node(\"hero\", \"hero\", \"centered\", {\n heading: input.message.headline,\n ...(input.message.subhead ? { body: input.message.subhead } : {}),\n primaryCta: cta,\n }),\n benefitsNode(input.message),\n testimonialsNode(),\n node(\"cta\", \"cta\", \"banner\", { heading: \"Ready when you are\", primaryCta: cta, surface: \"accent\" }),\n ];\n if (form) content.push(form);\n return {\n site: {\n schemaVersion: 1,\n name,\n header: [node(\"nav\", \"nav\", \"minimal\", { cta })],\n nav: [],\n footer: [footerNode(name, input.contact, \"minimal\")],\n seo: seo(name, input.message),\n },\n pages: [{ path: \"/\", kind: \"landing\", doc: { schemaVersion: 1, root: { props: { title: input.message.headline.slice(0, 120) } }, content, seo: input.message.subhead ? { description: input.message.subhead.slice(0, 200) } : undefined } }],\n };\n}\n\nexport interface OnePageSiteInput {\n name: string;\n logo?: { src: string; alt?: string };\n message: TemplateMessage;\n /** The brand's colours and fonts. */\n colors: SiteBrand[\"colors\"];\n fonts?: SiteBrand[\"fonts\"];\n contact?: TemplateInput[\"contact\"];\n}\n\n/**\n * The smallest site, for seeding a brand that has none: a header, a hero,\n * a call to action and a footer. `brand` is the minimal brand to store\n * beside it (the host's `BrandSource` returns it).\n *\n * @stability Beta\n */\nexport function onePageSite(input: OnePageSiteInput): TemplateResult & { brand: SiteBrand } {\n const brand: SiteBrand = { name: input.name, colors: input.colors, ...(input.fonts ? { fonts: input.fonts } : {}), ...(input.logo ? { logo: input.logo } : {}) };\n const cta = input.message.cta ?? (input.contact?.email ? { label: \"Email us\", href: `mailto:${input.contact.email}` } : { label: \"Get in touch\", href: \"#contact\" });\n return {\n brand,\n site: {\n schemaVersion: 1,\n name: input.name,\n header: [node(\"nav\", \"nav\", \"minimal\", { cta })],\n nav: [],\n footer: [footerNode(input.name, input.contact)],\n seo: seo(input.name, input.message),\n },\n pages: [\n {\n path: \"/\",\n kind: \"page\",\n doc: {\n schemaVersion: 1,\n root: { props: { title: input.message.headline.slice(0, 120) } },\n content: [\n node(\"hero\", \"hero\", \"centered\", {\n heading: input.message.headline,\n ...(input.message.subhead ? { body: input.message.subhead } : {}),\n primaryCta: cta,\n }),\n node(\"contact\", \"cta\", \"card\", { heading: `Talk to ${input.name}`, primaryCta: cta, anchor: \"contact\" }),\n ],\n ...(input.message.subhead ? { seo: { description: input.message.subhead.slice(0, 200) } } : {}),\n },\n },\n ],\n };\n}\n\n/**\n * A three-page starter site: home, about and contact, with the menu, a\n * footer and redirects-ready paths.\n *\n * @stability Beta\n */\nexport function starterSite(input: TemplateInput): TemplateResult {\n const name = brandDisplayName(input.brand);\n const cta: Link = input.message.cta ?? { label: \"Contact us\", href: \"/contact\" };\n const form = formNode(input);\n const page = (title: string, content: SectionNode[], description?: string): PageDoc => ({\n schemaVersion: 1,\n root: { props: { title } },\n content,\n ...(description ? { seo: { description } } : {}),\n });\n return {\n site: {\n schemaVersion: 1,\n name,\n header: [node(\"nav\", \"nav\", \"simple\", { cta })],\n nav: [\n { label: \"Home\", href: \"/\" },\n { label: \"About\", href: \"/about\" },\n { label: \"Contact\", href: \"/contact\" },\n ],\n footer: [\n node(\"footer\", \"footer\", \"columns\", {\n tagline: input.message.subhead?.slice(0, 200),\n columns: [\n { heading: name, links: [{ label: \"About\", href: \"/about\" }, { label: \"Contact\", href: \"/contact\" }] },\n ...(input.contact?.email || input.contact?.phone\n ? [\n {\n heading: \"Get in touch\",\n links: [\n ...(input.contact.email ? [{ label: input.contact.email, href: `mailto:${input.contact.email}` }] : []),\n ...(input.contact.phone ? [{ label: input.contact.phone, href: `tel:${input.contact.phone.replace(/[^+\\d]/g, \"\")}` }] : []),\n ],\n },\n ]\n : []),\n ],\n copyright: `© ${year()} ${name}`,\n }),\n ],\n seo: seo(name, input.message),\n },\n pages: [\n {\n path: \"/\",\n kind: \"page\",\n doc: page(\n \"Home\",\n [\n node(\"hero\", \"hero\", \"split\", {\n heading: input.message.headline,\n ...(input.message.subhead ? { body: input.message.subhead } : {}),\n primaryCta: cta,\n secondaryCta: { label: \"About us\", href: \"/about\" },\n }),\n benefitsNode(input.message),\n testimonialsNode(),\n node(\"cta\", \"cta\", \"banner\", { heading: \"Come and see for yourself\", primaryCta: cta, surface: \"accent\" }),\n ],\n input.message.subhead?.slice(0, 200),\n ),\n },\n {\n path: \"/about\",\n kind: \"page\",\n doc: page(\n \"About\",\n [\n node(\"hero\", \"hero\", \"centered\", { eyebrow: \"About\", heading: `About ${name}` }),\n node(\n \"story\",\n \"rich-text\",\n \"narrow\",\n {\n body: {\n type: \"doc\",\n content: [\n { type: \"heading\", attrs: { level: 2 }, content: [{ type: \"text\", text: \"Our story\" }] },\n { type: \"paragraph\", content: [{ type: \"text\", text: \"Tell people how you started, who you help and what you believe.\" }] },\n ],\n },\n },\n { placeholder: true },\n ),\n ],\n `About ${name}.`,\n ),\n },\n {\n path: \"/contact\",\n kind: \"page\",\n doc: page(\n \"Contact\",\n [\n node(\"hero\", \"hero\", \"centered\", { eyebrow: \"Contact\", heading: \"Get in touch\", ...(input.message.subhead ? { body: input.message.subhead } : {}) }),\n ...(form ? [form] : [node(\"contact\", \"cta\", \"card\", { heading: `Talk to ${name}`, primaryCta: input.contact?.email ? { label: \"Email us\", href: `mailto:${input.contact.email}` } : cta })]),\n ],\n `How to reach ${name}.`,\n ),\n },\n ],\n };\n}\n","/**\n * Made-up brands for demos, tests and docs. They are not anyone's real\n * business.\n */\nimport type { SiteBrand } from \"./theme\";\n\nexport const exampleBrands = Object.freeze({\n riverside: {\n name: \"Riverside Gym\",\n colors: { primary: \"#0f766e\", primaryDark: \"#2dd4bf\" },\n fonts: { heading: \"space-grotesk\", body: \"inter\" },\n },\n hilltop: {\n name: \"Hilltop Fitness\",\n colors: { primary: \"#b45309\", primaryDark: \"#fbbf24\" },\n fonts: { heading: \"playfair-display\", body: \"work-sans\" },\n },\n} satisfies Record<string, SiteBrand>);\n"]}