RESTful API to display articles and categories from October CMS to mobile or web applications.
- Requirements
- Installation
- Configuration
- API Endpoints
- Usage Examples
- Folder Structure
- Database
- Error Handling
- Contributing
- PHP 7.4 or higher
- October CMS 3.0 or higher
- MySQL 5.7 or higher
- Composer
- RainLab Blog Plugin (optional, for using built-in blog features)
git clone https://github.com/ferryops/headless-cms-php.git
cd headless-cms-phpcomposer create-project october/october
cd october
php artisan october:installCopy the plugin folder to the plugins directory:
cp -r headless-cms-php plugins/ferryops/articleapiOr install via command:
php artisan plugin:install Ferryops.ArticleAPIphp artisan plugin:install RainLab.Blogphp artisan migrateEdit the file plugins/ferryops/articleapi/routes.php:
Route::prefix('api/v1')->group(function () {
Route::get('articles', 'Ferryops\ArticleAPI\Controllers\Articles@list');
Route::get('articles/{id}', 'Ferryops\ArticleAPI\Controllers\Articles@show');
Route::get('articles/search/{keyword}', 'Ferryops\ArticleAPI\Controllers\Articles@search');
Route::get('categories', 'Ferryops\ArticleAPI\Controllers\Articles@categories');
Route::get('categories/{id}/articles', 'Ferryops\ArticleAPI\Controllers\Articles@byCategory');
});To access the API from mobile applications across domains, add CORS middleware at app/Http/Middleware/Cors.php:
<?php namespace App\Http\Middleware;
use Closure;
class Cors
{
public function handle($request, Closure $next)
{
return $next($request)
->header('Access-Control-Allow-Origin', '*')
->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
->header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
}
}Then register it in app/Http/Kernel.php:
protected $middleware = [
// ...
\App\Http\Middleware\Cors::class,
];Endpoint:
GET /api/v1/articles?page=1
Parameters:
page(optional): Page number, default 1
Response:
{
"status": "success",
"message": "Articles retrieved successfully",
"data": [
{
"id": 1,
"title": "Article Title",
"content": "Article content...",
"slug": "article-title",
"published": true,
"created_at": "2025-01-15 10:30:00",
"updated_at": "2025-01-15 10:30:00",
"categories": [
{
"id": 1,
"name": "Technology",
"slug": "technology"
}
]
}
],
"pagination": {
"total": 50,
"per_page": 10,
"current_page": 1,
"last_page": 5
}
}Endpoint:
GET /api/v1/articles/{id}
Parameters:
id(required): Article ID
Response:
{
"status": "success",
"message": "Article detail retrieved successfully",
"data": {
"id": 1,
"title": "Article Title",
"content": "Full article content...",
"slug": "article-title",
"published": true,
"created_at": "2025-01-15 10:30:00",
"categories": [
{
"id": 1,
"name": "Technology",
"slug": "technology"
}
]
}
}Endpoint:
GET /api/v1/articles/search/{keyword}?page=1
Parameters:
keyword(required): Search keywordpage(optional): Page number
Response:
{
"status": "success",
"message": "Search results retrieved successfully",
"data": [
{
"id": 1,
"title": "Article about Laravel",
"content": "...",
"categories": []
}
],
"pagination": {
"total": 5,
"per_page": 10
}
}Endpoint:
GET /api/v1/categories
Response:
{
"status": "success",
"message": "Categories retrieved successfully",
"data": [
{
"id": 1,
"name": "Technology",
"slug": "technology",
"code": "",
"description": "Articles about technology"
},
{
"id": 2,
"name": "Business",
"slug": "business",
"code": "",
"description": "Articles about business"
}
]
}Endpoint:
GET /api/v1/categories/{id}/articles?page=1
Parameters:
id(required): Category IDpage(optional): Page number
Response:
{
"status": "success",
"message": "Articles in Technology category retrieved successfully",
"category": {
"id": 1,
"name": "Technology",
"slug": "technology"
},
"data": [
{
"id": 1,
"title": "Technology Article",
"content": "...",
"categories": [
{
"id": 1,
"name": "Technology",
"slug": "technology"
}
]
}
],
"pagination": {
"total": 15,
"per_page": 10,
"current_page": 1,
"last_page": 2
}
}import 'package:http/http.dart' as http;
import 'dart:convert';
class ArticleService {
final String baseUrl = 'https://yoursite.com/api/v1';
Future<List<Article>> getArticles({int page = 1}) async {
try {
final response = await http.get(
Uri.parse('$baseUrl/articles?page=$page'),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
return (data['data'] as List)
.map((article) => Article.fromJson(article))
.toList();
}
throw Exception('Failed to load articles');
} catch (e) {
throw Exception('Error: $e');
}
}
Future<Article> getArticleDetail(int id) async {
final response = await http.get(
Uri.parse('$baseUrl/articles/$id'),
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
return Article.fromJson(data['data']);
}
throw Exception('Failed to load article detail');
}
Future<List<Article>> searchArticles(String keyword) async {
final response = await http.get(
Uri.parse('$baseUrl/articles/search/$keyword'),
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
return (data['data'] as List)
.map((article) => Article.fromJson(article))
.toList();
}
throw Exception('Failed to search articles');
}
}
class Article {
final int id;
final String title;
final String content;
final String slug;
final bool published;
final List<Category> categories;
Article({
required this.id,
required this.title,
required this.content,
required this.slug,
required this.published,
required this.categories,
});
factory Article.fromJson(Map<String, dynamic> json) {
return Article(
id: json['id'],
title: json['title'],
content: json['content'],
slug: json['slug'],
published: json['published'],
categories: (json['categories'] as List? ?? [])
.map((cat) => Category.fromJson(cat))
.toList(),
);
}
}
class Category {
final int id;
final String name;
final String slug;
Category({
required this.id,
required this.name,
required this.slug,
});
factory Category.fromJson(Map<String, dynamic> json) {
return Category(
id: json['id'],
name: json['name'],
slug: json['slug'],
);
}
}const API_URL = 'https://yoursite.com/api/v1';
export const articleService = {
getArticles: async (page = 1) => {
try {
const response = await fetch(`${API_URL}/articles?page=${page}`);
const data = await response.json();
return data.data;
} catch (error) {
console.error('Error fetching articles:', error);
throw error;
}
},
getArticleDetail: async (id) => {
try {
const response = await fetch(`${API_URL}/articles/${id}`);
const data = await response.json();
return data.data;
} catch (error) {
console.error('Error fetching article detail:', error);
throw error;
}
},
searchArticles: async (keyword) => {
try {
const response = await fetch(`${API_URL}/articles/search/${keyword}`);
const data = await response.json();
return data.data;
} catch (error) {
console.error('Error searching articles:', error);
throw error;
}
},
getCategories: async () => {
try {
const response = await fetch(`${API_URL}/categories`);
const data = await response.json();
return data.data;
} catch (error) {
console.error('Error fetching categories:', error);
throw error;
}
},
getArticlesByCategory: async (categoryId, page = 1) => {
try {
const response = await fetch(`${API_URL}/categories/${categoryId}/articles?page=${page}`);
const data = await response.json();
return data.data;
} catch (error) {
console.error('Error fetching articles by category:', error);
throw error;
}
}
};plugins/
βββ ferryops/
β βββ articleapi/
β βββ controllers/
β β βββ Articles.php # Main API controller
β βββ models/
β β βββ Article.php # Article model
β β βββ Category.php # Category model
β βββ Plugin.php # Plugin class
β βββ routes.php # API routes
β βββ README.md # Plugin documentation
-
rainlab_blog_posts
- Main table for storing articles
-
rainlab_blog_categories
- Table for storing article categories
-
rainlab_blog_posts_categories
- Pivot table for many-to-many relationship between posts and categories
-- rainlab_blog_posts_categories
CREATE TABLE rainlab_blog_posts_categories (
post_id INT NOT NULL,
category_id INT NOT NULL,
PRIMARY KEY (post_id, category_id)
);{
"status": "error",
"message": "Error message here"
}| HTTP Status | Error | Solution |
|---|---|---|
| 404 | Article not found | Check article ID |
| 500 | SQLSTATE Error | Check model relationships |
| 422 | Validation Error | Check parameters sent |
-
Rate Limiting
Route::middleware('throttle:60,1')->prefix('api/v1')->group(function () { // routes });
-
API Authentication (Coming Soon)
Route::middleware('auth:api')->prefix('api/v1')->group(function () { // protected routes });
-
Input Validation
- Always validate input from mobile applications
- Use sanitization to prevent SQL injection
-
HTTPS
- Ensure API is accessed via HTTPS in production
- β List articles with pagination
- β Article detail
- β Search articles
- β List categories
- β Filter articles by category
- π API Authentication with tokens
- π Rate limiting
- π Filter articles by date
- π Article sorting
Contributions are welcome! Please create a pull request or open an issue for discussion.
MIT License - See LICENSE file for details
For help or questions, please:
- Create an issue on GitHub
- Email: support@example.com
- Visit: https://octobercms.com/
Last Updated: October 2025
