@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.
- package/.claude/settings.json +7 -0
- package/.claude/settings.local.json +24 -0
- package/FEATURE_ROADMAP.md +343 -0
- package/README.md +205 -0
- package/TESTING.md +131 -0
- package/bin/create-agility-app.js +48 -0
- package/dist/agility/api-keys/generateApiKeys.d.ts +9 -0
- package/dist/agility/api-keys/generateApiKeys.d.ts.map +1 -0
- package/dist/agility/api-keys/generateApiKeys.js +99 -0
- package/dist/agility/api-keys/generateApiKeys.js.map +1 -0
- package/dist/agility/api-keys/getApiKeys.d.ts +9 -0
- package/dist/agility/api-keys/getApiKeys.d.ts.map +1 -0
- package/dist/agility/api-keys/getApiKeys.js +14 -0
- package/dist/agility/api-keys/getApiKeys.js.map +1 -0
- package/dist/agility/index.d.ts +3 -0
- package/dist/agility/index.d.ts.map +1 -0
- package/dist/agility/index.js +8 -0
- package/dist/agility/index.js.map +1 -0
- package/dist/agility/instance/createNewInstance.d.ts +8 -0
- package/dist/agility/instance/createNewInstance.d.ts.map +1 -0
- package/dist/agility/instance/createNewInstance.js +65 -0
- package/dist/agility/instance/createNewInstance.js.map +1 -0
- package/dist/agility/instance/getAvailableInstances.d.ts +8 -0
- package/dist/agility/instance/getAvailableInstances.d.ts.map +1 -0
- package/dist/agility/instance/getAvailableInstances.js +43 -0
- package/dist/agility/instance/getAvailableInstances.js.map +1 -0
- package/dist/agility/instance/manageInstance.d.ts +9 -0
- package/dist/agility/instance/manageInstance.d.ts.map +1 -0
- package/dist/agility/instance/manageInstance.js +82 -0
- package/dist/agility/instance/manageInstance.js.map +1 -0
- package/dist/agility/utils/getMgmtAPIUrl.d.ts +20 -0
- package/dist/agility/utils/getMgmtAPIUrl.d.ts.map +1 -0
- package/dist/agility/utils/getMgmtAPIUrl.js +61 -0
- package/dist/agility/utils/getMgmtAPIUrl.js.map +1 -0
- package/dist/auth/api-key/authenticateWithApiKey.d.ts +6 -0
- package/dist/auth/api-key/authenticateWithApiKey.d.ts.map +1 -0
- package/dist/auth/api-key/authenticateWithApiKey.js +28 -0
- package/dist/auth/api-key/authenticateWithApiKey.js.map +1 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +8 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/oauth/authenticate.d.ts +6 -0
- package/dist/auth/oauth/authenticate.d.ts.map +1 -0
- package/dist/auth/oauth/authenticate.js +162 -0
- package/dist/auth/oauth/authenticate.js.map +1 -0
- package/dist/auth/oauth/constants.d.ts +5 -0
- package/dist/auth/oauth/constants.d.ts.map +1 -0
- package/dist/auth/oauth/constants.js +9 -0
- package/dist/auth/oauth/constants.js.map +1 -0
- package/dist/auth/oauth/exchangeCodeForToken.d.ts +7 -0
- package/dist/auth/oauth/exchangeCodeForToken.d.ts.map +1 -0
- package/dist/auth/oauth/exchangeCodeForToken.js +39 -0
- package/dist/auth/oauth/exchangeCodeForToken.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +290 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/promptForMissingOptions.d.ts +8 -0
- package/dist/cli/promptForMissingOptions.d.ts.map +1 -0
- package/dist/cli/promptForMissingOptions.js +92 -0
- package/dist/cli/promptForMissingOptions.js.map +1 -0
- package/dist/config/env/createEnvFile.d.ts +6 -0
- package/dist/config/env/createEnvFile.d.ts.map +1 -0
- package/dist/config/env/createEnvFile.js +31 -0
- package/dist/config/env/createEnvFile.js.map +1 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +6 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/mcp/createMcpConfig.d.ts +5 -0
- package/dist/config/mcp/createMcpConfig.d.ts.map +1 -0
- package/dist/config/mcp/createMcpConfig.js +32 -0
- package/dist/config/mcp/createMcpConfig.js.map +1 -0
- package/dist/config/packages/installAgilityPackages.d.ts +6 -0
- package/dist/config/packages/installAgilityPackages.d.ts.map +1 -0
- package/dist/config/packages/installAgilityPackages.js +61 -0
- package/dist/config/packages/installAgilityPackages.js.map +1 -0
- package/dist/config/setupProject.d.ts +8 -0
- package/dist/config/setupProject.d.ts.map +1 -0
- package/dist/config/setupProject.js +32 -0
- package/dist/config/setupProject.js.map +1 -0
- package/dist/create-next-app/createNextApp.d.ts +9 -0
- package/dist/create-next-app/createNextApp.d.ts.map +1 -0
- package/dist/create-next-app/createNextApp.js +83 -0
- package/dist/create-next-app/createNextApp.js.map +1 -0
- package/dist/create-next-app/index.d.ts +3 -0
- package/dist/create-next-app/index.d.ts.map +1 -0
- package/dist/create-next-app/index.js +8 -0
- package/dist/create-next-app/index.js.map +1 -0
- package/dist/scaffold/components/createPageComponents.d.ts +6 -0
- package/dist/scaffold/components/createPageComponents.d.ts.map +1 -0
- package/dist/scaffold/components/createPageComponents.js +62 -0
- package/dist/scaffold/components/createPageComponents.js.map +1 -0
- package/dist/scaffold/containers/createContainers.d.ts +6 -0
- package/dist/scaffold/containers/createContainers.d.ts.map +1 -0
- package/dist/scaffold/containers/createContainers.js +48 -0
- package/dist/scaffold/containers/createContainers.js.map +1 -0
- package/dist/scaffold/index.d.ts +2 -0
- package/dist/scaffold/index.d.ts.map +1 -0
- package/dist/scaffold/index.js +6 -0
- package/dist/scaffold/index.js.map +1 -0
- package/dist/scaffold/instance/createBlankInstance.d.ts +8 -0
- package/dist/scaffold/instance/createBlankInstance.d.ts.map +1 -0
- package/dist/scaffold/instance/createBlankInstance.js +51 -0
- package/dist/scaffold/instance/createBlankInstance.js.map +1 -0
- package/dist/scaffold/models/createContentModels.d.ts +6 -0
- package/dist/scaffold/models/createContentModels.d.ts.map +1 -0
- package/dist/scaffold/models/createContentModels.js +70 -0
- package/dist/scaffold/models/createContentModels.js.map +1 -0
- package/dist/templates/copyDirectory.d.ts +5 -0
- package/dist/templates/copyDirectory.d.ts.map +1 -0
- package/dist/templates/copyDirectory.js +28 -0
- package/dist/templates/copyDirectory.js.map +1 -0
- package/dist/templates/copyTemplates.d.ts +8 -0
- package/dist/templates/copyTemplates.d.ts.map +1 -0
- package/dist/templates/copyTemplates.js +58 -0
- package/dist/templates/copyTemplates.js.map +1 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +6 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/types/index.d.ts +50 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/git.d.ts +9 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +71 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/validation.d.ts +45 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +180 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +45 -0
- package/src/agility/api-keys/generateApiKeys.ts +100 -0
- package/src/agility/api-keys/getApiKeys.ts +13 -0
- package/src/agility/index.ts +3 -0
- package/src/agility/instance/createNewInstance.ts +67 -0
- package/src/agility/instance/getAvailableInstances.ts +49 -0
- package/src/agility/instance/manageInstance.ts +90 -0
- package/src/agility/utils/getMgmtAPIUrl.ts +68 -0
- package/src/auth/api-key/authenticateWithApiKey.ts +24 -0
- package/src/auth/index.ts +3 -0
- package/src/auth/oauth/authenticate.ts +165 -0
- package/src/auth/oauth/constants.ts +6 -0
- package/src/auth/oauth/exchangeCodeForToken.ts +43 -0
- package/src/cli/index.ts +281 -0
- package/src/cli/promptForMissingOptions.ts +104 -0
- package/src/config/env/createEnvFile.ts +30 -0
- package/src/config/index.ts +2 -0
- package/src/config/mcp/createMcpConfig.ts +30 -0
- package/src/config/packages/installAgilityPackages.ts +63 -0
- package/src/config/setupProject.ts +31 -0
- package/src/create-next-app/createNextApp.ts +75 -0
- package/src/create-next-app/index.ts +3 -0
- package/src/scaffold/components/createPageComponents.ts +74 -0
- package/src/scaffold/containers/createContainers.ts +55 -0
- package/src/scaffold/index.ts +2 -0
- package/src/scaffold/instance/createBlankInstance.ts +55 -0
- package/src/scaffold/models/createContentModels.ts +83 -0
- package/src/templates/copyDirectory.ts +24 -0
- package/src/templates/copyTemplates.ts +57 -0
- package/src/templates/index.ts +2 -0
- package/src/types/index.ts +55 -0
- package/src/utils/git.ts +74 -0
- package/src/utils/validation.ts +184 -0
- package/templates/.claude/QUICK-START.md +230 -0
- package/templates/.claude/README.md +32 -0
- package/templates/.claude/settings.json +8 -0
- package/templates/BLANK-INSTANCE-SETUP.md +375 -0
- package/templates/DEVELOPMENT.md +160 -0
- package/templates/EXAMPLE-PROMPTS.md +643 -0
- package/templates/PROMPTS.md +410 -0
- package/templates/README.md +281 -0
- package/templates/agents.md +429 -0
- package/templates/app/[locale]/[...slug]/error.tsx +17 -0
- package/templates/app/[locale]/[...slug]/not-found.tsx +9 -0
- package/templates/app/[locale]/[...slug]/page.tsx +102 -0
- package/templates/app/[locale]/layout.tsx +22 -0
- package/templates/app/[locale]/page.tsx +12 -0
- package/templates/app/api/dynamic-redirect/route.ts +24 -0
- package/templates/app/api/preview/exit/route.ts +34 -0
- package/templates/app/api/preview/route.ts +63 -0
- package/templates/app/api/revalidate/route.ts +118 -0
- package/templates/components/agility-components/RichTextArea.tsx +66 -0
- package/templates/components/agility-components/index.ts +30 -0
- package/templates/components/agility-pages/MainTemplate.tsx +36 -0
- package/templates/components/agility-pages/index.ts +11 -0
- package/templates/docs/01-agility-cms-overview.md +139 -0
- package/templates/docs/02-page-routing.md +251 -0
- package/templates/docs/03-creating-components.md +462 -0
- package/templates/docs/04-data-fetching.md +484 -0
- package/templates/docs/05-containers-and-lists.md +596 -0
- package/templates/docs/06-localization.md +561 -0
- package/templates/docs/07-caching-strategies.md +410 -0
- package/templates/docs/08-common-components.md +756 -0
- package/templates/docs/09-whats-included.md +279 -0
- package/templates/docs/10-mcp-server-setup.md +153 -0
- package/templates/docs/11-linked-nested-content.md +611 -0
- package/templates/docs/README.md +164 -0
- package/templates/lib/cms/getAgilityContext.ts +28 -0
- package/templates/lib/cms/getAgilityPage.ts +51 -0
- package/templates/lib/cms/getAgilitySDK.ts +22 -0
- package/templates/lib/cms/getContentItem.ts +20 -0
- package/templates/lib/cms/getContentList.ts +19 -0
- package/templates/lib/cms/getRedirections.ts +85 -0
- package/templates/lib/cms/getSitemapFlat.ts +19 -0
- package/templates/lib/cms/getSitemapNested.ts +19 -0
- package/templates/lib/env.ts +99 -0
- package/templates/lib/i18n/config.ts +28 -0
- package/templates/proxy.ts +101 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Page Routing
|
|
2
|
+
|
|
3
|
+
This document explains how page routing works with Agility CMS in this Next.js project.
|
|
4
|
+
|
|
5
|
+
## Dynamic Page Route
|
|
6
|
+
|
|
7
|
+
All Agility CMS pages are handled by a single dynamic route:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
app/[locale]/[...slug]/page.tsx
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This route:
|
|
14
|
+
- **`[locale]`**: Handles locale/language (e.g., `en-us`, `fr`, `es`)
|
|
15
|
+
- **`[...slug]`**: Captures the entire path (e.g., `about`, `blog/post-1`, `products/category/item`)
|
|
16
|
+
|
|
17
|
+
## How It Works
|
|
18
|
+
|
|
19
|
+
### 1. Generate Static Params
|
|
20
|
+
|
|
21
|
+
At build time, Next.js generates all pages from the Agility sitemap:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
export async function generateStaticParams() {
|
|
25
|
+
const allPaths: { locale: string; slug: string[] }[] = [];
|
|
26
|
+
|
|
27
|
+
for (const locale of locales) {
|
|
28
|
+
const sitemap = await agilityClient.getSitemapFlat({
|
|
29
|
+
channelName: "website",
|
|
30
|
+
languageCode: locale,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const localePaths = Object.values(sitemap)
|
|
34
|
+
.filter((node) => !node.redirect && !node.isFolder)
|
|
35
|
+
.map((node) => ({
|
|
36
|
+
locale,
|
|
37
|
+
slug: node.path.split("/").slice(1),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
allPaths.push(...localePaths);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return allPaths;
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Result**: All pages are pre-rendered at build time for optimal performance.
|
|
48
|
+
|
|
49
|
+
### 2. Generate Metadata
|
|
50
|
+
|
|
51
|
+
Each page generates SEO metadata:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
export async function generateMetadata(props: PageProps): Promise<Metadata> {
|
|
55
|
+
const agilityData = await getAgilityPage({ params });
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
title: agilityData.page?.seo?.metaTitle || 'Page',
|
|
59
|
+
description: agilityData.page?.seo?.metaDescription || '',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. Render Page
|
|
65
|
+
|
|
66
|
+
The page component fetches data and renders:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
export default async function Page({ params }: PageProps) {
|
|
70
|
+
const agilityData = await getAgilityPage({ params });
|
|
71
|
+
if (!agilityData.page) notFound();
|
|
72
|
+
|
|
73
|
+
const AgilityPageTemplate = getPageTemplate(
|
|
74
|
+
agilityData.pageTemplateName || "MainTemplate"
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div data-agility-page={agilityData.page?.pageID}>
|
|
79
|
+
<AgilityPageTemplate {...agilityData} />
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Proxy Flow
|
|
86
|
+
|
|
87
|
+
The `proxy.ts` file handles special routing cases:
|
|
88
|
+
|
|
89
|
+
### 1. Preview Mode
|
|
90
|
+
```
|
|
91
|
+
URL: /about?agilitypreviewkey=xxx
|
|
92
|
+
→ Redirects to: /api/preview?slug=/about&agilitypreviewkey=xxx
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 2. Exit Preview
|
|
96
|
+
```
|
|
97
|
+
URL: /about?AgilityPreview=0
|
|
98
|
+
→ Redirects to: /api/preview/exit?slug=/about
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 3. Content ID Routing
|
|
102
|
+
```
|
|
103
|
+
URL: /?ContentID=123
|
|
104
|
+
→ Rewrite to: /api/dynamic-redirect?ContentID=123
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 4. Locale Query Param
|
|
108
|
+
```
|
|
109
|
+
URL: /about?lang=fr
|
|
110
|
+
→ Redirects to: /fr/about
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 5. Search Params Encoding
|
|
114
|
+
```
|
|
115
|
+
URL: /products?category=shirts&size=large
|
|
116
|
+
→ Rewrite to: /products/~~~category=shirts&size=large~~~
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
This encoding allows Next.js to cache pages with different query parameters.
|
|
120
|
+
|
|
121
|
+
### 6. Default Locale Handling
|
|
122
|
+
```
|
|
123
|
+
URL: /about (no locale prefix)
|
|
124
|
+
→ Rewrite to: /en-us/about (internal)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The default locale doesn't appear in URLs for cleaner paths.
|
|
128
|
+
|
|
129
|
+
## Locale Configuration
|
|
130
|
+
|
|
131
|
+
Locales are configured in `src/lib/i18n/config.ts`:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
export const defaultLocale = "en-us"
|
|
135
|
+
export const locales = ["en-us", "fr", "es"] as const
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### URL Structure
|
|
139
|
+
|
|
140
|
+
- **Default locale**: `/about` (no prefix)
|
|
141
|
+
- **Other locales**: `/fr/about`, `/es/about`
|
|
142
|
+
|
|
143
|
+
### Adding a New Locale
|
|
144
|
+
|
|
145
|
+
1. Update environment variable:
|
|
146
|
+
```env
|
|
147
|
+
AGILITY_LOCALES=en-us,fr,es,de
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
2. Update config file:
|
|
151
|
+
```typescript
|
|
152
|
+
export const locales = ["en-us", "fr", "es", "de"] as const
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
3. Add content in Agility CMS for the new locale
|
|
156
|
+
|
|
157
|
+
4. Rebuild the site
|
|
158
|
+
|
|
159
|
+
## Static Generation with ISR
|
|
160
|
+
|
|
161
|
+
The project uses Incremental Static Regeneration (ISR):
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
export const revalidate = 60 // Revalidate every 60 seconds
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**How it works**:
|
|
168
|
+
1. First request: Page is generated statically at build time
|
|
169
|
+
2. Subsequent requests: Serve cached page
|
|
170
|
+
3. After 60 seconds: Next request triggers regeneration in background
|
|
171
|
+
4. New version: Cached for next 60 seconds
|
|
172
|
+
|
|
173
|
+
## Dynamic Routing Scenarios
|
|
174
|
+
|
|
175
|
+
### Scenario 1: Simple Page
|
|
176
|
+
```
|
|
177
|
+
Path: /about
|
|
178
|
+
Locale: en-us
|
|
179
|
+
Template: MainTemplate
|
|
180
|
+
Zones: [main-content-zone]
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Scenario 2: Blog Post
|
|
184
|
+
```
|
|
185
|
+
Path: /blog/my-first-post
|
|
186
|
+
Locale: en-us
|
|
187
|
+
Template: BlogPostTemplate
|
|
188
|
+
Zones: [main-content-zone]
|
|
189
|
+
Dynamic Content: BlogPost item
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Scenario 3: Multi-locale Page
|
|
193
|
+
```
|
|
194
|
+
Path: /about (en-us)
|
|
195
|
+
Path: /fr/about (fr)
|
|
196
|
+
Path: /es/acerca-de (es)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Search Parameters
|
|
200
|
+
|
|
201
|
+
Search parameters are supported and passed to page templates:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// URL: /products?category=shirts&size=large
|
|
205
|
+
const globalSearchParams = agilityData.globalData?.["searchParams"] || {};
|
|
206
|
+
|
|
207
|
+
// In your module component:
|
|
208
|
+
function ProductListing({ searchParams }) {
|
|
209
|
+
const category = searchParams.category; // "shirts"
|
|
210
|
+
const size = searchParams.size; // "large"
|
|
211
|
+
// ... filter products
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Important Notes
|
|
216
|
+
|
|
217
|
+
1. **Case Sensitivity**: Page paths in Agility CMS are case-insensitive
|
|
218
|
+
2. **Trailing Slashes**: Automatically handled by proxy
|
|
219
|
+
3. **404 Pages**: Non-existent pages return 404 with `not-found.tsx`
|
|
220
|
+
4. **Error Handling**: Errors are caught by `error.tsx`
|
|
221
|
+
5. **Folders**: Folder pages in Agility CMS are filtered out (not rendered)
|
|
222
|
+
6. **Redirects**: Redirect pages are handled by proxy
|
|
223
|
+
|
|
224
|
+
## Common Routing Patterns
|
|
225
|
+
|
|
226
|
+
### Pattern 1: Homepage
|
|
227
|
+
```
|
|
228
|
+
Path: /
|
|
229
|
+
Slug: []
|
|
230
|
+
Template: HomeTemplate
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Pattern 2: Single Level
|
|
234
|
+
```
|
|
235
|
+
Path: /about
|
|
236
|
+
Slug: ["about"]
|
|
237
|
+
Template: MainTemplate
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Pattern 3: Multi Level
|
|
241
|
+
```
|
|
242
|
+
Path: /blog/category/post-title
|
|
243
|
+
Slug: ["blog", "category", "post-title"]
|
|
244
|
+
Template: BlogPostTemplate
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Next Steps
|
|
248
|
+
|
|
249
|
+
- Read [03-creating-components.md](./03-creating-components.md) to add components to pages
|
|
250
|
+
- Read [04-data-fetching.md](./04-data-fetching.md) to fetch dynamic content
|
|
251
|
+
- Read [06-localization.md](./06-localization.md) for multi-language support
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
# Creating Agility Components
|
|
2
|
+
|
|
3
|
+
This document explains how to create new components (also called modules in the CMS) for Agility CMS.
|
|
4
|
+
|
|
5
|
+
## What is an Agility Component?
|
|
6
|
+
|
|
7
|
+
An **Agility Component** (also referred to as a "module" in the CMS interface) is a reusable component that editors can add to pages in Agility CMS. Common examples:
|
|
8
|
+
- Hero sections
|
|
9
|
+
- Rich text content
|
|
10
|
+
- Image galleries
|
|
11
|
+
- Blog post listings
|
|
12
|
+
- Contact forms
|
|
13
|
+
- Testimonials
|
|
14
|
+
- Call-to-action buttons
|
|
15
|
+
|
|
16
|
+
**Note**: While these are called "modules" in the Agility CMS interface, we refer to them as "components" in code to align with React terminology.
|
|
17
|
+
|
|
18
|
+
## Component Architecture
|
|
19
|
+
|
|
20
|
+
### Server vs Client Components
|
|
21
|
+
|
|
22
|
+
**Server Components** (default):
|
|
23
|
+
- Fetch data from Agility CMS or other APIs
|
|
24
|
+
- No client-side JavaScript
|
|
25
|
+
- Better performance
|
|
26
|
+
- Use `.server.tsx` extension (optional but recommended)
|
|
27
|
+
|
|
28
|
+
**Client Components**:
|
|
29
|
+
- Interactive features (forms, carousels, tabs)
|
|
30
|
+
- Use React hooks (useState, useEffect, etc.)
|
|
31
|
+
- Must have `"use client"` directive
|
|
32
|
+
- Use `.client.tsx` extension (optional but recommended)
|
|
33
|
+
|
|
34
|
+
**Hybrid Pattern** (recommended for complex components):
|
|
35
|
+
- Server component fetches data
|
|
36
|
+
- Passes data to client component for interactivity
|
|
37
|
+
|
|
38
|
+
## Creating a New Component
|
|
39
|
+
|
|
40
|
+
### Step 1: Define the Component Model in Agility CMS
|
|
41
|
+
|
|
42
|
+
In Agility CMS:
|
|
43
|
+
1. Go to **Settings > Content Definitions**
|
|
44
|
+
2. Click **New Module** (Components are called "modules" in the CMS interface)
|
|
45
|
+
3. Add a **Reference Name** (e.g., "Hero")
|
|
46
|
+
4. Add **Fields** (e.g., title, subtitle, image, ctaButton)
|
|
47
|
+
|
|
48
|
+
### Step 2: Create the Component File
|
|
49
|
+
|
|
50
|
+
Create a new file in `src/components/agility-components/`:
|
|
51
|
+
|
|
52
|
+
#### Example 1: Simple Server Component
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
// src/components/agility-components/Hero.tsx
|
|
56
|
+
|
|
57
|
+
interface HeroProps {
|
|
58
|
+
module: { // Note: prop is called "module" for historical reasons
|
|
59
|
+
fields: {
|
|
60
|
+
title: string;
|
|
61
|
+
subtitle: string;
|
|
62
|
+
backgroundImage: {
|
|
63
|
+
url: string;
|
|
64
|
+
label: string;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
locale: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default function Hero({ module, locale }: HeroProps) {
|
|
72
|
+
const { title, subtitle, backgroundImage } = module.fields;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="relative h-screen">
|
|
76
|
+
<img
|
|
77
|
+
src={backgroundImage.url}
|
|
78
|
+
alt={backgroundImage.label}
|
|
79
|
+
className="absolute inset-0 w-full h-full object-cover"
|
|
80
|
+
/>
|
|
81
|
+
<div className="relative z-10 flex flex-col items-center justify-center h-full text-white">
|
|
82
|
+
<h1 className="text-6xl font-bold">{title}</h1>
|
|
83
|
+
<p className="text-2xl mt-4">{subtitle}</p>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### Example 2: Client Component with Interactivity
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// src/components/agility-components/Carousel.client.tsx
|
|
94
|
+
"use client";
|
|
95
|
+
|
|
96
|
+
import { useState } from "react";
|
|
97
|
+
|
|
98
|
+
interface CarouselProps {
|
|
99
|
+
module: {
|
|
100
|
+
fields: {
|
|
101
|
+
slides: Array<{
|
|
102
|
+
image: { url: string; label: string };
|
|
103
|
+
caption: string;
|
|
104
|
+
}>;
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default function Carousel({ module }: CarouselProps) {
|
|
110
|
+
const { slides } = module.fields;
|
|
111
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
112
|
+
|
|
113
|
+
const next = () => setCurrentIndex((i) => (i + 1) % slides.length);
|
|
114
|
+
const prev = () => setCurrentIndex((i) => (i - 1 + slides.length) % slides.length);
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div className="relative">
|
|
118
|
+
<img
|
|
119
|
+
src={slides[currentIndex].image.url}
|
|
120
|
+
alt={slides[currentIndex].image.label}
|
|
121
|
+
className="w-full h-96 object-cover"
|
|
122
|
+
/>
|
|
123
|
+
<p className="text-center mt-2">{slides[currentIndex].caption}</p>
|
|
124
|
+
<button onClick={prev} className="absolute left-4 top-1/2">←</button>
|
|
125
|
+
<button onClick={next} className="absolute right-4 top-1/2">→</button>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### Example 3: Hybrid Pattern (Server + Client)
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
// src/components/agility-components/PostListing.server.tsx
|
|
135
|
+
|
|
136
|
+
import { getContentList } from "@/lib/cms/getContentList";
|
|
137
|
+
import PostListingClient from "./PostListing.client";
|
|
138
|
+
|
|
139
|
+
interface PostListingProps {
|
|
140
|
+
module: {
|
|
141
|
+
fields: {
|
|
142
|
+
title: string;
|
|
143
|
+
postsToShow: number;
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
locale: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default async function PostListing({ module, locale }: PostListingProps) {
|
|
150
|
+
const { title, postsToShow } = module.fields;
|
|
151
|
+
|
|
152
|
+
// Fetch posts from Agility CMS
|
|
153
|
+
const posts = await getContentList({
|
|
154
|
+
referenceName: "posts",
|
|
155
|
+
locale,
|
|
156
|
+
take: postsToShow,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div>
|
|
161
|
+
<h2>{title}</h2>
|
|
162
|
+
<PostListingClient posts={posts} />
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
// src/components/agility-components/PostListing.client.tsx
|
|
170
|
+
"use client";
|
|
171
|
+
|
|
172
|
+
import { useState } from "react";
|
|
173
|
+
|
|
174
|
+
interface Post {
|
|
175
|
+
contentID: number;
|
|
176
|
+
fields: {
|
|
177
|
+
title: string;
|
|
178
|
+
excerpt: string;
|
|
179
|
+
date: string;
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface PostListingClientProps {
|
|
184
|
+
posts: Post[];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export default function PostListingClient({ posts }: PostListingClientProps) {
|
|
188
|
+
const [filter, setFilter] = useState("");
|
|
189
|
+
|
|
190
|
+
const filteredPosts = posts.filter((post) =>
|
|
191
|
+
post.fields.title.toLowerCase().includes(filter.toLowerCase())
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<div>
|
|
196
|
+
<input
|
|
197
|
+
type="text"
|
|
198
|
+
placeholder="Search posts..."
|
|
199
|
+
value={filter}
|
|
200
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
201
|
+
className="border p-2 mb-4"
|
|
202
|
+
/>
|
|
203
|
+
<div className="grid gap-4">
|
|
204
|
+
{filteredPosts.map((post) => (
|
|
205
|
+
<article key={post.contentID}>
|
|
206
|
+
<h3>{post.fields.title}</h3>
|
|
207
|
+
<p>{post.fields.excerpt}</p>
|
|
208
|
+
<time>{post.fields.date}</time>
|
|
209
|
+
</article>
|
|
210
|
+
))}
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Step 3: Register the Component
|
|
218
|
+
|
|
219
|
+
Add the component to `src/components/agility-components/index.ts`:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import RichTextArea from "./RichTextArea";
|
|
223
|
+
import Hero from "./Hero";
|
|
224
|
+
import Carousel from "./Carousel.client";
|
|
225
|
+
import PostListing from "./PostListing.server";
|
|
226
|
+
|
|
227
|
+
export const getModule = (moduleName: string) => {
|
|
228
|
+
switch (moduleName) {
|
|
229
|
+
case "RichTextArea":
|
|
230
|
+
return RichTextArea;
|
|
231
|
+
case "Hero":
|
|
232
|
+
return Hero;
|
|
233
|
+
case "Carousel":
|
|
234
|
+
return Carousel;
|
|
235
|
+
case "PostListing":
|
|
236
|
+
return PostListing;
|
|
237
|
+
default:
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Important**: The `moduleName` must match the **Reference Name** you defined in Agility CMS.
|
|
244
|
+
|
|
245
|
+
### Step 4: Add Component to Page in Agility CMS
|
|
246
|
+
|
|
247
|
+
In Agility CMS:
|
|
248
|
+
1. Edit a page
|
|
249
|
+
2. Click **Add Module** in a content zone (this adds a component instance)
|
|
250
|
+
3. Select your new component
|
|
251
|
+
4. Fill in the fields
|
|
252
|
+
5. Save and publish
|
|
253
|
+
|
|
254
|
+
## Component Props
|
|
255
|
+
|
|
256
|
+
Every Agility component receives these props:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
interface ComponentProps {
|
|
260
|
+
module: { // Note: prop is called "module" for historical reasons
|
|
261
|
+
contentID: number; // Unique ID
|
|
262
|
+
properties: {
|
|
263
|
+
referenceName: string; // Component reference name
|
|
264
|
+
};
|
|
265
|
+
fields: { // Your custom fields
|
|
266
|
+
[key: string]: any;
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
locale: string; // Current locale (e.g., "en-us")
|
|
270
|
+
sitemap: Array<any>; // Full sitemap
|
|
271
|
+
page: { // Current page data
|
|
272
|
+
pageID: number;
|
|
273
|
+
title: string;
|
|
274
|
+
seo: {
|
|
275
|
+
metaTitle: string;
|
|
276
|
+
metaDescription: string;
|
|
277
|
+
};
|
|
278
|
+
};
|
|
279
|
+
pageTemplateName: string; // Template name
|
|
280
|
+
dynamicPageItem?: any; // For dynamic pages
|
|
281
|
+
searchParams?: Record<string, string>; // Query parameters
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Common Field Types
|
|
286
|
+
|
|
287
|
+
### Text Fields
|
|
288
|
+
```typescript
|
|
289
|
+
fields: {
|
|
290
|
+
title: string;
|
|
291
|
+
description: string;
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Rich Text
|
|
296
|
+
```typescript
|
|
297
|
+
fields: {
|
|
298
|
+
content: string; // HTML string
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Images
|
|
303
|
+
```typescript
|
|
304
|
+
fields: {
|
|
305
|
+
image: {
|
|
306
|
+
url: string;
|
|
307
|
+
label: string;
|
|
308
|
+
width: number;
|
|
309
|
+
height: number;
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Content References
|
|
315
|
+
```typescript
|
|
316
|
+
fields: {
|
|
317
|
+
featuredPost: {
|
|
318
|
+
contentID: number;
|
|
319
|
+
fields: {
|
|
320
|
+
title: string;
|
|
321
|
+
// ... other post fields
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Content Lists
|
|
328
|
+
```typescript
|
|
329
|
+
fields: {
|
|
330
|
+
posts: {
|
|
331
|
+
referenceName: string; // "posts"
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Links
|
|
337
|
+
```typescript
|
|
338
|
+
fields: {
|
|
339
|
+
ctaButton: {
|
|
340
|
+
href: string;
|
|
341
|
+
text: string;
|
|
342
|
+
target: string;
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Fetching Nested Content
|
|
348
|
+
|
|
349
|
+
If a component references content or content lists, fetch it in the component:
|
|
350
|
+
|
|
351
|
+
### Example: Featured Post Component
|
|
352
|
+
|
|
353
|
+
```tsx
|
|
354
|
+
import { getContentItem } from "@/lib/cms/getContentItem";
|
|
355
|
+
|
|
356
|
+
interface FeaturedPostProps {
|
|
357
|
+
module: {
|
|
358
|
+
fields: {
|
|
359
|
+
title: string;
|
|
360
|
+
post: {
|
|
361
|
+
contentID: number;
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
locale: string;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export default async function FeaturedPost({ module, locale }: FeaturedPostProps) {
|
|
369
|
+
const { title, post } = module.fields;
|
|
370
|
+
|
|
371
|
+
// Fetch the full post data
|
|
372
|
+
const fullPost = await getContentItem({
|
|
373
|
+
contentID: post.contentID,
|
|
374
|
+
locale,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
return (
|
|
378
|
+
<div>
|
|
379
|
+
<h2>{title}</h2>
|
|
380
|
+
<article>
|
|
381
|
+
<h3>{fullPost.fields.title}</h3>
|
|
382
|
+
<p>{fullPost.fields.excerpt}</p>
|
|
383
|
+
<a href={`/blog/${fullPost.fields.slug}`}>Read More</a>
|
|
384
|
+
</article>
|
|
385
|
+
</div>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Example: Post Listing with Categories
|
|
391
|
+
|
|
392
|
+
```tsx
|
|
393
|
+
import { getContentList } from "@/lib/cms/getContentList";
|
|
394
|
+
|
|
395
|
+
export default async function PostListing({ module, locale }: any) {
|
|
396
|
+
const posts = await getContentList({
|
|
397
|
+
referenceName: "posts",
|
|
398
|
+
locale,
|
|
399
|
+
sort: "fields.date desc",
|
|
400
|
+
take: 10,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Fetch category for each post
|
|
404
|
+
const postsWithCategories = await Promise.all(
|
|
405
|
+
posts.map(async (post) => {
|
|
406
|
+
if (post.fields.category?.contentID) {
|
|
407
|
+
const category = await getContentItem({
|
|
408
|
+
contentID: post.fields.category.contentID,
|
|
409
|
+
locale,
|
|
410
|
+
});
|
|
411
|
+
return { ...post, category };
|
|
412
|
+
}
|
|
413
|
+
return post;
|
|
414
|
+
})
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
return (
|
|
418
|
+
<div className="grid gap-4">
|
|
419
|
+
{postsWithCategories.map((post) => (
|
|
420
|
+
<article key={post.contentID}>
|
|
421
|
+
<h3>{post.fields.title}</h3>
|
|
422
|
+
{post.category && (
|
|
423
|
+
<span className="badge">{post.category.fields.name}</span>
|
|
424
|
+
)}
|
|
425
|
+
<p>{post.fields.excerpt}</p>
|
|
426
|
+
</article>
|
|
427
|
+
))}
|
|
428
|
+
</div>
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## Best Practices
|
|
434
|
+
|
|
435
|
+
1. **Type Safety**: Define TypeScript interfaces for component props
|
|
436
|
+
2. **Error Handling**: Check for missing fields and provide defaults
|
|
437
|
+
3. **Loading States**: Use Suspense for data fetching
|
|
438
|
+
4. **Caching**: Data is automatically cached by Next.js
|
|
439
|
+
5. **Responsive Design**: Use Tailwind classes for responsive layouts
|
|
440
|
+
6. **Accessibility**: Include proper ARIA labels and semantic HTML
|
|
441
|
+
7. **Performance**: Optimize images with next/image when possible
|
|
442
|
+
|
|
443
|
+
## Component Examples from Demo Site
|
|
444
|
+
|
|
445
|
+
Here are real-world component examples from the demo site:
|
|
446
|
+
|
|
447
|
+
1. **BackgroundHero**: Hero component with image/gradient backgrounds
|
|
448
|
+
2. **BentoSection**: Grid layout component with nested card collection
|
|
449
|
+
3. **LogoStrip**: Logo gallery component with CTA
|
|
450
|
+
4. **PostListing**: Blog listing component with pagination
|
|
451
|
+
5. **Testimonials**: Testimonial carousel component
|
|
452
|
+
6. **TeamListing**: Team member grid component
|
|
453
|
+
7. **CompanyStats**: Animated statistics component
|
|
454
|
+
8. **ContactUs**: Contact form component (hybrid pattern)
|
|
455
|
+
9. **FrequentlyAskedQuestions**: FAQ accordion component
|
|
456
|
+
10. **PricingCards**: Pricing display component
|
|
457
|
+
|
|
458
|
+
## Next Steps
|
|
459
|
+
|
|
460
|
+
- Read [04-data-fetching.md](./04-data-fetching.md) for data fetching patterns
|
|
461
|
+
- Read [05-containers-and-lists.md](./05-containers-and-lists.md) for content lists
|
|
462
|
+
- Read [08-common-components.md](./08-common-components.md) for more examples
|