Read @webhouse/cms content from Go — Hugo themes, Gin handlers, and standard library file I/O.
Setup
my-project/
cms.config.ts
content/ # JSON documents
public/uploads/
main.go
templates/Reader package
Create internal/webhouse/webhouse.go:
package webhouse
import (
"encoding/json"
"os"
"path/filepath"
"sort"
"strings"
)
type Document struct {
ID string `json:"id"`
Slug string `json:"slug"`
Status string `json:"status"`
Locale string `json:"locale,omitempty"`
TranslationGroup string `json:"translationGroup,omitempty"`
Data map[string]interface{} `json:"data"`
CreatedAt string `json:"createdAt,omitempty"`
UpdatedAt string `json:"updatedAt,omitempty"`
}
type Client struct {
ContentDir string
}
func New(contentDir string) *Client {
return &Client{ContentDir: contentDir}
}
func (c *Client) Collection(name, locale string) ([]Document, error) {
dir := filepath.Join(c.ContentDir, name)
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var docs []Document
for _, e := range entries {
if !strings.HasSuffix(e.Name(), ".json") {
continue
}
raw, err := os.ReadFile(filepath.Join(dir, e.Name()))
if err != nil {
continue
}
var d Document
if err := json.Unmarshal(raw, &d); err != nil {
continue
}
if d.Status != "published" {
continue
}
if locale != "" && d.Locale != locale {
continue
}
docs = append(docs, d)
}
// Sort by date desc
sort.Slice(docs, func(i, j int) bool {
di, _ := docs[i].Data["date"].(string)
dj, _ := docs[j].Data["date"].(string)
return di > dj
})
return docs, nil
}
func (c *Client) Document(collection, slug string) (*Document, error) {
path := filepath.Join(c.ContentDir, collection, slug+".json")
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var d Document
if err := json.Unmarshal(raw, &d); err != nil {
return nil, err
}
if d.Status != "published" {
return nil, nil
}
return &d, nil
}
func (c *Client) FindTranslation(doc *Document, collection string) (*Document, error) {
if doc.TranslationGroup == "" {
return nil, nil
}
docs, err := c.Collection(collection, "")
if err != nil {
return nil, err
}
for _, other := range docs {
if other.TranslationGroup == doc.TranslationGroup && other.Locale != doc.Locale {
return &other, nil
}
}
return nil, nil
}Gin handler
package main
import (
"html/template"
"net/http"
"github.com/gin-gonic/gin"
"myproject/internal/webhouse"
)
func main() {
wh := webhouse.New("content")
r := gin.Default()
r.LoadHTMLGlob("templates/*")
r.Static("/uploads", "./public/uploads")
r.GET("/", func(c *gin.Context) {
posts, _ := wh.Collection("posts", "en")
c.HTML(http.StatusOK, "home.html", gin.H{"Posts": posts})
})
r.GET("/blog/:slug", func(c *gin.Context) {
post, _ := wh.Document("posts", c.Param("slug"))
if post == nil {
c.AbortWithStatus(404)
return
}
c.HTML(http.StatusOK, "post.html", gin.H{"Post": post})
})
r.Run(":8080")
}Template
<!-- templates/post.html -->
<article>
<h1>{{ index .Post.Data "title" }}</h1>
<time>{{ index .Post.Data "date" }}</time>
<div class="prose">
{{ index .Post.Data "content" | markdown }}
</div>
</article>Hugo integration
Hugo uses its own content system, but you can write a small script that converts @webhouse/cms JSON to Hugo's front matter + markdown format at build time:
// scripts/sync-to-hugo.go
func main() {
wh := webhouse.New("content")
posts, _ := wh.Collection("posts", "")
for _, p := range posts {
frontMatter := fmt.Sprintf("---\ntitle: %q\ndate: %s\n---\n\n%s\n",
p.Data["title"], p.Data["date"], p.Data["content"])
os.WriteFile(fmt.Sprintf("hugo/content/posts/%s.md", p.Slug), []byte(frontMatter), 0644)
}
}Run this before hugo build to sync content.
Caching
Use a sync.Map or in-memory cache with TTL:
var cache sync.Map
func (c *Client) CollectionCached(name, locale string) ([]Document, error) {
key := name + ":" + locale
if v, ok := cache.Load(key); ok {
return v.([]Document), nil
}
docs, err := c.Collection(name, locale)
if err != nil {
return nil, err
}
cache.Store(key, docs)
return docs, nil
}Next steps
- See the Go example
- Learn about Framework-Agnostic Architecture