2
0

feat(blog): add series support and v2 API endpoints

Adds blog series field to group related posts together. Implements v2 API endpoints for listing, creating, updating, and deleting blog posts with proper error codes. Adds series filtering to explore page and sitemap support with pagination. Includes BlogPostV2 structs with author/repo references, HTML URLs, and content rendering. Updates editor UI with series input field.
This commit is contained in:
2026-02-02 13:04:30 -05:00
parent e55529992c
commit 6bc3693cef
15 changed files with 592 additions and 5 deletions

View File

@@ -157,6 +157,12 @@ const (
WikiDisabled ErrorCode = "WIKI_DISABLED"
)
// Blog errors (BLOG_)
const (
BlogPostNotFound ErrorCode = "BLOG_POST_NOT_FOUND"
BlogDisabled ErrorCode = "BLOG_DISABLED"
)
// errorInfo contains metadata about an error code
type errorInfo struct {
Message string
@@ -277,6 +283,10 @@ var errorCatalog = map[ErrorCode]errorInfo{
WikiPageAlreadyExists: {"Wiki page already exists", http.StatusConflict},
WikiReservedName: {"Wiki page name is reserved", http.StatusBadRequest},
WikiDisabled: {"Wiki is disabled for this repository", http.StatusForbidden},
// Blog errors
BlogPostNotFound: {"Blog post not found", http.StatusNotFound},
BlogDisabled: {"Blogs are disabled", http.StatusForbidden},
}
// Message returns the human-readable message for an error code

View File

@@ -0,0 +1,72 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
import "time"
// BlogPostV2 represents a blog post in v2 API format
type BlogPostV2 struct {
ID int64 `json:"id"`
Title string `json:"title"`
Subtitle string `json:"subtitle,omitempty"`
Series string `json:"series,omitempty"`
Content string `json:"content,omitempty"`
ContentHTML string `json:"content_html,omitempty"`
Tags []string `json:"tags"`
Status string `json:"status"`
AllowComments bool `json:"allow_comments"`
FeaturedImageURL string `json:"featured_image_url,omitempty"`
Author *BlogAuthorV2 `json:"author,omitempty"`
Repo *BlogRepoRefV2 `json:"repo,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
PublishedAt *time.Time `json:"published_at,omitempty"`
HTMLURL string `json:"html_url"`
}
// BlogAuthorV2 represents the author of a blog post
type BlogAuthorV2 struct {
ID int64 `json:"id"`
Username string `json:"username"`
Name string `json:"name"`
AvatarURL string `json:"avatar_url"`
}
// BlogRepoRefV2 is a minimal repo reference in blog responses
type BlogRepoRefV2 struct {
ID int64 `json:"id"`
FullName string `json:"full_name"`
HTMLURL string `json:"html_url"`
}
// BlogPostListV2 represents a paginated list of blog posts
type BlogPostListV2 struct {
Posts []*BlogPostV2 `json:"posts"`
TotalCount int64 `json:"total_count"`
HasMore bool `json:"has_more"`
}
// CreateBlogPostV2Option represents options for creating a blog post
type CreateBlogPostV2Option struct {
Title string `json:"title" binding:"Required;MaxSize(255)"`
Subtitle string `json:"subtitle" binding:"MaxSize(500)"`
Series string `json:"series" binding:"MaxSize(255)"`
Content string `json:"content" binding:"Required"`
Tags []string `json:"tags"`
Status string `json:"status"` // draft, public, published
AllowComments bool `json:"allow_comments"`
FeaturedImageUUID string `json:"featured_image_uuid"`
}
// UpdateBlogPostV2Option represents options for updating a blog post
type UpdateBlogPostV2Option struct {
Title *string `json:"title" binding:"MaxSize(255)"`
Subtitle *string `json:"subtitle" binding:"MaxSize(500)"`
Series *string `json:"series" binding:"MaxSize(255)"`
Content *string `json:"content"`
Tags []string `json:"tags"`
Status *string `json:"status"` // draft, public, published
AllowComments *bool `json:"allow_comments"`
FeaturedImageUUID *string `json:"featured_image_uuid"`
}