@rune-kit/rune 2.2.0 → 2.2.1
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/README.md +40 -34
- package/compiler/__tests__/pack-split.test.js +145 -0
- package/compiler/bin/rune.js +0 -5
- package/compiler/doctor.js +42 -0
- package/compiler/emitter.js +27 -4
- package/compiler/parser.js +41 -3
- package/compiler/transformer.js +10 -6
- package/compiler/transforms/compliance.js +40 -0
- package/extensions/ai-ml/PACK.md +38 -474
- package/extensions/ai-ml/skills/ai-agents.md +172 -0
- package/extensions/ai-ml/skills/code-sandbox.md +187 -0
- package/extensions/ai-ml/skills/deep-research.md +146 -0
- package/extensions/ai-ml/skills/embedding-search.md +66 -0
- package/extensions/ai-ml/skills/fine-tuning-guide.md +74 -0
- package/extensions/ai-ml/skills/llm-architect.md +125 -0
- package/extensions/ai-ml/skills/llm-integration.md +64 -0
- package/extensions/ai-ml/skills/prompt-patterns.md +72 -0
- package/extensions/ai-ml/skills/rag-patterns.md +66 -0
- package/extensions/ai-ml/skills/web-extraction.md +114 -0
- package/extensions/analytics/PACK.md +19 -484
- package/extensions/analytics/skills/ab-testing.md +72 -0
- package/extensions/analytics/skills/dashboard-patterns.md +83 -0
- package/extensions/analytics/skills/data-validation.md +68 -0
- package/extensions/analytics/skills/funnel-analysis.md +81 -0
- package/extensions/analytics/skills/sql-patterns.md +57 -0
- package/extensions/analytics/skills/statistical-analysis.md +79 -0
- package/extensions/analytics/skills/tracking-setup.md +71 -0
- package/extensions/backend/PACK.md +44 -618
- package/extensions/backend/skills/api-patterns.md +84 -0
- package/extensions/backend/skills/async-pipeline.md +193 -0
- package/extensions/backend/skills/auth-patterns.md +97 -0
- package/extensions/backend/skills/background-jobs.md +133 -0
- package/extensions/backend/skills/caching-patterns.md +108 -0
- package/extensions/backend/skills/cli-generation.md +133 -0
- package/extensions/backend/skills/database-patterns.md +87 -0
- package/extensions/backend/skills/middleware-patterns.md +104 -0
- package/extensions/chrome-ext/PACK.md +19 -921
- package/extensions/chrome-ext/skills/cws-preflight.md +143 -0
- package/extensions/chrome-ext/skills/cws-publish.md +104 -0
- package/extensions/chrome-ext/skills/ext-ai-integration.md +251 -0
- package/extensions/chrome-ext/skills/ext-messaging.md +139 -0
- package/extensions/chrome-ext/skills/ext-storage.md +133 -0
- package/extensions/chrome-ext/skills/mv3-scaffold.md +164 -0
- package/extensions/content/PACK.md +43 -335
- package/extensions/content/skills/blog-patterns.md +88 -0
- package/extensions/content/skills/cms-integration.md +131 -0
- package/extensions/content/skills/content-scoring.md +107 -0
- package/extensions/content/skills/i18n.md +83 -0
- package/extensions/content/skills/mdx-authoring.md +137 -0
- package/extensions/content/skills/reference.md +1014 -0
- package/extensions/content/skills/seo-patterns.md +67 -0
- package/extensions/content/skills/video-repurpose.md +153 -0
- package/extensions/devops/PACK.md +38 -457
- package/extensions/devops/skills/chaos-testing.md +67 -0
- package/extensions/devops/skills/ci-cd.md +75 -0
- package/extensions/devops/skills/docker.md +58 -0
- package/extensions/devops/skills/edge-serverless.md +163 -0
- package/extensions/devops/skills/infra-as-code.md +158 -0
- package/extensions/devops/skills/kubernetes.md +110 -0
- package/extensions/devops/skills/monitoring.md +57 -0
- package/extensions/devops/skills/server-setup.md +64 -0
- package/extensions/devops/skills/ssl-domain.md +42 -0
- package/extensions/ecommerce/PACK.md +62 -226
- package/extensions/ecommerce/skills/cart-system.md +79 -0
- package/extensions/ecommerce/skills/inventory-mgmt.md +102 -0
- package/extensions/ecommerce/skills/order-management.md +126 -0
- package/extensions/ecommerce/skills/payment-integration.md +472 -0
- package/extensions/ecommerce/skills/shopify-dev.md +69 -0
- package/extensions/ecommerce/skills/subscription-billing.md +93 -0
- package/extensions/ecommerce/skills/tax-compliance.md +117 -0
- package/extensions/gamedev/PACK.md +66 -317
- package/extensions/gamedev/skills/asset-pipeline.md +74 -0
- package/extensions/gamedev/skills/audio-system.md +129 -0
- package/extensions/gamedev/skills/camera-system.md +87 -0
- package/extensions/gamedev/skills/ecs.md +98 -0
- package/extensions/gamedev/skills/game-loops.md +72 -0
- package/extensions/gamedev/skills/input-system.md +199 -0
- package/extensions/gamedev/skills/multiplayer.md +180 -0
- package/extensions/gamedev/skills/particles.md +105 -0
- package/extensions/gamedev/skills/physics-engine.md +89 -0
- package/extensions/gamedev/skills/scene-management.md +146 -0
- package/extensions/gamedev/skills/threejs-patterns.md +90 -0
- package/extensions/gamedev/skills/webgl.md +71 -0
- package/extensions/mobile/PACK.md +56 -223
- package/extensions/mobile/skills/app-store-connect.md +152 -0
- package/extensions/mobile/skills/app-store-prep.md +66 -0
- package/extensions/mobile/skills/deep-linking.md +109 -0
- package/extensions/mobile/skills/flutter.md +60 -0
- package/extensions/mobile/skills/ios-build-pipeline.md +142 -0
- package/extensions/mobile/skills/native-bridge.md +66 -0
- package/extensions/mobile/skills/ota-updates.md +97 -0
- package/extensions/mobile/skills/push-notifications.md +111 -0
- package/extensions/mobile/skills/react-native.md +82 -0
- package/extensions/saas/PACK.md +26 -720
- package/extensions/saas/skills/billing-integration.md +121 -0
- package/extensions/saas/skills/feature-flags.md +130 -0
- package/extensions/saas/skills/multi-tenant.md +103 -0
- package/extensions/saas/skills/onboarding-flow.md +139 -0
- package/extensions/saas/skills/subscription-flow.md +95 -0
- package/extensions/saas/skills/team-management.md +144 -0
- package/extensions/security/PACK.md +10 -448
- package/extensions/security/skills/api-security.md +140 -0
- package/extensions/security/skills/compliance.md +68 -0
- package/extensions/security/skills/owasp-audit.md +64 -0
- package/extensions/security/skills/pentest-patterns.md +77 -0
- package/extensions/security/skills/secret-mgmt.md +65 -0
- package/extensions/security/skills/supply-chain.md +65 -0
- package/extensions/trading/PACK.md +18 -535
- package/extensions/trading/skills/chart-components.md +55 -0
- package/extensions/trading/skills/experiment-loop.md +125 -0
- package/extensions/trading/skills/fintech-patterns.md +47 -0
- package/extensions/trading/skills/indicator-library.md +58 -0
- package/extensions/trading/skills/quant-analysis.md +111 -0
- package/extensions/trading/skills/realtime-data.md +58 -0
- package/extensions/trading/skills/trade-logic.md +104 -0
- package/extensions/ui/PACK.md +34 -853
- package/extensions/ui/skills/a11y-audit.md +91 -0
- package/extensions/ui/skills/animation-patterns.md +106 -0
- package/extensions/ui/skills/component-patterns.md +75 -0
- package/extensions/ui/skills/design-decision.md +98 -0
- package/extensions/ui/skills/design-system.md +68 -0
- package/extensions/ui/skills/landing-patterns.md +155 -0
- package/extensions/ui/skills/palette-picker.md +162 -0
- package/extensions/ui/skills/react-health.md +90 -0
- package/extensions/ui/skills/type-system.md +125 -0
- package/extensions/ui/skills/web-vitals.md +153 -0
- package/extensions/zalo/PACK.md +117 -0
- package/extensions/zalo/skills/zalo-oa-mcp.md +317 -0
- package/extensions/zalo/skills/zalo-oa-messaging.md +429 -0
- package/extensions/zalo/skills/zalo-oa-setup.md +236 -0
- package/extensions/zalo/skills/zalo-oa-webhook.md +189 -0
- package/extensions/zalo/skills/zalo-personal-messaging.md +194 -0
- package/extensions/zalo/skills/zalo-personal-setup.md +153 -0
- package/extensions/zalo/skills/zalo-rate-guard.md +219 -0
- package/package.json +1 -1
- package/skills/brainstorm/SKILL.md +63 -1
- package/skills/cook/SKILL.md +84 -5
- package/skills/mcp-builder/SKILL.md +48 -1
- package/skills/review/SKILL.md +42 -5
- package/skills/review-intake/SKILL.md +17 -1
- package/skills/skill-router/SKILL.md +89 -7
- package/skills/team/SKILL.md +24 -1
- package/skills/test/SKILL.md +18 -0
- package/skills/verification/SKILL.md +40 -1
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "@rune/content"
|
|
3
|
-
description: Content platform patterns — blog systems, CMS integration, MDX authoring, internationalization, and
|
|
3
|
+
description: Content platform patterns — blog systems, CMS integration, MDX authoring, internationalization, SEO, video repurposing pipelines, and content scoring.
|
|
4
4
|
metadata:
|
|
5
5
|
author: runedev
|
|
6
|
-
version: "0.
|
|
6
|
+
version: "0.3.0"
|
|
7
7
|
layer: L4
|
|
8
8
|
price: "$9"
|
|
9
9
|
target: Content creators
|
|
10
|
+
format: split
|
|
10
11
|
---
|
|
11
12
|
|
|
12
13
|
# @rune/content
|
|
13
14
|
|
|
14
15
|
## Purpose
|
|
15
16
|
|
|
16
|
-
Content-driven sites break in ways that don't show up until production: blog pages that return 404 after a CMS slug change, MDX files that crash the build when a custom component is missing, translations that show raw keys because the fallback chain is misconfigured, and pages that rank poorly because structured data is malformed or canonical URLs point to the wrong locale. This pack covers the full content stack — authoring, management, localization, and
|
|
17
|
+
Content-driven sites break in ways that don't show up until production: blog pages that return 404 after a CMS slug change, MDX files that crash the build when a custom component is missing, translations that show raw keys because the fallback chain is misconfigured, and pages that rank poorly because structured data is malformed or canonical URLs point to the wrong locale. This pack covers the full content stack — authoring, management, localization, discovery, performance, and analytics — with patterns that keep content sites correct, fast, and findable.
|
|
17
18
|
|
|
18
19
|
## Triggers
|
|
19
20
|
|
|
@@ -23,359 +24,66 @@ Content-driven sites break in ways that don't show up until production: blog pag
|
|
|
23
24
|
- `/rune mdx-authoring` — configure MDX pipeline with custom components
|
|
24
25
|
- `/rune i18n` — implement or audit internationalization
|
|
25
26
|
- `/rune seo-patterns` — audit SEO, structured data, and meta tags
|
|
27
|
+
- `/rune video-repurpose` — build long-to-short video repurposing pipeline
|
|
28
|
+
- `/rune content-scoring` — implement engagement/virality scoring for content
|
|
26
29
|
- Called by `cook` (L1) when content project detected
|
|
27
30
|
- Called by `marketing` (L2) when creating blog content
|
|
28
31
|
|
|
29
32
|
## Skills Included
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return allPosts
|
|
54
|
-
.filter(p => p.status === 'published')
|
|
55
|
-
.filter(p => !category || p.category === category)
|
|
56
|
-
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Reading time utility
|
|
60
|
-
function readingTime(content: string): string {
|
|
61
|
-
const words = content.trim().split(/\s+/).length;
|
|
62
|
-
const minutes = Math.ceil(words / 238);
|
|
63
|
-
return `${minutes} min read`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// RSS feed — app/feed.xml/route.ts
|
|
67
|
-
export async function GET() {
|
|
68
|
-
const posts = getPublishedPosts();
|
|
69
|
-
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
70
|
-
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
71
|
-
<channel>
|
|
72
|
-
<title>My Blog</title>
|
|
73
|
-
<link>${process.env.SITE_URL}</link>
|
|
74
|
-
<atom:link href="${process.env.SITE_URL}/feed.xml" rel="self" type="application/rss+xml"/>
|
|
75
|
-
${posts.slice(0, 20).map(p => `<item>
|
|
76
|
-
<title>${escapeXml(p.title)}</title>
|
|
77
|
-
<link>${process.env.SITE_URL}${p.url}</link>
|
|
78
|
-
<pubDate>${new Date(p.date).toUTCString()}</pubDate>
|
|
79
|
-
<description>${escapeXml(p.excerpt)}</description>
|
|
80
|
-
</item>`).join('\n')}
|
|
81
|
-
</channel>
|
|
82
|
-
</rss>`;
|
|
83
|
-
return new Response(xml, { headers: { 'Content-Type': 'application/xml' } });
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
---
|
|
88
|
-
|
|
89
|
-
### cms-integration
|
|
90
|
-
|
|
91
|
-
CMS integration — Sanity, Contentful, Strapi, PocketBase. Content modeling, preview mode, webhook-triggered rebuilds, draft/published workflows.
|
|
92
|
-
|
|
93
|
-
#### Workflow
|
|
94
|
-
|
|
95
|
-
**Step 1 — Detect CMS setup**
|
|
96
|
-
Use Grep to find CMS SDK usage: `createClient` (Sanity), `contentful`, `strapi`, `PocketBase`, `GROQ`, `graphql` in content-fetching files. Read the CMS client initialization and content queries to understand: CMS provider, content types, preview mode setup, and caching strategy.
|
|
97
|
-
|
|
98
|
-
**Step 2 — Audit CMS integration**
|
|
99
|
-
Check for: no preview/draft mode (editors can't preview before publish), missing webhook for on-demand ISR (content updates require full rebuild), no content validation (malformed CMS data crashes the page), stale cache without revalidation strategy, images served from CMS without optimization (no next/image or equivalent), and missing error boundary for CMS fetch failures.
|
|
100
|
-
|
|
101
|
-
**Step 3 — Emit CMS patterns**
|
|
102
|
-
For Sanity: emit typed GROQ queries with Zod validation, preview mode toggle, and webhook handler. For Contentful: emit typed GraphQL queries, draft/published content switching. For any CMS: emit ISR revalidation endpoint and image optimization pipeline.
|
|
103
|
-
|
|
104
|
-
#### Example
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
// Sanity — typed client with preview mode and ISR webhook
|
|
108
|
-
import { createClient, type QueryParams } from '@sanity/client';
|
|
109
|
-
import { z } from 'zod';
|
|
110
|
-
|
|
111
|
-
const client = createClient({
|
|
112
|
-
projectId: process.env.SANITY_PROJECT_ID!,
|
|
113
|
-
dataset: 'production',
|
|
114
|
-
apiVersion: '2024-01-01',
|
|
115
|
-
useCdn: true,
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
const previewClient = client.withConfig({ useCdn: false, token: process.env.SANITY_PREVIEW_TOKEN });
|
|
119
|
-
|
|
120
|
-
const PostSchema = z.object({
|
|
121
|
-
_id: z.string(),
|
|
122
|
-
title: z.string(),
|
|
123
|
-
slug: z.string(),
|
|
124
|
-
body: z.array(z.any()),
|
|
125
|
-
publishedAt: z.string().datetime(),
|
|
126
|
-
author: z.object({ name: z.string(), image: z.string().url().optional() }),
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
export async function getPost(slug: string, preview = false) {
|
|
130
|
-
const query = `*[_type == "post" && slug.current == $slug][0]{
|
|
131
|
-
_id, title, "slug": slug.current, body, publishedAt,
|
|
132
|
-
"author": author->{ name, "image": image.asset->url }
|
|
133
|
-
}`;
|
|
134
|
-
const result = await (preview ? previewClient : client).fetch(query, { slug });
|
|
135
|
-
return PostSchema.parse(result);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Webhook handler for on-demand ISR — app/api/revalidate/route.ts
|
|
139
|
-
export async function POST(req: Request) {
|
|
140
|
-
const body = await req.json();
|
|
141
|
-
const secret = req.headers.get('x-sanity-webhook-secret');
|
|
142
|
-
if (secret !== process.env.SANITY_WEBHOOK_SECRET) {
|
|
143
|
-
return new Response('Unauthorized', { status: 401 });
|
|
144
|
-
}
|
|
145
|
-
const { revalidatePath } = await import('next/cache');
|
|
146
|
-
revalidatePath(`/blog/${body.slug.current}`);
|
|
147
|
-
return Response.json({ revalidated: true });
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
---
|
|
152
|
-
|
|
153
|
-
### mdx-authoring
|
|
154
|
-
|
|
155
|
-
MDX authoring patterns — custom components in markdown, code blocks with syntax highlighting, interactive examples, table of contents generation.
|
|
156
|
-
|
|
157
|
-
#### Workflow
|
|
158
|
-
|
|
159
|
-
**Step 1 — Detect MDX setup**
|
|
160
|
-
Use Grep to find MDX configuration: `@next/mdx`, `mdx-bundler`, `next-mdx-remote`, `contentlayer`, `rehype`, `remark`. Read the MDX pipeline config to understand: compilation method, custom components registered, and remark/rehype plugin chain.
|
|
161
|
-
|
|
162
|
-
**Step 2 — Audit MDX pipeline**
|
|
163
|
-
Check for: no custom component fallback (missing component crashes build), code blocks without syntax highlighting (plain text), no table of contents generation (long articles hard to navigate), missing image optimization in MDX (raw `<img>` tags), no frontmatter validation (typos in dates or categories silently pass), and no interactive component sandboxing.
|
|
164
|
-
|
|
165
|
-
**Step 3 — Emit MDX patterns**
|
|
166
|
-
Emit: MDX component registry with fallback for missing components, code block with syntax highlighting (Shiki or Prism), auto-generated TOC from headings, frontmatter schema validation, and callout/admonition components.
|
|
167
|
-
|
|
168
|
-
#### Example
|
|
169
|
-
|
|
170
|
-
```tsx
|
|
171
|
-
// MDX component registry with safe fallback
|
|
172
|
-
import { type MDXComponents } from 'mdx/types';
|
|
173
|
-
import { Callout } from '@/components/callout';
|
|
174
|
-
import { CodeBlock } from '@/components/code-block';
|
|
175
|
-
import Image from 'next/image';
|
|
176
|
-
|
|
177
|
-
export function useMDXComponents(): MDXComponents {
|
|
178
|
-
return {
|
|
179
|
-
// Override default elements
|
|
180
|
-
img: ({ src, alt, ...props }) => (
|
|
181
|
-
<Image src={src!} alt={alt || ''} width={800} height={400} className="rounded-lg" {...props} />
|
|
182
|
-
),
|
|
183
|
-
pre: ({ children, ...props }) => <CodeBlock {...props}>{children}</CodeBlock>,
|
|
184
|
-
// Custom components available in MDX
|
|
185
|
-
Callout,
|
|
186
|
-
// Fallback for unknown components — warn instead of crash
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Auto-generated TOC from MDX content
|
|
191
|
-
interface TocItem { id: string; text: string; level: number }
|
|
192
|
-
|
|
193
|
-
function extractToc(raw: string): TocItem[] {
|
|
194
|
-
const headingRegex = /^(#{2,4})\s+(.+)$/gm;
|
|
195
|
-
const items: TocItem[] = [];
|
|
196
|
-
let match;
|
|
197
|
-
while ((match = headingRegex.exec(raw))) {
|
|
198
|
-
const text = match[2].replace(/[`*_~]/g, '');
|
|
199
|
-
items.push({ id: text.toLowerCase().replace(/\s+/g, '-'), text, level: match[1].length });
|
|
200
|
-
}
|
|
201
|
-
return items;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Callout component for MDX
|
|
205
|
-
function Callout({ type = 'info', children }: { type?: 'info' | 'warning' | 'error'; children: React.ReactNode }) {
|
|
206
|
-
const styles = { info: 'bg-blue-50 border-blue-400', warning: 'bg-amber-50 border-amber-400', error: 'bg-red-50 border-red-400' };
|
|
207
|
-
return <div className={`border-l-4 p-4 my-4 rounded-r ${styles[type]}`}>{children}</div>;
|
|
208
|
-
}
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
---
|
|
212
|
-
|
|
213
|
-
### i18n
|
|
214
|
-
|
|
215
|
-
Internationalization — locale routing, translation management, RTL support, date/number formatting, content translation workflows, language detection.
|
|
216
|
-
|
|
217
|
-
#### Workflow
|
|
218
|
-
|
|
219
|
-
**Step 1 — Detect i18n setup**
|
|
220
|
-
Use Grep to find i18n libraries: `next-intl`, `i18next`, `react-intl`, `@formatjs`, `lingui`, `paraglide`. Use Glob to find translation files: `locales/`, `messages/`, `translations/`, `*.json` in locale directories. Read the i18n configuration to understand: supported locales, default locale, routing strategy, and translation loading method.
|
|
221
|
-
|
|
222
|
-
**Step 2 — Audit i18n correctness**
|
|
223
|
-
Check for: missing translations (keys present in default locale but not in others), no fallback chain (missing key shows raw key to user), locale not in URL (breaks SEO — Google can't index per-locale pages), no `hreflang` tags (search engines don't know about locale variants), hardcoded strings in components (bypassing translation system), date/number formatting without locale context (`toLocaleDateString()` without explicit locale), and no RTL support for Arabic/Hebrew locales.
|
|
224
|
-
|
|
225
|
-
**Step 3 — Emit i18n patterns**
|
|
226
|
-
Emit: type-safe translation keys with IDE autocomplete, locale routing middleware, `hreflang` tag generator, date/number formatting utilities, missing translation detection script, and RTL-aware layout component.
|
|
227
|
-
|
|
228
|
-
#### Example
|
|
229
|
-
|
|
230
|
-
```typescript
|
|
231
|
-
// next-intl — type-safe translations with locale routing (Next.js App Router)
|
|
232
|
-
// messages/en.json: { "home": { "title": "Welcome", "posts": "Latest {count, plural, one {post} other {posts}}" } }
|
|
233
|
-
// messages/vi.json: { "home": { "title": "Chao mung", "posts": "{count} bai viet moi nhat" } }
|
|
234
|
-
|
|
235
|
-
// middleware.ts — locale routing
|
|
236
|
-
import createMiddleware from 'next-intl/middleware';
|
|
237
|
-
|
|
238
|
-
export default createMiddleware({
|
|
239
|
-
locales: ['en', 'vi', 'ja'],
|
|
240
|
-
defaultLocale: 'en',
|
|
241
|
-
localePrefix: 'as-needed', // /en/about → /about (default), /vi/about stays
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
export const config = { matcher: ['/((?!api|_next|.*\\..*).*)'] };
|
|
245
|
-
|
|
246
|
-
// Hreflang tags — app/[locale]/layout.tsx
|
|
247
|
-
function HreflangTags({ locale, path }: { locale: string; path: string }) {
|
|
248
|
-
const locales = ['en', 'vi', 'ja'];
|
|
249
|
-
return (
|
|
250
|
-
<>
|
|
251
|
-
{locales.map(l => (
|
|
252
|
-
<link key={l} rel="alternate" hrefLang={l} href={`${process.env.SITE_URL}/${l}${path}`} />
|
|
253
|
-
))}
|
|
254
|
-
<link rel="alternate" hrefLang="x-default" href={`${process.env.SITE_URL}${path}`} />
|
|
255
|
-
</>
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Type-safe translations in components
|
|
260
|
-
import { useTranslations } from 'next-intl';
|
|
261
|
-
|
|
262
|
-
function HomePage() {
|
|
263
|
-
const t = useTranslations('home');
|
|
264
|
-
return <h1>{t('title')}</h1>; // IDE autocomplete for keys
|
|
265
|
-
}
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
---
|
|
269
|
-
|
|
270
|
-
### seo-patterns
|
|
271
|
-
|
|
272
|
-
SEO patterns — structured data (JSON-LD), sitemap generation, canonical URLs, meta tags, Open Graph, Twitter Cards, robots.txt, Core Web Vitals optimization.
|
|
273
|
-
|
|
274
|
-
#### Workflow
|
|
275
|
-
|
|
276
|
-
**Step 1 — Detect SEO implementation**
|
|
277
|
-
Use Grep to find SEO code: `generateMetadata`, `Head`, `next-seo`, `json-ld`, `sitemap`, `robots.txt`, `og:title`, `twitter:card`. Read the metadata configuration and sitemap generation to understand: current meta tag strategy, structured data presence, and sitemap coverage.
|
|
278
|
-
|
|
279
|
-
**Step 2 — Audit SEO completeness**
|
|
280
|
-
Check for: missing or duplicate `<title>` tags, no meta description (or same description on every page), no Open Graph tags (poor social sharing), missing canonical URL (duplicate content risk), no JSON-LD structured data (no rich snippets in search), sitemap not listing all public pages, robots.txt blocking important paths, missing `alt` text on images, and no Core Web Vitals monitoring (LCP, CLS, INP).
|
|
281
|
-
|
|
282
|
-
**Step 3 — Emit SEO patterns**
|
|
283
|
-
Emit: metadata generator with per-page overrides, JSON-LD templates (Article, Product, FAQ, BreadcrumbList), dynamic sitemap generator, canonical URL helper, and Core Web Vitals reporter.
|
|
284
|
-
|
|
285
|
-
#### Example
|
|
286
|
-
|
|
287
|
-
```typescript
|
|
288
|
-
// Next.js App Router — metadata + JSON-LD + sitemap
|
|
289
|
-
import { type Metadata } from 'next';
|
|
290
|
-
|
|
291
|
-
// Reusable metadata generator
|
|
292
|
-
function createMetadata({ title, description, path, image, type = 'website' }: {
|
|
293
|
-
title: string; description: string; path: string; image?: string; type?: string;
|
|
294
|
-
}): Metadata {
|
|
295
|
-
const url = `${process.env.SITE_URL}${path}`;
|
|
296
|
-
return {
|
|
297
|
-
title, description,
|
|
298
|
-
alternates: { canonical: url },
|
|
299
|
-
openGraph: { title, description, url, type, images: image ? [{ url: image, width: 1200, height: 630 }] : [] },
|
|
300
|
-
twitter: { card: 'summary_large_image', title, description, images: image ? [image] : [] },
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// JSON-LD for blog posts
|
|
305
|
-
function ArticleJsonLd({ post }: { post: Post }) {
|
|
306
|
-
const jsonLd = {
|
|
307
|
-
'@context': 'https://schema.org',
|
|
308
|
-
'@type': 'Article',
|
|
309
|
-
headline: post.title,
|
|
310
|
-
datePublished: post.publishedAt,
|
|
311
|
-
dateModified: post.updatedAt || post.publishedAt,
|
|
312
|
-
author: { '@type': 'Person', name: post.author.name },
|
|
313
|
-
image: post.ogImage,
|
|
314
|
-
description: post.excerpt,
|
|
315
|
-
};
|
|
316
|
-
return <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Dynamic sitemap — app/sitemap.ts
|
|
320
|
-
export default async function sitemap() {
|
|
321
|
-
const posts = await getAllPublishedPosts();
|
|
322
|
-
const staticPages = ['', '/about', '/blog', '/contact'];
|
|
323
|
-
return [
|
|
324
|
-
...staticPages.map(path => ({ url: `${process.env.SITE_URL}${path}`, lastModified: new Date(), changeFrequency: 'monthly' as const })),
|
|
325
|
-
...posts.map(post => ({ url: `${process.env.SITE_URL}/blog/${post.slug}`, lastModified: new Date(post.updatedAt || post.publishedAt), changeFrequency: 'weekly' as const })),
|
|
326
|
-
];
|
|
327
|
-
}
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
---
|
|
34
|
+
| Skill | Model | Description |
|
|
35
|
+
|-------|-------|-------------|
|
|
36
|
+
| [blog-patterns](skills/blog-patterns.md) | sonnet | Post management, RSS, pagination, categories |
|
|
37
|
+
| [cms-integration](skills/cms-integration.md) | sonnet | Sanity/Contentful/Strapi, preview, webhooks |
|
|
38
|
+
| [mdx-authoring](skills/mdx-authoring.md) | sonnet | Custom components, TOC, syntax highlighting |
|
|
39
|
+
| [i18n](skills/i18n.md) | sonnet | Locale routing, translations, hreflang, RTL |
|
|
40
|
+
| [seo-patterns](skills/seo-patterns.md) | sonnet | JSON-LD, sitemap, meta tags, Core Web Vitals |
|
|
41
|
+
| [video-repurpose](skills/video-repurpose.md) | sonnet | Long→short video pipeline, captions, face-crop |
|
|
42
|
+
| [content-scoring](skills/content-scoring.md) | sonnet | Virality scoring, engagement metrics, hook analysis |
|
|
43
|
+
| [reference](skills/reference.md) | — | Shared patterns: migration, search, email, perf, analytics, scheduling, a11y, rich media |
|
|
44
|
+
|
|
45
|
+
## Workflows
|
|
46
|
+
|
|
47
|
+
| Workflow | Skills Invoked | Trigger |
|
|
48
|
+
|----------|----------------|---------|
|
|
49
|
+
| New blog from scratch | blog-patterns → mdx-authoring → seo-patterns | `/rune blog-patterns` on empty project |
|
|
50
|
+
| CMS migration | cms-integration → seo-patterns → blog-patterns | New CMS detected, old slugs present |
|
|
51
|
+
| Launch-ready audit | seo-patterns + blog-patterns + i18n (parallel) | Pre-deploy checklist |
|
|
52
|
+
| Multilingual blog | i18n → blog-patterns → seo-patterns | `next-intl` or i18next detected |
|
|
53
|
+
| MDX component library | mdx-authoring → blog-patterns | `*.mdx` files without component registry |
|
|
54
|
+
| Performance audit | seo-patterns (CWV check) + blog-patterns (images) | LCP > 2.5s detected |
|
|
55
|
+
| Search setup | cms-integration + blog-patterns → search integration | Algolia/Meilisearch env vars detected |
|
|
331
56
|
|
|
332
57
|
## Connections
|
|
333
58
|
|
|
334
59
|
```
|
|
335
60
|
Calls → research (L3): SEO data and competitor analysis
|
|
336
61
|
Calls → marketing (L2): content promotion
|
|
62
|
+
Calls → @rune/ui (L4): typography system, article layout patterns, palette for content sites
|
|
337
63
|
Called By ← cook (L1): when content project detected
|
|
338
64
|
Called By ← marketing (L2): when creating blog content
|
|
339
65
|
```
|
|
340
66
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
|
344
|
-
|
|
345
|
-
|
|
|
346
|
-
|
|
|
347
|
-
| MDX | next-mdx-remote, mdx-bundler, @next/mdx | next-mdx-remote for dynamic content |
|
|
348
|
-
| i18n | next-intl, i18next, Paraglide | next-intl for App Router |
|
|
349
|
-
| SEO | Next.js Metadata API, next-seo | Metadata API built-in since Next.js 13 |
|
|
350
|
-
|
|
351
|
-
## Constraints
|
|
352
|
-
|
|
353
|
-
1. MUST validate all CMS content against a schema before rendering — malformed data from CMS should not crash pages.
|
|
354
|
-
2. MUST include `hreflang` tags on all locale-specific pages — missing hreflang hurts international SEO ranking.
|
|
355
|
-
3. MUST NOT hardcode strings in components when i18n is configured — every user-visible string goes through the translation system.
|
|
356
|
-
4. MUST generate sitemap dynamically from actual content — static sitemaps go stale and list nonexistent pages.
|
|
357
|
-
5. MUST provide fallback for missing MDX components — a missing custom component should render a warning, not crash the build.
|
|
67
|
+
| Pack | Connection | When |
|
|
68
|
+
|------|-----------|------|
|
|
69
|
+
| `@rune/analytics` | Page views, scroll depth, read time events → analytics pipeline | Any content site with tracking |
|
|
70
|
+
| `@rune/ui` | Article layout components, image galleries, typography system | Custom component-heavy MDX sites |
|
|
71
|
+
| `@rune/saas` | Auth-gated content (members-only posts), subscription paywalls | Premium content model |
|
|
72
|
+
| `@rune/ecommerce` | Product-linked blog posts, shoppable content, affiliate links | Commerce + content hybrid sites |
|
|
358
73
|
|
|
359
74
|
## Sharp Edges
|
|
360
75
|
|
|
361
76
|
| Failure Mode | Severity | Mitigation |
|
|
362
77
|
|---|---|---|
|
|
363
78
|
| CMS slug change breaks all inbound links (404 on old URLs) | HIGH | Implement redirect map in CMS; check for broken links on content publish webhook |
|
|
364
|
-
| Missing translation key shows raw key string
|
|
365
|
-
| MDX build crashes because custom component removed but still referenced
|
|
366
|
-
|
|
|
367
|
-
|
|
|
368
|
-
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
- Blog system serves paginated posts with RSS feed and reading time
|
|
373
|
-
- CMS integration has preview mode, webhook revalidation, and content validation
|
|
374
|
-
- MDX pipeline renders custom components with fallback for missing ones
|
|
375
|
-
- All user-facing strings go through i18n with fallback chain configured
|
|
376
|
-
- Every public page has unique title, description, OG tags, canonical URL, and JSON-LD
|
|
377
|
-
- Structured report emitted for each skill invoked
|
|
79
|
+
| Missing translation key shows raw key string to users | HIGH | Configure fallback to default locale; run missing key detection in CI |
|
|
80
|
+
| MDX build crashes because custom component removed but still referenced | HIGH | Register fallback component that renders warning in dev, empty div in prod |
|
|
81
|
+
| Search index out of sync after CMS publish | HIGH | Trigger index update in CMS publish webhook, same endpoint as ISR revalidation |
|
|
82
|
+
| Whisper large-v3 halluccinates on audio silence | HIGH | Preprocess audio: detect silence > 2s, split segments, skip silent chunks |
|
|
83
|
+
| yt-dlp breaks on YouTube bot detection (HTTP 429) | HIGH | Use browser-mimicking headers, exponential backoff, rotate user agents |
|
|
84
|
+
| Sitemap includes draft/unpublished pages | MEDIUM | Filter sitemap to `status === 'published'` only; add `noindex` to draft preview pages |
|
|
85
|
+
| `hreflang` tags point to wrong locale | MEDIUM | Generate hreflang from route params, not hardcoded; test with hreflang validator |
|
|
378
86
|
|
|
379
87
|
## Cost Profile
|
|
380
88
|
|
|
381
|
-
~
|
|
89
|
+
~16,000–28,000 tokens per full pack run (all 7 skills). Individual skill: ~2,000–5,000 tokens. Sonnet default. Use haiku for detection scans and alt-text audits; escalate to sonnet for CMS integration, SEO audit, video pipeline, and content scoring.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "blog-patterns"
|
|
3
|
+
pack: "@rune/content"
|
|
4
|
+
description: "Post management, categories/tags, pagination, RSS feeds, reading time, related posts, comment systems."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# blog-patterns
|
|
10
|
+
|
|
11
|
+
Blog system patterns — post management, categories/tags, pagination, RSS feeds, reading time, related posts, comment systems.
|
|
12
|
+
|
|
13
|
+
#### Workflow
|
|
14
|
+
|
|
15
|
+
**Step 1 — Detect blog architecture**
|
|
16
|
+
Use Glob to find blog-related files: `blog/`, `posts/`, `articles/`, `*.mdx`, `*.md` in content directories. Use Grep to find blog utilities: `getStaticPaths`, `generateStaticParams`, `allPosts`, `contentlayer`, `reading-time`. Read the post listing page and individual post page to understand: data source, routing strategy, and rendering pipeline.
|
|
17
|
+
|
|
18
|
+
**Step 2 — Audit blog completeness**
|
|
19
|
+
Check for: missing RSS feed (`feed.xml` or `/api/rss`), no reading time estimation, pagination absent on listing pages (all posts loaded at once), no category/tag filtering, missing related posts, no draft/published state, and OG images not generated per-post.
|
|
20
|
+
|
|
21
|
+
**Step 3 — Emit blog patterns**
|
|
22
|
+
Emit: typed post schema with frontmatter validation, paginated listing with category filter, RSS feed generator, reading time calculator, and related posts by tag similarity.
|
|
23
|
+
|
|
24
|
+
#### Example
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// Next.js App Router — blog listing with pagination and categories
|
|
28
|
+
import { allPosts, type Post } from 'contentlayer/generated';
|
|
29
|
+
|
|
30
|
+
function getPublishedPosts(category?: string): Post[] {
|
|
31
|
+
return allPosts
|
|
32
|
+
.filter(p => p.status === 'published')
|
|
33
|
+
.filter(p => !category || p.category === category)
|
|
34
|
+
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Reading time utility
|
|
38
|
+
function readingTime(content: string): string {
|
|
39
|
+
const words = content.trim().split(/\s+/).length;
|
|
40
|
+
const minutes = Math.ceil(words / 238);
|
|
41
|
+
return `${minutes} min read`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// RSS feed — app/feed.xml/route.ts
|
|
45
|
+
export async function GET() {
|
|
46
|
+
const posts = getPublishedPosts();
|
|
47
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
48
|
+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
49
|
+
<channel>
|
|
50
|
+
<title>My Blog</title>
|
|
51
|
+
<link>${process.env.SITE_URL}</link>
|
|
52
|
+
<atom:link href="${process.env.SITE_URL}/feed.xml" rel="self" type="application/rss+xml"/>
|
|
53
|
+
${posts.slice(0, 20).map(p => `<item>
|
|
54
|
+
<title>${escapeXml(p.title)}</title>
|
|
55
|
+
<link>${process.env.SITE_URL}${p.url}</link>
|
|
56
|
+
<pubDate>${new Date(p.date).toUTCString()}</pubDate>
|
|
57
|
+
<description>${escapeXml(p.excerpt)}</description>
|
|
58
|
+
</item>`).join('\n')}
|
|
59
|
+
</channel>
|
|
60
|
+
</rss>`;
|
|
61
|
+
return new Response(xml, { headers: { 'Content-Type': 'application/xml' } });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Related posts by tag overlap — score by number of shared tags
|
|
65
|
+
function getRelatedPosts(current: Post, all: Post[], limit = 3): Post[] {
|
|
66
|
+
const currentTags = new Set(current.tags ?? []);
|
|
67
|
+
return all
|
|
68
|
+
.filter(p => p.slug !== current.slug && p.status === 'published')
|
|
69
|
+
.map(p => ({ post: p, score: (p.tags ?? []).filter(t => currentTags.has(t)).length }))
|
|
70
|
+
.filter(({ score }) => score > 0)
|
|
71
|
+
.sort((a, b) => b.score - a.score)
|
|
72
|
+
.slice(0, limit)
|
|
73
|
+
.map(({ post }) => post);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Paginated listing
|
|
77
|
+
const PAGE_SIZE = 10;
|
|
78
|
+
function paginatePosts(posts: Post[], page: number) {
|
|
79
|
+
const start = (page - 1) * PAGE_SIZE;
|
|
80
|
+
return {
|
|
81
|
+
posts: posts.slice(start, start + PAGE_SIZE),
|
|
82
|
+
total: posts.length,
|
|
83
|
+
totalPages: Math.ceil(posts.length / PAGE_SIZE),
|
|
84
|
+
hasNext: start + PAGE_SIZE < posts.length,
|
|
85
|
+
hasPrev: page > 1,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
```
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "cms-integration"
|
|
3
|
+
pack: "@rune/content"
|
|
4
|
+
description: "Sanity, Contentful, Strapi, PocketBase. Content modeling, preview mode, webhook-triggered rebuilds, draft/published workflows."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# cms-integration
|
|
10
|
+
|
|
11
|
+
CMS integration — Sanity, Contentful, Strapi, PocketBase. Content modeling, preview mode, webhook-triggered rebuilds, draft/published workflows.
|
|
12
|
+
|
|
13
|
+
#### Workflow
|
|
14
|
+
|
|
15
|
+
**Step 1 — Detect CMS setup**
|
|
16
|
+
Use Grep to find CMS SDK usage: `createClient` (Sanity), `contentful`, `strapi`, `PocketBase`, `GROQ`, `graphql` in content-fetching files. Read the CMS client initialization and content queries to understand: CMS provider, content types, preview mode setup, and caching strategy.
|
|
17
|
+
|
|
18
|
+
**Step 2 — Audit CMS integration**
|
|
19
|
+
Check for: no preview/draft mode (editors can't preview before publish), missing webhook for on-demand ISR (content updates require full rebuild), no content validation (malformed CMS data crashes the page), stale cache without revalidation strategy, images served from CMS without optimization (no next/image or equivalent), and missing error boundary for CMS fetch failures.
|
|
20
|
+
|
|
21
|
+
**Step 3 — Emit CMS patterns**
|
|
22
|
+
For Sanity: emit typed GROQ queries with Zod validation, preview mode toggle, and webhook handler. For Contentful: emit typed GraphQL queries, draft/published content switching. For any CMS: emit ISR revalidation endpoint and image optimization pipeline.
|
|
23
|
+
|
|
24
|
+
#### Example — Sanity
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// Sanity — typed client with preview mode and ISR webhook
|
|
28
|
+
import { createClient, type QueryParams } from '@sanity/client';
|
|
29
|
+
import { z } from 'zod';
|
|
30
|
+
|
|
31
|
+
const client = createClient({
|
|
32
|
+
projectId: process.env.SANITY_PROJECT_ID!,
|
|
33
|
+
dataset: 'production',
|
|
34
|
+
apiVersion: '2024-01-01',
|
|
35
|
+
useCdn: true,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const previewClient = client.withConfig({ useCdn: false, token: process.env.SANITY_PREVIEW_TOKEN });
|
|
39
|
+
|
|
40
|
+
const PostSchema = z.object({
|
|
41
|
+
_id: z.string(),
|
|
42
|
+
title: z.string(),
|
|
43
|
+
slug: z.string(),
|
|
44
|
+
body: z.array(z.any()),
|
|
45
|
+
publishedAt: z.string().datetime(),
|
|
46
|
+
author: z.object({ name: z.string(), image: z.string().url().optional() }),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export async function getPost(slug: string, preview = false) {
|
|
50
|
+
const query = `*[_type == "post" && slug.current == $slug][0]{
|
|
51
|
+
_id, title, "slug": slug.current, body, publishedAt,
|
|
52
|
+
"author": author->{ name, "image": image.asset->url }
|
|
53
|
+
}`;
|
|
54
|
+
const result = await (preview ? previewClient : client).fetch(query, { slug });
|
|
55
|
+
return PostSchema.parse(result);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Webhook handler for on-demand ISR — app/api/revalidate/route.ts
|
|
59
|
+
export async function POST(req: Request) {
|
|
60
|
+
const body = await req.json();
|
|
61
|
+
const secret = req.headers.get('x-sanity-webhook-secret');
|
|
62
|
+
if (secret !== process.env.SANITY_WEBHOOK_SECRET) {
|
|
63
|
+
return new Response('Unauthorized', { status: 401 });
|
|
64
|
+
}
|
|
65
|
+
const { revalidatePath } = await import('next/cache');
|
|
66
|
+
revalidatePath(`/blog/${body.slug.current}`);
|
|
67
|
+
return Response.json({ revalidated: true });
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
#### Example — Contentful
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// Contentful — typed GraphQL with draft/published switching
|
|
75
|
+
import { createClient } from 'contentful';
|
|
76
|
+
|
|
77
|
+
const client = createClient({
|
|
78
|
+
space: process.env.CONTENTFUL_SPACE_ID!,
|
|
79
|
+
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const previewClient = createClient({
|
|
83
|
+
space: process.env.CONTENTFUL_SPACE_ID!,
|
|
84
|
+
accessToken: process.env.CONTENTFUL_PREVIEW_TOKEN!,
|
|
85
|
+
host: 'preview.contentful.com',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export async function getArticle(slug: string, preview = false) {
|
|
89
|
+
const c = preview ? previewClient : client;
|
|
90
|
+
const entries = await c.getEntries({
|
|
91
|
+
content_type: 'article',
|
|
92
|
+
'fields.slug': slug,
|
|
93
|
+
include: 2,
|
|
94
|
+
limit: 1,
|
|
95
|
+
});
|
|
96
|
+
if (!entries.items.length) return null;
|
|
97
|
+
const entry = entries.items[0];
|
|
98
|
+
return {
|
|
99
|
+
title: entry.fields.title as string,
|
|
100
|
+
slug: entry.fields.slug as string,
|
|
101
|
+
body: entry.fields.body,
|
|
102
|
+
publishedAt: entry.sys.createdAt,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### Example — Strapi
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// Strapi v5 — REST with populate and draft/live modes
|
|
111
|
+
const STRAPI = process.env.STRAPI_URL ?? 'http://localhost:1337';
|
|
112
|
+
const TOKEN = process.env.STRAPI_API_TOKEN!;
|
|
113
|
+
|
|
114
|
+
async function strapiGet<T>(path: string, params: Record<string, string> = {}): Promise<T> {
|
|
115
|
+
const url = new URL(`${STRAPI}/api${path}`);
|
|
116
|
+
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
|
|
117
|
+
const res = await fetch(url.toString(), {
|
|
118
|
+
headers: { Authorization: `Bearer ${TOKEN}` },
|
|
119
|
+
next: { revalidate: 60 },
|
|
120
|
+
});
|
|
121
|
+
if (!res.ok) throw new Error(`Strapi error: ${res.status}`);
|
|
122
|
+
return res.json();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const getArticles = () =>
|
|
126
|
+
strapiGet<{ data: StrapiArticle[] }>('/articles', {
|
|
127
|
+
'filters[publishedAt][$notNull]': 'true',
|
|
128
|
+
'populate': 'cover,author,category',
|
|
129
|
+
'sort': 'publishedAt:desc',
|
|
130
|
+
});
|
|
131
|
+
```
|