Skip to content

Repository files navigation

Screen Recording 2025-10-29 at 12 26 22 (1)

Article API - October CMS

RESTful API to display articles and categories from October CMS to mobile or web applications.

📋 Table of Contents

📦 Requirements

  • 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)

🚀 Installation

1. Clone or Download Project

git clone https://github.com/ferryops/headless-cms-php.git
cd headless-cms-php

2. Install October CMS (If not already installed)

composer create-project october/october
cd october
php artisan october:install

3. Install Article API Plugin

Copy the plugin folder to the plugins directory:

cp -r headless-cms-php plugins/ferryops/articleapi

Or install via command:

php artisan plugin:install Ferryops.ArticleAPI

4. Install RainLab Blog Plugin (Optional)

php artisan plugin:install RainLab.Blog

5. Database Migration

php artisan migrate

⚙️ Configuration

1. Update Routes

Edit 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');
});

2. CORS Configuration (Optional)

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,
];

📡 API Endpoints

1. List Articles (Paginated)

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
}
}

2. Get Article Detail

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"
}
]
}
}

3. Search Articles

Endpoint:

GET /api/v1/articles/search/{keyword}?page=1

Parameters:

  • keyword (required): Search keyword
  • page (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
}
}

4. List Categories

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"
}
]
}

5. Get Articles by Category

Endpoint:

GET /api/v1/categories/{id}/articles?page=1

Parameters:

  • id (required): Category ID
  • page (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
}
}

💻 Usage Examples

Flutter

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'],
);
}
}

React Native / JavaScript

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;}}};

📁 Folder Structure

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

🗄️ Database

Tables Used

  1. rainlab_blog_posts

    • Main table for storing articles
  2. rainlab_blog_categories

    • Table for storing article categories
  3. rainlab_blog_posts_categories

    • Pivot table for many-to-many relationship between posts and categories

Table Structure

-- rainlab_blog_posts_categoriesCREATETABLErainlab_blog_posts_categories (
post_id INTNOT NULL,
category_id INTNOT NULL,
PRIMARY KEY (post_id, category_id)
);

⚠️ Error Handling

Error Response Format

{
"status": "error",
"message": "Error message here"
}

Common Errors

HTTP StatusErrorSolution
404Article not foundCheck article ID
500SQLSTATE ErrorCheck model relationships
422Validation ErrorCheck parameters sent

🔒 Security

Best Practices

  1. Rate Limiting

    Route::middleware('throttle:60,1')->prefix('api/v1')->group(function () {
    // routes
    });
  2. API Authentication (Coming Soon)

    Route::middleware('auth:api')->prefix('api/v1')->group(function () {
    // protected routes
    });
  3. Input Validation

    • Always validate input from mobile applications
    • Use sanitization to prevent SQL injection
  4. HTTPS

    • Ensure API is accessed via HTTPS in production

📝 Version Changes

v1.0.0 (Current)

  • ✅ List articles with pagination
  • ✅ Article detail
  • ✅ Search articles
  • ✅ List categories
  • ✅ Filter articles by category

v1.1.0 (Coming Soon)

  • 🚀 API Authentication with tokens
  • 🚀 Rate limiting
  • 🚀 Filter articles by date
  • 🚀 Article sorting

🤝 Contributing

Contributions are welcome! Please create a pull request or open an issue for discussion.

📄 License

MIT License - See LICENSE file for details

📞 Support

For help or questions, please:

📚 References


Last Updated: October 2025

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages