@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,164 @@
1
+ # Agility CMS + Next.js Documentation
2
+
3
+ Complete documentation for building websites with Agility CMS and Next.js. Optimized for both human developers and AI coding assistants.
4
+
5
+ ## Quick Start
6
+
7
+ 1. **[01-agility-cms-overview.md](./01-agility-cms-overview.md)** - Understand how Agility CMS works
8
+ 2. **[03-creating-components.md](./03-creating-components.md)** - Create your first module
9
+ 3. **[08-common-components.md](./08-common-components.md)** - See production-ready examples
10
+
11
+ ## 📚 Documentation Index
12
+
13
+ ### Core Concepts
14
+ - **[01-agility-cms-overview.md](./01-agility-cms-overview.md)** - How Agility CMS works with Next.js
15
+ - **[02-page-routing.md](./02-page-routing.md)** - Dynamic routing, URLs, and the proxy
16
+
17
+ ### Development Guide
18
+ - **[03-creating-components.md](./03-creating-components.md)** - Build module components step-by-step
19
+ - **[04-data-fetching.md](./04-data-fetching.md)** - Fetch content from Agility CMS
20
+ - **[05-containers-and-lists.md](./05-containers-and-lists.md)** - Work with content lists (blogs, testimonials, etc.)
21
+ - **[08-common-components.md](./08-common-components.md)** - Production-ready component examples
22
+
23
+ ### Advanced Features
24
+ - **[06-localization.md](./06-localization.md)** - Multi-language support
25
+ - **[07-caching-strategies.md](./07-caching-strategies.md)** - Performance and caching
26
+ - **[11-linked-nested-content.md](./11-linked-nested-content.md)** - Linked and nested content patterns
27
+
28
+ ### Setup & Configuration
29
+ - **[10-mcp-server-setup.md](./10-mcp-server-setup.md)** - Connect AI assistants to Agility CMS via MCP
30
+ - **[09-whats-included.md](./09-whats-included.md)** - Feature checklist
31
+
32
+ ## 🎯 Common Tasks
33
+
34
+ ### Add a Blog
35
+ 1. Read [05-containers-and-lists.md](./05-containers-and-lists.md)
36
+ 2. Create PostListing component using examples
37
+ 3. Configure "posts" content list in Agility CMS
38
+ 4. Add content and it renders automatically
39
+
40
+ ### Add a Hero Section
41
+ 1. See examples in [08-common-components.md](./08-common-components.md)
42
+ 2. Copy Hero component code
43
+ 3. Register in `components/agility-components/index.ts`
44
+ 4. Create Hero module in Agility CMS
45
+
46
+ ### Enable Multi-Language
47
+ 1. Read [06-localization.md](./06-localization.md)
48
+ 2. Update `AGILITY_LOCALES` environment variable
49
+ 3. Update `lib/i18n/config.ts` with new locales
50
+ 4. Add locale-specific content in Agility CMS
51
+
52
+ ### Optimize Performance
53
+ 1. Read [07-caching-strategies.md](./07-caching-strategies.md)
54
+ 2. Configure revalidation time in page routes
55
+ 3. Set up webhooks for on-demand revalidation
56
+ 4. Monitor and debug cache behavior
57
+
58
+ ## 🏗️ Project Architecture
59
+
60
+ ```
61
+ /
62
+ ├── app/
63
+ │ └── [locale]/
64
+ │ ├── [...slug]/
65
+ │ │ └── page.tsx # All pages route here
66
+ │ └── layout.tsx
67
+ ├── components/
68
+ │ ├── agility-components/ # CMS modules (Hero, Blog, etc.)
69
+ │ │ ├── index.ts # Component registry
70
+ │ │ ├── RichTextArea.tsx
71
+ │ │ └── ...
72
+ │ └── agility-pages/ # Page templates
73
+ │ ├── index.ts # Template registry
74
+ │ ├── MainTemplate.tsx
75
+ │ └── ...
76
+ ├── lib/
77
+ │ ├── cms/ # CMS helper functions
78
+ │ │ ├── getAgilityPage.ts # Fetch page with modules
79
+ │ │ ├── getContentItem.ts # Fetch single item
80
+ │ │ ├── getContentList.ts # Fetch list of items
81
+ │ │ └── ...
82
+ │ └── i18n/
83
+ │ └── config.ts # Locale configuration
84
+ ├── proxy.ts # Routing proxy (Next.js 16+)
85
+ └── docs/ # This documentation
86
+ ```
87
+
88
+ ## 🔑 Key Concepts
89
+
90
+ ### Modules
91
+ Reusable components that content editors add to pages in Agility CMS.
92
+ - Examples: Hero, PostListing, Testimonials, ContactForm
93
+ - Stored in: `components/agility-components/`
94
+ - Registered in: `components/agility-components/index.ts`
95
+
96
+ ### Page Templates
97
+ Define the layout structure of pages. Each template has named content zones where modules can be placed.
98
+ - Stored in: `components/agility-pages/`
99
+ - Registered in: `components/agility-pages/index.ts`
100
+
101
+ ### Content Lists
102
+ Collections of structured content items (posts, team members, testimonials).
103
+ - Defined in Agility CMS
104
+ - Fetched with `getContentList()`
105
+ - Can be filtered, sorted, and paginated
106
+
107
+ ### Locales
108
+ Multi-language support with clean URLs.
109
+ - Default locale has no prefix: `/about`
110
+ - Other locales use prefix: `/fr/about`, `/es/about`
111
+ - Configured in: `lib/i18n/config.ts`
112
+
113
+ ### Caching
114
+ Automatic caching with Next.js Data Cache.
115
+ - Time-based revalidation (ISR)
116
+ - On-demand revalidation via webhooks
117
+ - Cache tags for targeted updates
118
+
119
+ ## 🛠️ CMS Helper Functions
120
+
121
+ | Function | Purpose | Example |
122
+ |----------|---------|---------|
123
+ | `getAgilityPage()` | Fetch complete page with modules | Page routes |
124
+ | `getContentItem()` | Fetch single content item | Module component |
125
+ | `getContentList()` | Fetch collection of items | Blog listing |
126
+ | `getSitemapFlat()` | Fetch flat sitemap | Navigation |
127
+ | `getSitemapNested()` | Fetch hierarchical sitemap | Breadcrumbs |
128
+
129
+ ## 🤖 For AI Coding Assistants
130
+
131
+ This documentation enables AI tools to:
132
+ - ✅ Generate production-ready components following established patterns
133
+ - ✅ Fetch content correctly with proper TypeScript types
134
+ - ✅ Handle locales, caching, and routing automatically
135
+ - ✅ Create maintainable, type-safe code
136
+ - ✅ Connect to Agility CMS via MCP server (optional)
137
+
138
+ **AI-Specific Guide**: See [../.claude/agents.md](../.claude/agents.md)
139
+
140
+ ## 📖 Documentation Philosophy
141
+
142
+ 1. **Example-Driven** - Lots of copy-paste ready code
143
+ 2. **Pattern-Based** - Consistent patterns throughout
144
+ 3. **Production-Ready** - Real-world examples that work
145
+ 4. **Type-Safe** - TypeScript types in all examples
146
+
147
+ ## ✅ Success Metrics
148
+
149
+ You know the docs are working when:
150
+ - You can build features without checking other files
151
+ - Generated code follows existing patterns
152
+ - TypeScript types are always included
153
+ - CMS integration "just works"
154
+
155
+ ## 🔗 Related Documentation
156
+
157
+ - **[../.claude/agents.md](../.claude/agents.md)** - Guide for AI coding assistants
158
+ - **[../.claude/QUICK-START.md](../.claude/QUICK-START.md)** - 5-minute quick start
159
+ - **[../DEVELOPMENT.md](../DEVELOPMENT.md)** - Development workflow
160
+ - **[../README.md](../README.md)** - Project overview
161
+
162
+ ---
163
+
164
+ **Need help?** Start with [01-agility-cms-overview.md](./01-agility-cms-overview.md) or jump to [08-common-components.md](./08-common-components.md) for examples. 🚀
@@ -0,0 +1,28 @@
1
+ import { draftMode } from 'next/headers';
2
+ import { agilityConfig } from "@agility/nextjs"
3
+ import { type Locale, defaultLocale, isValidLocale, locales } from "@/lib/i18n/config"
4
+
5
+ /**
6
+ * Gets the Agility context for the current request.
7
+ */
8
+ export const getAgilityContext = async (locale?: string) => {
9
+ //determine if we're in preview mode based on "draft" mode from next.js
10
+ const { isEnabled } = await draftMode()
11
+
12
+ const isDevelopmentMode = process.env.NODE_ENV === "development"
13
+
14
+ //determine whether it's preview or dev mode
15
+ const isPreview = isEnabled || isDevelopmentMode
16
+
17
+ // Validate and use the provided locale, fallback to default
18
+ const validatedLocale: Locale = (locale && isValidLocale(locale, locales)) ? locale : defaultLocale
19
+
20
+ return {
21
+ locales: agilityConfig.locales,
22
+ locale: validatedLocale,
23
+ sitemap: agilityConfig.channelName,
24
+ isPreview,
25
+ isDevelopmentMode
26
+ }
27
+ }
28
+
@@ -0,0 +1,51 @@
1
+ import "server-only";
2
+ import { getAgilityPageProps } from "@agility/nextjs/node";
3
+ import { getAgilityContext } from "./getAgilityContext";
4
+
5
+ export interface PageProps {
6
+ params: Promise<{ slug: string[], locale: string }>
7
+ searchParams?: Promise<{ [key: string]: string | string[] | undefined }>
8
+ }
9
+
10
+ /**
11
+ * Get a page with caching information added.
12
+ */
13
+ export const getAgilityPage = async ({ params }: PageProps) => {
14
+ const awaitedParams = await params
15
+ const { isPreview: preview, locale } = await getAgilityContext(awaitedParams.locale)
16
+
17
+ if (!awaitedParams.slug) awaitedParams.slug = [""]
18
+
19
+ //check the last element of the slug to see if it has search params encoded (from middleware)
20
+ let lastSlug = awaitedParams.slug[awaitedParams.slug.length - 1]
21
+ let searchParams: { [key: string]: string } = {}
22
+ if (lastSlug && lastSlug.startsWith("~~~") && lastSlug.endsWith("~~~")) {
23
+ //we have search params encoded here
24
+ lastSlug = lastSlug.replace(/~~~+/g, "")
25
+ const decoded = decodeURIComponent(lastSlug)
26
+ const parts = decoded.split("&").map(part => part.trim())
27
+
28
+ parts.forEach(part => {
29
+ const kvp = part.split("=")
30
+ if (kvp.length === 2) {
31
+ searchParams[kvp[0]] = kvp[1]
32
+ }
33
+ })
34
+
35
+ awaitedParams.slug = awaitedParams.slug.slice(0, awaitedParams.slug.length - 1)
36
+ if (awaitedParams.slug.length === 0) awaitedParams.slug = [""]
37
+ }
38
+
39
+ //get the page
40
+ const page = await getAgilityPageProps({
41
+ params: awaitedParams, preview, locale, apiOptions: {
42
+ contentLinkDepth: 0
43
+ }
44
+ })
45
+
46
+ page.globalData = page.globalData || {};
47
+ page.globalData["searchParams"] = searchParams;
48
+
49
+ return page
50
+ }
51
+
@@ -0,0 +1,22 @@
1
+ import "server-only";
2
+
3
+ import agility from '@agility/content-fetch'
4
+ import { draftMode } from 'next/headers';
5
+
6
+ const getAgilitySDK = async () => {
7
+ //get the preview data
8
+ const isDevelopmentMode = process.env.NODE_ENV === "development"
9
+ const { isEnabled: isDraftMode } = await draftMode()
10
+ const isPreview = isDevelopmentMode || isDraftMode
11
+
12
+ const apiKey = isPreview ? process.env.AGILITY_API_PREVIEW_KEY : process.env.AGILITY_API_FETCH_KEY
13
+
14
+ return agility.getApi({
15
+ guid: process.env.AGILITY_GUID,
16
+ apiKey,
17
+ isPreview
18
+ });
19
+ }
20
+
21
+ export default getAgilitySDK
22
+
@@ -0,0 +1,20 @@
1
+ import { type ContentItemRequestParams } from "@agility/content-fetch/dist/methods/getContentItem"
2
+ import getAgilitySDK from "@/lib/cms/getAgilitySDK"
3
+ import { type ContentItem } from "@agility/content-fetch"
4
+
5
+ /**
6
+ * Get a content item with caching information added.
7
+ */
8
+ export const getContentItem = async <T>(params: ContentItemRequestParams) => {
9
+ const agilitySDK = await getAgilitySDK()
10
+
11
+ agilitySDK.config.fetchConfig = {
12
+ next: {
13
+ tags: [`agility-content-${params.contentID}-${params.languageCode || params.locale}`],
14
+ revalidate: 60,
15
+ },
16
+ }
17
+
18
+ return await agilitySDK.getContentItem(params) as ContentItem<T>
19
+ }
20
+
@@ -0,0 +1,19 @@
1
+ import getAgilitySDK from "@/lib/cms/getAgilitySDK"
2
+ import type { ContentListRequestParams } from "@agility/content-fetch/dist/methods/getContentList"
3
+
4
+ /**
5
+ * Get a content list with caching information added.
6
+ */
7
+ export const getContentList = async <T>(params: ContentListRequestParams) => {
8
+ const agilitySDK = await getAgilitySDK()
9
+
10
+ agilitySDK.config.fetchConfig = {
11
+ next: {
12
+ tags: [`agility-content-${params.referenceName.toLowerCase()}-${params.languageCode || params.locale}`],
13
+ revalidate: 60,
14
+ },
15
+ }
16
+
17
+ return await agilitySDK.getContentList(params)
18
+ }
19
+
@@ -0,0 +1,85 @@
1
+ import agility from '@agility/content-fetch'
2
+
3
+ export interface Redirection {
4
+ id: number
5
+ originUrl: string
6
+ destinationUrl: string
7
+ statusCode: number
8
+ }
9
+
10
+ interface Redirections {
11
+ lastAccessDate: string
12
+ isUpToDate: boolean
13
+ items: Redirection[]
14
+ }
15
+
16
+ export interface RedirectionsMap {
17
+ lastAccessDate: string
18
+ isUpToDate: boolean
19
+ items: { [key: string]: Redirection }
20
+ }
21
+
22
+ interface Props {
23
+ forceUpdate?: boolean
24
+ }
25
+
26
+ /**
27
+ * Get the list of redirections from Agility CMS.
28
+ */
29
+ export const getRedirections = async ({ forceUpdate = false }: Props): Promise<RedirectionsMap> => {
30
+ const apiKey = process.env.AGILITY_API_FETCH_KEY
31
+
32
+ const agilitySDK = agility.getApi({
33
+ guid: process.env.AGILITY_GUID,
34
+ apiKey,
35
+ isPreview: false
36
+ });
37
+
38
+ agilitySDK.config.fetchConfig = {
39
+ next: {
40
+ revalidate: 0,
41
+ },
42
+ }
43
+
44
+ try {
45
+ let lastAccessDate: Date | null | undefined = undefined
46
+ const redirectionsFromServer = await agilitySDK.getUrlRedirections({ lastAccessDate }) as Redirections
47
+
48
+ if (!redirectionsFromServer.isUpToDate || forceUpdate) {
49
+ const redirectionsMap: RedirectionsMap = {
50
+ lastAccessDate: redirectionsFromServer.lastAccessDate,
51
+ isUpToDate: redirectionsFromServer.isUpToDate,
52
+ items: {}
53
+ }
54
+
55
+ redirectionsFromServer.items.forEach((redirection) => {
56
+ let key = redirection.originUrl.toLowerCase()
57
+ if (key.startsWith("~/")) key = key.substring(1)
58
+ if (key.includes("://")) {
59
+ const hostIndex = key.indexOf("/", key.indexOf("://") + 3)
60
+ key = key.substring(hostIndex)
61
+ }
62
+ if (redirection.destinationUrl.startsWith("~/")) {
63
+ redirection.destinationUrl = redirection.destinationUrl.substring(1)
64
+ }
65
+ redirectionsMap.items[key] = redirection
66
+ });
67
+
68
+ return redirectionsMap
69
+ }
70
+
71
+ return {
72
+ lastAccessDate: redirectionsFromServer.lastAccessDate,
73
+ isUpToDate: redirectionsFromServer.isUpToDate,
74
+ items: {}
75
+ }
76
+ } catch (error) {
77
+ console.error('Failed to fetch redirections:', error);
78
+ return {
79
+ lastAccessDate: new Date().toISOString(),
80
+ isUpToDate: false,
81
+ items: {}
82
+ }
83
+ }
84
+ }
85
+
@@ -0,0 +1,19 @@
1
+ import getAgilitySDK from "@/lib/cms/getAgilitySDK"
2
+ import { type SitemapFlatRequestParams } from "@agility/content-fetch/dist/methods/getSitemapFlat"
3
+
4
+ /**
5
+ * Get the flat sitemap for the given language code, with caching information added.
6
+ */
7
+ export const getSitemapFlat = async (params: SitemapFlatRequestParams) => {
8
+ const agilitySDK = await getAgilitySDK()
9
+
10
+ agilitySDK.config.fetchConfig = {
11
+ next: {
12
+ tags: [`agility-sitemap-flat-${params.languageCode || params.locale}`],
13
+ revalidate: 60,
14
+ },
15
+ }
16
+
17
+ return await agilitySDK.getSitemapFlat(params)
18
+ }
19
+
@@ -0,0 +1,19 @@
1
+ import getAgilitySDK from "@/lib/cms/getAgilitySDK"
2
+ import { type SitemapNestedRequestParams } from "@agility/content-fetch/dist/methods/getSitemapNested"
3
+
4
+ /**
5
+ * Get the nested sitemap for the given language code, with caching information added.
6
+ */
7
+ export const getSitemapNested = async (params: SitemapNestedRequestParams) => {
8
+ const agilitySDK = await getAgilitySDK()
9
+
10
+ agilitySDK.config.fetchConfig = {
11
+ next: {
12
+ tags: [`agility-sitemap-nested-${params.languageCode || params.locale}`],
13
+ revalidate: 60,
14
+ },
15
+ }
16
+
17
+ return await agilitySDK.getSitemapNested(params)
18
+ }
19
+
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Strongly typed environment variables utility
3
+ * Provides runtime validation and type safety for environment variables
4
+ */
5
+
6
+ type RequiredEnvVars = {
7
+ // Agility CMS
8
+ AGILITY_GUID: string
9
+ AGILITY_API_FETCH_KEY: string
10
+ AGILITY_API_PREVIEW_KEY: string
11
+ AGILITY_SECURITY_KEY: string
12
+ AGILITY_LOCALES: string
13
+ AGILITY_SITEMAP: string
14
+ AGILITY_FETCH_CACHE_DURATION: string
15
+ AGILITY_PATH_REVALIDATE_DURATION: string
16
+
17
+ // Node.js
18
+ NODE_ENV: 'development' | 'production' | 'test'
19
+ }
20
+
21
+ type OptionalEnvVars = {
22
+ // Add any optional environment variables here
23
+ }
24
+
25
+ type EnvVars = RequiredEnvVars & Partial<OptionalEnvVars>
26
+
27
+ /**
28
+ * Get a required environment variable with runtime validation
29
+ */
30
+ function getRequiredEnvVar<K extends keyof RequiredEnvVars>(key: K): RequiredEnvVars[K] {
31
+ const value = process.env[key]
32
+
33
+ if (!value) {
34
+ throw new Error(`Missing required environment variable: ${key}`)
35
+ }
36
+
37
+ // Special validation for NODE_ENV
38
+ if (key === 'NODE_ENV' && !['development', 'production', 'test'].includes(value)) {
39
+ throw new Error(`Invalid NODE_ENV value: ${value}. Must be 'development', 'production', or 'test'`)
40
+ }
41
+
42
+ return value as RequiredEnvVars[K]
43
+ }
44
+
45
+ /**
46
+ * Get an optional environment variable
47
+ */
48
+ function getOptionalEnvVar<K extends keyof OptionalEnvVars>(key: K, defaultValue?: OptionalEnvVars[K]): OptionalEnvVars[K] | undefined {
49
+ return process.env[key] as OptionalEnvVars[K] || defaultValue
50
+ }
51
+
52
+ /**
53
+ * Get all environment variables with validation
54
+ */
55
+ function getAllEnvVars(): EnvVars {
56
+ return {
57
+ // Agility CMS
58
+ AGILITY_GUID: getRequiredEnvVar('AGILITY_GUID'),
59
+ AGILITY_API_FETCH_KEY: getRequiredEnvVar('AGILITY_API_FETCH_KEY'),
60
+ AGILITY_API_PREVIEW_KEY: getRequiredEnvVar('AGILITY_API_PREVIEW_KEY'),
61
+ AGILITY_SECURITY_KEY: getRequiredEnvVar('AGILITY_SECURITY_KEY'),
62
+ AGILITY_LOCALES: getRequiredEnvVar('AGILITY_LOCALES'),
63
+ AGILITY_SITEMAP: getRequiredEnvVar('AGILITY_SITEMAP'),
64
+ AGILITY_FETCH_CACHE_DURATION: getRequiredEnvVar('AGILITY_FETCH_CACHE_DURATION'),
65
+ AGILITY_PATH_REVALIDATE_DURATION: getRequiredEnvVar('AGILITY_PATH_REVALIDATE_DURATION'),
66
+
67
+ // Node.js
68
+ NODE_ENV: getRequiredEnvVar('NODE_ENV'),
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Typed environment variables object
74
+ * Use this instead of process.env for type safety
75
+ */
76
+ export const env = {
77
+ get: getRequiredEnvVar,
78
+ getOptional: getOptionalEnvVar,
79
+ getAll: getAllEnvVars,
80
+
81
+ // Direct access to commonly used variables
82
+ get AGILITY_GUID() { return getRequiredEnvVar('AGILITY_GUID') },
83
+ get AGILITY_API_FETCH_KEY() { return getRequiredEnvVar('AGILITY_API_FETCH_KEY') },
84
+ get AGILITY_API_PREVIEW_KEY() { return getRequiredEnvVar('AGILITY_API_PREVIEW_KEY') },
85
+ get AGILITY_SECURITY_KEY() { return getRequiredEnvVar('AGILITY_SECURITY_KEY') },
86
+ get AGILITY_LOCALES() { return getRequiredEnvVar('AGILITY_LOCALES') },
87
+ get AGILITY_SITEMAP() { return getRequiredEnvVar('AGILITY_SITEMAP') },
88
+ get AGILITY_FETCH_CACHE_DURATION() { return getRequiredEnvVar('AGILITY_FETCH_CACHE_DURATION') },
89
+ get AGILITY_PATH_REVALIDATE_DURATION() { return getRequiredEnvVar('AGILITY_PATH_REVALIDATE_DURATION') },
90
+ get NODE_ENV() { return getRequiredEnvVar('NODE_ENV') },
91
+
92
+ // Computed values
93
+ get isDevelopment() { return getRequiredEnvVar('NODE_ENV') === 'development' },
94
+ get isProduction() { return getRequiredEnvVar('NODE_ENV') === 'production' },
95
+ get isTest() { return getRequiredEnvVar('NODE_ENV') === 'test' },
96
+ } as const
97
+
98
+ export type { RequiredEnvVars, OptionalEnvVars, EnvVars }
99
+
@@ -0,0 +1,28 @@
1
+ // Get locales from environment variable, fallback to default
2
+ const envLocales = process.env.AGILITY_LOCALES?.split(',') || ['en-us']
3
+ export const locales = envLocales as readonly string[]
4
+ export const defaultLocale = envLocales[0] || 'en-us'
5
+ export type Locale = typeof locales[number]
6
+
7
+ export function isValidLocale(locale: string, locales: readonly string[]): locale is Locale {
8
+ return locales.includes(locale as Locale)
9
+ }
10
+
11
+ export function getLocaleFromPathname(pathname: string, locales: readonly string[]): Locale | null {
12
+ const segments = pathname.split('/')
13
+ const potentialLocale = segments[1]
14
+
15
+ if (isValidLocale(potentialLocale, locales)) {
16
+ return potentialLocale
17
+ }
18
+
19
+ return null
20
+ }
21
+
22
+ export function removeLocaleFromPathname(pathname: string, locale: Locale): string {
23
+ if (pathname.startsWith(`/${locale}`)) {
24
+ return pathname.slice(`/${locale}`.length) || '/'
25
+ }
26
+ return pathname
27
+ }
28
+
@@ -0,0 +1,101 @@
1
+ import { NextResponse } from 'next/server'
2
+ import type { NextRequest } from 'next/server'
3
+ import { defaultLocale, locales, isValidLocale, getLocaleFromPathname, removeLocaleFromPathname } from './lib/i18n/config'
4
+
5
+ export async function proxy(request: NextRequest) {
6
+ /*****************************
7
+ * *** AGILITY PROXY ***
8
+ * 1: Check if this is a preview request,
9
+ * 2: Check if we are exiting preview
10
+ * 3: Check if this is a direct to a dynamic page
11
+ * based on a content id
12
+ *******************************/
13
+
14
+ let pathname = request.nextUrl.pathname
15
+ const previewQ = request.nextUrl.searchParams.get("AgilityPreview")
16
+ let contentIDStr = request.nextUrl.searchParams.get("ContentID") as string || ""
17
+
18
+ const ext = request.nextUrl.pathname.includes(".") ? request.nextUrl.pathname.split('.').pop() : null
19
+
20
+ if (request.nextUrl.searchParams.has("agilitypreviewkey")) {
21
+ //*** this is a preview request ***
22
+ const agilityPreviewKey = request.nextUrl.searchParams.get("agilitypreviewkey") || ""
23
+ const locale = request.nextUrl.searchParams.get("lang")
24
+ const slug = request.nextUrl.pathname
25
+ let redirectUrl = `${request.nextUrl.protocol}//${request.nextUrl.host}/api/preview?locale=${locale}&ContentID=${contentIDStr}&slug=${encodeURIComponent(slug)}&agilitypreviewkey=${encodeURIComponent(agilityPreviewKey)}`
26
+ return NextResponse.redirect(redirectUrl)
27
+ } else if (previewQ === "0") {
28
+ //*** exit preview
29
+ const locale = request.nextUrl.searchParams.get("lang")
30
+ const slug = request.nextUrl.pathname
31
+ let redirectUrl = `${request.nextUrl.protocol}//${request.nextUrl.host}/api/preview/exit?locale=${locale}&ContentID=${contentIDStr}&slug=${encodeURIComponent(slug)}`
32
+ return NextResponse.redirect(redirectUrl)
33
+ } else if (contentIDStr) {
34
+ const contentID = parseInt(contentIDStr)
35
+ if (!isNaN(contentID) && contentID > 0) {
36
+ //*** this is a dynamic page request ***
37
+ let dynredirectUrl = `${request.nextUrl.protocol}//${request.nextUrl.host}/api/dynamic-redirect?ContentID=${contentID}`
38
+ return NextResponse.rewrite(dynredirectUrl)
39
+ }
40
+ } else if ((!ext || ext.length === 0)) {
41
+ /**************************************
42
+ * SPECIAL CASE FOR lang= QUERY PARAM *
43
+ **************************************/
44
+ const langParam = request.nextUrl.searchParams.get("lang")
45
+ const currentLocale = getLocaleFromPathname(pathname, locales) || defaultLocale
46
+
47
+ if (langParam && isValidLocale(langParam, locales) && langParam !== currentLocale) {
48
+ if (langParam === defaultLocale) {
49
+ const redirectUrl = new URL(request.nextUrl.toString())
50
+ redirectUrl.pathname = removeLocaleFromPathname(pathname, currentLocale)
51
+ redirectUrl.searchParams.delete("lang")
52
+ return NextResponse.redirect(redirectUrl)
53
+ } else {
54
+ const redirectUrl = new URL(request.nextUrl.toString())
55
+ const pathnameWithoutLocale = removeLocaleFromPathname(pathname, currentLocale)
56
+ redirectUrl.pathname = `/${langParam}${pathnameWithoutLocale}`
57
+ redirectUrl.searchParams.delete("lang")
58
+ return NextResponse.redirect(redirectUrl)
59
+ }
60
+ }
61
+
62
+ /************************
63
+ * HANDLE SEARCH PARAMS *
64
+ ************************/
65
+ let searchParams = request.nextUrl.searchParams.toString()
66
+ let hasSearchParams = searchParams && searchParams.length > 0
67
+ if (!hasSearchParams) {
68
+ searchParams = ""
69
+ }
70
+
71
+ if (searchParams && searchParams.length > 0) {
72
+ const searchParamPortion = `~~~${encodeURIComponent(searchParams)}~~~`
73
+ pathname = pathname.endsWith("/") ? `${pathname}${searchParamPortion}` : `${pathname}/${searchParamPortion}`
74
+ }
75
+
76
+ /************************
77
+ * LOCALE BASED ROUTING *
78
+ ************************/
79
+ const hasLocalePrefix = locales.some(locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`)
80
+ const isStaticFile = pathname.includes('.') || pathname.startsWith('/_next')
81
+
82
+ const baseUrl = request.nextUrl.origin
83
+
84
+ if (!hasLocalePrefix && !isStaticFile) {
85
+ const localeBasedUrl = new URL(`/${defaultLocale}${pathname}`, baseUrl)
86
+ return NextResponse.rewrite(localeBasedUrl)
87
+ }
88
+
89
+ if (hasSearchParams) {
90
+ const searchParamUrl = new URL(pathname, baseUrl)
91
+ return NextResponse.rewrite(searchParamUrl)
92
+ }
93
+ }
94
+ }
95
+
96
+ export const config = {
97
+ matcher: [
98
+ '/((?!api|assets|_next/static|_next/image|favicon.ico).*)',
99
+ ],
100
+ }
101
+
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "moduleResolution": "node",
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist", "templates"]
20
+ }
21
+