webhouse.appwebhouse.appdocs

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:

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

go
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

html
<!-- 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:

go
// 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:

go
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

Tags:FrameworksFilesystem Adapter
Previous
Consume from Rails (Ruby)
Next
Consume from C# / .NET
JSON API →Edit on GitHub →