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:
<?phpnamespaceApp\Http\Middleware;
useClosure;
class Cors
{
publicfunctionhandle($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';
classArticleService {
finalString 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'] asList)
.map((article) =>Article.fromJson(article))
.toList();
}
throwException('Failed to load articles');
} catch (e) {
throwException('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);
returnArticle.fromJson(data['data']);
}
throwException('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'] asList)
.map((article) =>Article.fromJson(article))
.toList();
}
throwException('Failed to search articles');
}
}
classArticle {
finalint id;
finalString title;
finalString content;
finalString slug;
finalbool published;
finalList<Category> categories;
Article({
requiredthis.id,
requiredthis.title,
requiredthis.content,
requiredthis.slug,
requiredthis.published,
requiredthis.categories,
});
factoryArticle.fromJson(Map<String, dynamic> json) {
returnArticle(
id: json['id'],
title: json['title'],
content: json['content'],
slug: json['slug'],
published: json['published'],
categories: (json['categories'] asList??? [])
.map((cat) =>Category.fromJson(cat))
.toList(),
);
}
}
classCategory {
finalint id;
finalString name;
finalString slug;
Category({
requiredthis.id,
requiredthis.name,
requiredthis.slug,
});
factoryCategory.fromJson(Map<String, dynamic> json) {
returnCategory(
id: json['id'],
name: json['name'],
slug: json['slug'],
);
}
}constAPI_URL='https://yoursite.com/api/v1';exportconstarticleService={getArticles: async(page=1)=>{try{constresponse=awaitfetch(`${API_URL}/articles?page=${page}`);constdata=awaitresponse.json();returndata.data;}catch(error){console.error('Error fetching articles:',error);throwerror;}},getArticleDetail: async(id)=>{try{constresponse=awaitfetch(`${API_URL}/articles/${id}`);constdata=awaitresponse.json();returndata.data;}catch(error){console.error('Error fetching article detail:',error);throwerror;}},searchArticles: async(keyword)=>{try{constresponse=awaitfetch(`${API_URL}/articles/search/${keyword}`);constdata=awaitresponse.json();returndata.data;}catch(error){console.error('Error searching articles:',error);throwerror;}},getCategories: async()=>{try{constresponse=awaitfetch(`${API_URL}/categories`);constdata=awaitresponse.json();returndata.data;}catch(error){console.error('Error fetching categories:',error);throwerror;}},getArticlesByCategory: async(categoryId,page=1)=>{try{constresponse=awaitfetch(`${API_URL}/categories/${categoryId}/articles?page=${page}`);constdata=awaitresponse.json();returndata.data;}catch(error){console.error('Error fetching articles by category:',error);throwerror;}}};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_categoriesCREATETABLErainlab_blog_posts_categories (
post_id INTNOT NULL,
category_id INTNOT 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
