Skip to content

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:

ColumnTypeDescription
idUUIDPrimary key
titleVARCHAR(255)Page title
slugVARCHAR(255)URL-friendly identifier (unique)
summaryTEXTShort description or excerpt
detailsTEXTFull page content (rich text / HTML)
button_textVARCHAR(100)Optional call-to-action button label
seoJSONBSEO metadata object
translationsJSONBPer-language translations
is_activeBOOLEANWhether the page is published
publish_atTIMESTAMPScheduled publish date
expire_atTIMESTAMPScheduled expiration date
sort_orderINTEGERDisplay ordering
created_atTIMESTAMPRecord creation timestamp
updated_atTIMESTAMPLast update timestamp

SEO Object Structure

The seo JSONB column stores search engine optimization metadata:

json
{
  "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:

json
{
  "en": {
    "title": "About Us",
    "summary": "Learn about our company",
    "details": "<p>Full content in English...</p>"
  },
  "ar": {
    "title": "من نحن",
    "summary": "تعرف على شركتنا",
    "details": "<p>المحتوى الكامل بالعربية...</p>"
  }
}

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.

rust
// 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

MethodPathDescription
GET/api/contentsList all active content pages
GET/api/contents/:slugRetrieve a single page by slug

Admin Endpoints

MethodPathDescription
POST/api/admin/contentsCreate a new content page
PUT/api/admin/contents/:idUpdate an existing page
DELETE/api/admin/contents/:idDelete 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

bash
# 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 details field, 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_at and expire_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.

tsx
// 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.

Released under the MIT License.