2
0

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

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:
2026-03-07 11:24:56 -05:00
parent 238f0974d8
commit 727ae54f91
6 changed files with 574 additions and 84 deletions

View File

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

View File

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