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.
- package/README.md +39 -0
- package/dist/index.mjs +297 -0
- package/dist/template/.claude/settings.json +6 -0
- package/dist/template/.claude/skills/.gitkeep +0 -0
- package/dist/template/.claude-plugin/channel/server.mjs +254 -0
- package/dist/template/.claude-plugin/channel/server.ts +369 -0
- package/dist/template/.claude-plugin/plugin.json +17 -0
- package/dist/template/.mcp.json +8 -0
- package/dist/template/BRAND.md +49 -0
- package/dist/template/CLAUDE.md +157 -0
- package/dist/template/agntcms/config.ts +49 -0
- package/dist/template/agntcms/sections/ArticleBody/component.tsx +32 -0
- package/dist/template/agntcms/sections/ArticleBody/index.ts +10 -0
- package/dist/template/agntcms/sections/ArticleBody/schema.ts +5 -0
- package/dist/template/agntcms/sections/ArticleHero/component.tsx +87 -0
- package/dist/template/agntcms/sections/ArticleHero/index.ts +10 -0
- package/dist/template/agntcms/sections/ArticleHero/schema.ts +12 -0
- package/dist/template/agntcms/sections/Banner/component.tsx +83 -0
- package/dist/template/agntcms/sections/Banner/index.ts +10 -0
- package/dist/template/agntcms/sections/Banner/schema.ts +9 -0
- package/dist/template/agntcms/sections/BlogIndex/component.tsx +173 -0
- package/dist/template/agntcms/sections/BlogIndex/index.ts +10 -0
- package/dist/template/agntcms/sections/BlogIndex/schema.ts +33 -0
- package/dist/template/agntcms/sections/BlogIndexHeader/component.tsx +44 -0
- package/dist/template/agntcms/sections/BlogIndexHeader/index.ts +10 -0
- package/dist/template/agntcms/sections/BlogIndexHeader/schema.ts +8 -0
- package/dist/template/agntcms/sections/BlogPostBody/component.tsx +50 -0
- package/dist/template/agntcms/sections/BlogPostBody/index.ts +10 -0
- package/dist/template/agntcms/sections/BlogPostBody/schema.ts +10 -0
- package/dist/template/agntcms/sections/BlogPostHero/component.tsx +88 -0
- package/dist/template/agntcms/sections/BlogPostHero/index.ts +10 -0
- package/dist/template/agntcms/sections/BlogPostHero/schema.ts +35 -0
- package/dist/template/agntcms/sections/CaseStudies/component.tsx +92 -0
- package/dist/template/agntcms/sections/CaseStudies/index.ts +10 -0
- package/dist/template/agntcms/sections/CaseStudies/schema.ts +17 -0
- package/dist/template/agntcms/sections/ContactForm/component.tsx +119 -0
- package/dist/template/agntcms/sections/ContactForm/index.ts +10 -0
- package/dist/template/agntcms/sections/ContactForm/schema.ts +15 -0
- package/dist/template/agntcms/sections/DocsArticle/component.tsx +266 -0
- package/dist/template/agntcms/sections/DocsArticle/index.ts +10 -0
- package/dist/template/agntcms/sections/DocsArticle/schema.ts +33 -0
- package/dist/template/agntcms/sections/FAQ/component.tsx +57 -0
- package/dist/template/agntcms/sections/FAQ/index.ts +10 -0
- package/dist/template/agntcms/sections/FAQ/schema.ts +11 -0
- package/dist/template/agntcms/sections/FeatureGrid/component.tsx +117 -0
- package/dist/template/agntcms/sections/FeatureGrid/index.ts +10 -0
- package/dist/template/agntcms/sections/FeatureGrid/schema.ts +21 -0
- package/dist/template/agntcms/sections/FeaturedArticles/component.tsx +99 -0
- package/dist/template/agntcms/sections/FeaturedArticles/index.ts +10 -0
- package/dist/template/agntcms/sections/FeaturedArticles/schema.ts +17 -0
- package/dist/template/agntcms/sections/GettingStarted/component.tsx +116 -0
- package/dist/template/agntcms/sections/GettingStarted/index.ts +10 -0
- package/dist/template/agntcms/sections/GettingStarted/schema.ts +11 -0
- package/dist/template/agntcms/sections/Hero/component.tsx +148 -0
- package/dist/template/agntcms/sections/Hero/index.ts +10 -0
- package/dist/template/agntcms/sections/Hero/schema.ts +16 -0
- package/dist/template/agntcms/sections/HowItWorks/component.tsx +57 -0
- package/dist/template/agntcms/sections/HowItWorks/index.ts +10 -0
- package/dist/template/agntcms/sections/HowItWorks/schema.ts +11 -0
- package/dist/template/agntcms/sections/ImageText/component.tsx +110 -0
- package/dist/template/agntcms/sections/ImageText/index.ts +10 -0
- package/dist/template/agntcms/sections/ImageText/schema.ts +14 -0
- package/dist/template/agntcms/sections/LogoStrip/component.tsx +37 -0
- package/dist/template/agntcms/sections/LogoStrip/index.ts +10 -0
- package/dist/template/agntcms/sections/LogoStrip/schema.ts +6 -0
- package/dist/template/agntcms/sections/Newsletter/component.tsx +48 -0
- package/dist/template/agntcms/sections/Newsletter/index.ts +10 -0
- package/dist/template/agntcms/sections/Newsletter/schema.ts +8 -0
- package/dist/template/agntcms/sections/OpenSource/component.tsx +99 -0
- package/dist/template/agntcms/sections/OpenSource/index.ts +10 -0
- package/dist/template/agntcms/sections/OpenSource/schema.ts +13 -0
- package/dist/template/agntcms/sections/PainAnswer/component.tsx +81 -0
- package/dist/template/agntcms/sections/PainAnswer/index.ts +10 -0
- package/dist/template/agntcms/sections/PainAnswer/schema.ts +15 -0
- package/dist/template/agntcms/sections/PricingPlans/component.tsx +100 -0
- package/dist/template/agntcms/sections/PricingPlans/index.ts +10 -0
- package/dist/template/agntcms/sections/PricingPlans/schema.ts +13 -0
- package/dist/template/agntcms/sections/Problem/component.tsx +49 -0
- package/dist/template/agntcms/sections/Problem/index.ts +10 -0
- package/dist/template/agntcms/sections/Problem/schema.ts +12 -0
- package/dist/template/agntcms/sections/SiteFooter/component.tsx +88 -0
- package/dist/template/agntcms/sections/SiteFooter/index.ts +10 -0
- package/dist/template/agntcms/sections/SiteFooter/schema.ts +13 -0
- package/dist/template/agntcms/sections/SiteHeader/component.tsx +99 -0
- package/dist/template/agntcms/sections/SiteHeader/index.ts +10 -0
- package/dist/template/agntcms/sections/SiteHeader/schema.ts +14 -0
- package/dist/template/agntcms/sections/SiteMeta/component.tsx +26 -0
- package/dist/template/agntcms/sections/SiteMeta/index.ts +13 -0
- package/dist/template/agntcms/sections/SiteMeta/schema.ts +18 -0
- package/dist/template/agntcms/sections/TabbedFeatures/component.tsx +120 -0
- package/dist/template/agntcms/sections/TabbedFeatures/index.ts +10 -0
- package/dist/template/agntcms/sections/TabbedFeatures/schema.ts +13 -0
- package/dist/template/agntcms/sections/TeamGrid/component.tsx +77 -0
- package/dist/template/agntcms/sections/TeamGrid/index.ts +10 -0
- package/dist/template/agntcms/sections/TeamGrid/schema.ts +14 -0
- package/dist/template/agntcms/sections/Testimonials/component.tsx +76 -0
- package/dist/template/agntcms/sections/Testimonials/index.ts +10 -0
- package/dist/template/agntcms/sections/Testimonials/schema.ts +12 -0
- package/dist/template/agntcms/sections/WhatIsBuilt/component.tsx +86 -0
- package/dist/template/agntcms/sections/WhatIsBuilt/index.ts +10 -0
- package/dist/template/agntcms/sections/WhatIsBuilt/schema.ts +20 -0
- package/dist/template/agntcms/site-meta.ts +81 -0
- package/dist/template/app/[[...slug]]/page.tsx +123 -0
- package/dist/template/app/admin/AdminPageClient.tsx +77 -0
- package/dist/template/app/admin/AdminPageDynamic.tsx +24 -0
- package/dist/template/app/admin/page.tsx +14 -0
- package/dist/template/app/api/agntcms/_shared.ts +80 -0
- package/dist/template/app/api/agntcms/assets/route.ts +11 -0
- package/dist/template/app/api/agntcms/assets/upload/route.ts +11 -0
- package/dist/template/app/api/agntcms/draft/discard/route.ts +12 -0
- package/dist/template/app/api/agntcms/draft/list/route.ts +11 -0
- package/dist/template/app/api/agntcms/draft/publish/route.ts +11 -0
- package/dist/template/app/api/agntcms/draft/reorder/route.ts +10 -0
- package/dist/template/app/api/agntcms/draft/save/route.ts +11 -0
- package/dist/template/app/api/agntcms/events/route.ts +12 -0
- package/dist/template/app/api/agntcms/forms/delete/route.ts +17 -0
- package/dist/template/app/api/agntcms/forms/list/route.ts +24 -0
- package/dist/template/app/api/agntcms/forms/read/route.ts +23 -0
- package/dist/template/app/api/agntcms/forms/submit/route.ts +17 -0
- package/dist/template/app/api/agntcms/global/delete/route.ts +13 -0
- package/dist/template/app/api/agntcms/global/history/route.ts +10 -0
- package/dist/template/app/api/agntcms/global/list/route.ts +14 -0
- package/dist/template/app/api/agntcms/global/read/route.ts +11 -0
- package/dist/template/app/api/agntcms/global/rollback/route.ts +10 -0
- package/dist/template/app/api/agntcms/global/save/route.ts +14 -0
- package/dist/template/app/api/agntcms/mcp/route.ts +12 -0
- package/dist/template/app/api/agntcms/page/delete/route.ts +10 -0
- package/dist/template/app/api/agntcms/page/duplicate/route.ts +11 -0
- package/dist/template/app/api/agntcms/page/history/route.ts +10 -0
- package/dist/template/app/api/agntcms/page/list/route.ts +10 -0
- package/dist/template/app/api/agntcms/page/read/route.ts +11 -0
- package/dist/template/app/api/agntcms/page/rename/route.ts +10 -0
- package/dist/template/app/api/agntcms/page/rollback/route.ts +10 -0
- package/dist/template/app/api/agntcms/page/unpublish/route.ts +11 -0
- package/dist/template/app/api/agntcms/preview/enter/route.ts +13 -0
- package/dist/template/app/api/agntcms/preview/exit/route.ts +10 -0
- package/dist/template/app/api/agntcms/preview/issue/route.ts +12 -0
- package/dist/template/app/api/agntcms/template/list/route.ts +15 -0
- package/dist/template/app/apple-icon.svg +9 -0
- package/dist/template/app/icon.svg +9 -0
- package/dist/template/app/layout.tsx +107 -0
- package/dist/template/app/not-found.tsx +75 -0
- package/dist/template/app/robots.ts +33 -0
- package/dist/template/app/sitemap.ts +49 -0
- package/dist/template/content/globals/site-footer.json +53 -0
- package/dist/template/content/globals/site-header.json +18 -0
- package/dist/template/content/globals/site-meta.json +13 -0
- package/dist/template/content/pages/404.json +34 -0
- package/dist/template/content/pages/about.json +307 -0
- package/dist/template/content/pages/article-editor.json +61 -0
- package/dist/template/content/pages/article-schemas.json +61 -0
- package/dist/template/content/pages/blog.json +162 -0
- package/dist/template/content/pages/contact.json +29 -0
- package/dist/template/content/pages/home.json +243 -0
- package/dist/template/content/pages/pricing.json +219 -0
- package/dist/template/content/pages/services.json +177 -0
- package/dist/template/fonts/Satoshi-Medium.woff2 +0 -0
- package/dist/template/fonts/Satoshi-Regular.woff2 +0 -0
- package/dist/template/next.config.ts +6 -0
- package/dist/template/package.json +36 -0
- package/dist/template/postcss.config.mjs +5 -0
- package/dist/template/public/assets/.gitkeep +0 -0
- package/dist/template/public/assets/0418d7ed21f57e7b9e0546725c92b8419daeaa355675d9070fab0c2013cf1524.jpg +0 -0
- package/dist/template/public/assets/0d0475f21aa96435a8ed3cdb2fddcc6278492e76ae842f569432454f4d33631a.jpg +0 -0
- package/dist/template/public/assets/27457a1adee2372030d9876b0d52c44d46be98843999935eaef2526b9b961f12.jpg +0 -0
- package/dist/template/public/assets/3855d91192f0c6120b01427b78ef84e52baa9f4b5a17d4271e41c1bfd95a5b0c.jpg +0 -0
- package/dist/template/public/assets/3b3b90c5084635b746be673ede92a328f002f5621a42c9a5cb89c5e2435652cb.jpg +0 -0
- package/dist/template/public/assets/3e76165a78fd3e7b8ed1e93dee50803ae11110c756c8c1c89229a2dec2bc0abf.jpg +0 -0
- package/dist/template/public/assets/4a3e28f85dc850c347ea0fd931696aa936a6bd45f193e7f1c9328b5896fb272c.jpg +0 -0
- package/dist/template/public/assets/579f67d5fd4c9106c6cdf2ef29f50df934ad0fc2b7849bac1e1cfb1e3f92303b.jpg +0 -0
- package/dist/template/public/assets/5b95209269661bb60fb250f1da682e05b9efa64dd42f350608b299e6bf1f2f35.jpg +0 -0
- package/dist/template/public/assets/5e04b46f8317ef95a7ddf85aedfe5c098a755f05056325d0251eccf95ce51172.jpg +0 -0
- package/dist/template/public/assets/6167a9164be2cf1183bdfdd4946bf9b908570e79e92a2380c25f0bb702422bbd.jpg +0 -0
- package/dist/template/public/assets/75e723ec316de28247924e5dfb73a4b266e10de605e749f150883d280ed8ed16.jpg +0 -0
- package/dist/template/public/assets/816a11e6a7245feaf51bbebf09d1bda3f125b334bc24fc3b8f47b5380a7b4294.jpg +0 -0
- package/dist/template/public/assets/81eba6f5654b8746a9b0cba1a9521a67f2b4afaaefc7c88d66dfab1461270d8f.jpg +0 -0
- package/dist/template/public/assets/82a2ce9e49361098f77a28755779dc5a7c026831cbd135175749c1304e21dacc.jpg +0 -0
- package/dist/template/public/assets/8d7b02ba277ba56bdafdbd47b01f7df6d993c714b4dc2305eb65a1307c09647d.jpg +0 -0
- package/dist/template/public/assets/b303185b471678e4d62f678a1549ee26022f4745407d08cae44ecb1c25352293.jpg +0 -0
- package/dist/template/public/assets/b69b49169c11546100d6dd5280073bc0d84cbbcc6d33fa01ecf6a5866fa42237.jpg +0 -0
- package/dist/template/public/assets/c4d2f0d1a310e457ac722a399693652e3c86c55b294243d5ffc679394e12f9d1.jpg +0 -0
- package/dist/template/public/assets/cae09f4729f8a348b67267c2f2a550be0f3bfa420689afe1a5cf8b7e2b146238.png +0 -0
- package/dist/template/public/assets/cb3acf58b57417a4b26474ba04c096af7103c4320ed2f4f3683f79d7670a055c.jpg +0 -0
- package/dist/template/public/assets/d5a0701b2d156284e0ce851cd2534ec632db34f91fbcbee3b8a7784d45ce78d2.jpg +0 -0
- package/dist/template/public/assets/d6ef1c3f48b0e488521794fb60701da1fd2c3a1621d6ac5f17ccfd4909d3be60.jpg +0 -0
- package/dist/template/public/assets/de249ff9be2539cf0d1ce092de3c57001839b6c3e14fcee3fc31a7b7673ae007.jpg +0 -0
- package/dist/template/public/assets/eac45438956be187b010e24b3289757aa00f227c190d49ee99fea510552dd2ba.jpg +0 -0
- package/dist/template/public/assets/f8b9200065b5436c6a88361839edc2b89be88d3037c84a80d3ee95c32891510b.jpg +0 -0
- package/dist/template/public/assets/placeholder.png +0 -0
- package/dist/template/public/brand/mark.svg +6 -0
- package/dist/template/public/brand/wordmark-light.svg +6 -0
- package/dist/template/public/brand/wordmark.svg +6 -0
- package/dist/template/styles/globals.css +69 -0
- package/dist/template/styles/theme.css +492 -0
- package/dist/template/styles/typography.css +469 -0
- package/dist/template/tsconfig.json +30 -0
- 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
|
+
}
|