בנינו אתר סטטי על Next.js עם תוכן המאוחסן ב-Cockpit CMS. הכל עבד במצב פיתוח, אבל בייצור קיבלנו שגיאת 401. התברר שטוקן ה-API לא הועבר בכותרת הבקשה של הבנייה הסטטית. סיפור טיפוסי: CMS ללא ראש נותן גמישות אך דורש ארכיטקטורת בקשות נכונה. נסקור כיצד להגדיר את האינטגרציה כדי להימנע מהפתעות כאלה. להלן המחזור המלא: מהגדרת CORS ועד פריסה עם ISR. מניסיוננו, אינטגרציה טיפוסית יכולה להתבצע תוך 2–4 ימים, וחוסכת כ-$2,500 מתקציב פרויקט של $5,000 בהשוואה ל-Strapi או Contentful. גישת אינטגרציה זו של Cockpit CMS מבטיחה ביצועים חזקים.
למה Cockpit CMS נוח לפרונטאנד?
Cockpit הוא CMS קל משקל ללא ראש ללא סכמה נוקשה. אתה מגדיר אוספים ו-singletons דרך פאנל הניהול, והפרונטאנד מקבל אובייקטי JSON מוכנים. זה מאפשר לשנות את מבנה התוכן ללא מיגרציות מסד נתונים. עבור אתרים סטטיים (SSG) ודפים דינמיים (ISR), Cockpit מספק נקודת כניסה יחידה. לפי התיעוד הרשמי של Cockpit, זה אחד מה-CMS ללא ראש הקלים ביותר לפריסה — 10 דקות עד לבקשה הראשונה.
אילו בעיות אנו פותרים במהלך האינטגרציה
- אימות: הטוקן לא צריך להיכנס לקוד הלקוח. אנו מעבירים אותו דרך משתני סביבה בשרת (
process.env.COCKPIT_API_TOKEN). - מדיניות CORS וטיפול ב-preflight: אם Cockpit פרוס על דומיין אחר, הפרונטאנד לא יכול לבצע בקשות ישירות. אנו מגדירים כותרות CORS על שרת ה-Cockpit (
cockpit/config/cors.php). - אסטרטגיות ביטול מטמון ו-revalidation: קריאות API תכופות מאטות את הטעינה. אנו משתמשים ב-ISR ב-Next.js או במטמון Redis עבור שאילתות טיפוסיות.
- תמונות: Cockpit מייצר URLs עם פרמטרי טרנספורמציה תוך כדי תנועה. אל תשמור קישורים מקוריים — השתמש תמיד ב-
/api/cockpit/image. מתודולוגיית האינטגרציה שלנו ל-Cockpit CMS מכסה את כל ההיבטים הללו.
איך אנחנו עושים את זה: מקרה מלא
עבור פרויקט אחד עם 3 אוספים (מאמרים, שירותים, ביקורות) ו-singleton הגדרות, יישמנו את האינטגרציה תוך 3 ימים. מחסנית: Next.js 14, Cockpit 2.3, TypeScript בשרת, ISR עבור כל סוג תוכן.
לקוח בסיסי
// lib/cockpit.ts
class CockpitClient {
private baseUrl: string;
private token: string;
constructor(url: string, token: string) {
this.baseUrl = url.replace(/\/$/, '');
this.token = token;
}
private async request(path: string, options: RequestInit = {}) {
const res = await fetch(`${this.baseUrl}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
'Cockpit-Token': this.token,
...options.headers,
},
next: { revalidate: 3600 }, // Next.js ISR
});
if (!res.ok) throw new Error(`Cockpit API error: ${res.status}`);
return res.json();
}
// Записи коллекции
async getCollection(name: string, params: CollectionParams = {}) {
const body = {
limit: params.limit || 100,
skip: params.skip || 0,
sort: params.sort || { _created: -1 },
filter: params.filter || {},
populate: params.populate || 1,
fields: params.fields,
};
return this.request(`/api/collections/get/${name}`, {
method: 'POST',
body: JSON.stringify(body),
});
}
// Одна запись по ID
async getCollectionItem(collection: string, id: string) {
return this.request(`/api/collections/get/${collection}`, {
method: 'POST',
body: JSON.stringify({ filter: { _id: id }, limit: 1 }),
});
}
// Singleton
async getSingleton(name: string) {
return this.request(`/api/singletons/get/${name}`);
}
// Изображение с трансформацией
getImageUrl(path: string, options: ImageOptions = {}) {
const params = new URLSearchParams({
src: path,
w: String(options.width || 800),
h: String(options.height || 600),
m: options.mode || 'thumbnail',
q: String(options.quality || 80),
o: '1',
});
return `${this.baseUrl}/api/cockpit/image?${params}&token=${this.token}`;
}
}
export const cockpit = new CockpitClient(
process.env.COCKPIT_URL!,
process.env.COCKPIT_API_TOKEN!
);
Next.js: דפים סטטיים
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const { entries } = await cockpit.getCollection('posts', {
filter: { published: true },
fields: { slug: 1 },
});
return entries.map((post: any) => ({
slug: post.slug,
}));
}
export default async function PostPage({ params }) {
const { entries } = await cockpit.getCollection('posts', {
filter: { slug: params.slug, published: true },
limit: 1,
populate: 2,
});
if (!entries.length) notFound();
const post = entries[0];
return (
<article>
<h1>{post.title}</h1>
{post.image && (
<img
src={cockpit.getImageUrl(post.image.path, {
width: 1200,
height: 630,
})}
alt={post.title}
/>
)}
<div dangerouslySetInnerHTML={{ __html: post.description }} />
</article>
);
} יישום חיפוש
REST API של Cockpit לא תומך בחיפוש טקסט מלא באופן טבעי. אנו מיישמים דרך פילטר regex:
async function searchPosts(query: string) {
const { entries } = await cockpit.getCollection('posts', {
filter: {
published: true,
$or: [
{ title: { $regex: query, $options: 'i' } },
{ description: { $regex: query, $options: 'i' } },
],
},
limit: 20,
});
return entries;
}עבור חיפוש ייצור מלא, אנו מייצרים אינדקס ב-Algolia דרך webhook בעת שינויים.
GraphQL API
Cockpit מספק גם נקודת קצה GraphQL ב-// lib/cockpit.ts class CockpitClient { private baseUrl: string; private token: string; constructor(url: string, token: string) { this.baseUrl = url.replace(/\/$/, ''); this.token = token; } private async request(path: string, options: RequestInit = {}) { const res = await fetch(`${this.baseUrl}${path}`, { ...options, headers: { 'Content-Type': 'application/json', 'Cockpit-Token': this.token, ...options.headers, }, next: { revalidate: 3600 }, // Next.js ISR }); if (!res.ok) throw new Error(`Cockpit API error: ${res.status}`); return res.json(); } // Записи коллекции async getCollection(name: string, params: CollectionParams = {}) { const body = { limit: params.limit || 100, skip: params.skip || 0, sort: params.sort || { _created: -1 }, filter: params.filter || {}, populate: params.populate || 1, fields: params.fields, }; return this.request(`/api/collections/get/${name}`, { method: 'POST', body: JSON.stringify(body), }); } // Одна запись по ID async getCollectionItem(collection: string, id: string) { return this.request(`/api/collections/get/${collection}`, { method: 'POST', body: JSON.stringify({ filter: { _id: id }, limit: 1 }), }); } // Singleton async getSingleton(name: string) { return this.request(`/api/singletons/get/${name}`); } // Изображение с трансформацией getImageUrl(path: string, options: ImageOptions = {}) { const params = new URLSearchParams({ src: path, w: String(options.width || 800), h: String(options.height || 600), m: options.mode || 'thumbnail', q: String(options.quality || 80), o: '1', }); return `${this.baseUrl}/api/cockpit/image?${params}&token=${this.token}`; } } export const cockpit = new CockpitClient( process.env.COCKPIT_URL!, process.env.COCKPIT_API_TOKEN! ); :
query {
posts: collectionGet(collection: "posts", limit: 10, sort: {_created: -1}) {
entries {
_id
title
slug
image
}
total
}
homepage: singletonGet(singleton: "homepage") {
hero_title
hero_subtitle
hero_image
}
} מדריך הגדרה שלב אחר שלב
- התקן את Cockpit על שרת (תיעוד: https://cockpitcms.io).
- צור אוסף בפאנל הניהול, הוסף שדות.
- צור טוקן API בהגדרות.
- הגדר CORS ב-
// app/blog/[slug]/page.tsx export async function generateStaticParams() { const { entries } = await cockpit.getCollection('posts', { filter: { published: true }, fields: { slug: 1 }, }); return entries.map((post: any) => ({ slug: post.slug })); } export default async function PostPage({ params }) { const { entries } = await cockpit.getCollection('posts', { filter: { slug: params.slug, published: true }, limit: 1, populate: 2, }); if (!entries.length) notFound(); const post = entries[0]; return ( <article> <h1>{post.title}</h1> {post.image && ( <img src={cockpit.getImageUrl(post.image.path, { width: 1200, height: 630 })} alt={post.title} /> )} <div dangerouslySetInnerHTML={{ __html: post.description }} /> </article> ); }. - יישם את הלקוח כפי שמוצג לעיל.
- השתמש ב-ISR ב-Next.js עבור מטמון.
השוואה בין Cockpit ל-CMS ללא ראש אחרים
| קריטריון | Cockpit | Strapi | Contentful |
|---|---|---|---|
| זמן פריסה | 10 דקות | 15 דקות | ענן |
| חינם | כן | כן | מוגבל |
| REST+GraphQL | כן | כן | כן |
| לוקליזציה | טבעית | תוסף | מובנית |
Cockpit מנצח בפשטות: הפריסה מהירה פי 1.5 מ-Strapi, ולפרויקטים קטנים הוא חוסך עד 40% בעלויות תשתית.
תהליך העבודה
| שלב | משך | תוצאה |
|---|---|---|
| ניתוח סכמת תוכן | 0.5–1 יום | רשימת אוספים, singletons, פעולות |
| הגדרת API ו-CORS | 0.5 יום | לקוח עובד עם אימות, פילטרים |
| יישום אינטגרציה | 1–2 ימים | קוד לקוח, דפים סטטיים, ISR |
| בדיקות | 0.5 יום | אימות כל נקודות הכניסה, מטמון |
| פריסה ותיעוד | 0.5 יום | Readme, גישה, הוראות |
לוחות זמנים ומה כלול
אינטגרציה של 2–3 אוספים + singleton + תמונות דרך CDN אורכת 2 עד 4 ימים. כתוצאה מכך אתה מקבל:
- לקוח טיפוסי ב-TypeScript
- דפים מוכנים עם יצירה סטטית ו-ISR
- CORS מוגדר והעברת טוקן מאובטחת
- תיעוד על עדכוני תוכן
- שעת ייעוץ על תפעול
שגיאות טיפוסיות
- טוקן בלקוח: לעולם אל תעביר את הטוקן דרך
async function searchPosts(query: string) { const { entries } = await cockpit.getCollection('posts', { filter: { published: true, $or: [ { title: { $regex: query, $options: 'i' } }, { description: { $regex: query, $options: 'i' } }, ], }, limit: 20, }); return entries; }או fetch בצד הלקוח — השתמש ברכיבי שרת ב-Next.js. - חוסר populate: אם לאוסף יש הפניות לרשומות אחרות, אל תשכח את
/api/graphql, אחרת תקבל רק מזהים. - ביטול מטמון: כאשר התוכן משתנה ב-Cockpit, יש צורך לאפס את מטמון ה-ISR. פתרון — webhook ל-
query { posts: collectionGet(collection: "posts", limit: 10, sort: {_created: -1}) { entries { _id title slug image } total } homepage: singletonGet(singleton: "homepage") { hero_title hero_subtitle hero_image } }ב-Next.js.
קבל ייעוץ על אינטגרציה של Cockpit CMS — נעריך את הפרויקט תוך יום אחד. צור קשר, יש לנו מעל 40 אינטגרציות מוצלחות ואנו מבטיחים פעולה יציבה.







