Edit file File name : BLOG_API_DOCUMENTATION.md Content :# Blog API Documentation Complete API documentation for integrating blog functionality into your Flutter app. ## Base URL ``` https://your-domain.com/api/v1 ``` ## Response Format All responses follow this JSON structure: ```json { "data": { "id": 1, "title": "Blog Post Title", "slug": "blog-post-title", "excerpt": "Short excerpt of the blog post...", "content": "Full blog post content...", "featured_image": "url/to/image.jpg", "author": "John Doe", "is_published": true, "published_at": "2024-01-15T10:30:00Z", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z", "app_id": 1 } } ``` For collections (list endpoints): ```json { "data": [ { /* blog post object */ }, { /* blog post object */ } ], "links": { "first": "https://your-domain.com/api/v1/blogs?page=1", "last": "https://your-domain.com/api/v1/blogs?page=5", "prev": null, "next": "https://your-domain.com/api/v1/blogs?page=2" }, "meta": { "current_page": 1, "from": 1, "last_page": 5, "path": "https://your-domain.com/api/v1/blogs", "per_page": 15, "to": 15, "total": 75 } } ``` --- ## Public Endpoints (No Authentication Required) ### 1. Get All Published Blogs Retrieve all published blog posts across all apps with pagination and sorting. **Endpoint:** `GET /blogs` **Query Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `per_page` | integer | 15 | Number of posts per page | | `sort_by` | string | published_at | Sort field: `published_at`, `created_at`, or `title` | | `sort` | string | desc | Sort order: `asc` or `desc` | | `page` | integer | 1 | Page number for pagination | **Example Request:** ```bash curl -X GET "https://your-domain.com/api/v1/blogs?per_page=10&sort_by=published_at&sort=desc&page=1" ``` **Example Response:** ```json { "data": [ { "id": 1, "title": "Getting Started with Flutter", "slug": "getting-started-with-flutter", "excerpt": "Learn the basics of Flutter development...", "content": "Full content here...", "featured_image": "https://cdn.example.com/image1.jpg", "author": "Jane Smith", "is_published": true, "published_at": "2024-01-15T10:30:00Z", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z", "app_id": 1 } ], "links": { /* pagination links */ }, "meta": { /* pagination meta */ } } ``` **HTTP Status Codes:** - `200` - Success - `400` - Bad Request (invalid parameters) --- ### 2. Get Blog Posts by App Retrieve published blog posts for a specific app with pagination. **Endpoint:** `GET /blogs/by-app/{appId}` **Path Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `appId` | integer | The app ID to fetch blogs for | **Query Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `per_page` | integer | 10 | Number of posts per page | | `page` | integer | 1 | Page number for pagination | **Example Request:** ```bash curl -X GET "https://your-domain.com/api/v1/blogs/by-app/5?per_page=10&page=1" ``` **Example Response:** ```json { "data": [ { "id": 1, "title": "MyApp Feature Update", "slug": "myapp-feature-update", "excerpt": "Exciting new features...", "content": "Full content here...", "featured_image": "https://cdn.example.com/image1.jpg", "author": "John Doe", "is_published": true, "published_at": "2024-01-20T14:20:00Z", "created_at": "2024-01-20T14:20:00Z", "updated_at": "2024-01-20T14:20:00Z", "app_id": 5 } ], "links": { /* pagination links */ }, "meta": { /* pagination meta */ } } ``` **HTTP Status Codes:** - `200` - Success - `404` - App not found --- ### 3. Get Single Blog Post by Slug Retrieve a single published blog post using its slug. **Endpoint:** `GET /blog-posts/{slug}` **Path Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `slug` | string | URL-friendly slug of the blog post | **Query Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `app_id` | integer | Required. The app ID the blog belongs to | **Example Request:** ```bash curl -X GET "https://your-domain.com/api/v1/blog-posts/getting-started-with-flutter?app_id=1" ``` **Example Response:** ```json { "data": { "id": 1, "title": "Getting Started with Flutter", "slug": "getting-started-with-flutter", "excerpt": "Learn the basics of Flutter development...", "content": "Full blog content with markdown or HTML...", "featured_image": "https://cdn.example.com/image1.jpg", "author": "Jane Smith", "is_published": true, "published_at": "2024-01-15T10:30:00Z", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z", "app_id": 1 } } ``` **HTTP Status Codes:** - `200` - Success - `400` - Bad Request (missing app_id) - `404` - Blog post not found --- ### 4. Search Blog Posts Search for blog posts by title, content, or excerpt. **Endpoint:** `GET /blogs/search` **Query Parameters:** | Parameter | Type | Min Length | Description | |-----------|------|-----------|-------------| | `q` | string | 2 | Search query | | `per_page` | integer | - | Number of results per page (default: 10) | | `page` | integer | - | Page number for pagination (default: 1) | **Example Request:** ```bash curl -X GET "https://your-domain.com/api/v1/blogs/search?q=flutter&per_page=10&page=1" ``` **Example Response:** ```json { "data": [ { "id": 1, "title": "Getting Started with Flutter", "slug": "getting-started-with-flutter", "excerpt": "Learn the basics of Flutter development...", "content": "Full content here...", "featured_image": "https://cdn.example.com/image1.jpg", "author": "Jane Smith", "is_published": true, "published_at": "2024-01-15T10:30:00Z", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z", "app_id": 1 } ], "links": { /* pagination links */ }, "meta": { /* pagination meta */ } } ``` **HTTP Status Codes:** - `200` - Success - `400` - Bad Request (query less than 2 characters) --- ### 5. Get Recent Blog Posts Retrieve the most recent published blog posts. **Endpoint:** `GET /blogs/recent` **Query Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `limit` | integer | 5 | Maximum number of posts to retrieve | **Example Request:** ```bash curl -X GET "https://your-domain.com/api/v1/blogs/recent?limit=5" ``` **Example Response:** ```json { "data": [ { "id": 5, "title": "Latest Blog Post", "slug": "latest-blog-post", "excerpt": "This is the most recent post...", "content": "Full content here...", "featured_image": "https://cdn.example.com/image5.jpg", "author": "John Smith", "is_published": true, "published_at": "2024-01-25T15:00:00Z", "created_at": "2024-01-25T15:00:00Z", "updated_at": "2024-01-25T15:00:00Z", "app_id": 1 } ] } ``` **HTTP Status Codes:** - `200` - Success --- ## Protected Endpoints (Authentication Required) These endpoints require Sanctum authentication. Include the authentication token in the header: ``` Authorization: Bearer {your_auth_token} ``` ### 6. Create Blog Post Create a new blog post (authenticated user must own the app). **Endpoint:** `POST /blog-posts` **Headers:** ``` Authorization: Bearer {your_auth_token} Content-Type: application/json ``` **Request Body:** ```json { "app_id": 1, "title": "My New Blog Post", "slug": "my-new-blog-post", "content": "The full content of the blog post...", "excerpt": "A short summary of the post...", "featured_image": "https://example.com/image.jpg", "author": "John Doe", "is_published": true, "published_at": "2024-01-26T10:00:00Z" } ``` **Request Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `app_id` | integer | Yes | ID of the app this blog belongs to | | `title` | string | Yes | Blog post title (max 255 characters) | | `slug` | string | No | URL-friendly slug (auto-generated from title if not provided) | | `content` | string | Yes | Full blog post content | | `excerpt` | string | No | Short summary of the post | | `featured_image` | string | No | URL to the featured image | | `author` | string | No | Author name (max 255 characters) | | `is_published` | boolean | No | Publication status (default: false) | | `published_at` | datetime | No | ISO 8601 format publication date | **Example Request:** ```bash curl -X POST "https://your-domain.com/api/v1/blog-posts" \ -H "Authorization: Bearer YOUR_AUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "app_id": 1, "title": "My New Blog Post", "content": "The full content of the blog post...", "excerpt": "A short summary...", "author": "John Doe", "is_published": true, "published_at": "2024-01-26T10:00:00Z" }' ``` **Example Response:** ```json { "data": { "id": 10, "title": "My New Blog Post", "slug": "my-new-blog-post", "excerpt": "A short summary...", "content": "The full content of the blog post...", "featured_image": "https://example.com/image.jpg", "author": "John Doe", "is_published": true, "published_at": "2024-01-26T10:00:00Z", "created_at": "2024-01-26T10:00:00Z", "updated_at": "2024-01-26T10:00:00Z", "app_id": 1 } } ``` **HTTP Status Codes:** - `201` - Created - `400` - Validation failed - `403` - Unauthorized (user doesn't own the app) - `422` - Unprocessable Entity (validation error) --- ### 7. Update Blog Post Update an existing blog post (authenticated user must own the app). **Endpoint:** `PUT /blog-posts/{blogPost}` **Path Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `blogPost` | integer/string | Blog post ID or slug | **Headers:** ``` Authorization: Bearer {your_auth_token} Content-Type: application/json ``` **Request Body:** ```json { "title": "Updated Blog Title", "slug": "updated-blog-title", "content": "Updated content...", "excerpt": "Updated excerpt...", "featured_image": "https://example.com/new-image.jpg", "author": "John Doe", "is_published": true, "published_at": "2024-01-26T10:00:00Z" } ``` **Request Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `title` | string | Blog post title (max 255 characters) | | `slug` | string | URL-friendly slug (must be unique except for this post) | | `content` | string | Full blog post content | | `excerpt` | string | Short summary of the post | | `featured_image` | string | URL to the featured image | | `author` | string | Author name (max 255 characters) | | `is_published` | boolean | Publication status | | `published_at` | datetime | ISO 8601 format publication date | **Example Request:** ```bash curl -X PUT "https://your-domain.com/api/v1/blog-posts/10" \ -H "Authorization: Bearer YOUR_AUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "title": "Updated Blog Title", "content": "Updated content here...", "is_published": true }' ``` **Example Response:** ```json { "data": { "id": 10, "title": "Updated Blog Title", "slug": "updated-blog-title", "excerpt": "Updated excerpt...", "content": "Updated content here...", "featured_image": "https://example.com/new-image.jpg", "author": "John Doe", "is_published": true, "published_at": "2024-01-26T10:00:00Z", "created_at": "2024-01-26T10:00:00Z", "updated_at": "2024-01-26T10:50:00Z", "app_id": 1 } } ``` **HTTP Status Codes:** - `200` - Success - `400` - Validation failed - `403` - Unauthorized (user doesn't own the app) - `404` - Blog post not found - `422` - Unprocessable Entity (validation error) --- ### 8. Delete Blog Post Delete a blog post (authenticated user must own the app). **Endpoint:** `DELETE /blog-posts/{blogPost}` **Path Parameters:** | Parameter | Type | Description | |-----------|------|-------------| | `blogPost` | integer/string | Blog post ID or slug | **Headers:** ``` Authorization: Bearer {your_auth_token} ``` **Example Request:** ```bash curl -X DELETE "https://your-domain.com/api/v1/blog-posts/10" \ -H "Authorization: Bearer YOUR_AUTH_TOKEN" ``` **Example Response:** ```json { "message": "Blog post deleted" } ``` **HTTP Status Codes:** - `200` - Success - `403` - Unauthorized (user doesn't own the app) - `404` - Blog post not found --- ## Flutter Implementation Examples ### Setup (pubspec.yaml) ```yaml dependencies: http: ^1.1.0 flutter_dotenv: ^5.1.0 ``` ### 1. Get All Blogs ```dart import 'package:http/http.dart' as http; import 'dart:convert'; class BlogService { static const String baseUrl = 'https://your-domain.com/api/v1'; Future<List<BlogPost>> getAllBlogs({ int page = 1, int perPage = 15, String sortBy = 'published_at', String sort = 'desc', }) async { try { final queryParams = { 'page': page.toString(), 'per_page': perPage.toString(), 'sort_by': sortBy, 'sort': sort, }; final uri = Uri.parse('$baseUrl/blogs').replace(queryParameters: queryParams); final response = await http.get(uri); if (response.statusCode == 200) { final json = jsonDecode(response.body); final List<dynamic> data = json['data']; return data.map((blog) => BlogPost.fromJson(blog)).toList(); } else { throw Exception('Failed to load blogs'); } } catch (e) { throw Exception('Error: $e'); } } } class BlogPost { final int id; final String title; final String slug; final String excerpt; final String content; final String? featuredImage; final String author; final bool isPublished; final DateTime publishedAt; final DateTime createdAt; final DateTime updatedAt; final int appId; BlogPost({ required this.id, required this.title, required this.slug, required this.excerpt, required this.content, this.featuredImage, required this.author, required this.isPublished, required this.publishedAt, required this.createdAt, required this.updatedAt, required this.appId, }); factory BlogPost.fromJson(Map<String, dynamic> json) { return BlogPost( id: json['id'], title: json['title'], slug: json['slug'], excerpt: json['excerpt'] ?? '', content: json['content'], featuredImage: json['featured_image'], author: json['author'] ?? 'Unknown', isPublished: json['is_published'] ?? false, publishedAt: DateTime.parse(json['published_at']), createdAt: DateTime.parse(json['created_at']), updatedAt: DateTime.parse(json['updated_at']), appId: json['app_id'], ); } } ``` ### 2. Search Blogs ```dart Future<List<BlogPost>> searchBlogs({ required String query, int page = 1, int perPage = 10, }) async { if (query.length < 2) { throw Exception('Search query must be at least 2 characters'); } try { final queryParams = { 'q': query, 'page': page.toString(), 'per_page': perPage.toString(), }; final uri = Uri.parse('$baseUrl/blogs/search').replace(queryParameters: queryParams); final response = await http.get(uri); if (response.statusCode == 200) { final json = jsonDecode(response.body); final List<dynamic> data = json['data']; return data.map((blog) => BlogPost.fromJson(blog)).toList(); } else { throw Exception('Failed to search blogs'); } } catch (e) { throw Exception('Error: $e'); } } ``` ### 3. Get Blog by App ```dart Future<List<BlogPost>> getBlogsByApp({ required int appId, int page = 1, int perPage = 10, }) async { try { final queryParams = { 'page': page.toString(), 'per_page': perPage.toString(), }; final uri = Uri.parse('$baseUrl/blogs/by-app/$appId') .replace(queryParameters: queryParams); final response = await http.get(uri); if (response.statusCode == 200) { final json = jsonDecode(response.body); final List<dynamic> data = json['data']; return data.map((blog) => BlogPost.fromJson(blog)).toList(); } else if (response.statusCode == 404) { throw Exception('App not found'); } else { throw Exception('Failed to load blogs'); } } catch (e) { throw Exception('Error: $e'); } } ``` ### 4. Get Single Blog by Slug ```dart Future<BlogPost> getBlogBySlug({ required String slug, required int appId, }) async { try { final queryParams = { 'app_id': appId.toString(), }; final uri = Uri.parse('$baseUrl/blog-posts/$slug') .replace(queryParameters: queryParams); final response = await http.get(uri); if (response.statusCode == 200) { final json = jsonDecode(response.body); return BlogPost.fromJson(json['data']); } else if (response.statusCode == 404) { throw Exception('Blog post not found'); } else { throw Exception('Failed to load blog'); } } catch (e) { throw Exception('Error: $e'); } } ``` ### 5. Get Recent Blogs ```dart Future<List<BlogPost>> getRecentBlogs({int limit = 5}) async { try { final queryParams = { 'limit': limit.toString(), }; final uri = Uri.parse('$baseUrl/blogs/recent') .replace(queryParameters: queryParams); final response = await http.get(uri); if (response.statusCode == 200) { final json = jsonDecode(response.body); final List<dynamic> data = json['data']; return data.map((blog) => BlogPost.fromJson(blog)).toList(); } else { throw Exception('Failed to load recent blogs'); } } catch (e) { throw Exception('Error: $e'); } } ``` ### 6. Create Blog Post (Authenticated) ```dart Future<BlogPost> createBlogPost({ required String authToken, required int appId, required String title, required String content, String? excerpt, String? slug, String? featuredImage, String? author, bool isPublished = false, String? publishedAt, }) async { try { final body = { 'app_id': appId, 'title': title, 'content': content, if (excerpt != null) 'excerpt': excerpt, if (slug != null) 'slug': slug, if (featuredImage != null) 'featured_image': featuredImage, if (author != null) 'author': author, 'is_published': isPublished, if (publishedAt != null) 'published_at': publishedAt, }; final response = await http.post( Uri.parse('$baseUrl/blog-posts'), headers: { 'Authorization': 'Bearer $authToken', 'Content-Type': 'application/json', }, body: jsonEncode(body), ); if (response.statusCode == 201) { final json = jsonDecode(response.body); return BlogPost.fromJson(json['data']); } else if (response.statusCode == 403) { throw Exception('Unauthorized - you do not own this app'); } else if (response.statusCode == 422) { throw Exception('Validation failed - check your input'); } else { throw Exception('Failed to create blog post'); } } catch (e) { throw Exception('Error: $e'); } } ``` ### 7. Update Blog Post (Authenticated) ```dart Future<BlogPost> updateBlogPost({ required String authToken, required int blogPostId, String? title, String? content, String? excerpt, String? slug, String? featuredImage, String? author, bool? isPublished, String? publishedAt, }) async { try { final body = { if (title != null) 'title': title, if (content != null) 'content': content, if (excerpt != null) 'excerpt': excerpt, if (slug != null) 'slug': slug, if (featuredImage != null) 'featured_image': featuredImage, if (author != null) 'author': author, if (isPublished != null) 'is_published': isPublished, if (publishedAt != null) 'published_at': publishedAt, }; final response = await http.put( Uri.parse('$baseUrl/blog-posts/$blogPostId'), headers: { 'Authorization': 'Bearer $authToken', 'Content-Type': 'application/json', }, body: jsonEncode(body), ); if (response.statusCode == 200) { final json = jsonDecode(response.body); return BlogPost.fromJson(json['data']); } else if (response.statusCode == 403) { throw Exception('Unauthorized - you do not own this app'); } else if (response.statusCode == 404) { throw Exception('Blog post not found'); } else { throw Exception('Failed to update blog post'); } } catch (e) { throw Exception('Error: $e'); } } ``` ### 8. Delete Blog Post (Authenticated) ```dart Future<void> deleteBlogPost({ required String authToken, required int blogPostId, }) async { try { final response = await http.delete( Uri.parse('$baseUrl/blog-posts/$blogPostId'), headers: { 'Authorization': 'Bearer $authToken', }, ); if (response.statusCode == 200) { // Successfully deleted return; } else if (response.statusCode == 403) { throw Exception('Unauthorized - you do not own this app'); } else if (response.statusCode == 404) { throw Exception('Blog post not found'); } else { throw Exception('Failed to delete blog post'); } } catch (e) { throw Exception('Error: $e'); } } ``` --- ## Error Handling All errors return JSON responses with status codes: | Status Code | Meaning | |-------------|---------| | `200` | OK - Request successful | | `201` | Created - Resource created successfully | | `400` | Bad Request - Invalid parameters or missing required fields | | `403` | Forbidden - User doesn't have permission (doesn't own app) | | `404` | Not Found - Resource doesn't exist | | `422` | Unprocessable Entity - Validation errors | | `500` | Internal Server Error - Server error | **Example Error Response:** ```json { "error": "Unauthorized" } ``` or ```json { "message": "Validation failed", "errors": { "title": ["The title field is required."], "content": ["The content field is required."] } } ``` --- ## Rate Limiting Currently, no rate limiting is enforced. However, it's recommended to implement exponential backoff in your Flutter app for production use. --- ## Pagination List endpoints support pagination with the following parameters: - `page` - Current page (default: 1) - `per_page` - Results per page (varies by endpoint) The response includes pagination metadata: ```json { "meta": { "current_page": 1, "from": 1, "last_page": 5, "per_page": 15, "total": 75 }, "links": { "first": "...", "last": "...", "prev": null, "next": "..." } } ``` --- ## Date Formats All dates are in ISO 8601 format with UTC timezone: ``` 2024-01-26T15:30:45Z ``` Parse in Flutter: ```dart DateTime publishedAt = DateTime.parse('2024-01-26T15:30:45Z'); ``` --- ## Authentication For protected endpoints, authenticate using Sanctum tokens: 1. Obtain auth token from your login endpoint 2. Include in all requests: `Authorization: Bearer {token}` Example: ```dart final response = await http.post( Uri.parse('$baseUrl/blog-posts'), headers: { 'Authorization': 'Bearer $authToken', 'Content-Type': 'application/json', }, body: jsonEncode({ 'app_id': 1, 'title': 'My Post', 'content': 'Content here...', }), ); ``` --- ## Testing with cURL Test public endpoints without authentication: ```bash # Get all blogs curl -X GET "https://your-domain.com/api/v1/blogs" # Search blogs curl -X GET "https://your-domain.com/api/v1/blogs/search?q=flutter" # Get blog by app curl -X GET "https://your-domain.com/api/v1/blogs/by-app/1" # Get single blog curl -X GET "https://your-domain.com/api/v1/blog-posts/my-post-slug?app_id=1" # Get recent blogs curl -X GET "https://your-domain.com/api/v1/blogs/recent?limit=5" ``` Test protected endpoints with authentication: ```bash # Create blog post curl -X POST "https://your-domain.com/api/v1/blog-posts" \ -H "Authorization: Bearer YOUR_AUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "app_id": 1, "title": "My New Post", "content": "Content here..." }' # Update blog post curl -X PUT "https://your-domain.com/api/v1/blog-posts/1" \ -H "Authorization: Bearer YOUR_AUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{"title": "Updated Title"}' # Delete blog post curl -X DELETE "https://your-domain.com/api/v1/blog-posts/1" \ -H "Authorization: Bearer YOUR_AUTH_TOKEN" ``` --- ## Static Storage Access Featured images and uploaded files are stored in public storage and accessible via static URLs: **Image URL Format:** ``` https://your-domain.com/storage/blog-posts/{filename} ``` **Example:** ``` https://your-domain.com/storage/blog-posts/D9IYbUZR8bKnPf5V3QIYky5ZOvmmAmps91v6R4Kc.jpg ``` Files are stored with secure filenames. The `featured_image` field in API responses contains the filename, which can be appended to the base storage URL. --- ## Notes - All blog posts must be published and have a `published_at` date to appear in public endpoints - Draft posts are only visible to authenticated users who own the app - Slugs are automatically generated from titles if not provided - Featured images are stored in `/storage/blog-posts/` and accessible via static URLs (no redirect) - The `app_id` parameter is required for fetching a single blog post to ensure proper scoping - Users can only create, update, or delete blog posts for apps they own - Storage symlink is configured to allow direct access to uploaded files Save