Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

7 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

HeadlessCMS

A self-hosted, Strapi-inspired Headless CMS built with Laravel 9 and PHP 8.0. Designed for shared hosting (DirectAdmin / cPanel) β€” no Redis, no queue worker required.


✨ Features

Feature Description
πŸ—‚ Content-Type Builder Create Collection and Single types with 16 field types
🧩 Components & Dynamic Zone Reusable field groups and polymorphic content blocks
🌐 Internationalization (i18n) Per-locale entry variants with locale-aware API queries
πŸ”‘ End-User Auth JWT-based authentication for frontend users with role-based permissions
⚑ Webhooks HTTP callbacks on content events (create, publish, delete, etc.)
πŸ–Ό Media Library File uploads with folder organization, alt/caption, and CDN-ready URLs
πŸ” API Tokens Bearer tokens with full-access, read-only, or custom ability scopes
πŸ“‘ REST API Strapi v4-compatible JSON responses with filtering, sorting, pagination, and population

πŸ›  Tech Stack

  • Backend: Laravel 9, PHP 8.0
  • Database: MySQL 5.7+ / MariaDB 10.2+
  • Frontend (Admin): Blade, Alpine.js (CDN), Tailwind CSS (CDN), Tiptap (Rich Text editor)
  • Auth: Laravel Sessions (Admin) + SHA-256 Bearer Tokens + HS256 JWT (End-User)
  • Storage: Laravel Storage disk (local/public)
  • HTTP Client: Guzzle (for Webhook delivery)

βš™οΈ Requirements

Requirement Minimum
PHP 8.0+
MySQL / MariaDB 5.7+ / 10.2+
Composer 2.x
PHP Extensions pdo_mysql, mbstring, openssl, json, gd or imagick, fileinfo
Web Server Apache (with mod_rewrite) or Nginx
Disk ~50 MB for application files

πŸš€ Quick Start

1. Clone and Install

git clone <repository-url> headless-cms
cd headless-cms
composer install

2. Environment Setup

cp .env.example .env
php artisan key:generate

Edit .env with your database credentials:

APP_URL=https://your-domain.com

DB_HOST=127.0.0.1
DB_DATABASE=your_database
DB_USERNAME=your_username
DB_PASSWORD=your_password

3. Database Setup

php artisan migrate
php artisan db:seed --class=AdminSeeder
php artisan storage:link

4. Run Development Server

php artisan serve
# Visit http://127.0.0.1:8000

Tip: Use the built-in Setup Wizard at /setup for a guided installation experience.

5. First Login

Field Value
URL http://127.0.0.1:8000/admin
Email admin@yourcompany.com
Password Admin@1234!

⚠️ Change the default password immediately after first login.


πŸ“‹ Admin Panel Guide

Dashboard /admin

Overview statistics:

  • Total Content Types, Entries, Media files, API Tokens
  • List of 5 most recently updated entries with creator name

Content-Type Builder /admin/content-type-builder

Create and manage your content structure.

Content Type Kinds:

Kind Description
Collection Multiple entries (e.g. Blog Posts, Products, Articles)
Single One entry only (e.g. Homepage settings, About page)

Type Settings:

  • Display Name, API Slug (singular / plural), Description, Icon (emoji)
  • β˜‘ Enable Draft/Publish system β€” allows entries to be in Draft or Published state
  • β˜‘ Enable Internationalization (i18n) β€” enables per-locale entry variants

Field Management: Click Add another field β†’ select a type β†’ configure options. See Field Types Reference for the complete list of 16 field types.


Components /admin/content-type-builder/components

Components are reusable groups of fields that can be embedded in any content type.

Naming convention: namespace.componentName

Examples: shared.seo, blocks.hero, sections.testimonial

Usage in content types:

  • Component field β€” embed a specific component (optionally repeatable)
  • Dynamic Zone field β€” embed an array of heterogeneous component types

Content Manager /admin/content-manager/{slug}

Create and manage entries for each content type.

Entry List:

  • Filter by Status (Draft / Published) β€” only for types with Draft/Publish enabled
  • Filter by Locale (e.g. EN / TH tabs) β€” only for localized types
  • Pagination: 20 entries per page

Entry Form:

  • Dynamic field rendering based on content type definition
  • Sidebar panels:
    • Publication β€” Save as Draft / Publish / Delete
    • Locale (if localized) β€” current locale badge, translate to missing locales, links to existing translations
    • API β€” quick reference URL for this entry

Media Library /admin/media-library

Upload:

  • Max file size: 20 MB per file
  • Files auto-organized into uploads/YYYY/MM/ folders
  • Drag-and-drop or file picker

Features:

  • Search by filename or alt text
  • Filter by folder and file type
  • Edit metadata: alt text, caption
  • Inline Media Picker modal for use in entry forms

Settings β€” Global /admin/settings/global

Setting Description
site_name Displayed in the admin panel title
app_url Base URL used for generating media file URLs in API responses
cors_origins Comma-separated allowed origins. Use * to allow all.
timezone Server timezone for date/time display

Settings β€” Internationalization

Located in the lower section of Settings β†’ Global.

  • Add Locale β€” Enter an ISO 639-1 code: en, th, fr, de, ja, zh-tw, etc.
  • Set Default β€” The fallback locale when no ?locale= parameter is sent in API requests
  • Remove Locale β€” Cannot remove the currently set default locale

Settings β€” API Tokens /admin/settings/api-tokens

Create Bearer tokens to authenticate API requests from your frontend application.

Token Types:

Type Permissions
full-access All read/write actions on all content types and media
read-only find and findOne on all content types + media listing
custom Choose specific actions per content type

Available actions: find, findOne, create, update, delete Upload actions: upload.find, upload.findOne, upload.upload, upload.delete

Optional: Set an expiration duration in days.

⚠️ The token value is shown only once at creation. Store it securely (e.g. .env file).


Settings β€” Users & Permissions /admin/settings/users-permissions

Manage frontend (API) user accounts and role-based access control.

Roles:

  • Public β€” Unauthenticated requests. Configure which endpoints are publicly accessible.
  • Authenticated β€” Logged-in API users. Grant additional permissions.

Permission Matrix: For each role, toggle find, findOne, create, update, delete per content type.

API Users Tab: List all registered users. Block or unblock accounts.


Settings β€” Webhooks /admin/settings/webhooks

Send HTTP POST notifications to external URLs when CMS events occur.

Available events:

Event When it fires
entry.create A new entry is created
entry.update Entry content is updated
entry.publish Entry is published
entry.unpublish Entry is reverted to draft
entry.delete Entry is deleted
media.upload A file is uploaded
media.delete A file is deleted

Custom Headers: Add arbitrary HTTP headers (e.g. Authorization: Bearer secret).

Delivery Logs: View last 10 delivery attempts β€” HTTP status code, response body, success/failure.


Settings β€” Admin Users /admin/settings/users

Manage admin panel accounts.

Roles: superadmin, editor


πŸ“‘ API Reference

Base URL

https://your-domain.com/api

Authentication

API Token (for server-to-server or static site generation):

Authorization: Bearer <api-token>

JWT (for logged-in frontend users):

Authorization: Bearer <jwt-token>

Auth Endpoints

Login

POST /api/auth/local
Content-Type: application/json

{
  "identifier": "user@example.com",
  "password": "password123"
}

Response:

{
  "jwt": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": 1,
    "username": "johndoe",
    "email": "user@example.com",
    "confirmed": true,
    "blocked": false,
    "role": { "id": 2, "name": "Authenticated" }
  }
}

Register

POST /api/auth/local/register
Content-Type: application/json

{
  "username": "johndoe",
  "email": "user@example.com",
  "password": "password123"
}

Requires the Public role to have the appropriate permission enabled.

Get Current User

GET /api/users/me
Authorization: Bearer <jwt>

Update Profile

PUT /api/users/me
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "username": "new_username"
}

Content Endpoints

Use the plural API slug of the content type (e.g. posts, products, articles).

List Entries

GET /api/v1/{slug}
Authorization: Bearer <token>

Get Single Entry

GET /api/v1/{slug}/{id}
Authorization: Bearer <token>

Create Entry

POST /api/v1/{slug}
Authorization: Bearer <token>
Content-Type: application/json

{
  "data": {
    "title": "Hello World",
    "content": "<p>My content</p>",
    "publishedAt": "now"
  }
}

Include "publishedAt": "now" in data to publish immediately. Omit it to save as draft.

Update Entry

PUT /api/v1/{slug}/{id}
Authorization: Bearer <token>
Content-Type: application/json

{
  "data": {
    "title": "Updated Title"
  }
}

Delete Entry

DELETE /api/v1/{slug}/{id}
Authorization: Bearer <token>

Media Endpoints

Upload File

POST /api/v1/upload
Authorization: Bearer <token>
Content-Type: multipart/form-data

files=@/path/to/file.jpg
path=blog-images

List Files

GET /api/v1/upload/files
Authorization: Bearer <token>

Get Single File

GET /api/v1/upload/files/{id}
Authorization: Bearer <token>

Delete File

DELETE /api/v1/upload/files/{id}
Authorization: Bearer <token>

Query Parameters

Filtering

GET /api/v1/posts?filters[title][$eq]=Hello
GET /api/v1/posts?filters[title][$contains]=world
GET /api/v1/posts?filters[views][$gte]=100
GET /api/v1/posts?filters[status][$in][0]=published&filters[status][$in][1]=draft

Supported operators:

Operator Description
$eq Equal to
$ne Not equal to
$lt Less than
$lte Less than or equal
$gt Greater than
$gte Greater than or equal
$contains Contains substring (case-insensitive)
$in Value in array
$null Is null (true/false)

Sorting

GET /api/v1/posts?sort=createdAt:desc
GET /api/v1/posts?sort=title:asc

Pagination

GET /api/v1/posts?pagination[page]=2&pagination[pageSize]=10

Default: page=1, pageSize=25. Maximum pageSize: 100.

Population (Relations & Media)

GET /api/v1/posts?populate=*
GET /api/v1/posts?populate=thumbnail
GET /api/v1/posts?populate=author,tags

Without populate, relation and media fields return {"data": null}.

Publication State

GET /api/v1/posts?publicationState=live     (default β€” published only)
GET /api/v1/posts?publicationState=preview  (include drafts β€” requires full-access token)

Locale (i18n)

GET /api/v1/posts                  (default locale)
GET /api/v1/posts?locale=th        (Thai entries only)
GET /api/v1/posts?locale=all       (all locales)

Response Format

All responses follow the Strapi v4 format.

Collection (list):

{
  "data": [
    {
      "id": 1,
      "attributes": {
        "title": "My First Post",
        "locale": "en",
        "createdAt": "2026-03-15T10:00:00.000Z",
        "updatedAt": "2026-03-15T10:00:00.000Z",
        "publishedAt": "2026-03-15T10:00:00.000Z"
      }
    }
  ],
  "meta": {
    "pagination": {
      "page": 1,
      "pageSize": 25,
      "pageCount": 4,
      "total": 92
    }
  }
}

Single entry:

{
  "data": {
    "id": 1,
    "attributes": {
      "title": "My First Post",
      "thumbnail": {
        "data": {
          "id": 5,
          "attributes": {
            "name": "cover.jpg",
            "url": "/storage/uploads/2026/03/cover.jpg",
            "mime": "image/jpeg",
            "size": 120.5,
            "width": 1200,
            "height": 630
          }
        }
      }
    }
  }
}

Error:

{
  "data": null,
  "error": {
    "status": 404,
    "name": "entry.notFound",
    "message": "Entry not found.",
    "details": {}
  }
}

Code Examples

JavaScript / TypeScript

const API_URL = 'https://your-domain.com/api/v1';
const TOKEN   = 'your-api-token';

// List published posts with populated thumbnail
const res  = await fetch(`${API_URL}/posts?populate=thumbnail`, {
  headers: { Authorization: `Bearer ${TOKEN}` }
});
const { data, meta } = await res.json();

// Create a post (published immediately)
const post = await fetch(`${API_URL}/posts`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${TOKEN}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    data: { title: 'Hello World', content: '<p>...</p>', publishedAt: 'now' }
  })
});

cURL

# List posts filtered by title
curl -H "Authorization: Bearer TOKEN" \
  "https://your-domain.com/api/v1/posts?filters[title][\$contains]=hello&populate=*"

# Upload a file
curl -X POST \
  -H "Authorization: Bearer TOKEN" \
  -F "files=@./image.jpg" \
  -F "path=blog" \
  "https://your-domain.com/api/v1/upload"

πŸ“ Field Types Reference

Type Description API Output Example
text Single-line string "Hello World"
textarea Multi-line string "Line 1\nLine 2"
richtext HTML from WYSIWYG editor (Tiptap) "<p>Hello <strong>World</strong></p>"
email Email address with format validation "user@example.com"
number Integer or float 42 or 3.14
boolean True/False true
date Date only (YYYY-MM-DD) "2026-03-15"
datetime Date + time (ISO 8601) "2026-03-15T10:00:00"
password Bcrypt-hashed β€” write only, never returned in API β€”
enumeration Select from predefined list "published"
uid URL-safe slug (auto-generated from source field) "my-post-slug"
media File/image attachment { "data": { "id": 1, "attributes": {...} } }
relation Link to entries of another content type { "data": [ {"id": 2, ...} ] }
json Free-form JSON object { "key": "value" }
component Embedded reusable component { "__component": "shared.seo", "metaTitle": "..." }
dynamiczone Array of mixed component types [{ "__component": "blocks.hero", "heading": "..." }]

Common Field Options:

Option Applicable Types Description
required Most Value must be provided on save
private Most Field hidden from API responses
unique text, email Value must be unique within the content type
minLength / maxLength text, textarea, email String length constraints
min / max number Numeric range constraints
enum_values enumeration Newline-separated list of allowed values
target_field uid Field name to auto-generate slug from
multiple media Allow multiple file selections
relation relation oneToOne, oneToMany, or manyToMany
component_id component ID of the component to embed
repeatable component Allow multiple instances in an array
allowed_component_ids dynamiczone Array of component IDs that can be added
localizable all Whether value varies per locale (default: true)

⚑ Webhooks

Payload Format

Content events (entry.*):

{
  "event": "entry.publish",
  "createdAt": "2026-03-15T10:00:00.000Z",
  "model": "posts",
  "uid": "api::posts.posts",
  "entry": {
    "id": 1,
    "status": "published",
    "createdAt": "2026-03-15T09:00:00.000Z",
    "updatedAt": "2026-03-15T10:00:00.000Z"
  }
}

Media events (media.*):

{
  "event": "media.upload",
  "createdAt": "2026-03-15T10:00:00.000Z",
  "media": {
    "id": 5,
    "filename": "cover.jpg",
    "url": "/storage/uploads/2026/03/cover.jpg",
    "mime_type": "image/jpeg",
    "size": 123456
  }
}

Next.js ISR Revalidation Example

Webhook configuration:

  • URL: https://your-nextjs.com/api/revalidate
  • Events: entry.publish, entry.unpublish, entry.delete
  • Custom Header: x-webhook-secret: YOUR_SECRET
// app/api/revalidate/route.ts (Next.js App Router)
export async function POST(req: Request) {
  const secret = req.headers.get('x-webhook-secret');
  if (secret !== process.env.WEBHOOK_SECRET) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }
  const body = await req.json();
  revalidatePath(`/blog`);
  if (body.entry?.id) revalidatePath(`/blog/${body.entry.id}`);
  return Response.json({ revalidated: true });
}

🌐 Internationalization (i18n)

Setup

  1. Settings β†’ Global β†’ Internationalization β†’ Add locale codes Examples: en, th, fr, de, ja, ko, zh-tw
  2. Set the default locale (fallback when ?locale= is not specified in API calls)

Enable on a Content Type

  1. Content-Type Builder β†’ [Type Name] β†’ Type Settings
  2. Check "Enable Internationalization (i18n)"
  3. Save Changes

Creating Translations

  1. Open an entry in Content Manager
  2. In the sidebar Locale card β†’ click + Add translation for the target locale
  3. A new entry form opens pre-filled with non-localizable field values (shared across locales)
  4. Fill in the translated content and save

Per-Field Localizability

By default all fields are localizable (each locale has its own value). To share a value across all locales (e.g. a category relation), edit the field β†’ uncheck Localizable.

API Filtering

# Default locale entries (no param)
GET /api/v1/posts

# Specific locale
GET /api/v1/posts?locale=th

# All locales (each entry includes "locale" in attributes)
GET /api/v1/posts?locale=all

🚒 Deployment

Shared Hosting β€” DirectAdmin / cPanel

1. Upload Files

Upload the project to your server via SFTP (e.g. to ~/headless-cms/).

2. Configure Environment

cp .env.production.example .env
APP_ENV=production
APP_DEBUG=false
APP_URL=https://your-domain.com

DB_HOST=localhost
DB_DATABASE=your_db
DB_USERNAME=your_user
DB_PASSWORD=your_password

SESSION_DRIVER=file
CACHE_DRIVER=file

3. Install & Setup

composer install --no-dev --optimize-autoloader
php artisan key:generate
php artisan migrate --force
php artisan db:seed --class=AdminSeeder --force
php artisan storage:link

4. Optimize

php artisan config:cache
php artisan route:cache
php artisan view:cache
# or simply:
bash optimize.sh

5. Point DocumentRoot to /public

In DirectAdmin β†’ Domains β†’ your domain β†’ Document Root:

/home/username/headless-cms/public

The /public/.htaccess file handles URL rewriting automatically (mod_rewrite must be enabled).


VPS / Dedicated Server β€” Nginx

server {
    listen 80;
    server_name your-domain.com;
    root /var/www/headless-cms/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.0-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
        expires max;
        log_not_found off;
    }
}

πŸ“ Project Structure

headless-cms/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ Http/
β”‚   β”‚   β”œβ”€β”€ Controllers/Admin/    # Admin panel controllers
β”‚   β”‚   β”œβ”€β”€ Controllers/Api/      # REST API controllers
β”‚   β”‚   β”œβ”€β”€ Controllers/Setup/    # Installation wizard
β”‚   β”‚   └── Middleware/           # ApiTokenAuth, cors, installed checks
β”‚   β”œβ”€β”€ Models/                   # Eloquent models (Entry, Field, Component, ApiUser, ...)
β”‚   └── Services/                 # Business logic (FieldRenderer, EntryService, WebhookService, ...)
β”œβ”€β”€ database/
β”‚   └── migrations/               # 17 migration files
β”œβ”€β”€ resources/
β”‚   └── views/admin/              # Blade templates for every admin section
β”œβ”€β”€ routes/
β”‚   β”œβ”€β”€ web.php                   # Admin panel routes (auth-protected)
β”‚   └── api.php                   # REST API routes
β”œβ”€β”€ storage/app/public/           # Uploaded files (linked to public/storage)
└── public/                       # Web root β€” index.php + .htaccess

πŸ”’ Default Credentials

Account Email Password
Admin admin@yourcompany.com Admin@1234!

⚠️ Change these immediately after installation via Settings β†’ Users.


πŸ“„ License

MIT License β€” free for personal and commercial use.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages