feat(pages): add blog post detail and list views to landing pages
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Unit Tests (push) Successful in 3m22s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m29s
Build and Release / Lint (push) Failing after 5m48s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Some checks failed
Build and Release / Create Release (push) Successful in 1s
Build and Release / Unit Tests (push) Successful in 3m22s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m29s
Build and Release / Lint (push) Failing after 5m48s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
Enable blog functionality on custom domains with dedicated views: - Blog post detail page with markdown rendering - Paginated blog list view - Shared context setup for consistent navigation/footer - Route handling for /blog and /blog/:id paths - Template updates across all landing page themes
This commit is contained in:
@@ -5,9 +5,11 @@ package pages
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -54,7 +56,24 @@ func ServeLandingPage(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for blog paths on custom domain
|
||||
if config.Blog.Enabled && repo.BlogEnabled {
|
||||
if strings.HasPrefix(requestPath, "/blog/") {
|
||||
idStr := strings.TrimPrefix(requestPath, "/blog/")
|
||||
idStr = strings.TrimRight(idStr, "/")
|
||||
blogID, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err == nil && blogID > 0 {
|
||||
serveBlogDetail(ctx, repo, config, blogID, "/blog")
|
||||
return
|
||||
}
|
||||
} else if requestPath == "/blog" || requestPath == "/blog/" {
|
||||
serveBlogList(ctx, repo, config, "/blog")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Render the landing page
|
||||
ctx.Data["BlogBaseURL"] = "/blog"
|
||||
renderLandingPage(ctx, repo, config)
|
||||
}
|
||||
|
||||
@@ -99,28 +118,17 @@ func getRepoFromRequest(ctx *context.Context) (*repo_model.Repository, *pages_mo
|
||||
return repo, config, nil
|
||||
}
|
||||
|
||||
// renderLandingPage renders the landing page based on the template
|
||||
func renderLandingPage(ctx *context.Context, repo *repo_model.Repository, config *pages_module.LandingConfig) {
|
||||
// Set up context data
|
||||
// setupLandingPageContext sets up the shared template context data used by both
|
||||
// the landing page and blog views (nav, footer, logo, repo info, etc.)
|
||||
func setupLandingPageContext(ctx *context.Context, repo *repo_model.Repository, config *pages_module.LandingConfig) {
|
||||
ctx.Data["Repository"] = repo
|
||||
ctx.Data["Config"] = config
|
||||
ctx.Data["Title"] = getPageTitle(repo, config)
|
||||
ctx.Data["PageIsPagesLanding"] = true
|
||||
|
||||
// Provide absolute repo URL for links on custom domains
|
||||
ctx.Data["RepoURL"] = repo.HTMLURL()
|
||||
|
||||
// Resolve logo URL based on logo_source config
|
||||
ctx.Data["LogoURL"] = resolveLogoURL(ctx, repo, config)
|
||||
|
||||
// Load README content
|
||||
readme, err := loadReadmeContent(ctx, repo)
|
||||
if err != nil {
|
||||
log.Warn("Failed to load README: %v", err)
|
||||
if ctx.Data["Title"] == nil || ctx.Data["Title"] == "" {
|
||||
ctx.Data["Title"] = getPageTitle(repo, config)
|
||||
}
|
||||
ctx.Data["ReadmeContent"] = readme
|
||||
|
||||
// Load repo stats
|
||||
ctx.Data["PageIsPagesLanding"] = true
|
||||
ctx.Data["RepoURL"] = repo.HTMLURL()
|
||||
ctx.Data["LogoURL"] = resolveLogoURL(ctx, repo, config)
|
||||
ctx.Data["NumStars"] = repo.NumStars
|
||||
ctx.Data["NumForks"] = repo.NumForks
|
||||
ctx.Data["Year"] = time.Now().Year()
|
||||
@@ -130,10 +138,21 @@ func renderLandingPage(ctx *context.Context, repo *repo_model.Repository, config
|
||||
if err == nil && release != nil {
|
||||
_ = repo_model.GetReleaseAttachments(ctx, release)
|
||||
ctx.Data["LatestRelease"] = release
|
||||
// Provide tag name with leading "v" stripped to avoid double "v" in templates
|
||||
ctx.Data["LatestReleaseTag"] = strings.TrimPrefix(release.TagName, "v")
|
||||
}
|
||||
ctx.Data["PublicReleases"] = config.Advanced.PublicReleases
|
||||
}
|
||||
|
||||
// renderLandingPage renders the landing page based on the template
|
||||
func renderLandingPage(ctx *context.Context, repo *repo_model.Repository, config *pages_module.LandingConfig) {
|
||||
setupLandingPageContext(ctx, repo, config)
|
||||
|
||||
// Load README content
|
||||
readme, err := loadReadmeContent(ctx, repo)
|
||||
if err != nil {
|
||||
log.Warn("Failed to load README: %v", err)
|
||||
}
|
||||
ctx.Data["ReadmeContent"] = readme
|
||||
|
||||
// Load recent blog posts if blog section is enabled
|
||||
if config.Blog.Enabled && repo.BlogEnabled {
|
||||
@@ -156,9 +175,92 @@ func renderLandingPage(ctx *context.Context, repo *repo_model.Repository, config
|
||||
}
|
||||
}
|
||||
|
||||
// Select template based on config
|
||||
tpl := selectTemplate(config.Template)
|
||||
ctx.HTML(http.StatusOK, tpl)
|
||||
}
|
||||
|
||||
// serveBlogDetail renders a single blog post within the landing page theme
|
||||
func serveBlogDetail(ctx *context.Context, repo *repo_model.Repository, config *pages_module.LandingConfig, blogID int64, blogBaseURL string) {
|
||||
post, err := blog_model.GetBlogPostByID(ctx, blogID)
|
||||
if err != nil {
|
||||
ctx.NotFound(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify post belongs to this repo and is public
|
||||
if post.RepoID != repo.ID {
|
||||
ctx.NotFound(errors.New("post not found"))
|
||||
return
|
||||
}
|
||||
if post.Status < blog_model.BlogPostPublic {
|
||||
ctx.NotFound(errors.New("post not available"))
|
||||
return
|
||||
}
|
||||
|
||||
_ = post.LoadAuthor(ctx)
|
||||
_ = post.LoadFeaturedImage(ctx)
|
||||
|
||||
// Render markdown content
|
||||
rctx := renderhelper.NewRenderContextRepoComment(ctx, repo)
|
||||
rendered, err := markdown.RenderString(rctx, post.Content)
|
||||
if err != nil {
|
||||
log.Error("Failed to render blog post markdown: %v", err)
|
||||
ctx.ServerError("Failed to render blog post", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = post.Title + " - " + getPageTitle(repo, config)
|
||||
ctx.Data["BlogBaseURL"] = blogBaseURL
|
||||
setupLandingPageContext(ctx, repo, config)
|
||||
|
||||
ctx.Data["PageIsBlogDetail"] = true
|
||||
ctx.Data["BlogPost"] = post
|
||||
ctx.Data["BlogRenderedContent"] = rendered
|
||||
if post.Tags != "" {
|
||||
ctx.Data["BlogTags"] = strings.Split(post.Tags, ",")
|
||||
}
|
||||
|
||||
tpl := selectTemplate(config.Template)
|
||||
ctx.HTML(http.StatusOK, tpl)
|
||||
}
|
||||
|
||||
// serveBlogList renders a paginated list of blog posts within the landing page theme
|
||||
func serveBlogList(ctx *context.Context, repo *repo_model.Repository, config *pages_module.LandingConfig, blogBaseURL string) {
|
||||
page := ctx.FormInt("page")
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
pageSize := 9
|
||||
|
||||
posts, total, err := blog_model.GetBlogPostsByRepoID(ctx, &blog_model.BlogPostSearchOptions{
|
||||
RepoID: repo.ID,
|
||||
AnyPublicStatus: true,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.NotFound(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, post := range posts {
|
||||
_ = post.LoadAuthor(ctx)
|
||||
_ = post.LoadFeaturedImage(ctx)
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = "Blog - " + getPageTitle(repo, config)
|
||||
ctx.Data["BlogBaseURL"] = blogBaseURL
|
||||
setupLandingPageContext(ctx, repo, config)
|
||||
|
||||
ctx.Data["PageIsBlogList"] = true
|
||||
ctx.Data["BlogListPosts"] = posts
|
||||
ctx.Data["BlogListTotal"] = total
|
||||
|
||||
pager := context.NewPagination(int(total), pageSize, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
tpl := selectTemplate(config.Template)
|
||||
ctx.HTML(http.StatusOK, tpl)
|
||||
}
|
||||
|
||||
@@ -371,10 +473,62 @@ func ServeRepoLandingPage(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Render the landing page
|
||||
ctx.Data["BlogBaseURL"] = fmt.Sprintf("/%s/%s/pages/blog", repo.OwnerName, repo.Name)
|
||||
renderLandingPage(ctx, repo, config)
|
||||
}
|
||||
|
||||
// ServeRepoBlogList serves the blog listing within the landing page theme via URL path
|
||||
func ServeRepoBlogList(ctx *context.Context) {
|
||||
repo := ctx.Repo.Repository
|
||||
if repo == nil {
|
||||
ctx.NotFound(errors.New("repository not found"))
|
||||
return
|
||||
}
|
||||
|
||||
config, err := pages_service.GetPagesConfig(ctx, repo)
|
||||
if err != nil || config == nil || !config.Enabled {
|
||||
ctx.NotFound(errors.New("pages not enabled"))
|
||||
return
|
||||
}
|
||||
|
||||
if !config.Blog.Enabled || !repo.BlogEnabled {
|
||||
ctx.NotFound(errors.New("blog not enabled"))
|
||||
return
|
||||
}
|
||||
|
||||
blogBaseURL := fmt.Sprintf("/%s/%s/pages/blog", repo.OwnerName, repo.Name)
|
||||
serveBlogList(ctx, repo, config, blogBaseURL)
|
||||
}
|
||||
|
||||
// ServeRepoBlogDetail serves a single blog post within the landing page theme via URL path
|
||||
func ServeRepoBlogDetail(ctx *context.Context) {
|
||||
repo := ctx.Repo.Repository
|
||||
if repo == nil {
|
||||
ctx.NotFound(errors.New("repository not found"))
|
||||
return
|
||||
}
|
||||
|
||||
config, err := pages_service.GetPagesConfig(ctx, repo)
|
||||
if err != nil || config == nil || !config.Enabled {
|
||||
ctx.NotFound(errors.New("pages not enabled"))
|
||||
return
|
||||
}
|
||||
|
||||
if !config.Blog.Enabled || !repo.BlogEnabled {
|
||||
ctx.NotFound(errors.New("blog not enabled"))
|
||||
return
|
||||
}
|
||||
|
||||
blogID := ctx.PathParamInt64("id")
|
||||
if blogID <= 0 {
|
||||
ctx.NotFound(errors.New("invalid blog post ID"))
|
||||
return
|
||||
}
|
||||
|
||||
blogBaseURL := fmt.Sprintf("/%s/%s/pages/blog", repo.OwnerName, repo.Name)
|
||||
serveBlogDetail(ctx, repo, config, blogID, blogBaseURL)
|
||||
}
|
||||
|
||||
// ServeRepoPageAsset serves static assets for the landing page via URL path
|
||||
func ServeRepoPageAsset(ctx *context.Context) {
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
@@ -1792,6 +1792,8 @@ func registerWebRoutes(m *web.Router) {
|
||||
|
||||
m.Group("/{username}/{reponame}/pages", func() {
|
||||
m.Get("", pages.ServeRepoLandingPage)
|
||||
m.Get("/blog", pages.ServeRepoBlogList)
|
||||
m.Get("/blog/{id}", pages.ServeRepoBlogDetail)
|
||||
m.Get("/assets/*", pages.ServeRepoPageAsset)
|
||||
}, optSignIn, context.RepoAssignment, func(ctx *context.Context) {
|
||||
ctx.Data["PageIsPagesLanding"] = true
|
||||
|
||||
Reference in New Issue
Block a user