@mars-stack/cli 0.2.0 → 1.0.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 (175) hide show
  1. package/dist/index.js +137 -12
  2. package/dist/index.js.map +1 -1
  3. package/package.json +4 -3
  4. package/template/.cursor/rules/composition-patterns.mdc +186 -0
  5. package/template/.cursor/rules/data-access.mdc +29 -0
  6. package/template/.cursor/rules/project-structure.mdc +34 -0
  7. package/template/.cursor/rules/security.mdc +25 -0
  8. package/template/.cursor/rules/testing.mdc +24 -0
  9. package/template/.cursor/rules/ui-conventions.mdc +29 -0
  10. package/template/.cursor/skills/add-api-route/SKILL.md +122 -0
  11. package/template/.cursor/skills/add-audit-log/SKILL.md +375 -0
  12. package/template/.cursor/skills/add-blog/SKILL.md +447 -0
  13. package/template/.cursor/skills/add-command-palette/SKILL.md +438 -0
  14. package/template/.cursor/skills/add-component/SKILL.md +158 -0
  15. package/template/.cursor/skills/add-crud-routes/SKILL.md +221 -0
  16. package/template/.cursor/skills/add-e2e-test/SKILL.md +227 -0
  17. package/template/.cursor/skills/add-error-boundary/SKILL.md +472 -0
  18. package/template/.cursor/skills/add-feature/SKILL.md +174 -0
  19. package/template/.cursor/skills/add-middleware/SKILL.md +135 -0
  20. package/template/.cursor/skills/add-page/SKILL.md +151 -0
  21. package/template/.cursor/skills/add-prisma-model/SKILL.md +148 -0
  22. package/template/.cursor/skills/add-protected-resource/SKILL.md +192 -0
  23. package/template/.cursor/skills/add-role/SKILL.md +156 -0
  24. package/template/.cursor/skills/add-server-action/SKILL.md +167 -0
  25. package/template/.cursor/skills/add-webhook/SKILL.md +192 -0
  26. package/template/.cursor/skills/build-complete-feature/SKILL.md +227 -0
  27. package/template/.cursor/skills/build-dashboard/SKILL.md +211 -0
  28. package/template/.cursor/skills/build-data-table/SKILL.md +283 -0
  29. package/template/.cursor/skills/build-form/SKILL.md +231 -0
  30. package/template/.cursor/skills/build-landing-page/SKILL.md +248 -0
  31. package/template/.cursor/skills/configure-ai/SKILL.md +617 -0
  32. package/template/.cursor/skills/configure-analytics/SKILL.md +413 -0
  33. package/template/.cursor/skills/configure-dark-mode/SKILL.md +309 -0
  34. package/template/.cursor/skills/configure-email/SKILL.md +170 -0
  35. package/template/.cursor/skills/configure-email-verification/SKILL.md +333 -0
  36. package/template/.cursor/skills/configure-feature-flags/SKILL.md +361 -0
  37. package/template/.cursor/skills/configure-i18n/SKILL.md +518 -0
  38. package/template/.cursor/skills/configure-jobs/SKILL.md +500 -0
  39. package/template/.cursor/skills/configure-magic-links/SKILL.md +385 -0
  40. package/template/.cursor/skills/configure-multi-tenancy/SKILL.md +611 -0
  41. package/template/.cursor/skills/configure-notifications/SKILL.md +569 -0
  42. package/template/.cursor/skills/configure-oauth/SKILL.md +217 -0
  43. package/template/.cursor/skills/configure-onboarding/SKILL.md +483 -0
  44. package/template/.cursor/skills/configure-payments/SKILL.md +243 -0
  45. package/template/.cursor/skills/configure-realtime/SKILL.md +733 -0
  46. package/template/.cursor/skills/configure-search/SKILL.md +581 -0
  47. package/template/.cursor/skills/configure-storage/SKILL.md +273 -0
  48. package/template/.cursor/skills/configure-two-factor/SKILL.md +518 -0
  49. package/template/.cursor/skills/create-execution-plan/SKILL.md +204 -0
  50. package/template/.cursor/skills/create-seed/SKILL.md +191 -0
  51. package/template/.cursor/skills/deploy-to-vercel/SKILL.md +300 -0
  52. package/template/.cursor/skills/design-tokens/SKILL.md +138 -0
  53. package/template/.cursor/skills/mars-capture-conversation-context/SKILL.md +119 -0
  54. package/template/.cursor/skills/setup-billing/SKILL.md +322 -0
  55. package/template/.cursor/skills/setup-project/SKILL.md +104 -0
  56. package/template/.cursor/skills/setup-teams/SKILL.md +682 -0
  57. package/template/.cursor/skills/test-api-route/SKILL.md +219 -0
  58. package/template/.cursor/skills/update-architecture-docs/SKILL.md +99 -0
  59. package/template/AGENTS.md +104 -0
  60. package/template/ARCHITECTURE.md +102 -0
  61. package/template/docs/QUALITY_SCORE.md +20 -0
  62. package/template/docs/design-docs/conversation-as-system-record.md +70 -0
  63. package/template/docs/design-docs/core-beliefs.md +43 -0
  64. package/template/docs/design-docs/index.md +8 -0
  65. package/template/docs/exec-plans/active/.gitkeep +0 -0
  66. package/template/docs/exec-plans/completed/.gitkeep +0 -0
  67. package/template/docs/exec-plans/tech-debt.md +7 -0
  68. package/template/docs/generated/.gitkeep +0 -0
  69. package/template/docs/product-specs/index.md +7 -0
  70. package/template/docs/references/index.md +18 -0
  71. package/template/e2e/api.spec.ts +20 -0
  72. package/template/e2e/auth.spec.ts +24 -0
  73. package/template/e2e/public.spec.ts +25 -0
  74. package/template/eslint.config.mjs +24 -0
  75. package/template/next-env.d.ts +6 -0
  76. package/template/next.config.ts +45 -0
  77. package/template/package.json +80 -0
  78. package/template/playwright.config.ts +31 -0
  79. package/template/postcss.config.mjs +8 -0
  80. package/template/prisma/generated/prisma/browser.ts +49 -0
  81. package/template/prisma/generated/prisma/client.ts +73 -0
  82. package/template/prisma/generated/prisma/commonInputTypes.ts +406 -0
  83. package/template/prisma/generated/prisma/enums.ts +15 -0
  84. package/template/prisma/generated/prisma/internal/class.ts +254 -0
  85. package/template/prisma/generated/prisma/internal/prismaNamespace.ts +1240 -0
  86. package/template/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts +190 -0
  87. package/template/prisma/generated/prisma/models/Account.ts +1543 -0
  88. package/template/prisma/generated/prisma/models/File.ts +1529 -0
  89. package/template/prisma/generated/prisma/models/Session.ts +1415 -0
  90. package/template/prisma/generated/prisma/models/Subscription.ts +1455 -0
  91. package/template/prisma/generated/prisma/models/User.ts +2235 -0
  92. package/template/prisma/generated/prisma/models/VerificationToken.ts +1099 -0
  93. package/template/prisma/generated/prisma/models.ts +17 -0
  94. package/template/prisma/schema/auth.prisma +69 -0
  95. package/template/prisma/schema/base.prisma +8 -0
  96. package/template/prisma/schema/file.prisma +15 -0
  97. package/template/prisma/schema/subscription.prisma +17 -0
  98. package/template/prisma.config.ts +13 -0
  99. package/template/scripts/check-architecture.ts +221 -0
  100. package/template/scripts/check-doc-freshness.ts +242 -0
  101. package/template/scripts/ensure-db.mjs +291 -0
  102. package/template/scripts/generate-docs.ts +143 -0
  103. package/template/scripts/generate-env-example.ts +89 -0
  104. package/template/scripts/seed.ts +56 -0
  105. package/template/scripts/update-quality-score.ts +263 -0
  106. package/template/src/__tests__/architecture.test.ts +114 -0
  107. package/template/src/app/(auth)/forgotten-password/page.tsx +92 -0
  108. package/template/src/app/(auth)/layout.tsx +11 -0
  109. package/template/src/app/(auth)/register/page.tsx +162 -0
  110. package/template/src/app/(auth)/reset-password/page.tsx +109 -0
  111. package/template/src/app/(auth)/sign-in/page.tsx +122 -0
  112. package/template/src/app/(auth)/verify/[token]/page.tsx +87 -0
  113. package/template/src/app/(auth)/verify/page.tsx +56 -0
  114. package/template/src/app/(protected)/admin/page.tsx +108 -0
  115. package/template/src/app/(protected)/dashboard/loading.tsx +20 -0
  116. package/template/src/app/(protected)/dashboard/page.tsx +22 -0
  117. package/template/src/app/(protected)/layout.tsx +262 -0
  118. package/template/src/app/(protected)/settings/page.tsx +370 -0
  119. package/template/src/app/api/auth/forgot/route.ts +63 -0
  120. package/template/src/app/api/auth/login/route.ts +121 -0
  121. package/template/src/app/api/auth/logout/route.ts +19 -0
  122. package/template/src/app/api/auth/me/route.ts +30 -0
  123. package/template/src/app/api/auth/reset/route.ts +45 -0
  124. package/template/src/app/api/auth/signup/route.ts +85 -0
  125. package/template/src/app/api/auth/verify/route.ts +46 -0
  126. package/template/src/app/api/csrf/route.ts +12 -0
  127. package/template/src/app/api/health/route.ts +10 -0
  128. package/template/src/app/api/protected/admin/users/route.ts +24 -0
  129. package/template/src/app/api/protected/billing/checkout/route.ts +83 -0
  130. package/template/src/app/api/protected/billing/portal/route.ts +39 -0
  131. package/template/src/app/api/protected/files/[fileId]/route.ts +86 -0
  132. package/template/src/app/api/protected/files/upload/route.ts +64 -0
  133. package/template/src/app/api/protected/user/password/route.ts +63 -0
  134. package/template/src/app/api/protected/user/profile/route.ts +35 -0
  135. package/template/src/app/api/protected/user/sessions/[sessionId]/route.ts +33 -0
  136. package/template/src/app/api/protected/user/sessions/route.ts +22 -0
  137. package/template/src/app/api/readiness/route.ts +15 -0
  138. package/template/src/app/api/webhooks/stripe/route.ts +166 -0
  139. package/template/src/app/error.tsx +33 -0
  140. package/template/src/app/layout.tsx +29 -0
  141. package/template/src/app/not-found.tsx +20 -0
  142. package/template/src/app/page.tsx +136 -0
  143. package/template/src/app/privacy/page.tsx +178 -0
  144. package/template/src/app/providers.tsx +8 -0
  145. package/template/src/app/terms/page.tsx +139 -0
  146. package/template/src/config/app.config.ts +70 -0
  147. package/template/src/config/routes.ts +17 -0
  148. package/template/src/features/admin/index.ts +11 -0
  149. package/template/src/features/admin/permissions.ts +64 -0
  150. package/template/src/features/auth/context/AuthContext.tsx +96 -0
  151. package/template/src/features/auth/context/index.ts +2 -0
  152. package/template/src/features/auth/index.ts +3 -0
  153. package/template/src/features/auth/server/consent.ts +66 -0
  154. package/template/src/features/auth/server/session-revocation.ts +20 -0
  155. package/template/src/features/auth/server/sessions.ts +66 -0
  156. package/template/src/features/auth/server/user.ts +166 -0
  157. package/template/src/features/auth/types.ts +19 -0
  158. package/template/src/features/auth/validators.ts +29 -0
  159. package/template/src/features/billing/server/index.ts +66 -0
  160. package/template/src/features/billing/types.ts +43 -0
  161. package/template/src/features/uploads/server/index.ts +49 -0
  162. package/template/src/features/uploads/types.ts +26 -0
  163. package/template/src/lib/core/email/templates/base-layout.ts +122 -0
  164. package/template/src/lib/core/email/templates/index.ts +4 -0
  165. package/template/src/lib/core/email/templates/password-reset-email.ts +42 -0
  166. package/template/src/lib/core/email/templates/verification-email.ts +41 -0
  167. package/template/src/lib/core/email/templates/welcome-email.ts +40 -0
  168. package/template/src/lib/mars.ts +56 -0
  169. package/template/src/lib/prisma.ts +19 -0
  170. package/template/src/proxy.ts +92 -0
  171. package/template/src/styles/brand.css +15 -0
  172. package/template/src/styles/globals.css +7 -0
  173. package/template/tsconfig.json +59 -0
  174. package/template/vitest.config.ts +41 -0
  175. package/template/vitest.setup.ts +24 -0
@@ -0,0 +1,447 @@
1
+ # Skill: Add Blog
2
+
3
+ Set up an MDX-based blog with frontmatter, listing pages, RSS feed, and SEO in a MARS application.
4
+
5
+ ## When to Use
6
+
7
+ Use this skill when the user asks to add a blog, create a content section, set up MDX pages, or add articles/posts to the app.
8
+
9
+ ## Prerequisites
10
+
11
+ - Next.js app set up with the MARS template
12
+ - `appConfig.features.blog` set to `true` in `src/config/app.config.ts`
13
+
14
+ ## Architecture
15
+
16
+ The blog uses MDX files stored in the repository with filesystem-based routing. Each post is an `.mdx` file with YAML frontmatter. The system provides:
17
+ 1. **Content directory** — `content/blog/` at the project root
18
+ 2. **Frontmatter schema** — validated with Zod
19
+ 3. **Listing page** — paginated blog index with metadata
20
+ 4. **Post page** — individual post rendering with table of contents
21
+ 5. **RSS feed** — auto-generated `feed.xml`
22
+ 6. **SEO** — JSON-LD structured data and Open Graph tags
23
+
24
+ ## Step 1: Install Dependencies
25
+
26
+ ```bash
27
+ yarn add @next/mdx @mdx-js/loader @mdx-js/react gray-matter reading-time remark-gfm rehype-pretty-code
28
+ yarn add -D @types/mdx
29
+ ```
30
+
31
+ ## Step 2: Directory Structure
32
+
33
+ ```
34
+ content/
35
+ └── blog/
36
+ ├── getting-started.mdx
37
+ └── building-with-mars.mdx
38
+
39
+ src/
40
+ ├── features/blog/
41
+ │ ├── server/
42
+ │ │ └── index.ts # Post loading, parsing, RSS generation
43
+ │ ├── components/
44
+ │ │ ├── PostCard.tsx # Blog listing card
45
+ │ │ ├── PostHeader.tsx # Post title, date, reading time
46
+ │ │ ├── TableOfContents.tsx
47
+ │ │ └── index.ts
48
+ │ ├── validation/
49
+ │ │ └── schemas.ts # Frontmatter schema
50
+ │ └── types.ts
51
+ ├── app/
52
+ │ └── blog/
53
+ │ ├── page.tsx # Blog listing
54
+ │ └── [slug]/
55
+ │ └── page.tsx # Individual post
56
+ │ └── feed.xml/
57
+ │ └── route.ts # RSS feed
58
+ ```
59
+
60
+ ## Step 3: Frontmatter Schema
61
+
62
+ ```typescript
63
+ // src/features/blog/validation/schemas.ts
64
+ import { z } from 'zod';
65
+
66
+ export const frontmatterSchema = z.object({
67
+ title: z.string().min(1),
68
+ description: z.string().min(1).max(160),
69
+ date: z.string().refine((val) => !isNaN(Date.parse(val)), 'Invalid date'),
70
+ author: z.string().default('Team'),
71
+ tags: z.array(z.string()).default([]),
72
+ image: z.string().optional(),
73
+ published: z.boolean().default(true),
74
+ });
75
+
76
+ export type Frontmatter = z.infer<typeof frontmatterSchema>;
77
+ ```
78
+
79
+ ## Step 4: Blog Types
80
+
81
+ ```typescript
82
+ // src/features/blog/types.ts
83
+ import type { Frontmatter } from './validation/schemas';
84
+
85
+ export interface BlogPost {
86
+ slug: string;
87
+ frontmatter: Frontmatter;
88
+ content: string;
89
+ readingTime: string;
90
+ excerpt: string;
91
+ }
92
+ ```
93
+
94
+ ## Step 5: Server-Side Post Loading
95
+
96
+ ```typescript
97
+ // src/features/blog/server/index.ts
98
+ import 'server-only';
99
+
100
+ import fs from 'fs';
101
+ import path from 'path';
102
+ import matter from 'gray-matter';
103
+ import readingTime from 'reading-time';
104
+ import { frontmatterSchema, type Frontmatter } from '../validation/schemas';
105
+ import type { BlogPost } from '../types';
106
+
107
+ const CONTENT_DIR = path.join(process.cwd(), 'content', 'blog');
108
+
109
+ export function getAllPosts(): BlogPost[] {
110
+ if (!fs.existsSync(CONTENT_DIR)) return [];
111
+
112
+ const files = fs.readdirSync(CONTENT_DIR).filter((f) => f.endsWith('.mdx'));
113
+
114
+ const posts = files
115
+ .map((filename) => {
116
+ const slug = filename.replace(/\.mdx$/, '');
117
+ const filePath = path.join(CONTENT_DIR, filename);
118
+ const raw = fs.readFileSync(filePath, 'utf-8');
119
+ const { data, content } = matter(raw);
120
+
121
+ const parsed = frontmatterSchema.safeParse(data);
122
+ if (!parsed.success) {
123
+ console.warn(`Invalid frontmatter in ${filename}:`, parsed.error.message);
124
+ return null;
125
+ }
126
+
127
+ if (!parsed.data.published) return null;
128
+
129
+ const stats = readingTime(content);
130
+ const excerpt = content.replace(/^#+\s.*$/gm, '').trim().slice(0, 200) + '...';
131
+
132
+ return {
133
+ slug,
134
+ frontmatter: parsed.data,
135
+ content,
136
+ readingTime: stats.text,
137
+ excerpt,
138
+ };
139
+ })
140
+ .filter(Boolean) as BlogPost[];
141
+
142
+ return posts.sort(
143
+ (a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime(),
144
+ );
145
+ }
146
+
147
+ export function getPostBySlug(slug: string): BlogPost | null {
148
+ const filePath = path.join(CONTENT_DIR, `${slug}.mdx`);
149
+
150
+ if (!fs.existsSync(filePath)) return null;
151
+
152
+ const raw = fs.readFileSync(filePath, 'utf-8');
153
+ const { data, content } = matter(raw);
154
+
155
+ const parsed = frontmatterSchema.safeParse(data);
156
+ if (!parsed.success) return null;
157
+
158
+ const stats = readingTime(content);
159
+ const excerpt = content.replace(/^#+\s.*$/gm, '').trim().slice(0, 200) + '...';
160
+
161
+ return {
162
+ slug,
163
+ frontmatter: parsed.data,
164
+ content,
165
+ readingTime: stats.text,
166
+ excerpt,
167
+ };
168
+ }
169
+
170
+ export function generateRSSFeed(posts: BlogPost[], siteUrl: string): string {
171
+ const items = posts
172
+ .map(
173
+ (post) => `
174
+ <item>
175
+ <title><![CDATA[${post.frontmatter.title}]]></title>
176
+ <link>${siteUrl}/blog/${post.slug}</link>
177
+ <guid isPermaLink="true">${siteUrl}/blog/${post.slug}</guid>
178
+ <description><![CDATA[${post.frontmatter.description}]]></description>
179
+ <pubDate>${new Date(post.frontmatter.date).toUTCString()}</pubDate>
180
+ ${post.frontmatter.tags.map((tag) => `<category>${tag}</category>`).join('\n ')}
181
+ </item>`,
182
+ )
183
+ .join('\n');
184
+
185
+ return `<?xml version="1.0" encoding="UTF-8"?>
186
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
187
+ <channel>
188
+ <title>Blog</title>
189
+ <link>${siteUrl}/blog</link>
190
+ <description>Latest posts</description>
191
+ <atom:link href="${siteUrl}/feed.xml" rel="self" type="application/rss+xml" />
192
+ ${items}
193
+ </channel>
194
+ </rss>`;
195
+ }
196
+ ```
197
+
198
+ ## Step 6: Blog Listing Page
199
+
200
+ ```typescript
201
+ // src/app/blog/page.tsx
202
+ import { Metadata } from 'next';
203
+ import { getAllPosts } from '@/features/blog/server';
204
+ import { PostCard } from '@/features/blog/components/PostCard';
205
+
206
+ export const metadata: Metadata = {
207
+ title: 'Blog',
208
+ description: 'Latest articles and updates',
209
+ };
210
+
211
+ export default function BlogPage() {
212
+ const posts = getAllPosts();
213
+
214
+ return (
215
+ <div className="mx-auto max-w-4xl px-4 py-12">
216
+ <h1 className="text-3xl font-bold text-content-primary">Blog</h1>
217
+ <p className="mt-2 text-content-secondary">Latest articles and updates</p>
218
+
219
+ <div className="mt-8 space-y-8">
220
+ {posts.length === 0 && (
221
+ <p className="text-content-tertiary">No posts yet. Add .mdx files to content/blog/.</p>
222
+ )}
223
+ {posts.map((post) => (
224
+ <PostCard key={post.slug} post={post} />
225
+ ))}
226
+ </div>
227
+ </div>
228
+ );
229
+ }
230
+ ```
231
+
232
+ ## Step 7: Post Card Component
233
+
234
+ ```typescript
235
+ // src/features/blog/components/PostCard.tsx
236
+ import Link from 'next/link';
237
+ import type { BlogPost } from '../types';
238
+
239
+ export function PostCard({ post }: { post: BlogPost }) {
240
+ const date = new Date(post.frontmatter.date).toLocaleDateString('en-US', {
241
+ year: 'numeric',
242
+ month: 'long',
243
+ day: 'numeric',
244
+ });
245
+
246
+ return (
247
+ <article className="group">
248
+ <Link href={`/blog/${post.slug}`} className="block">
249
+ <div className="rounded-lg border border-border-primary bg-surface-primary p-6 transition-colors hover:border-interactive-primary">
250
+ <div className="flex items-center gap-3 text-sm text-content-tertiary">
251
+ <time dateTime={post.frontmatter.date}>{date}</time>
252
+ <span aria-hidden="true">&middot;</span>
253
+ <span>{post.readingTime}</span>
254
+ </div>
255
+ <h2 className="mt-2 text-xl font-semibold text-content-primary group-hover:text-interactive-primary">
256
+ {post.frontmatter.title}
257
+ </h2>
258
+ <p className="mt-2 text-content-secondary">{post.frontmatter.description}</p>
259
+ {post.frontmatter.tags.length > 0 && (
260
+ <div className="mt-3 flex flex-wrap gap-2">
261
+ {post.frontmatter.tags.map((tag) => (
262
+ <span
263
+ key={tag}
264
+ className="rounded-full bg-surface-secondary px-2.5 py-0.5 text-xs font-medium text-content-secondary"
265
+ >
266
+ {tag}
267
+ </span>
268
+ ))}
269
+ </div>
270
+ )}
271
+ </div>
272
+ </Link>
273
+ </article>
274
+ );
275
+ }
276
+ ```
277
+
278
+ ## Step 8: Individual Post Page
279
+
280
+ ```typescript
281
+ // src/app/blog/[slug]/page.tsx
282
+ import { Metadata } from 'next';
283
+ import { notFound } from 'next/navigation';
284
+ import { getAllPosts, getPostBySlug } from '@/features/blog/server';
285
+ import { PostHeader } from '@/features/blog/components/PostHeader';
286
+ import { appConfig } from '@/config/app.config';
287
+
288
+ interface PostPageProps {
289
+ params: Promise<{ slug: string }>;
290
+ }
291
+
292
+ export async function generateStaticParams() {
293
+ const posts = getAllPosts();
294
+ return posts.map((post) => ({ slug: post.slug }));
295
+ }
296
+
297
+ export async function generateMetadata({ params }: PostPageProps): Promise<Metadata> {
298
+ const { slug } = await params;
299
+ const post = getPostBySlug(slug);
300
+ if (!post) return {};
301
+
302
+ const siteUrl = process.env.APP_URL || 'http://localhost:3000';
303
+
304
+ return {
305
+ title: post.frontmatter.title,
306
+ description: post.frontmatter.description,
307
+ openGraph: {
308
+ title: post.frontmatter.title,
309
+ description: post.frontmatter.description,
310
+ type: 'article',
311
+ publishedTime: post.frontmatter.date,
312
+ url: `${siteUrl}/blog/${slug}`,
313
+ images: post.frontmatter.image ? [{ url: post.frontmatter.image }] : [],
314
+ },
315
+ };
316
+ }
317
+
318
+ export default async function PostPage({ params }: PostPageProps) {
319
+ const { slug } = await params;
320
+ const post = getPostBySlug(slug);
321
+ if (!post) notFound();
322
+
323
+ const siteUrl = process.env.APP_URL || 'http://localhost:3000';
324
+
325
+ const jsonLd = {
326
+ '@context': 'https://schema.org',
327
+ '@type': 'BlogPosting',
328
+ headline: post.frontmatter.title,
329
+ description: post.frontmatter.description,
330
+ datePublished: post.frontmatter.date,
331
+ author: { '@type': 'Person', name: post.frontmatter.author },
332
+ url: `${siteUrl}/blog/${slug}`,
333
+ };
334
+
335
+ return (
336
+ <article className="mx-auto max-w-3xl px-4 py-12">
337
+ <script
338
+ type="application/ld+json"
339
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
340
+ />
341
+ <PostHeader
342
+ title={post.frontmatter.title}
343
+ date={post.frontmatter.date}
344
+ author={post.frontmatter.author}
345
+ readingTime={post.readingTime}
346
+ tags={post.frontmatter.tags}
347
+ />
348
+ <div className="prose prose-neutral dark:prose-invert mt-8 max-w-none">
349
+ {/* Render MDX content here using your MDX pipeline */}
350
+ </div>
351
+ </article>
352
+ );
353
+ }
354
+ ```
355
+
356
+ ## Step 9: RSS Feed Route
357
+
358
+ ```typescript
359
+ // src/app/feed.xml/route.ts
360
+ import { getAllPosts, generateRSSFeed } from '@/features/blog/server';
361
+
362
+ export async function GET() {
363
+ const posts = getAllPosts();
364
+ const siteUrl = process.env.APP_URL || 'http://localhost:3000';
365
+ const feed = generateRSSFeed(posts, siteUrl);
366
+
367
+ return new Response(feed, {
368
+ headers: {
369
+ 'Content-Type': 'application/xml',
370
+ 'Cache-Control': 'public, max-age=3600, s-maxage=3600',
371
+ },
372
+ });
373
+ }
374
+ ```
375
+
376
+ ## Step 10: Sample Post
377
+
378
+ ```markdown
379
+ ---
380
+ title: "Getting Started with Your App"
381
+ description: "A quick guide to setting up and customizing your new application."
382
+ date: "2025-01-15"
383
+ author: "Team"
384
+ tags: ["getting-started", "tutorial"]
385
+ published: true
386
+ ---
387
+
388
+ # Getting Started
389
+
390
+ Welcome to your new application! This guide walks you through the basics.
391
+
392
+ ## Prerequisites
393
+
394
+ Make sure you have the following installed:
395
+ - Node.js 18+
396
+ - Yarn
397
+
398
+ ## Next Steps
399
+
400
+ Explore the dashboard and customize your settings.
401
+ ```
402
+
403
+ ## Step 11: Next.js MDX Configuration
404
+
405
+ Update `next.config.mjs`:
406
+
407
+ ```typescript
408
+ import createMDX from '@next/mdx';
409
+ import remarkGfm from 'remark-gfm';
410
+
411
+ const withMDX = createMDX({
412
+ options: {
413
+ remarkPlugins: [remarkGfm],
414
+ rehypePlugins: [],
415
+ },
416
+ });
417
+
418
+ const nextConfig = {
419
+ pageExtensions: ['ts', 'tsx', 'md', 'mdx'],
420
+ // ... existing config
421
+ };
422
+
423
+ export default withMDX(nextConfig);
424
+ ```
425
+
426
+ ## Testing
427
+
428
+ 1. Create a `.mdx` file in `content/blog/` — verify it appears on `/blog`.
429
+ 2. Click a post — verify the individual page renders with correct metadata.
430
+ 3. Visit `/feed.xml` — verify valid RSS with all published posts.
431
+ 4. Add a post with `published: false` — verify it is excluded from the listing and feed.
432
+ 5. Check Open Graph tags with a social card validator.
433
+ 6. Verify JSON-LD structured data with Google's Rich Results Test.
434
+
435
+ ## Checklist
436
+
437
+ - [ ] MDX dependencies installed (`@next/mdx`, `gray-matter`, `reading-time`, etc.)
438
+ - [ ] `content/blog/` directory created with at least one sample post
439
+ - [ ] Frontmatter schema validates all post metadata
440
+ - [ ] Blog listing page renders all published posts sorted by date
441
+ - [ ] Individual post page renders MDX content with metadata
442
+ - [ ] RSS feed generated at `/feed.xml`
443
+ - [ ] JSON-LD structured data on each post page
444
+ - [ ] Open Graph metadata for social sharing
445
+ - [ ] `generateStaticParams` enables static generation
446
+ - [ ] `next.config.mjs` updated for MDX support
447
+ - [ ] Feature flag checked (`appConfig.features.blog`)