create-agntcms-app 0.2.1

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 (197) hide show
  1. package/README.md +39 -0
  2. package/dist/index.mjs +297 -0
  3. package/dist/template/.claude/settings.json +6 -0
  4. package/dist/template/.claude/skills/.gitkeep +0 -0
  5. package/dist/template/.claude-plugin/channel/server.mjs +254 -0
  6. package/dist/template/.claude-plugin/channel/server.ts +369 -0
  7. package/dist/template/.claude-plugin/plugin.json +17 -0
  8. package/dist/template/.mcp.json +8 -0
  9. package/dist/template/BRAND.md +49 -0
  10. package/dist/template/CLAUDE.md +157 -0
  11. package/dist/template/agntcms/config.ts +49 -0
  12. package/dist/template/agntcms/sections/ArticleBody/component.tsx +32 -0
  13. package/dist/template/agntcms/sections/ArticleBody/index.ts +10 -0
  14. package/dist/template/agntcms/sections/ArticleBody/schema.ts +5 -0
  15. package/dist/template/agntcms/sections/ArticleHero/component.tsx +87 -0
  16. package/dist/template/agntcms/sections/ArticleHero/index.ts +10 -0
  17. package/dist/template/agntcms/sections/ArticleHero/schema.ts +12 -0
  18. package/dist/template/agntcms/sections/Banner/component.tsx +83 -0
  19. package/dist/template/agntcms/sections/Banner/index.ts +10 -0
  20. package/dist/template/agntcms/sections/Banner/schema.ts +9 -0
  21. package/dist/template/agntcms/sections/BlogIndex/component.tsx +173 -0
  22. package/dist/template/agntcms/sections/BlogIndex/index.ts +10 -0
  23. package/dist/template/agntcms/sections/BlogIndex/schema.ts +33 -0
  24. package/dist/template/agntcms/sections/BlogIndexHeader/component.tsx +44 -0
  25. package/dist/template/agntcms/sections/BlogIndexHeader/index.ts +10 -0
  26. package/dist/template/agntcms/sections/BlogIndexHeader/schema.ts +8 -0
  27. package/dist/template/agntcms/sections/BlogPostBody/component.tsx +50 -0
  28. package/dist/template/agntcms/sections/BlogPostBody/index.ts +10 -0
  29. package/dist/template/agntcms/sections/BlogPostBody/schema.ts +10 -0
  30. package/dist/template/agntcms/sections/BlogPostHero/component.tsx +88 -0
  31. package/dist/template/agntcms/sections/BlogPostHero/index.ts +10 -0
  32. package/dist/template/agntcms/sections/BlogPostHero/schema.ts +35 -0
  33. package/dist/template/agntcms/sections/CaseStudies/component.tsx +92 -0
  34. package/dist/template/agntcms/sections/CaseStudies/index.ts +10 -0
  35. package/dist/template/agntcms/sections/CaseStudies/schema.ts +17 -0
  36. package/dist/template/agntcms/sections/ContactForm/component.tsx +119 -0
  37. package/dist/template/agntcms/sections/ContactForm/index.ts +10 -0
  38. package/dist/template/agntcms/sections/ContactForm/schema.ts +15 -0
  39. package/dist/template/agntcms/sections/DocsArticle/component.tsx +266 -0
  40. package/dist/template/agntcms/sections/DocsArticle/index.ts +10 -0
  41. package/dist/template/agntcms/sections/DocsArticle/schema.ts +33 -0
  42. package/dist/template/agntcms/sections/FAQ/component.tsx +57 -0
  43. package/dist/template/agntcms/sections/FAQ/index.ts +10 -0
  44. package/dist/template/agntcms/sections/FAQ/schema.ts +11 -0
  45. package/dist/template/agntcms/sections/FeatureGrid/component.tsx +117 -0
  46. package/dist/template/agntcms/sections/FeatureGrid/index.ts +10 -0
  47. package/dist/template/agntcms/sections/FeatureGrid/schema.ts +21 -0
  48. package/dist/template/agntcms/sections/FeaturedArticles/component.tsx +99 -0
  49. package/dist/template/agntcms/sections/FeaturedArticles/index.ts +10 -0
  50. package/dist/template/agntcms/sections/FeaturedArticles/schema.ts +17 -0
  51. package/dist/template/agntcms/sections/GettingStarted/component.tsx +116 -0
  52. package/dist/template/agntcms/sections/GettingStarted/index.ts +10 -0
  53. package/dist/template/agntcms/sections/GettingStarted/schema.ts +11 -0
  54. package/dist/template/agntcms/sections/Hero/component.tsx +148 -0
  55. package/dist/template/agntcms/sections/Hero/index.ts +10 -0
  56. package/dist/template/agntcms/sections/Hero/schema.ts +16 -0
  57. package/dist/template/agntcms/sections/HowItWorks/component.tsx +57 -0
  58. package/dist/template/agntcms/sections/HowItWorks/index.ts +10 -0
  59. package/dist/template/agntcms/sections/HowItWorks/schema.ts +11 -0
  60. package/dist/template/agntcms/sections/ImageText/component.tsx +110 -0
  61. package/dist/template/agntcms/sections/ImageText/index.ts +10 -0
  62. package/dist/template/agntcms/sections/ImageText/schema.ts +14 -0
  63. package/dist/template/agntcms/sections/LogoStrip/component.tsx +37 -0
  64. package/dist/template/agntcms/sections/LogoStrip/index.ts +10 -0
  65. package/dist/template/agntcms/sections/LogoStrip/schema.ts +6 -0
  66. package/dist/template/agntcms/sections/Newsletter/component.tsx +48 -0
  67. package/dist/template/agntcms/sections/Newsletter/index.ts +10 -0
  68. package/dist/template/agntcms/sections/Newsletter/schema.ts +8 -0
  69. package/dist/template/agntcms/sections/OpenSource/component.tsx +99 -0
  70. package/dist/template/agntcms/sections/OpenSource/index.ts +10 -0
  71. package/dist/template/agntcms/sections/OpenSource/schema.ts +13 -0
  72. package/dist/template/agntcms/sections/PainAnswer/component.tsx +81 -0
  73. package/dist/template/agntcms/sections/PainAnswer/index.ts +10 -0
  74. package/dist/template/agntcms/sections/PainAnswer/schema.ts +15 -0
  75. package/dist/template/agntcms/sections/PricingPlans/component.tsx +100 -0
  76. package/dist/template/agntcms/sections/PricingPlans/index.ts +10 -0
  77. package/dist/template/agntcms/sections/PricingPlans/schema.ts +13 -0
  78. package/dist/template/agntcms/sections/Problem/component.tsx +49 -0
  79. package/dist/template/agntcms/sections/Problem/index.ts +10 -0
  80. package/dist/template/agntcms/sections/Problem/schema.ts +12 -0
  81. package/dist/template/agntcms/sections/SiteFooter/component.tsx +88 -0
  82. package/dist/template/agntcms/sections/SiteFooter/index.ts +10 -0
  83. package/dist/template/agntcms/sections/SiteFooter/schema.ts +13 -0
  84. package/dist/template/agntcms/sections/SiteHeader/component.tsx +99 -0
  85. package/dist/template/agntcms/sections/SiteHeader/index.ts +10 -0
  86. package/dist/template/agntcms/sections/SiteHeader/schema.ts +14 -0
  87. package/dist/template/agntcms/sections/SiteMeta/component.tsx +26 -0
  88. package/dist/template/agntcms/sections/SiteMeta/index.ts +13 -0
  89. package/dist/template/agntcms/sections/SiteMeta/schema.ts +18 -0
  90. package/dist/template/agntcms/sections/TabbedFeatures/component.tsx +120 -0
  91. package/dist/template/agntcms/sections/TabbedFeatures/index.ts +10 -0
  92. package/dist/template/agntcms/sections/TabbedFeatures/schema.ts +13 -0
  93. package/dist/template/agntcms/sections/TeamGrid/component.tsx +77 -0
  94. package/dist/template/agntcms/sections/TeamGrid/index.ts +10 -0
  95. package/dist/template/agntcms/sections/TeamGrid/schema.ts +14 -0
  96. package/dist/template/agntcms/sections/Testimonials/component.tsx +76 -0
  97. package/dist/template/agntcms/sections/Testimonials/index.ts +10 -0
  98. package/dist/template/agntcms/sections/Testimonials/schema.ts +12 -0
  99. package/dist/template/agntcms/sections/WhatIsBuilt/component.tsx +86 -0
  100. package/dist/template/agntcms/sections/WhatIsBuilt/index.ts +10 -0
  101. package/dist/template/agntcms/sections/WhatIsBuilt/schema.ts +20 -0
  102. package/dist/template/agntcms/site-meta.ts +81 -0
  103. package/dist/template/app/[[...slug]]/page.tsx +123 -0
  104. package/dist/template/app/admin/AdminPageClient.tsx +77 -0
  105. package/dist/template/app/admin/AdminPageDynamic.tsx +24 -0
  106. package/dist/template/app/admin/page.tsx +14 -0
  107. package/dist/template/app/api/agntcms/_shared.ts +80 -0
  108. package/dist/template/app/api/agntcms/assets/route.ts +11 -0
  109. package/dist/template/app/api/agntcms/assets/upload/route.ts +11 -0
  110. package/dist/template/app/api/agntcms/draft/discard/route.ts +12 -0
  111. package/dist/template/app/api/agntcms/draft/list/route.ts +11 -0
  112. package/dist/template/app/api/agntcms/draft/publish/route.ts +11 -0
  113. package/dist/template/app/api/agntcms/draft/reorder/route.ts +10 -0
  114. package/dist/template/app/api/agntcms/draft/save/route.ts +11 -0
  115. package/dist/template/app/api/agntcms/events/route.ts +12 -0
  116. package/dist/template/app/api/agntcms/forms/delete/route.ts +17 -0
  117. package/dist/template/app/api/agntcms/forms/list/route.ts +24 -0
  118. package/dist/template/app/api/agntcms/forms/read/route.ts +23 -0
  119. package/dist/template/app/api/agntcms/forms/submit/route.ts +17 -0
  120. package/dist/template/app/api/agntcms/global/delete/route.ts +13 -0
  121. package/dist/template/app/api/agntcms/global/history/route.ts +10 -0
  122. package/dist/template/app/api/agntcms/global/list/route.ts +14 -0
  123. package/dist/template/app/api/agntcms/global/read/route.ts +11 -0
  124. package/dist/template/app/api/agntcms/global/rollback/route.ts +10 -0
  125. package/dist/template/app/api/agntcms/global/save/route.ts +14 -0
  126. package/dist/template/app/api/agntcms/mcp/route.ts +12 -0
  127. package/dist/template/app/api/agntcms/page/delete/route.ts +10 -0
  128. package/dist/template/app/api/agntcms/page/duplicate/route.ts +11 -0
  129. package/dist/template/app/api/agntcms/page/history/route.ts +10 -0
  130. package/dist/template/app/api/agntcms/page/list/route.ts +10 -0
  131. package/dist/template/app/api/agntcms/page/read/route.ts +11 -0
  132. package/dist/template/app/api/agntcms/page/rename/route.ts +10 -0
  133. package/dist/template/app/api/agntcms/page/rollback/route.ts +10 -0
  134. package/dist/template/app/api/agntcms/page/unpublish/route.ts +11 -0
  135. package/dist/template/app/api/agntcms/preview/enter/route.ts +13 -0
  136. package/dist/template/app/api/agntcms/preview/exit/route.ts +10 -0
  137. package/dist/template/app/api/agntcms/preview/issue/route.ts +12 -0
  138. package/dist/template/app/api/agntcms/template/list/route.ts +15 -0
  139. package/dist/template/app/apple-icon.svg +9 -0
  140. package/dist/template/app/icon.svg +9 -0
  141. package/dist/template/app/layout.tsx +107 -0
  142. package/dist/template/app/not-found.tsx +75 -0
  143. package/dist/template/app/robots.ts +33 -0
  144. package/dist/template/app/sitemap.ts +49 -0
  145. package/dist/template/content/globals/site-footer.json +53 -0
  146. package/dist/template/content/globals/site-header.json +18 -0
  147. package/dist/template/content/globals/site-meta.json +13 -0
  148. package/dist/template/content/pages/404.json +34 -0
  149. package/dist/template/content/pages/about.json +307 -0
  150. package/dist/template/content/pages/article-editor.json +61 -0
  151. package/dist/template/content/pages/article-schemas.json +61 -0
  152. package/dist/template/content/pages/blog.json +162 -0
  153. package/dist/template/content/pages/contact.json +29 -0
  154. package/dist/template/content/pages/home.json +243 -0
  155. package/dist/template/content/pages/pricing.json +219 -0
  156. package/dist/template/content/pages/services.json +177 -0
  157. package/dist/template/fonts/Satoshi-Medium.woff2 +0 -0
  158. package/dist/template/fonts/Satoshi-Regular.woff2 +0 -0
  159. package/dist/template/next.config.ts +6 -0
  160. package/dist/template/package.json +36 -0
  161. package/dist/template/postcss.config.mjs +5 -0
  162. package/dist/template/public/assets/.gitkeep +0 -0
  163. package/dist/template/public/assets/0418d7ed21f57e7b9e0546725c92b8419daeaa355675d9070fab0c2013cf1524.jpg +0 -0
  164. package/dist/template/public/assets/0d0475f21aa96435a8ed3cdb2fddcc6278492e76ae842f569432454f4d33631a.jpg +0 -0
  165. package/dist/template/public/assets/27457a1adee2372030d9876b0d52c44d46be98843999935eaef2526b9b961f12.jpg +0 -0
  166. package/dist/template/public/assets/3855d91192f0c6120b01427b78ef84e52baa9f4b5a17d4271e41c1bfd95a5b0c.jpg +0 -0
  167. package/dist/template/public/assets/3b3b90c5084635b746be673ede92a328f002f5621a42c9a5cb89c5e2435652cb.jpg +0 -0
  168. package/dist/template/public/assets/3e76165a78fd3e7b8ed1e93dee50803ae11110c756c8c1c89229a2dec2bc0abf.jpg +0 -0
  169. package/dist/template/public/assets/4a3e28f85dc850c347ea0fd931696aa936a6bd45f193e7f1c9328b5896fb272c.jpg +0 -0
  170. package/dist/template/public/assets/579f67d5fd4c9106c6cdf2ef29f50df934ad0fc2b7849bac1e1cfb1e3f92303b.jpg +0 -0
  171. package/dist/template/public/assets/5b95209269661bb60fb250f1da682e05b9efa64dd42f350608b299e6bf1f2f35.jpg +0 -0
  172. package/dist/template/public/assets/5e04b46f8317ef95a7ddf85aedfe5c098a755f05056325d0251eccf95ce51172.jpg +0 -0
  173. package/dist/template/public/assets/6167a9164be2cf1183bdfdd4946bf9b908570e79e92a2380c25f0bb702422bbd.jpg +0 -0
  174. package/dist/template/public/assets/75e723ec316de28247924e5dfb73a4b266e10de605e749f150883d280ed8ed16.jpg +0 -0
  175. package/dist/template/public/assets/816a11e6a7245feaf51bbebf09d1bda3f125b334bc24fc3b8f47b5380a7b4294.jpg +0 -0
  176. package/dist/template/public/assets/81eba6f5654b8746a9b0cba1a9521a67f2b4afaaefc7c88d66dfab1461270d8f.jpg +0 -0
  177. package/dist/template/public/assets/82a2ce9e49361098f77a28755779dc5a7c026831cbd135175749c1304e21dacc.jpg +0 -0
  178. package/dist/template/public/assets/8d7b02ba277ba56bdafdbd47b01f7df6d993c714b4dc2305eb65a1307c09647d.jpg +0 -0
  179. package/dist/template/public/assets/b303185b471678e4d62f678a1549ee26022f4745407d08cae44ecb1c25352293.jpg +0 -0
  180. package/dist/template/public/assets/b69b49169c11546100d6dd5280073bc0d84cbbcc6d33fa01ecf6a5866fa42237.jpg +0 -0
  181. package/dist/template/public/assets/c4d2f0d1a310e457ac722a399693652e3c86c55b294243d5ffc679394e12f9d1.jpg +0 -0
  182. package/dist/template/public/assets/cae09f4729f8a348b67267c2f2a550be0f3bfa420689afe1a5cf8b7e2b146238.png +0 -0
  183. package/dist/template/public/assets/cb3acf58b57417a4b26474ba04c096af7103c4320ed2f4f3683f79d7670a055c.jpg +0 -0
  184. package/dist/template/public/assets/d5a0701b2d156284e0ce851cd2534ec632db34f91fbcbee3b8a7784d45ce78d2.jpg +0 -0
  185. package/dist/template/public/assets/d6ef1c3f48b0e488521794fb60701da1fd2c3a1621d6ac5f17ccfd4909d3be60.jpg +0 -0
  186. package/dist/template/public/assets/de249ff9be2539cf0d1ce092de3c57001839b6c3e14fcee3fc31a7b7673ae007.jpg +0 -0
  187. package/dist/template/public/assets/eac45438956be187b010e24b3289757aa00f227c190d49ee99fea510552dd2ba.jpg +0 -0
  188. package/dist/template/public/assets/f8b9200065b5436c6a88361839edc2b89be88d3037c84a80d3ee95c32891510b.jpg +0 -0
  189. package/dist/template/public/assets/placeholder.png +0 -0
  190. package/dist/template/public/brand/mark.svg +6 -0
  191. package/dist/template/public/brand/wordmark-light.svg +6 -0
  192. package/dist/template/public/brand/wordmark.svg +6 -0
  193. package/dist/template/styles/globals.css +69 -0
  194. package/dist/template/styles/theme.css +492 -0
  195. package/dist/template/styles/typography.css +469 -0
  196. package/dist/template/tsconfig.json +30 -0
  197. package/package.json +30 -0
@@ -0,0 +1,120 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import {
5
+ EditableRichText,
6
+ EditableImage,
7
+ EditableLink,
8
+ EditableList,
9
+ read,
10
+ isSlotInPreview,
11
+ } from '@agntcms/next/client'
12
+ import type { EditableSlot, SlotItem } from '@agntcms/next/client'
13
+ import { hrefOf, isExternalLink } from '@agntcms/next'
14
+ import { schema } from './schema'
15
+
16
+ type Entry = SlotItem<typeof schema.entries.itemSchema>
17
+
18
+ interface Props {
19
+ readonly eyebrow: EditableSlot<'richText', string>
20
+ readonly headline: EditableSlot<'richText', string>
21
+ readonly lead: EditableSlot<'richText', string>
22
+ readonly entries: EditableSlot<'list', ReadonlyArray<Entry>>
23
+ }
24
+
25
+ export function TabbedFeaturesComponent({ eyebrow, headline, lead, entries }: Props) {
26
+ const [active, setActive] = useState(0)
27
+ const itemsRaw = read(entries) as ReadonlyArray<unknown> | undefined
28
+ const itemCount = itemsRaw?.length ?? 0
29
+ const safeIndex = itemCount > 0 ? Math.min(Math.max(active, 0), itemCount - 1) : 0
30
+
31
+ return (
32
+ <section className="bg-paper-2">
33
+ <div className="mx-auto w-full max-w-[1280px] px-8 py-[88px]">
34
+ <EditableRichText
35
+ field={eyebrow}
36
+ className="[&_p]:font-mono [&_p]:font-medium [&_p]:text-[12px] [&_p]:tracking-[0.07em] [&_p]:uppercase [&_p]:text-ink-3 [&_p]:m-0"
37
+ />
38
+ <div className="mt-3 [&_h2]:font-display [&_h2]:font-semibold [&_h2]:text-ink [&_h2]:m-0 [&_h2]:text-[44px] [&_h2]:leading-[1.05] [&_h2]:tracking-[-0.03em] [&_p]:font-display [&_p]:font-semibold [&_p]:text-ink [&_p]:m-0 [&_p]:text-[44px] [&_p]:leading-[1.05] [&_p]:tracking-[-0.03em] [&_em]:not-italic [&_em]:text-ink-3 [&_em]:font-semibold">
39
+ <EditableRichText field={headline} />
40
+ </div>
41
+ <EditableRichText
42
+ field={lead}
43
+ className="mt-4 max-w-[580px] [&_p]:text-[18px] [&_p]:leading-[1.55] [&_p]:text-ink-2 [&_p]:m-0"
44
+ />
45
+
46
+ <div className="mt-10 flex gap-7 border-b border-hairline overflow-x-auto">
47
+ <EditableList
48
+ field={entries}
49
+ itemSchema={schema.entries.itemSchema}
50
+ className="flex"
51
+ renderItem={(entry, index) => {
52
+ const isActive = index === safeIndex
53
+ return (
54
+ <button
55
+ type="button"
56
+ onClick={() => setActive(index)}
57
+ className={`-mb-px whitespace-nowrap border-b-2 px-0 pb-3.5 pt-3.5 pr-7 text-left text-[15px] font-medium ${
58
+ isActive ? 'border-ink text-ink' : 'border-transparent text-ink-3'
59
+ }`}
60
+ >
61
+ <EditableRichText
62
+ field={entry.headline}
63
+ as="span"
64
+ className="[&_p]:m-0 [&_p]:inline"
65
+ />
66
+ </button>
67
+ )
68
+ }}
69
+ />
70
+ </div>
71
+
72
+ <EditableList
73
+ field={entries}
74
+ itemSchema={schema.entries.itemSchema}
75
+ className="mt-10"
76
+ renderItem={(entry, index) => {
77
+ if (index !== safeIndex) return null
78
+ const link = read(entry.cta)
79
+ const href = hrefOf(link)
80
+ const showCta = Boolean(href) || isSlotInPreview(entry.cta)
81
+ return (
82
+ <div className="grid grid-cols-1 gap-14 lg:grid-cols-2 lg:items-center">
83
+ <div>
84
+ <EditableRichText
85
+ field={entry.headline}
86
+ className="[&_h3]:m-0 [&_h3]:font-display [&_h3]:font-semibold [&_h3]:text-ink [&_h3]:text-[32px] [&_h3]:leading-[1.15] [&_h3]:tracking-[-0.02em] [&_p]:m-0 [&_p]:font-display [&_p]:font-semibold [&_p]:text-ink [&_p]:text-[32px] [&_p]:leading-[1.15] [&_p]:tracking-[-0.02em]"
87
+ />
88
+ <EditableRichText
89
+ field={entry.description}
90
+ className="mt-3.5 [&_p]:m-0 [&_p]:text-[16.5px] [&_p]:leading-[1.6] [&_p]:text-ink-2 [&_p+p]:mt-3"
91
+ />
92
+ {showCta && (
93
+ <div className="mt-5">
94
+ <a
95
+ href={href || '#'}
96
+ target={isExternalLink(link) ? '_blank' : undefined}
97
+ rel={isExternalLink(link) ? 'noreferrer' : undefined}
98
+ >
99
+ <EditableLink
100
+ field={entry.cta}
101
+ className="inline-flex items-center gap-2 rounded-sm border border-hairline-2 bg-transparent px-[18px] py-[10px] text-sm font-medium text-ink no-underline hover:border-ink"
102
+ />
103
+ </a>
104
+ </div>
105
+ )}
106
+ </div>
107
+ <div className="aspect-[5/4] overflow-hidden border border-hairline bg-paper-2">
108
+ <EditableImage
109
+ field={entry.image}
110
+ className="!block h-full w-full object-cover [&_img]:h-full [&_img]:w-full [&_img]:object-cover"
111
+ />
112
+ </div>
113
+ </div>
114
+ )
115
+ }}
116
+ />
117
+ </div>
118
+ </section>
119
+ )
120
+ }
@@ -0,0 +1,10 @@
1
+ import { defineSection } from '@agntcms/next'
2
+ import { schema } from './schema'
3
+ import { TabbedFeaturesComponent } from './component'
4
+
5
+ export const TabbedFeatures = defineSection({
6
+ name: 'TabbedFeatures',
7
+ category: 'Features',
8
+ schema,
9
+ component: TabbedFeaturesComponent,
10
+ })
@@ -0,0 +1,13 @@
1
+ import { RichTextField, LinkField, ImageField, ListField } from '@agntcms/next'
2
+
3
+ export const schema = {
4
+ eyebrow: RichTextField,
5
+ headline: RichTextField,
6
+ lead: RichTextField,
7
+ entries: ListField({
8
+ headline: RichTextField,
9
+ description: RichTextField,
10
+ image: ImageField,
11
+ cta: LinkField,
12
+ }),
13
+ }
@@ -0,0 +1,77 @@
1
+ 'use client'
2
+
3
+ import {
4
+ EditableRichText,
5
+ EditableText,
6
+ EditableImage,
7
+ EditableList,
8
+ read,
9
+ } from '@agntcms/next/client'
10
+ import type { EditableSlot, SlotItem } from '@agntcms/next/client'
11
+ import { schema } from './schema'
12
+
13
+ type Person = SlotItem<typeof schema.people.itemSchema>
14
+
15
+ interface Props {
16
+ readonly eyebrow: EditableSlot<'richText', string>
17
+ readonly headline: EditableSlot<'richText', string>
18
+ readonly lead: EditableSlot<'richText', string>
19
+ readonly columns: EditableSlot<'text', string>
20
+ readonly people: EditableSlot<'list', ReadonlyArray<Person>>
21
+ }
22
+
23
+ export function TeamGridComponent({ eyebrow, headline, lead, columns, people }: Props) {
24
+ const cols = read(columns) === '3' ? 3 : 4
25
+ const colClass = cols === 3 ? 'lg:grid-cols-3' : 'lg:grid-cols-4'
26
+
27
+ return (
28
+ <section className="bg-paper">
29
+ <div className="mx-auto w-full max-w-[1280px] px-8 py-[88px]">
30
+ <div className="max-w-[720px]">
31
+ <EditableRichText
32
+ field={eyebrow}
33
+ className="[&_p]:font-mono [&_p]:font-medium [&_p]:text-[12px] [&_p]:tracking-[0.07em] [&_p]:uppercase [&_p]:text-ink-3 [&_p]:m-0"
34
+ />
35
+ <div className="mt-3 [&_h2]:m-0 [&_h2]:font-display [&_h2]:font-semibold [&_h2]:text-ink [&_h2]:text-[44px] [&_h2]:leading-[1.05] [&_h2]:tracking-[-0.03em] [&_p]:m-0 [&_p]:font-display [&_p]:font-semibold [&_p]:text-ink [&_p]:text-[44px] [&_p]:leading-[1.05] [&_p]:tracking-[-0.03em] [&_em]:not-italic [&_em]:text-ink-3 [&_em]:font-semibold">
36
+ <EditableRichText field={headline} />
37
+ </div>
38
+ <EditableRichText
39
+ field={lead}
40
+ className="mt-4 max-w-[580px] [&_p]:text-[18px] [&_p]:leading-[1.55] [&_p]:text-ink-2 [&_p]:m-0"
41
+ />
42
+ </div>
43
+ <EditableList
44
+ field={people}
45
+ itemSchema={schema.people.itemSchema}
46
+ className={`mt-12 grid grid-cols-1 ${colClass}`}
47
+ renderItem={(p) => (
48
+ <div className="border-r border-b border-hairline -mr-px -mb-px">
49
+ <div className="aspect-square overflow-hidden border-b border-hairline bg-paper-2">
50
+ <EditableImage
51
+ field={p.photo}
52
+ className="!block h-full w-full object-cover [&_img]:h-full [&_img]:w-full [&_img]:object-cover"
53
+ />
54
+ </div>
55
+ <div className="p-6">
56
+ <EditableText
57
+ field={p.name}
58
+ as="h4"
59
+ className="m-0 text-[17px] font-semibold tracking-[-0.01em] text-ink"
60
+ />
61
+ <EditableText
62
+ field={p.role}
63
+ as="div"
64
+ className="mt-1 font-mono text-[11px] tracking-[0.07em] uppercase text-ink-3"
65
+ />
66
+ <EditableRichText
67
+ field={p.bio}
68
+ className="mt-2.5 [&_p]:m-0 [&_p]:text-[14px] [&_p]:leading-[1.55] [&_p]:text-ink-3"
69
+ />
70
+ </div>
71
+ </div>
72
+ )}
73
+ />
74
+ </div>
75
+ </section>
76
+ )
77
+ }
@@ -0,0 +1,10 @@
1
+ import { defineSection } from '@agntcms/next'
2
+ import { schema } from './schema'
3
+ import { TeamGridComponent } from './component'
4
+
5
+ export const TeamGrid = defineSection({
6
+ name: 'TeamGrid',
7
+ category: 'People',
8
+ schema,
9
+ component: TeamGridComponent,
10
+ })
@@ -0,0 +1,14 @@
1
+ import { RichTextField, TextField, ImageField, ListField } from '@agntcms/next'
2
+
3
+ export const schema = {
4
+ eyebrow: RichTextField,
5
+ headline: RichTextField,
6
+ lead: RichTextField,
7
+ columns: TextField,
8
+ people: ListField({
9
+ photo: ImageField,
10
+ name: TextField,
11
+ role: TextField,
12
+ bio: RichTextField,
13
+ }),
14
+ }
@@ -0,0 +1,76 @@
1
+ 'use client'
2
+
3
+ import {
4
+ EditableRichText,
5
+ EditableText,
6
+ EditableImage,
7
+ EditableList,
8
+ } from '@agntcms/next/client'
9
+ import type { EditableSlot, SlotItem } from '@agntcms/next/client'
10
+ import { schema } from './schema'
11
+
12
+ type Item = SlotItem<typeof schema.items.itemSchema>
13
+
14
+ interface Props {
15
+ readonly eyebrow: EditableSlot<'richText', string>
16
+ readonly headline: EditableSlot<'richText', string>
17
+ readonly items: EditableSlot<'list', ReadonlyArray<Item>>
18
+ }
19
+
20
+ export function TestimonialsComponent({ eyebrow, headline, items }: Props) {
21
+ return (
22
+ <section className="bg-paper">
23
+ <div className="mx-auto w-full max-w-[1280px] px-8 py-[88px]">
24
+ <div className="max-w-[720px]">
25
+ <EditableRichText
26
+ field={eyebrow}
27
+ className="[&_p]:font-mono [&_p]:font-medium [&_p]:text-[12px] [&_p]:tracking-[0.07em] [&_p]:uppercase [&_p]:text-ink-3 [&_p]:m-0"
28
+ />
29
+ <div className="mt-3 [&_h2]:m-0 [&_h2]:font-display [&_h2]:font-semibold [&_h2]:text-ink [&_h2]:text-[44px] [&_h2]:leading-[1.05] [&_h2]:tracking-[-0.03em] [&_p]:m-0 [&_p]:font-display [&_p]:font-semibold [&_p]:text-ink [&_p]:text-[44px] [&_p]:leading-[1.05] [&_p]:tracking-[-0.03em] [&_em]:not-italic [&_em]:text-ink-3 [&_em]:font-semibold">
30
+ <EditableRichText field={headline} />
31
+ </div>
32
+ </div>
33
+ <EditableList
34
+ field={items}
35
+ itemSchema={schema.items.itemSchema}
36
+ className="mt-12 grid grid-cols-1 border-t border-hairline lg:grid-cols-2"
37
+ renderItem={(t, index) => {
38
+ const number = String(index + 1).padStart(2, '0')
39
+ const odd = index % 2 === 0
40
+ return (
41
+ <figure
42
+ className={`m-0 flex flex-col gap-4 border-b border-hairline p-8 ${odd ? 'lg:border-r' : ''} lg:pl-${odd ? '0' : '8'} pl-0`}
43
+ >
44
+ <span className="font-mono text-[11px] tracking-[0.07em] uppercase text-ink-3">
45
+ quote · {number}
46
+ </span>
47
+ <EditableRichText
48
+ field={t.quote}
49
+ className="[&_p]:m-0 [&_p]:text-[19px] [&_p]:leading-[1.45] [&_p]:tracking-[-0.01em] [&_p]:text-ink"
50
+ />
51
+ <figcaption className="mt-auto flex items-center gap-3">
52
+ <EditableImage
53
+ field={t.photo}
54
+ className="!block h-9 w-9 rounded-full object-cover grayscale [&_img]:h-9 [&_img]:w-9 [&_img]:rounded-full [&_img]:object-cover [&_img]:grayscale"
55
+ />
56
+ <div>
57
+ <EditableText
58
+ field={t.name}
59
+ as="div"
60
+ className="text-sm font-semibold text-ink"
61
+ />
62
+ <EditableText
63
+ field={t.role}
64
+ as="div"
65
+ className="font-mono text-[11px] tracking-[0.07em] uppercase text-ink-3"
66
+ />
67
+ </div>
68
+ </figcaption>
69
+ </figure>
70
+ )
71
+ }}
72
+ />
73
+ </div>
74
+ </section>
75
+ )
76
+ }
@@ -0,0 +1,10 @@
1
+ import { defineSection } from '@agntcms/next'
2
+ import { schema } from './schema'
3
+ import { TestimonialsComponent } from './component'
4
+
5
+ export const Testimonials = defineSection({
6
+ name: 'Testimonials',
7
+ category: 'Social proof',
8
+ schema,
9
+ component: TestimonialsComponent,
10
+ })
@@ -0,0 +1,12 @@
1
+ import { RichTextField, TextField, ImageField, ListField } from '@agntcms/next'
2
+
3
+ export const schema = {
4
+ eyebrow: RichTextField,
5
+ headline: RichTextField,
6
+ items: ListField({
7
+ quote: RichTextField,
8
+ name: TextField,
9
+ role: TextField,
10
+ photo: ImageField,
11
+ }),
12
+ }
@@ -0,0 +1,86 @@
1
+ 'use client'
2
+
3
+ import { EditableRichText, EditableList, read } from '@agntcms/next/client'
4
+ import type { EditableSlot, SlotItem } from '@agntcms/next/client'
5
+ import { schema } from './schema'
6
+
7
+ type WhatIsBuiltItem = SlotItem<typeof schema.items.itemSchema>
8
+
9
+ interface Props {
10
+ readonly eyebrow: EditableSlot<'richText', string>
11
+ readonly headline: EditableSlot<'richText', string>
12
+ readonly intro: EditableSlot<'richText', string>
13
+ readonly items: EditableSlot<'list', ReadonlyArray<WhatIsBuiltItem>>
14
+ }
15
+
16
+ function pillClasses(status: string): string {
17
+ if (status === 'shipped') {
18
+ return 'bg-bg-brand-primary text-text-brand-primary'
19
+ }
20
+ if (status === 'wip') {
21
+ return 'bg-[#2a2410] text-[#c9a700]'
22
+ }
23
+ return 'bg-bg-primary text-text-secondary border-[0.5px] border-border-primary'
24
+ }
25
+
26
+ function pillLabel(status: string): string {
27
+ if (status === 'shipped') return 'shipped'
28
+ if (status === 'wip') return 'in progress'
29
+ return 'next'
30
+ }
31
+
32
+ export function WhatIsBuiltComponent({ eyebrow, headline, intro, items }: Props) {
33
+ return (
34
+ <section id="built" className="bg-bg-primary">
35
+ <div className="mx-auto max-w-[1080px] px-8 py-16 border-t-[0.5px] border-border-secondary">
36
+ <EditableRichText
37
+ field={eyebrow}
38
+ className="prose mb-3.5 [&_p]:text-[11px] [&_p]:font-medium [&_p]:tracking-[0.10em] [&_p]:uppercase [&_p]:text-text-brand-primary [&_p]:m-0"
39
+ />
40
+ <EditableRichText
41
+ field={headline}
42
+ className="mb-6
43
+ [&_h2]:font-display [&_h2]:font-medium [&_h2]:text-text-primary [&_h2]:m-0
44
+ [&_h2]:max-w-[22ch]
45
+ [&_h2]:!text-[clamp(28px,3.6vw,40px)] [&_h2]:!leading-[1.1] [&_h2]:!tracking-[-0.02em]"
46
+ />
47
+ <EditableRichText
48
+ field={intro}
49
+ className="prose max-w-[60ch] mb-7 [&_p]:text-text-primary [&_p]:m-0"
50
+ />
51
+
52
+ <EditableList
53
+ field={items}
54
+ itemSchema={schema.items.itemSchema}
55
+ className="grid grid-cols-1 md:grid-cols-2 gap-3"
56
+ renderItem={(item) => {
57
+ const status = read(item.status)
58
+ return (
59
+ <div className="flex gap-3 items-start h-full bg-bg-secondary border-[0.5px] border-border-primary rounded-lg px-4 py-3.5">
60
+ <span
61
+ className={[
62
+ 'font-mono text-[10px] tracking-[0.06em] uppercase',
63
+ 'px-2 py-0.5 rounded-[3px] flex-shrink-0 mt-px',
64
+ pillClasses(status),
65
+ ].join(' ')}
66
+ >
67
+ {pillLabel(status)}
68
+ </span>
69
+ <div className="text-sm leading-[1.5]">
70
+ <EditableRichText
71
+ field={item.label}
72
+ className="prose [&_p]:m-0 [&_p]:font-medium [&_p]:text-text-primary"
73
+ />
74
+ <EditableRichText
75
+ field={item.description}
76
+ className="prose mt-0.5 [&_p]:m-0 [&_p]:text-text-secondary [&_p]:text-[13px]"
77
+ />
78
+ </div>
79
+ </div>
80
+ )
81
+ }}
82
+ />
83
+ </div>
84
+ </section>
85
+ )
86
+ }
@@ -0,0 +1,10 @@
1
+ import { defineSection } from '@agntcms/next'
2
+ import { schema } from './schema'
3
+ import { WhatIsBuiltComponent } from './component'
4
+
5
+ export const WhatIsBuilt = defineSection({
6
+ name: 'WhatIsBuilt',
7
+ category: 'Content',
8
+ schema,
9
+ component: WhatIsBuiltComponent,
10
+ })
@@ -0,0 +1,20 @@
1
+ import { RichTextField, SelectField, ListField } from '@agntcms/next'
2
+
3
+ export const schema = {
4
+ eyebrow: RichTextField,
5
+ headline: RichTextField,
6
+ intro: RichTextField,
7
+ items: ListField({
8
+ // status drives the pill colour: shipped (teal), wip (amber), next (neutral).
9
+ status: SelectField(
10
+ [
11
+ { value: 'shipped', label: 'Shipped' },
12
+ { value: 'wip', label: 'In progress' },
13
+ { value: 'next', label: 'Next' },
14
+ ],
15
+ { default: 'shipped' },
16
+ ),
17
+ label: RichTextField,
18
+ description: RichTextField,
19
+ }),
20
+ }
@@ -0,0 +1,81 @@
1
+ // Typed accessor for the `site-meta` global. All four metadata helpers
2
+ // (app/layout.tsx, app/[[...slug]]/page.tsx, app/sitemap.ts, app/robots.ts)
3
+ // read this global; centralizing the narrowing here means schema changes only
4
+ // require one update instead of four.
5
+
6
+ import { cache } from 'react'
7
+ import type { ImageValue } from '@agntcms/next'
8
+ import type { GetGlobal } from '@agntcms/next/server'
9
+
10
+ // The shape of the site-meta global's `data` field after narrowing.
11
+ // Matches the schema in agntcms/sections/SiteMeta/schema.ts exactly.
12
+ export interface SiteMeta {
13
+ siteName: string
14
+ baseUrl: string | null
15
+ defaultOgImage: ImageValue | null
16
+ defaultDescription: string
17
+ }
18
+
19
+ // Reads the `site-meta` global and returns a fully-typed, narrowed object.
20
+ // Falls back to safe defaults when the global is absent or a field is
21
+ // missing/wrong-typed — never throws.
22
+ // `cache` dedups multiple calls within the same server request so layout.tsx
23
+ // and [[...slug]]/page.tsx do not each trigger a separate adapter read.
24
+ export const getSiteMeta = cache(async (getGlobal: GetGlobal): Promise<SiteMeta> => {
25
+ // `getGlobal` is invoked in published mode unconditionally — globals do not
26
+ // have drafts in v0.1. If/when drafts arrive for globals, callers in preview
27
+ // contexts (e.g. `[[...slug]]/page.tsx generateMetadata`) must pass the mode.
28
+ const global = await getGlobal({ name: 'site-meta', mode: 'published' })
29
+ const data: unknown = global?.data
30
+
31
+ // Narrow data to a plain object first — if it isn't, all fields fall to
32
+ // their defaults below.
33
+ const d = typeof data === 'object' && data !== null ? (data as Record<string, unknown>) : {}
34
+
35
+ const siteName =
36
+ typeof d['siteName'] === 'string' && d['siteName'] !== ''
37
+ ? d['siteName']
38
+ : 'agntcms'
39
+
40
+ // Strip ALL trailing slashes (not just one), then validate that the result
41
+ // is a parseable absolute URL. A bare hostname like "example.com" or a value
42
+ // with extra slashes would crash `new URL(baseUrl)` in layout.tsx, so we
43
+ // degrade to null rather than propagate a broken value to any consumer.
44
+ let baseUrl: string | null = null
45
+ if (typeof d['baseUrl'] === 'string') {
46
+ const cleaned = d['baseUrl'].replace(/\/+$/, '')
47
+ if (cleaned !== '') {
48
+ try {
49
+ const parsed = new URL(cleaned)
50
+ // Reject URLs with a path component — a baseUrl of
51
+ // "https://example.com/foo" would produce wrong sitemap and canonical
52
+ // URLs like "https://example.com/foo/sitemap.xml".
53
+ if (parsed.pathname === '/' || parsed.pathname === '') {
54
+ baseUrl = cleaned
55
+ }
56
+ } catch {
57
+ // Malformed or missing protocol — treat as absent; all four consumers
58
+ // already handle baseUrl: null correctly.
59
+ baseUrl = null
60
+ }
61
+ }
62
+ }
63
+
64
+ // ImageValue requires both `filename` (string) and `alt` (string).
65
+ const rawImage = d['defaultOgImage']
66
+ const defaultOgImage: ImageValue | null =
67
+ typeof rawImage === 'object' &&
68
+ rawImage !== null &&
69
+ typeof (rawImage as Record<string, unknown>)['filename'] === 'string' &&
70
+ typeof (rawImage as Record<string, unknown>)['alt'] === 'string'
71
+ ? {
72
+ filename: (rawImage as Record<string, unknown>)['filename'] as string,
73
+ alt: (rawImage as Record<string, unknown>)['alt'] as string,
74
+ }
75
+ : null
76
+
77
+ const defaultDescription =
78
+ typeof d['defaultDescription'] === 'string' ? d['defaultDescription'] : ''
79
+
80
+ return { siteName, baseUrl, defaultOgImage, defaultDescription }
81
+ })
@@ -0,0 +1,123 @@
1
+ // FROZEN — do not edit. Framework file managed by agntcms.
2
+ //
3
+ // Catch-all route that renders all content pages. Every URL that is not
4
+ // handled by a more specific App Router segment falls here. The slug
5
+ // segments are joined into a single path string and handed to the runtime,
6
+ // which reads the page from the content adapter and returns it (or null
7
+ // when the page does not exist).
8
+ //
9
+ // Reads from draft bucket when preview cookie is set, published otherwise.
10
+
11
+ import type { Metadata } from 'next'
12
+ import { cookies } from 'next/headers'
13
+ import { notFound } from 'next/navigation'
14
+ import {
15
+ PageRenderer,
16
+ PreviewProvider,
17
+ PreviewToolbar,
18
+ SectionEditControls,
19
+ } from '@agntcms/next/client'
20
+ import { createRuntime } from '@agntcms/next/server'
21
+ import config from '@/agntcms/config'
22
+ import { getSiteMeta } from '@/agntcms/site-meta'
23
+
24
+ // The runtime is constructed once per module load. createRuntime is
25
+ // stateless — it holds no request-scoped data — so module-level
26
+ // construction is safe and avoids rebuilding the adapter on every request.
27
+ const runtime = createRuntime({ contentAdapter: config.contentAdapter })
28
+
29
+ // SEO metadata is always derived from published content. Crawlers and link
30
+ // previews should see canonical titles — draft changes only surface after
31
+ // publish, which is the correct behaviour for search indexing.
32
+ export async function generateMetadata({ params }: {
33
+ params: Promise<{ slug?: string[] }>
34
+ }): Promise<Metadata> {
35
+ const { slug: slugParts } = await params
36
+ const slug = slugParts ? slugParts.join('/') : 'home'
37
+
38
+ const [page, siteMeta] = await Promise.all([
39
+ runtime.getContent({ slug, mode: 'published' }),
40
+ getSiteMeta(runtime.getGlobal),
41
+ ])
42
+
43
+ if (!page) return {}
44
+
45
+ const baseUrl = siteMeta.baseUrl ?? ''
46
+
47
+ // Canonical: page-level override wins; otherwise derive from base URL.
48
+ // For the "home" slug the canonical is the root path with no trailing slash.
49
+ const canonicalPath = slug === 'home' ? '' : `/${slug}`
50
+ const canonical = page.seo.canonical ?? (baseUrl ? `${baseUrl}${canonicalPath}` : undefined)
51
+
52
+ // OG image resolution order: page.seo.ogImage → page.coverImage →
53
+ // siteMeta.defaultOgImage → nothing.
54
+ const ogImageValue =
55
+ page.seo.ogImage ??
56
+ page.coverImage ??
57
+ siteMeta.defaultOgImage
58
+
59
+ // Resolve OG image to an absolute URL when a base URL is available.
60
+ // The framework stores images as plain filename strings under /assets/.
61
+ const ogImageUrl =
62
+ ogImageValue && baseUrl
63
+ ? `${baseUrl}/assets/${ogImageValue.filename}`
64
+ : undefined
65
+
66
+ const meta: Metadata = {
67
+ title: page.seo.title,
68
+ description: page.seo.description,
69
+ openGraph: {
70
+ title: page.seo.title,
71
+ description: page.seo.description,
72
+ type: 'website',
73
+ ...(ogImageUrl ? { images: [{ url: ogImageUrl, alt: ogImageValue?.alt ?? '' }] } : {}),
74
+ },
75
+ twitter: {
76
+ card: 'summary_large_image',
77
+ title: page.seo.title,
78
+ description: page.seo.description,
79
+ ...(ogImageUrl ? { images: [ogImageUrl] } : {}),
80
+ },
81
+ }
82
+
83
+ if (canonical) {
84
+ meta.alternates = { canonical }
85
+ }
86
+
87
+ return meta
88
+ }
89
+
90
+ export default async function CatchAllPage({
91
+ params,
92
+ }: {
93
+ params: Promise<{ slug?: string[] }>
94
+ }) {
95
+ const { slug: slugParts } = await params
96
+
97
+ // Root path (/) has no slug segments; map it to the "home" page slug.
98
+ // All other paths join their segments with "/" to form the page slug.
99
+ const slug = slugParts ? slugParts.join('/') : 'home'
100
+
101
+ const cookieStore = await cookies()
102
+ const isPreview = cookieStore.get('__agntcms_preview')?.value === '1'
103
+ const mode = isPreview ? 'preview' : 'published'
104
+
105
+ const page = await runtime.getContent({ slug, mode })
106
+ if (!page) notFound()
107
+
108
+ return (
109
+ <PreviewProvider mode={mode}>
110
+ {isPreview ? (
111
+ <div data-agntcms-page={slug}>
112
+ <SectionEditControls
113
+ page={page}
114
+ definitions={config.sections}
115
+ />
116
+ </div>
117
+ ) : (
118
+ <PageRenderer page={page} definitions={config.sections} />
119
+ )}
120
+ {isPreview && <PreviewToolbar definitions={config.sections} />}
121
+ </PreviewProvider>
122
+ )
123
+ }