@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,596 @@
|
|
|
1
|
+
# Containers and Content Lists
|
|
2
|
+
|
|
3
|
+
This document explains how to work with content lists (containers) in Agility CMS, including common patterns like blog posts, testimonials, team members, and shared data.
|
|
4
|
+
|
|
5
|
+
## What are Content Lists?
|
|
6
|
+
|
|
7
|
+
Content lists (also called containers) are collections of content items of the same type. Common examples:
|
|
8
|
+
- **Blog Posts**: Articles, news, updates
|
|
9
|
+
- **Testimonials**: Customer reviews, quotes
|
|
10
|
+
- **Team Members**: Staff, leadership team
|
|
11
|
+
- **Products**: Catalog items
|
|
12
|
+
- **FAQs**: Question and answer pairs
|
|
13
|
+
- **Categories**: Taxonomy for organizing content
|
|
14
|
+
|
|
15
|
+
## Creating a Content List
|
|
16
|
+
|
|
17
|
+
### Step 1: Define Content Definition in Agility CMS
|
|
18
|
+
|
|
19
|
+
1. Go to **Settings > Content Definitions**
|
|
20
|
+
2. Click **New Content Definition**
|
|
21
|
+
3. Choose **Content List**
|
|
22
|
+
4. Add a **Reference Name** (e.g., "BlogPost")
|
|
23
|
+
5. Add fields (e.g., title, content, date, author, category)
|
|
24
|
+
|
|
25
|
+
### Step 2: Create Container in Agility CMS
|
|
26
|
+
|
|
27
|
+
1. Go to **Shared Content**
|
|
28
|
+
2. Click **New Container**
|
|
29
|
+
3. Select your content definition (e.g., "BlogPost")
|
|
30
|
+
4. Give it a **Reference Name** (e.g., "posts")
|
|
31
|
+
5. Add content items
|
|
32
|
+
|
|
33
|
+
## Displaying Content Lists
|
|
34
|
+
|
|
35
|
+
### Pattern 1: Simple List Module
|
|
36
|
+
|
|
37
|
+
Create a module that displays items from a content list:
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// src/components/agility-components/PostListing.tsx
|
|
41
|
+
|
|
42
|
+
import { getContentList } from "@/lib/cms/getContentList";
|
|
43
|
+
|
|
44
|
+
interface PostListingProps {
|
|
45
|
+
module: {
|
|
46
|
+
fields: {
|
|
47
|
+
title: string;
|
|
48
|
+
numberOfPosts: number;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
locale: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default async function PostListing({ module, locale }: PostListingProps) {
|
|
55
|
+
const { title, numberOfPosts } = module.fields;
|
|
56
|
+
|
|
57
|
+
const posts = await getContentList({
|
|
58
|
+
referenceName: "posts",
|
|
59
|
+
locale,
|
|
60
|
+
take: numberOfPosts || 10,
|
|
61
|
+
sort: "fields.date desc",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<section className="py-16">
|
|
66
|
+
<h2 className="text-4xl font-bold mb-8">{title}</h2>
|
|
67
|
+
<div className="grid md:grid-cols-3 gap-6">
|
|
68
|
+
{posts.map((post) => (
|
|
69
|
+
<article key={post.contentID} className="border rounded-lg p-6">
|
|
70
|
+
{post.fields.image && (
|
|
71
|
+
<img
|
|
72
|
+
src={post.fields.image.url}
|
|
73
|
+
alt={post.fields.image.label}
|
|
74
|
+
className="w-full h-48 object-cover rounded mb-4"
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
<h3 className="text-xl font-semibold mb-2">{post.fields.title}</h3>
|
|
78
|
+
<p className="text-gray-600 mb-4">{post.fields.excerpt}</p>
|
|
79
|
+
<a
|
|
80
|
+
href={`/blog/${post.fields.slug}`}
|
|
81
|
+
className="text-blue-600 hover:underline"
|
|
82
|
+
>
|
|
83
|
+
Read More →
|
|
84
|
+
</a>
|
|
85
|
+
</article>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
</section>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Pattern 2: Nested Content List Reference
|
|
94
|
+
|
|
95
|
+
When a module references a content list (not hardcoded):
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
// src/components/agility-components/CardGrid.tsx
|
|
99
|
+
|
|
100
|
+
import { getContentList } from "@/lib/cms/getContentList";
|
|
101
|
+
|
|
102
|
+
interface CardGridProps {
|
|
103
|
+
module: {
|
|
104
|
+
fields: {
|
|
105
|
+
title: string;
|
|
106
|
+
cards: {
|
|
107
|
+
referenceName: string; // The list reference name
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
locale: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default async function CardGrid({ module, locale }: CardGridProps) {
|
|
115
|
+
const { title, cards } = module.fields;
|
|
116
|
+
|
|
117
|
+
// Fetch items from the referenced list
|
|
118
|
+
const cardItems = await getContentList({
|
|
119
|
+
referenceName: cards.referenceName, // e.g., "testimonials", "team", etc.
|
|
120
|
+
locale,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<section className="py-16">
|
|
125
|
+
<h2 className="text-4xl font-bold mb-8 text-center">{title}</h2>
|
|
126
|
+
<div className="grid md:grid-cols-4 gap-6">
|
|
127
|
+
{cardItems.map((card) => (
|
|
128
|
+
<div key={card.contentID} className="text-center">
|
|
129
|
+
{card.fields.image && (
|
|
130
|
+
<img
|
|
131
|
+
src={card.fields.image.url}
|
|
132
|
+
alt={card.fields.image.label}
|
|
133
|
+
className="w-24 h-24 rounded-full mx-auto mb-4"
|
|
134
|
+
/>
|
|
135
|
+
)}
|
|
136
|
+
<h3 className="font-semibold">{card.fields.title}</h3>
|
|
137
|
+
<p className="text-sm text-gray-600">{card.fields.description}</p>
|
|
138
|
+
</div>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
</section>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Why this pattern?** Editors can choose different content lists without code changes.
|
|
147
|
+
|
|
148
|
+
### Pattern 3: Testimonials Carousel
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
// src/components/agility-components/Testimonials.server.tsx
|
|
152
|
+
|
|
153
|
+
import { getContentList } from "@/lib/cms/getContentList";
|
|
154
|
+
import TestimonialsClient from "./Testimonials.client";
|
|
155
|
+
|
|
156
|
+
export default async function Testimonials({ module, locale }: any) {
|
|
157
|
+
const { title } = module.fields;
|
|
158
|
+
|
|
159
|
+
const testimonials = await getContentList({
|
|
160
|
+
referenceName: "testimonials",
|
|
161
|
+
locale,
|
|
162
|
+
take: 10,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<section className="py-16 bg-gray-50">
|
|
167
|
+
<h2 className="text-4xl font-bold mb-8 text-center">{title}</h2>
|
|
168
|
+
<TestimonialsClient testimonials={testimonials} />
|
|
169
|
+
</section>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
// src/components/agility-components/Testimonials.client.tsx
|
|
176
|
+
"use client";
|
|
177
|
+
|
|
178
|
+
import { useState } from "react";
|
|
179
|
+
|
|
180
|
+
export default function TestimonialsClient({ testimonials }: any) {
|
|
181
|
+
const [current, setCurrent] = useState(0);
|
|
182
|
+
|
|
183
|
+
const next = () => setCurrent((i) => (i + 1) % testimonials.length);
|
|
184
|
+
const prev = () =>
|
|
185
|
+
setCurrent((i) => (i - 1 + testimonials.length) % testimonials.length);
|
|
186
|
+
|
|
187
|
+
const testimonial = testimonials[current];
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div className="max-w-4xl mx-auto relative">
|
|
191
|
+
<div className="text-center">
|
|
192
|
+
<p className="text-xl italic mb-4">"{testimonial.fields.quote}"</p>
|
|
193
|
+
<p className="font-semibold">{testimonial.fields.name}</p>
|
|
194
|
+
<p className="text-gray-600">{testimonial.fields.company}</p>
|
|
195
|
+
</div>
|
|
196
|
+
<div className="flex justify-center gap-4 mt-8">
|
|
197
|
+
<button onClick={prev} className="px-4 py-2 bg-gray-200 rounded">
|
|
198
|
+
←
|
|
199
|
+
</button>
|
|
200
|
+
<button onClick={next} className="px-4 py-2 bg-gray-200 rounded">
|
|
201
|
+
→
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Pattern 4: Team Listing
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
// src/components/agility-components/TeamListing.tsx
|
|
213
|
+
|
|
214
|
+
import { getContentList } from "@/lib/cms/getContentList";
|
|
215
|
+
|
|
216
|
+
export default async function TeamListing({ module, locale }: any) {
|
|
217
|
+
const { title, subtitle } = module.fields;
|
|
218
|
+
|
|
219
|
+
const team = await getContentList({
|
|
220
|
+
referenceName: "team",
|
|
221
|
+
locale,
|
|
222
|
+
sort: "fields.order asc",
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<section className="py-16">
|
|
227
|
+
<div className="text-center mb-12">
|
|
228
|
+
<h2 className="text-4xl font-bold mb-4">{title}</h2>
|
|
229
|
+
<p className="text-xl text-gray-600">{subtitle}</p>
|
|
230
|
+
</div>
|
|
231
|
+
<div className="grid md:grid-cols-4 gap-8">
|
|
232
|
+
{team.map((member) => (
|
|
233
|
+
<div key={member.contentID} className="text-center">
|
|
234
|
+
<img
|
|
235
|
+
src={member.fields.photo.url}
|
|
236
|
+
alt={member.fields.name}
|
|
237
|
+
className="w-48 h-48 rounded-full mx-auto mb-4 object-cover"
|
|
238
|
+
/>
|
|
239
|
+
<h3 className="text-xl font-semibold">{member.fields.name}</h3>
|
|
240
|
+
<p className="text-gray-600 mb-2">{member.fields.title}</p>
|
|
241
|
+
<p className="text-sm">{member.fields.bio}</p>
|
|
242
|
+
{member.fields.linkedin && (
|
|
243
|
+
<a
|
|
244
|
+
href={member.fields.linkedin.href}
|
|
245
|
+
className="text-blue-600 mt-2 inline-block"
|
|
246
|
+
>
|
|
247
|
+
LinkedIn →
|
|
248
|
+
</a>
|
|
249
|
+
)}
|
|
250
|
+
</div>
|
|
251
|
+
))}
|
|
252
|
+
</div>
|
|
253
|
+
</section>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Pagination
|
|
259
|
+
|
|
260
|
+
### Pattern 1: Simple Pagination
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
// src/components/agility-components/BlogListing.tsx
|
|
264
|
+
|
|
265
|
+
import { getContentList } from "@/lib/cms/getContentList";
|
|
266
|
+
|
|
267
|
+
export default async function BlogListing({ searchParams, locale }: any) {
|
|
268
|
+
const page = parseInt(searchParams?.page || "1");
|
|
269
|
+
const perPage = 12;
|
|
270
|
+
|
|
271
|
+
const posts = await getContentList({
|
|
272
|
+
referenceName: "posts",
|
|
273
|
+
locale,
|
|
274
|
+
take: perPage,
|
|
275
|
+
skip: (page - 1) * perPage,
|
|
276
|
+
sort: "fields.date desc",
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Calculate total pages (you may need to fetch total count separately)
|
|
280
|
+
const hasMore = posts.length === perPage;
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<div>
|
|
284
|
+
<div className="grid md:grid-cols-3 gap-6">
|
|
285
|
+
{posts.map((post) => (
|
|
286
|
+
<article key={post.contentID}>{/* Post card */}</article>
|
|
287
|
+
))}
|
|
288
|
+
</div>
|
|
289
|
+
<nav className="flex justify-center gap-4 mt-8">
|
|
290
|
+
{page > 1 && (
|
|
291
|
+
<a
|
|
292
|
+
href={`?page=${page - 1}`}
|
|
293
|
+
className="px-4 py-2 bg-blue-600 text-white rounded"
|
|
294
|
+
>
|
|
295
|
+
Previous
|
|
296
|
+
</a>
|
|
297
|
+
)}
|
|
298
|
+
<span className="px-4 py-2">Page {page}</span>
|
|
299
|
+
{hasMore && (
|
|
300
|
+
<a
|
|
301
|
+
href={`?page=${page + 1}`}
|
|
302
|
+
className="px-4 py-2 bg-blue-600 text-white rounded"
|
|
303
|
+
>
|
|
304
|
+
Next
|
|
305
|
+
</a>
|
|
306
|
+
)}
|
|
307
|
+
</nav>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Pattern 2: Load More Button
|
|
314
|
+
|
|
315
|
+
```tsx
|
|
316
|
+
// src/components/agility-components/PostListing.server.tsx
|
|
317
|
+
|
|
318
|
+
import { getContentList } from "@/lib/cms/getContentList";
|
|
319
|
+
import LoadMoreClient from "./LoadMore.client";
|
|
320
|
+
|
|
321
|
+
export default async function PostListing({ module, locale }: any) {
|
|
322
|
+
const initialPosts = await getContentList({
|
|
323
|
+
referenceName: "posts",
|
|
324
|
+
locale,
|
|
325
|
+
take: 6,
|
|
326
|
+
sort: "fields.date desc",
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<div>
|
|
331
|
+
<LoadMoreClient initialPosts={initialPosts} locale={locale} />
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
// src/components/agility-components/LoadMore.client.tsx
|
|
339
|
+
"use client";
|
|
340
|
+
|
|
341
|
+
import { useState } from "react";
|
|
342
|
+
|
|
343
|
+
export default function LoadMoreClient({ initialPosts, locale }: any) {
|
|
344
|
+
const [posts, setPosts] = useState(initialPosts);
|
|
345
|
+
const [skip, setSkip] = useState(6);
|
|
346
|
+
const [loading, setLoading] = useState(false);
|
|
347
|
+
|
|
348
|
+
const loadMore = async () => {
|
|
349
|
+
setLoading(true);
|
|
350
|
+
// Call API route to fetch more posts
|
|
351
|
+
const res = await fetch(`/api/posts?skip=${skip}&take=6&locale=${locale}`);
|
|
352
|
+
const newPosts = await res.json();
|
|
353
|
+
setPosts([...posts, ...newPosts]);
|
|
354
|
+
setSkip(skip + 6);
|
|
355
|
+
setLoading(false);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<div>
|
|
360
|
+
<div className="grid md:grid-cols-3 gap-6">
|
|
361
|
+
{posts.map((post) => (
|
|
362
|
+
<article key={post.contentID}>{/* Post card */}</article>
|
|
363
|
+
))}
|
|
364
|
+
</div>
|
|
365
|
+
<div className="text-center mt-8">
|
|
366
|
+
<button
|
|
367
|
+
onClick={loadMore}
|
|
368
|
+
disabled={loading}
|
|
369
|
+
className="px-6 py-3 bg-blue-600 text-white rounded"
|
|
370
|
+
>
|
|
371
|
+
{loading ? "Loading..." : "Load More"}
|
|
372
|
+
</button>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Filtering and Categories
|
|
380
|
+
|
|
381
|
+
### Pattern 1: Category Filter
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
// src/components/agility-components/BlogWithCategories.tsx
|
|
385
|
+
|
|
386
|
+
import { getContentList } from "@/lib/cms/getContentList";
|
|
387
|
+
|
|
388
|
+
export default async function BlogWithCategories({ searchParams, locale }: any) {
|
|
389
|
+
const categoryId = searchParams?.category;
|
|
390
|
+
|
|
391
|
+
// Fetch categories
|
|
392
|
+
const categories = await getContentList({
|
|
393
|
+
referenceName: "categories",
|
|
394
|
+
locale,
|
|
395
|
+
sort: "fields.name asc",
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Fetch all posts
|
|
399
|
+
const allPosts = await getContentList({
|
|
400
|
+
referenceName: "posts",
|
|
401
|
+
locale,
|
|
402
|
+
sort: "fields.date desc",
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Filter by category if selected
|
|
406
|
+
const posts = categoryId
|
|
407
|
+
? allPosts.filter((post) => post.fields.category?.contentID === parseInt(categoryId))
|
|
408
|
+
: allPosts;
|
|
409
|
+
|
|
410
|
+
return (
|
|
411
|
+
<div>
|
|
412
|
+
<nav className="flex gap-4 mb-8">
|
|
413
|
+
<a
|
|
414
|
+
href="?"
|
|
415
|
+
className={!categoryId ? "font-bold" : ""}
|
|
416
|
+
>
|
|
417
|
+
All
|
|
418
|
+
</a>
|
|
419
|
+
{categories.map((cat) => (
|
|
420
|
+
<a
|
|
421
|
+
key={cat.contentID}
|
|
422
|
+
href={`?category=${cat.contentID}`}
|
|
423
|
+
className={categoryId === String(cat.contentID) ? "font-bold" : ""}
|
|
424
|
+
>
|
|
425
|
+
{cat.fields.name}
|
|
426
|
+
</a>
|
|
427
|
+
))}
|
|
428
|
+
</nav>
|
|
429
|
+
<div className="grid md:grid-cols-3 gap-6">
|
|
430
|
+
{posts.map((post) => (
|
|
431
|
+
<article key={post.contentID}>{/* Post card */}</article>
|
|
432
|
+
))}
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Pattern 2: Search and Filter
|
|
440
|
+
|
|
441
|
+
```tsx
|
|
442
|
+
// src/components/agility-components/PostSearch.tsx
|
|
443
|
+
|
|
444
|
+
import { getContentList } from "@/lib/cms/getContentList";
|
|
445
|
+
|
|
446
|
+
export default async function PostSearch({ searchParams, locale }: any) {
|
|
447
|
+
const query = searchParams?.q?.toLowerCase() || "";
|
|
448
|
+
|
|
449
|
+
const allPosts = await getContentList({
|
|
450
|
+
referenceName: "posts",
|
|
451
|
+
locale,
|
|
452
|
+
sort: "fields.date desc",
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const filteredPosts = query
|
|
456
|
+
? allPosts.filter(
|
|
457
|
+
(post) =>
|
|
458
|
+
post.fields.title.toLowerCase().includes(query) ||
|
|
459
|
+
post.fields.excerpt.toLowerCase().includes(query)
|
|
460
|
+
)
|
|
461
|
+
: allPosts;
|
|
462
|
+
|
|
463
|
+
return (
|
|
464
|
+
<div>
|
|
465
|
+
<form method="GET" className="mb-8">
|
|
466
|
+
<input
|
|
467
|
+
type="text"
|
|
468
|
+
name="q"
|
|
469
|
+
defaultValue={query}
|
|
470
|
+
placeholder="Search posts..."
|
|
471
|
+
className="w-full p-4 border rounded"
|
|
472
|
+
/>
|
|
473
|
+
</form>
|
|
474
|
+
<p className="mb-4">
|
|
475
|
+
{filteredPosts.length} {filteredPosts.length === 1 ? "post" : "posts"} found
|
|
476
|
+
</p>
|
|
477
|
+
<div className="grid md:grid-cols-3 gap-6">
|
|
478
|
+
{filteredPosts.map((post) => (
|
|
479
|
+
<article key={post.contentID}>{/* Post card */}</article>
|
|
480
|
+
))}
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
## Shared Data Pattern
|
|
488
|
+
|
|
489
|
+
Content lists can be used as shared data across the site:
|
|
490
|
+
|
|
491
|
+
### Example: Site Settings
|
|
492
|
+
|
|
493
|
+
```tsx
|
|
494
|
+
// src/lib/cms/getSiteSettings.ts
|
|
495
|
+
|
|
496
|
+
import { getContentList } from "./getContentList";
|
|
497
|
+
|
|
498
|
+
export async function getSiteSettings(locale: string) {
|
|
499
|
+
const settings = await getContentList({
|
|
500
|
+
referenceName: "siteSettings",
|
|
501
|
+
locale,
|
|
502
|
+
take: 1,
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
return settings[0]?.fields || {};
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
```tsx
|
|
510
|
+
// src/components/Header.tsx
|
|
511
|
+
|
|
512
|
+
import { getSiteSettings } from "@/lib/cms/getSiteSettings";
|
|
513
|
+
|
|
514
|
+
export default async function Header({ locale }: any) {
|
|
515
|
+
const settings = await getSiteSettings(locale);
|
|
516
|
+
|
|
517
|
+
return (
|
|
518
|
+
<header>
|
|
519
|
+
<img src={settings.logo.url} alt={settings.siteName} />
|
|
520
|
+
<nav>
|
|
521
|
+
{settings.navigationLinks.map((link: any) => (
|
|
522
|
+
<a key={link.href} href={link.href}>
|
|
523
|
+
{link.text}
|
|
524
|
+
</a>
|
|
525
|
+
))}
|
|
526
|
+
</nav>
|
|
527
|
+
</header>
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
## Related Content
|
|
533
|
+
|
|
534
|
+
### Example: Posts with Author
|
|
535
|
+
|
|
536
|
+
```tsx
|
|
537
|
+
import { getContentList, getContentItem } from "@/lib/cms";
|
|
538
|
+
|
|
539
|
+
export default async function PostsWithAuthors({ locale }: any) {
|
|
540
|
+
const posts = await getContentList({
|
|
541
|
+
referenceName: "posts",
|
|
542
|
+
locale,
|
|
543
|
+
take: 10,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Fetch author for each post
|
|
547
|
+
const postsWithAuthors = await Promise.all(
|
|
548
|
+
posts.map(async (post) => {
|
|
549
|
+
if (post.fields.author?.contentID) {
|
|
550
|
+
const author = await getContentItem({
|
|
551
|
+
contentID: post.fields.author.contentID,
|
|
552
|
+
locale,
|
|
553
|
+
});
|
|
554
|
+
return { ...post, author };
|
|
555
|
+
}
|
|
556
|
+
return post;
|
|
557
|
+
})
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
return (
|
|
561
|
+
<div>
|
|
562
|
+
{postsWithAuthors.map((post) => (
|
|
563
|
+
<article key={post.contentID}>
|
|
564
|
+
<h3>{post.fields.title}</h3>
|
|
565
|
+
{post.author && (
|
|
566
|
+
<div className="flex items-center gap-2 mt-2">
|
|
567
|
+
<img
|
|
568
|
+
src={post.author.fields.avatar.url}
|
|
569
|
+
alt={post.author.fields.name}
|
|
570
|
+
className="w-8 h-8 rounded-full"
|
|
571
|
+
/>
|
|
572
|
+
<span>{post.author.fields.name}</span>
|
|
573
|
+
</div>
|
|
574
|
+
)}
|
|
575
|
+
</article>
|
|
576
|
+
))}
|
|
577
|
+
</div>
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
## Best Practices
|
|
583
|
+
|
|
584
|
+
1. **Use Reference Names**: Always use `referenceName`, not hardcoded strings
|
|
585
|
+
2. **Pagination**: Implement pagination for large lists
|
|
586
|
+
3. **Sorting**: Always specify a sort order for consistency
|
|
587
|
+
4. **Caching**: Let Next.js handle caching automatically
|
|
588
|
+
5. **Error Handling**: Check for empty lists and show appropriate messages
|
|
589
|
+
6. **Type Safety**: Define TypeScript interfaces for content types
|
|
590
|
+
7. **Performance**: Use `Promise.all()` for parallel fetching
|
|
591
|
+
|
|
592
|
+
## Next Steps
|
|
593
|
+
|
|
594
|
+
- Read [04-data-fetching.md](./04-data-fetching.md) for more data patterns
|
|
595
|
+
- Read [06-localization.md](./06-localization.md) for multi-locale lists
|
|
596
|
+
- Read [09-example-components.md](./09-example-components.md) for real examples
|