@agility/create-next-app 1.0.0-beta.2

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 (213) hide show
  1. package/.claude/settings.json +7 -0
  2. package/.claude/settings.local.json +24 -0
  3. package/FEATURE_ROADMAP.md +343 -0
  4. package/README.md +205 -0
  5. package/TESTING.md +131 -0
  6. package/bin/create-agility-app.js +48 -0
  7. package/dist/agility/api-keys/generateApiKeys.d.ts +9 -0
  8. package/dist/agility/api-keys/generateApiKeys.d.ts.map +1 -0
  9. package/dist/agility/api-keys/generateApiKeys.js +99 -0
  10. package/dist/agility/api-keys/generateApiKeys.js.map +1 -0
  11. package/dist/agility/api-keys/getApiKeys.d.ts +9 -0
  12. package/dist/agility/api-keys/getApiKeys.d.ts.map +1 -0
  13. package/dist/agility/api-keys/getApiKeys.js +14 -0
  14. package/dist/agility/api-keys/getApiKeys.js.map +1 -0
  15. package/dist/agility/index.d.ts +3 -0
  16. package/dist/agility/index.d.ts.map +1 -0
  17. package/dist/agility/index.js +8 -0
  18. package/dist/agility/index.js.map +1 -0
  19. package/dist/agility/instance/createNewInstance.d.ts +8 -0
  20. package/dist/agility/instance/createNewInstance.d.ts.map +1 -0
  21. package/dist/agility/instance/createNewInstance.js +65 -0
  22. package/dist/agility/instance/createNewInstance.js.map +1 -0
  23. package/dist/agility/instance/getAvailableInstances.d.ts +8 -0
  24. package/dist/agility/instance/getAvailableInstances.d.ts.map +1 -0
  25. package/dist/agility/instance/getAvailableInstances.js +43 -0
  26. package/dist/agility/instance/getAvailableInstances.js.map +1 -0
  27. package/dist/agility/instance/manageInstance.d.ts +9 -0
  28. package/dist/agility/instance/manageInstance.d.ts.map +1 -0
  29. package/dist/agility/instance/manageInstance.js +82 -0
  30. package/dist/agility/instance/manageInstance.js.map +1 -0
  31. package/dist/agility/utils/getMgmtAPIUrl.d.ts +20 -0
  32. package/dist/agility/utils/getMgmtAPIUrl.d.ts.map +1 -0
  33. package/dist/agility/utils/getMgmtAPIUrl.js +61 -0
  34. package/dist/agility/utils/getMgmtAPIUrl.js.map +1 -0
  35. package/dist/auth/api-key/authenticateWithApiKey.d.ts +6 -0
  36. package/dist/auth/api-key/authenticateWithApiKey.d.ts.map +1 -0
  37. package/dist/auth/api-key/authenticateWithApiKey.js +28 -0
  38. package/dist/auth/api-key/authenticateWithApiKey.js.map +1 -0
  39. package/dist/auth/index.d.ts +3 -0
  40. package/dist/auth/index.d.ts.map +1 -0
  41. package/dist/auth/index.js +8 -0
  42. package/dist/auth/index.js.map +1 -0
  43. package/dist/auth/oauth/authenticate.d.ts +6 -0
  44. package/dist/auth/oauth/authenticate.d.ts.map +1 -0
  45. package/dist/auth/oauth/authenticate.js +162 -0
  46. package/dist/auth/oauth/authenticate.js.map +1 -0
  47. package/dist/auth/oauth/constants.d.ts +5 -0
  48. package/dist/auth/oauth/constants.d.ts.map +1 -0
  49. package/dist/auth/oauth/constants.js +9 -0
  50. package/dist/auth/oauth/constants.js.map +1 -0
  51. package/dist/auth/oauth/exchangeCodeForToken.d.ts +7 -0
  52. package/dist/auth/oauth/exchangeCodeForToken.d.ts.map +1 -0
  53. package/dist/auth/oauth/exchangeCodeForToken.js +39 -0
  54. package/dist/auth/oauth/exchangeCodeForToken.js.map +1 -0
  55. package/dist/cli/index.d.ts +3 -0
  56. package/dist/cli/index.d.ts.map +1 -0
  57. package/dist/cli/index.js +290 -0
  58. package/dist/cli/index.js.map +1 -0
  59. package/dist/cli/promptForMissingOptions.d.ts +8 -0
  60. package/dist/cli/promptForMissingOptions.d.ts.map +1 -0
  61. package/dist/cli/promptForMissingOptions.js +92 -0
  62. package/dist/cli/promptForMissingOptions.js.map +1 -0
  63. package/dist/config/env/createEnvFile.d.ts +6 -0
  64. package/dist/config/env/createEnvFile.d.ts.map +1 -0
  65. package/dist/config/env/createEnvFile.js +31 -0
  66. package/dist/config/env/createEnvFile.js.map +1 -0
  67. package/dist/config/index.d.ts +2 -0
  68. package/dist/config/index.d.ts.map +1 -0
  69. package/dist/config/index.js +6 -0
  70. package/dist/config/index.js.map +1 -0
  71. package/dist/config/mcp/createMcpConfig.d.ts +5 -0
  72. package/dist/config/mcp/createMcpConfig.d.ts.map +1 -0
  73. package/dist/config/mcp/createMcpConfig.js +32 -0
  74. package/dist/config/mcp/createMcpConfig.js.map +1 -0
  75. package/dist/config/packages/installAgilityPackages.d.ts +6 -0
  76. package/dist/config/packages/installAgilityPackages.d.ts.map +1 -0
  77. package/dist/config/packages/installAgilityPackages.js +61 -0
  78. package/dist/config/packages/installAgilityPackages.js.map +1 -0
  79. package/dist/config/setupProject.d.ts +8 -0
  80. package/dist/config/setupProject.d.ts.map +1 -0
  81. package/dist/config/setupProject.js +32 -0
  82. package/dist/config/setupProject.js.map +1 -0
  83. package/dist/create-next-app/createNextApp.d.ts +9 -0
  84. package/dist/create-next-app/createNextApp.d.ts.map +1 -0
  85. package/dist/create-next-app/createNextApp.js +83 -0
  86. package/dist/create-next-app/createNextApp.js.map +1 -0
  87. package/dist/create-next-app/index.d.ts +3 -0
  88. package/dist/create-next-app/index.d.ts.map +1 -0
  89. package/dist/create-next-app/index.js +8 -0
  90. package/dist/create-next-app/index.js.map +1 -0
  91. package/dist/scaffold/components/createPageComponents.d.ts +6 -0
  92. package/dist/scaffold/components/createPageComponents.d.ts.map +1 -0
  93. package/dist/scaffold/components/createPageComponents.js +62 -0
  94. package/dist/scaffold/components/createPageComponents.js.map +1 -0
  95. package/dist/scaffold/containers/createContainers.d.ts +6 -0
  96. package/dist/scaffold/containers/createContainers.d.ts.map +1 -0
  97. package/dist/scaffold/containers/createContainers.js +48 -0
  98. package/dist/scaffold/containers/createContainers.js.map +1 -0
  99. package/dist/scaffold/index.d.ts +2 -0
  100. package/dist/scaffold/index.d.ts.map +1 -0
  101. package/dist/scaffold/index.js +6 -0
  102. package/dist/scaffold/index.js.map +1 -0
  103. package/dist/scaffold/instance/createBlankInstance.d.ts +8 -0
  104. package/dist/scaffold/instance/createBlankInstance.d.ts.map +1 -0
  105. package/dist/scaffold/instance/createBlankInstance.js +51 -0
  106. package/dist/scaffold/instance/createBlankInstance.js.map +1 -0
  107. package/dist/scaffold/models/createContentModels.d.ts +6 -0
  108. package/dist/scaffold/models/createContentModels.d.ts.map +1 -0
  109. package/dist/scaffold/models/createContentModels.js +70 -0
  110. package/dist/scaffold/models/createContentModels.js.map +1 -0
  111. package/dist/templates/copyDirectory.d.ts +5 -0
  112. package/dist/templates/copyDirectory.d.ts.map +1 -0
  113. package/dist/templates/copyDirectory.js +28 -0
  114. package/dist/templates/copyDirectory.js.map +1 -0
  115. package/dist/templates/copyTemplates.d.ts +8 -0
  116. package/dist/templates/copyTemplates.d.ts.map +1 -0
  117. package/dist/templates/copyTemplates.js +58 -0
  118. package/dist/templates/copyTemplates.js.map +1 -0
  119. package/dist/templates/index.d.ts +2 -0
  120. package/dist/templates/index.d.ts.map +1 -0
  121. package/dist/templates/index.js +6 -0
  122. package/dist/templates/index.js.map +1 -0
  123. package/dist/types/index.d.ts +50 -0
  124. package/dist/types/index.d.ts.map +1 -0
  125. package/dist/types/index.js +3 -0
  126. package/dist/types/index.js.map +1 -0
  127. package/dist/utils/git.d.ts +9 -0
  128. package/dist/utils/git.d.ts.map +1 -0
  129. package/dist/utils/git.js +71 -0
  130. package/dist/utils/git.js.map +1 -0
  131. package/dist/utils/validation.d.ts +45 -0
  132. package/dist/utils/validation.d.ts.map +1 -0
  133. package/dist/utils/validation.js +180 -0
  134. package/dist/utils/validation.js.map +1 -0
  135. package/package.json +45 -0
  136. package/src/agility/api-keys/generateApiKeys.ts +100 -0
  137. package/src/agility/api-keys/getApiKeys.ts +13 -0
  138. package/src/agility/index.ts +3 -0
  139. package/src/agility/instance/createNewInstance.ts +67 -0
  140. package/src/agility/instance/getAvailableInstances.ts +49 -0
  141. package/src/agility/instance/manageInstance.ts +90 -0
  142. package/src/agility/utils/getMgmtAPIUrl.ts +68 -0
  143. package/src/auth/api-key/authenticateWithApiKey.ts +24 -0
  144. package/src/auth/index.ts +3 -0
  145. package/src/auth/oauth/authenticate.ts +165 -0
  146. package/src/auth/oauth/constants.ts +6 -0
  147. package/src/auth/oauth/exchangeCodeForToken.ts +43 -0
  148. package/src/cli/index.ts +281 -0
  149. package/src/cli/promptForMissingOptions.ts +104 -0
  150. package/src/config/env/createEnvFile.ts +30 -0
  151. package/src/config/index.ts +2 -0
  152. package/src/config/mcp/createMcpConfig.ts +30 -0
  153. package/src/config/packages/installAgilityPackages.ts +63 -0
  154. package/src/config/setupProject.ts +31 -0
  155. package/src/create-next-app/createNextApp.ts +75 -0
  156. package/src/create-next-app/index.ts +3 -0
  157. package/src/scaffold/components/createPageComponents.ts +74 -0
  158. package/src/scaffold/containers/createContainers.ts +55 -0
  159. package/src/scaffold/index.ts +2 -0
  160. package/src/scaffold/instance/createBlankInstance.ts +55 -0
  161. package/src/scaffold/models/createContentModels.ts +83 -0
  162. package/src/templates/copyDirectory.ts +24 -0
  163. package/src/templates/copyTemplates.ts +57 -0
  164. package/src/templates/index.ts +2 -0
  165. package/src/types/index.ts +55 -0
  166. package/src/utils/git.ts +74 -0
  167. package/src/utils/validation.ts +184 -0
  168. package/templates/.claude/QUICK-START.md +230 -0
  169. package/templates/.claude/README.md +32 -0
  170. package/templates/.claude/settings.json +8 -0
  171. package/templates/BLANK-INSTANCE-SETUP.md +375 -0
  172. package/templates/DEVELOPMENT.md +160 -0
  173. package/templates/EXAMPLE-PROMPTS.md +643 -0
  174. package/templates/PROMPTS.md +410 -0
  175. package/templates/README.md +281 -0
  176. package/templates/agents.md +429 -0
  177. package/templates/app/[locale]/[...slug]/error.tsx +17 -0
  178. package/templates/app/[locale]/[...slug]/not-found.tsx +9 -0
  179. package/templates/app/[locale]/[...slug]/page.tsx +102 -0
  180. package/templates/app/[locale]/layout.tsx +22 -0
  181. package/templates/app/[locale]/page.tsx +12 -0
  182. package/templates/app/api/dynamic-redirect/route.ts +24 -0
  183. package/templates/app/api/preview/exit/route.ts +34 -0
  184. package/templates/app/api/preview/route.ts +63 -0
  185. package/templates/app/api/revalidate/route.ts +118 -0
  186. package/templates/components/agility-components/RichTextArea.tsx +66 -0
  187. package/templates/components/agility-components/index.ts +30 -0
  188. package/templates/components/agility-pages/MainTemplate.tsx +36 -0
  189. package/templates/components/agility-pages/index.ts +11 -0
  190. package/templates/docs/01-agility-cms-overview.md +139 -0
  191. package/templates/docs/02-page-routing.md +251 -0
  192. package/templates/docs/03-creating-components.md +462 -0
  193. package/templates/docs/04-data-fetching.md +484 -0
  194. package/templates/docs/05-containers-and-lists.md +596 -0
  195. package/templates/docs/06-localization.md +561 -0
  196. package/templates/docs/07-caching-strategies.md +410 -0
  197. package/templates/docs/08-common-components.md +756 -0
  198. package/templates/docs/09-whats-included.md +279 -0
  199. package/templates/docs/10-mcp-server-setup.md +153 -0
  200. package/templates/docs/11-linked-nested-content.md +611 -0
  201. package/templates/docs/README.md +164 -0
  202. package/templates/lib/cms/getAgilityContext.ts +28 -0
  203. package/templates/lib/cms/getAgilityPage.ts +51 -0
  204. package/templates/lib/cms/getAgilitySDK.ts +22 -0
  205. package/templates/lib/cms/getContentItem.ts +20 -0
  206. package/templates/lib/cms/getContentList.ts +19 -0
  207. package/templates/lib/cms/getRedirections.ts +85 -0
  208. package/templates/lib/cms/getSitemapFlat.ts +19 -0
  209. package/templates/lib/cms/getSitemapNested.ts +19 -0
  210. package/templates/lib/env.ts +99 -0
  211. package/templates/lib/i18n/config.ts +28 -0
  212. package/templates/proxy.ts +101 -0
  213. package/tsconfig.json +21 -0
@@ -0,0 +1,756 @@
1
+ # Common Components and Patterns
2
+
3
+ This document provides ready-to-use component examples for common Agility CMS patterns.
4
+
5
+ ## ⚠️ CRITICAL: Image Handling with AgilityPic
6
+
7
+ **ALL images from Agility CMS MUST be rendered using the `<AgilityPic>` component!**
8
+
9
+ ### Why AgilityPic?
10
+
11
+ The `<AgilityPic>` component provides:
12
+ - **Automatic optimization**: Serves appropriately sized images
13
+ - **Responsive images**: Built-in support for different screen sizes
14
+ - **CMS integration**: Maintains compatibility with Agility's inline editing
15
+ - **Performance**: Lazy loading and modern image formats
16
+
17
+ ### Basic Usage
18
+
19
+ ```tsx
20
+ import { AgilityPic } from "@agility/nextjs";
21
+ import type { ImageField } from "@agility/nextjs";
22
+
23
+ interface MyComponentProps {
24
+ module: {
25
+ fields: {
26
+ image: ImageField; // Always use ImageField type
27
+ };
28
+ };
29
+ }
30
+
31
+ export default function MyComponent({ module }: MyComponentProps) {
32
+ return (
33
+ <AgilityPic
34
+ image={module.fields.image}
35
+ fallbackWidth={600}
36
+ className="w-full h-auto"
37
+ data-agility-field="image"
38
+ />
39
+ );
40
+ }
41
+ ```
42
+
43
+ ### Key Props
44
+
45
+ - **`image`** (required): The `ImageField` object from Agility CMS
46
+ - **`fallbackWidth`**: Width in pixels for fallback (default: 640)
47
+ - **`alt`**: Alt text override (uses CMS alt text by default)
48
+ - **`className`**: CSS classes for styling
49
+ - **`priority`**: Set `true` for above-the-fold images (loads eagerly)
50
+ - **`sources`**: Array of responsive sources with media queries
51
+ - **`data-agility-field`**: Field name for CMS inline editing
52
+
53
+ ### Responsive Images Example
54
+
55
+ ```tsx
56
+ <AgilityPic
57
+ image={imageField}
58
+ fallbackWidth={800}
59
+ sources={[
60
+ { media: "(max-width: 639px)", width: 640 }, // Mobile
61
+ { media: "(max-width: 767px)", width: 800 }, // Tablet
62
+ { media: "(max-width: 1023px)", width: 1200 }, // Desktop
63
+ ]}
64
+ className="w-full h-full object-cover"
65
+ />
66
+ ```
67
+
68
+ ### ❌ WRONG - Don't Do This
69
+
70
+ ```tsx
71
+ // ❌ NEVER use Next.js Image for Agility images
72
+ import Image from "next/image";
73
+ <Image src={imageField.url} alt="..." />
74
+
75
+ // ❌ NEVER use plain img tags for Agility images
76
+ <img src={imageField.url} alt="..." />
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Header Component
82
+
83
+ A reusable header with navigation from Agility CMS:
84
+
85
+ ```tsx
86
+ // src/components/Header.tsx
87
+
88
+ import { getSitemapNested } from "@/lib/cms/getSitemapNested";
89
+ import LocaleSwitcher from "./LocaleSwitcher";
90
+
91
+ interface HeaderProps {
92
+ locale: string;
93
+ }
94
+
95
+ export default async function Header({ locale }: HeaderProps) {
96
+ const sitemap = await getSitemapNested({ locale });
97
+
98
+ // Get top-level nav items (children of home)
99
+ const navItems = sitemap[0]?.children || [];
100
+
101
+ return (
102
+ <header className="bg-white shadow">
103
+ <div className="container mx-auto px-4 py-4 flex justify-between items-center">
104
+ <a href="/" className="text-2xl font-bold">
105
+ Your Site
106
+ </a>
107
+
108
+ <nav className="flex gap-6">
109
+ {navItems.map((item: any) => (
110
+ <a
111
+ key={item.pageID}
112
+ href={item.path}
113
+ className="hover:text-blue-600"
114
+ >
115
+ {item.menuText || item.title}
116
+ </a>
117
+ ))}
118
+ </nav>
119
+
120
+ <LocaleSwitcher currentLocale={locale} />
121
+ </div>
122
+ </header>
123
+ );
124
+ }
125
+ ```
126
+
127
+ ## Footer Component
128
+
129
+ ```tsx
130
+ // src/components/Footer.tsx
131
+
132
+ import { getContentItem } from "@/lib/cms/getContentItem";
133
+
134
+ interface FooterProps {
135
+ locale: string;
136
+ }
137
+
138
+ export default async function Footer({ locale }: FooterProps) {
139
+ const footer = await getContentItem({
140
+ referenceName: "footerSettings",
141
+ locale,
142
+ });
143
+
144
+ if (!footer) return null;
145
+
146
+ const { copyrightText, socialLinks, columns } = footer.fields;
147
+
148
+ return (
149
+ <footer className="bg-gray-900 text-white py-12">
150
+ <div className="container mx-auto px-4">
151
+ <div className="grid md:grid-cols-4 gap-8">
152
+ {columns?.map((column: any, index: number) => (
153
+ <div key={index}>
154
+ <h3 className="font-bold mb-4">{column.title}</h3>
155
+ <ul className="space-y-2">
156
+ {column.links?.map((link: any, i: number) => (
157
+ <li key={i}>
158
+ <a href={link.href} className="hover:text-gray-300">
159
+ {link.text}
160
+ </a>
161
+ </li>
162
+ ))}
163
+ </ul>
164
+ </div>
165
+ ))}
166
+ </div>
167
+
168
+ <div className="mt-8 pt-8 border-t border-gray-800 flex justify-between items-center">
169
+ <p>{copyrightText}</p>
170
+
171
+ {socialLinks && (
172
+ <div className="flex gap-4">
173
+ {socialLinks.map((social: any, i: number) => (
174
+ <a
175
+ key={i}
176
+ href={social.href}
177
+ target="_blank"
178
+ rel="noopener noreferrer"
179
+ className="hover:text-gray-300"
180
+ >
181
+ {social.text}
182
+ </a>
183
+ ))}
184
+ </div>
185
+ )}
186
+ </div>
187
+ </div>
188
+ </footer>
189
+ );
190
+ }
191
+ ```
192
+
193
+ ## Hero Component
194
+
195
+ **IMPORTANT**: Always use `<AgilityPic>` for images from Agility CMS!
196
+
197
+ ```tsx
198
+ // src/components/agility-components/Hero.tsx
199
+
200
+ import { AgilityPic } from "@agility/nextjs";
201
+ import type { ImageField, URLField } from "@agility/nextjs";
202
+
203
+ interface HeroProps {
204
+ module: {
205
+ fields: {
206
+ title: string;
207
+ subtitle: string;
208
+ ctaButton?: URLField;
209
+ backgroundImage?: ImageField; // Use ImageField type
210
+ };
211
+ };
212
+ locale: string;
213
+ }
214
+
215
+ export default function Hero({ module, locale }: HeroProps) {
216
+ const { title, subtitle, ctaButton, backgroundImage } = module.fields;
217
+
218
+ return (
219
+ <section className="relative h-screen flex items-center justify-center text-white">
220
+ {backgroundImage && (
221
+ <div className="absolute inset-0 z-0">
222
+ {/* ✅ CORRECT: Use AgilityPic for Agility CMS images */}
223
+ <AgilityPic
224
+ image={backgroundImage}
225
+ fallbackWidth={1920}
226
+ className="w-full h-full object-cover"
227
+ priority={true} // Above-the-fold image
228
+ data-agility-field="backgroundImage"
229
+ />
230
+ <div className="absolute inset-0 bg-black bg-opacity-50" />
231
+ </div>
232
+ )}
233
+
234
+ <div className="relative z-10 text-center max-w-4xl px-4">
235
+ <h1 className="text-6xl font-bold mb-6" data-agility-field="title">
236
+ {title}
237
+ </h1>
238
+ <p className="text-2xl mb-8" data-agility-field="subtitle">
239
+ {subtitle}
240
+ </p>
241
+
242
+ {ctaButton && (
243
+ <a
244
+ href={ctaButton.href}
245
+ target={ctaButton.target}
246
+ className="inline-block bg-blue-600 hover:bg-blue-700 px-8 py-4 rounded-lg text-lg font-semibold transition"
247
+ data-agility-field="ctaButton"
248
+ >
249
+ {ctaButton.text}
250
+ </a>
251
+ )}
252
+ </div>
253
+ </section>
254
+ );
255
+ }
256
+ ```
257
+
258
+ ## Blog Post Listing
259
+
260
+ **IMPORTANT**: Always use `<AgilityPic>` for post images with responsive sources!
261
+
262
+ ```tsx
263
+ // src/components/agility-components/PostListing.tsx
264
+
265
+ import { getContentList } from "@/lib/cms/getContentList";
266
+ import { AgilityPic } from "@agility/nextjs";
267
+ import type { ImageField } from "@agility/nextjs";
268
+ import Link from "next/link";
269
+
270
+ interface Post {
271
+ contentID: number;
272
+ fields: {
273
+ title: string;
274
+ slug: string;
275
+ date: string;
276
+ excerpt: string;
277
+ image?: ImageField; // Use ImageField type
278
+ };
279
+ }
280
+
281
+ interface PostListingProps {
282
+ module: {
283
+ fields: {
284
+ title: string;
285
+ numberOfPosts: number;
286
+ };
287
+ };
288
+ locale: string;
289
+ searchParams?: {
290
+ page?: string;
291
+ };
292
+ }
293
+
294
+ export default async function PostListing({
295
+ module,
296
+ locale,
297
+ searchParams,
298
+ }: PostListingProps) {
299
+ const { title, numberOfPosts } = module.fields;
300
+ const page = parseInt(searchParams?.page || "1");
301
+ const perPage = numberOfPosts || 9;
302
+
303
+ const posts = await getContentList<Post>({
304
+ referenceName: "posts",
305
+ locale,
306
+ take: perPage,
307
+ skip: (page - 1) * perPage,
308
+ sort: "fields.date desc",
309
+ });
310
+
311
+ return (
312
+ <section className="py-16">
313
+ <div className="container mx-auto px-4">
314
+ <h2 className="text-4xl font-bold mb-12 text-center" data-agility-field="title">
315
+ {title}
316
+ </h2>
317
+
318
+ <div className="grid md:grid-cols-3 gap-8">
319
+ {posts.map((post) => (
320
+ <article
321
+ key={post.contentID}
322
+ className="border rounded-lg overflow-hidden hover:shadow-lg transition"
323
+ >
324
+ {/* ✅ CORRECT: Use AgilityPic with responsive sources */}
325
+ {post.fields.image && (
326
+ <AgilityPic
327
+ image={post.fields.image}
328
+ fallbackWidth={400}
329
+ className="w-full h-48 object-cover"
330
+ sources={[
331
+ { media: "(max-width: 639px)", width: 640 },
332
+ { media: "(max-width: 1023px)", width: 800 },
333
+ ]}
334
+ />
335
+ )}
336
+
337
+ <div className="p-6">
338
+ <time className="text-sm text-gray-600">
339
+ {new Date(post.fields.date).toLocaleDateString(locale)}
340
+ </time>
341
+
342
+ <h3 className="text-xl font-semibold mt-2 mb-3">
343
+ {post.fields.title}
344
+ </h3>
345
+
346
+ <p className="text-gray-600 mb-4">{post.fields.excerpt}</p>
347
+
348
+ <Link
349
+ href={`/blog/${post.fields.slug}`}
350
+ className="text-blue-600 hover:underline"
351
+ >
352
+ Read More →
353
+ </Link>
354
+ </div>
355
+ </article>
356
+ ))}
357
+ </div>
358
+
359
+ {/* Pagination */}
360
+ <nav className="flex justify-center gap-2 mt-12">
361
+ {page > 1 && (
362
+ <a
363
+ href={`?page=${page - 1}`}
364
+ className="px-4 py-2 border rounded hover:bg-gray-100"
365
+ >
366
+ Previous
367
+ </a>
368
+ )}
369
+
370
+ <span className="px-4 py-2">Page {page}</span>
371
+
372
+ {posts.length === perPage && (
373
+ <a
374
+ href={`?page=${page + 1}`}
375
+ className="px-4 py-2 border rounded hover:bg-gray-100"
376
+ >
377
+ Next
378
+ </a>
379
+ )}
380
+ </nav>
381
+ </div>
382
+ </section>
383
+ );
384
+ }
385
+ ```
386
+
387
+ ## Contact Form
388
+
389
+ ```tsx
390
+ // src/components/agility-components/ContactForm.server.tsx
391
+
392
+ import ContactFormClient from "./ContactForm.client";
393
+
394
+ interface ContactFormProps {
395
+ module: {
396
+ fields: {
397
+ title: string;
398
+ subtitle: string;
399
+ submitButtonText: string;
400
+ };
401
+ };
402
+ }
403
+
404
+ export default function ContactForm({ module }: ContactFormProps) {
405
+ const { title, subtitle, submitButtonText } = module.fields;
406
+
407
+ return (
408
+ <section className="py-16 bg-gray-50">
409
+ <div className="container mx-auto px-4 max-w-2xl">
410
+ <div className="text-center mb-8">
411
+ <h2 className="text-4xl font-bold mb-4">{title}</h2>
412
+ <p className="text-xl text-gray-600">{subtitle}</p>
413
+ </div>
414
+
415
+ <ContactFormClient submitButtonText={submitButtonText} />
416
+ </div>
417
+ </section>
418
+ );
419
+ }
420
+ ```
421
+
422
+ ```tsx
423
+ // src/components/agility-components/ContactForm.client.tsx
424
+ "use client";
425
+
426
+ import { useState } from "react";
427
+
428
+ export default function ContactFormClient({ submitButtonText }: any) {
429
+ const [formData, setFormData] = useState({
430
+ name: "",
431
+ email: "",
432
+ message: "",
433
+ });
434
+ const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
435
+
436
+ const handleSubmit = async (e: React.FormEvent) => {
437
+ e.preventDefault();
438
+ setStatus("loading");
439
+
440
+ try {
441
+ const response = await fetch("/api/contact", {
442
+ method: "POST",
443
+ headers: { "Content-Type": "application/json" },
444
+ body: JSON.stringify(formData),
445
+ });
446
+
447
+ if (response.ok) {
448
+ setStatus("success");
449
+ setFormData({ name: "", email: "", message: "" });
450
+ } else {
451
+ setStatus("error");
452
+ }
453
+ } catch (error) {
454
+ setStatus("error");
455
+ }
456
+ };
457
+
458
+ return (
459
+ <form onSubmit={handleSubmit} className="space-y-6">
460
+ <div>
461
+ <label htmlFor="name" className="block font-semibold mb-2">
462
+ Name
463
+ </label>
464
+ <input
465
+ id="name"
466
+ type="text"
467
+ value={formData.name}
468
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
469
+ required
470
+ className="w-full p-3 border rounded"
471
+ />
472
+ </div>
473
+
474
+ <div>
475
+ <label htmlFor="email" className="block font-semibold mb-2">
476
+ Email
477
+ </label>
478
+ <input
479
+ id="email"
480
+ type="email"
481
+ value={formData.email}
482
+ onChange={(e) => setFormData({ ...formData, email: e.target.value })}
483
+ required
484
+ className="w-full p-3 border rounded"
485
+ />
486
+ </div>
487
+
488
+ <div>
489
+ <label htmlFor="message" className="block font-semibold mb-2">
490
+ Message
491
+ </label>
492
+ <textarea
493
+ id="message"
494
+ value={formData.message}
495
+ onChange={(e) => setFormData({ ...formData, message: e.target.value })}
496
+ required
497
+ rows={5}
498
+ className="w-full p-3 border rounded"
499
+ />
500
+ </div>
501
+
502
+ <button
503
+ type="submit"
504
+ disabled={status === "loading"}
505
+ className="w-full bg-blue-600 text-white py-3 rounded font-semibold hover:bg-blue-700 disabled:bg-gray-400"
506
+ >
507
+ {status === "loading" ? "Sending..." : submitButtonText}
508
+ </button>
509
+
510
+ {status === "success" && (
511
+ <p className="text-green-600 text-center">Message sent successfully!</p>
512
+ )}
513
+ {status === "error" && (
514
+ <p className="text-red-600 text-center">Error sending message. Please try again.</p>
515
+ )}
516
+ </form>
517
+ );
518
+ }
519
+ ```
520
+
521
+ ## FAQ Accordion
522
+
523
+ ```tsx
524
+ // src/components/agility-components/FAQ.server.tsx
525
+
526
+ import { getContentList } from "@/lib/cms/getContentList";
527
+ import FAQClient from "./FAQ.client";
528
+
529
+ export default async function FAQ({ module, locale }: any) {
530
+ const { title } = module.fields;
531
+
532
+ const faqs = await getContentList({
533
+ referenceName: "faqs",
534
+ locale,
535
+ sort: "fields.order asc",
536
+ });
537
+
538
+ return (
539
+ <section className="py-16">
540
+ <div className="container mx-auto px-4 max-w-4xl">
541
+ <h2 className="text-4xl font-bold mb-12 text-center">{title}</h2>
542
+ <FAQClient faqs={faqs} />
543
+ </div>
544
+ </section>
545
+ );
546
+ }
547
+ ```
548
+
549
+ ```tsx
550
+ // src/components/agility-components/FAQ.client.tsx
551
+ "use client";
552
+
553
+ import { useState } from "react";
554
+
555
+ export default function FAQClient({ faqs }: any) {
556
+ const [openIndex, setOpenIndex] = useState<number | null>(null);
557
+
558
+ return (
559
+ <div className="space-y-4">
560
+ {faqs.map((faq: any, index: number) => (
561
+ <div key={faq.contentID} className="border rounded-lg">
562
+ <button
563
+ onClick={() => setOpenIndex(openIndex === index ? null : index)}
564
+ className="w-full p-6 text-left flex justify-between items-center hover:bg-gray-50"
565
+ >
566
+ <span className="font-semibold text-lg">{faq.fields.question}</span>
567
+ <span className="text-2xl">{openIndex === index ? "−" : "+"}</span>
568
+ </button>
569
+
570
+ {openIndex === index && (
571
+ <div
572
+ className="p-6 pt-0 text-gray-600"
573
+ dangerouslySetInnerHTML={{ __html: faq.fields.answer }}
574
+ />
575
+ )}
576
+ </div>
577
+ ))}
578
+ </div>
579
+ );
580
+ }
581
+ ```
582
+
583
+ ## Image Gallery
584
+
585
+ ```tsx
586
+ // src/components/agility-components/ImageGallery.server.tsx
587
+
588
+ import { getContentList } from "@/lib/cms/getContentList";
589
+ import ImageGalleryClient from "./ImageGallery.client";
590
+
591
+ export default async function ImageGallery({ module, locale }: any) {
592
+ const { title, gallery } = module.fields;
593
+
594
+ const images = await getContentList({
595
+ referenceName: gallery.referenceName,
596
+ locale,
597
+ });
598
+
599
+ return (
600
+ <section className="py-16">
601
+ <div className="container mx-auto px-4">
602
+ <h2 className="text-4xl font-bold mb-12 text-center">{title}</h2>
603
+ <ImageGalleryClient images={images} />
604
+ </div>
605
+ </section>
606
+ );
607
+ }
608
+ ```
609
+
610
+ ```tsx
611
+ // src/components/agility-components/ImageGallery.client.tsx
612
+ "use client";
613
+
614
+ import { useState } from "react";
615
+
616
+ export default function ImageGalleryClient({ images }: any) {
617
+ const [selectedImage, setSelectedImage] = useState<any>(null);
618
+
619
+ return (
620
+ <>
621
+ <div className="grid md:grid-cols-4 gap-4">
622
+ {images.map((image: any) => (
623
+ <button
624
+ key={image.contentID}
625
+ onClick={() => setSelectedImage(image)}
626
+ className="aspect-square overflow-hidden rounded-lg hover:opacity-75 transition"
627
+ >
628
+ <img
629
+ src={image.fields.image.url}
630
+ alt={image.fields.image.label}
631
+ className="w-full h-full object-cover"
632
+ />
633
+ </button>
634
+ ))}
635
+ </div>
636
+
637
+ {/* Lightbox */}
638
+ {selectedImage && (
639
+ <div
640
+ className="fixed inset-0 z-50 bg-black bg-opacity-90 flex items-center justify-center p-4"
641
+ onClick={() => setSelectedImage(null)}
642
+ >
643
+ <button
644
+ className="absolute top-4 right-4 text-white text-4xl"
645
+ onClick={() => setSelectedImage(null)}
646
+ >
647
+ ×
648
+ </button>
649
+ <img
650
+ src={selectedImage.fields.image.url}
651
+ alt={selectedImage.fields.image.label}
652
+ className="max-w-full max-h-full"
653
+ />
654
+ </div>
655
+ )}
656
+ </>
657
+ );
658
+ }
659
+ ```
660
+
661
+ ## Testimonial Slider
662
+
663
+ ```tsx
664
+ // src/components/agility-components/Testimonials.server.tsx
665
+
666
+ import { getContentList } from "@/lib/cms/getContentList";
667
+ import TestimonialsClient from "./Testimonials.client";
668
+
669
+ export default async function Testimonials({ module, locale }: any) {
670
+ const { title } = module.fields;
671
+
672
+ const testimonials = await getContentList({
673
+ referenceName: "testimonials",
674
+ locale,
675
+ });
676
+
677
+ return (
678
+ <section className="py-16 bg-gray-50">
679
+ <div className="container mx-auto px-4">
680
+ <h2 className="text-4xl font-bold mb-12 text-center">{title}</h2>
681
+ <TestimonialsClient testimonials={testimonials} />
682
+ </div>
683
+ </section>
684
+ );
685
+ }
686
+ ```
687
+
688
+ ```tsx
689
+ // src/components/agility-components/Testimonials.client.tsx
690
+ "use client";
691
+
692
+ import { useState, useEffect } from "react";
693
+
694
+ export default function TestimonialsClient({ testimonials }: any) {
695
+ const [current, setCurrent] = useState(0);
696
+
697
+ useEffect(() => {
698
+ const timer = setInterval(() => {
699
+ setCurrent((i) => (i + 1) % testimonials.length);
700
+ }, 5000);
701
+ return () => clearInterval(timer);
702
+ }, [testimonials.length]);
703
+
704
+ const testimonial = testimonials[current];
705
+
706
+ return (
707
+ <div className="max-w-4xl mx-auto">
708
+ <div className="text-center">
709
+ <p className="text-2xl italic mb-6">"{testimonial.fields.quote}"</p>
710
+
711
+ <div className="flex items-center justify-center gap-4">
712
+ {testimonial.fields.photo && (
713
+ <img
714
+ src={testimonial.fields.photo.url}
715
+ alt={testimonial.fields.name}
716
+ className="w-16 h-16 rounded-full"
717
+ />
718
+ )}
719
+ <div className="text-left">
720
+ <p className="font-semibold">{testimonial.fields.name}</p>
721
+ <p className="text-gray-600">{testimonial.fields.company}</p>
722
+ </div>
723
+ </div>
724
+ </div>
725
+
726
+ <div className="flex justify-center gap-2 mt-8">
727
+ {testimonials.map((_: any, index: number) => (
728
+ <button
729
+ key={index}
730
+ onClick={() => setCurrent(index)}
731
+ className={`w-3 h-3 rounded-full transition ${
732
+ index === current ? "bg-blue-600" : "bg-gray-300"
733
+ }`}
734
+ />
735
+ ))}
736
+ </div>
737
+ </div>
738
+ );
739
+ }
740
+ ```
741
+
742
+ ## Best Practices
743
+
744
+ 1. **Server Components**: Use for data fetching (default)
745
+ 2. **Client Components**: Use for interactivity only
746
+ 3. **Hybrid Pattern**: Server fetches, client renders interactive parts
747
+ 4. **Type Safety**: Define interfaces for props
748
+ 5. **Accessibility**: Use semantic HTML and ARIA labels
749
+ 6. **Responsive**: Use Tailwind responsive classes
750
+ 7. **Loading States**: Show loading indicators for async actions
751
+
752
+ ## Next Steps
753
+
754
+ - Read [03-creating-components.md](./03-creating-components.md) for module creation
755
+ - Read [09-example-components.md](./09-example-components.md) for more examples
756
+ - Read [04-data-fetching.md](./04-data-fetching.md) for data patterns