Content Management
FORGE includes a built-in content management system for creating and managing static pages such as About, Terms of Service, Privacy Policy, and any custom content pages your application requires.
Overview
The CMS provides a complete workflow for content creation, including rich text editing, SEO metadata, multi-language translations, featured images, and scheduled publishing.
Database Schema
The contents table stores all managed pages:
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
title | VARCHAR(255) | Page title |
slug | VARCHAR(255) | URL-friendly identifier (unique) |
summary | TEXT | Short description or excerpt |
details | TEXT | Full page content (rich text / HTML) |
button_text | VARCHAR(100) | Optional call-to-action button label |
seo | JSONB | SEO metadata object |
translations | JSONB | Per-language translations |
is_active | BOOLEAN | Whether the page is published |
publish_at | TIMESTAMP | Scheduled publish date |
expire_at | TIMESTAMP | Scheduled expiration date |
sort_order | INTEGER | Display ordering |
created_at | TIMESTAMP | Record creation timestamp |
updated_at | TIMESTAMP | Last update timestamp |
SEO Object Structure
The seo JSONB column stores search engine optimization metadata:
{
"title": "Custom SEO Title",
"description": "Meta description for search engines",
"keywords": "forge, cms, content",
"robots": "index, follow",
"canonical": "https://example.com/about"
}Translations Object Structure
The translations JSONB column holds content for each supported language:
{
"en": {
"title": "About Us",
"summary": "Learn about our company",
"details": "<p>Full content in English...</p>"
},
"ar": {
"title": "من نحن",
"summary": "تعرف على شركتنا",
"details": "<p>المحتوى الكامل بالعربية...</p>"
}
}Featured Images
Content pages support featured images through the polymorphic media system. Images are associated via the media table using model_type = 'content' and model_id referencing the content record.
// Attach a featured image to a content page
media_service.attach(file, "content", content.id, "featured").await?;
// Retrieve the featured image
let image = media_service.get_first_for_model("content", content.id, "featured").await?;API Endpoints
Public Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/contents | List all active content pages |
GET | /api/contents/:slug | Retrieve a single page by slug |
Admin Endpoints
| Method | Path | Description |
|---|---|---|
POST | /api/admin/contents | Create a new content page |
PUT | /api/admin/contents/:id | Update an existing page |
DELETE | /api/admin/contents/:id | Delete a content page |
TIP
Public endpoints automatically filter by is_active = true and respect publish_at / expire_at scheduling. Only currently valid pages are returned.
Example Request
# Create a new content page
curl -X POST /api/admin/contents \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"title": "About Us",
"slug": "about",
"summary": "Learn about our company",
"details": "<h2>Our Story</h2><p>Founded in 2024...</p>",
"button_text": "Contact Us",
"seo": {
"title": "About Us | My App",
"description": "Learn about our company and mission",
"robots": "index, follow"
},
"translations": {
"ar": {
"title": "من نحن",
"summary": "تعرف على شركتنا",
"details": "<h2>قصتنا</h2><p>تأسست في 2024...</p>"
}
},
"is_active": true,
"sort_order": 1
}'Admin Interface
The admin panel provides a full CRUD interface for content management:
- List View -- Table of all content pages with status, title, slug, and actions
- Create / Edit Form -- Rich text editor for the
detailsfield, input fields for title and slug, SEO metadata fields, and translation tabs for each active language - Image Upload -- Drag-and-drop featured image upload with preview
- Scheduling -- Date pickers for
publish_atandexpire_at
WARNING
The slug field must be unique across all content pages. Changing a slug after publication will break existing links unless you implement redirects at the application level.
Public Frontend
Content pages are rendered via dynamic [slug] routes on the public-facing site. When a visitor navigates to /about, the frontend fetches the content record with slug = "about" and renders it.
// Next.js dynamic route: app/[slug]/page.tsx
export default async function ContentPage({ params }: { params: { slug: string } }) {
const content = await fetch(`${API_URL}/api/contents/${params.slug}`);
const data = await content.json();
return (
<article>
<h1>{data.title}</h1>
<div dangerouslySetInnerHTML={{ __html: data.details }} />
</article>
);
}TIP
The frontend automatically selects the correct translation based on the user's active language. If a translation is not available, the default language content is displayed.