README
¶
Block Renderers
This directory contains individual block renderers organized by block type. Each block type has its own folder with its renderer implementation and any associated assets.
Structure
blocks/
├── html/
│ ├── renderer.go # HTMLRenderer implementation
│ └── assets/ # CSS, JS, images for HTML blocks (if needed)
├── menu/
│ ├── renderer.go # Menu BlockRenderer implementation
│ ├── menu_renderer.go # MenuRenderer (comprehensive menu rendering)
│ └── assets/ # CSS, JS, images for menu blocks (if needed)
└── [block-type]/
├── renderer.go # Block renderer implementation
└── assets/ # Block-specific assets
Adding Custom Block Types (External Packages)
Projects that import this package can register their own custom block types without modifying the cmsstore package. This is the recommended approach for extending functionality.
Quick Example
package main
import (
"context"
"github.com/dracory/cmsstore"
)
// 1. Define your custom renderer
type GalleryRenderer struct {
store cmsstore.StoreInterface
}
func (r *GalleryRenderer) Render(ctx context.Context, block cmsstore.BlockInterface) (string, error) {
// Your custom rendering logic
images := parseImages(block.Content())
layout := block.Meta("layout")
return renderGalleryHTML(images, layout), nil
}
// 2. Register it after creating the frontend
func main() {
store := cmsstore.NewStore(...)
frontend := cmsstore.NewFrontend(store, ...)
// Register your custom block type
frontend.BlockRegistry().Register("gallery", &GalleryRenderer{store: store})
// Now blocks with Type() == "gallery" will use your renderer
}
Complete Example with Multiple Custom Block Types
package main
import (
"context"
"fmt"
"github.com/dracory/cmsstore"
)
// Video block renderer
type VideoRenderer struct{}
func (r *VideoRenderer) Render(ctx context.Context, block cmsstore.BlockInterface) (string, error) {
videoURL := block.Meta("video_url")
autoplay := block.Meta("autoplay") == "true"
html := fmt.Sprintf(`
<video src="%s" controls %s>
Your browser does not support the video tag.
</video>
`, videoURL, map[bool]string{true: "autoplay", false: ""}[autoplay])
return html, nil
}
// Carousel block renderer
type CarouselRenderer struct {
store cmsstore.StoreInterface
}
func (r *CarouselRenderer) Render(ctx context.Context, block cmsstore.BlockInterface) (string, error) {
items := parseCarouselItems(block.Content())
interval := block.Meta("interval")
return buildCarouselHTML(items, interval), nil
}
func main() {
store := cmsstore.NewStore(...)
frontend := cmsstore.NewFrontend(store, ...)
// Register multiple custom block types
frontend.BlockRegistry().Register("video", &VideoRenderer{})
frontend.BlockRegistry().Register("carousel", &CarouselRenderer{store: store})
// Start your application
http.HandleFunc("/", frontend.Handler)
http.ListenAndServe(":8080", nil)
}
Best Practices for Custom Renderers
-
Implement the BlockRenderer interface
type BlockRenderer interface { Render(ctx context.Context, block cmsstore.BlockInterface) (string, error) } -
Use block metadata for configuration
layout := block.Meta("layout") cssClass := block.Meta("css_class") -
Handle errors gracefully
if block.Content() == "" { return "<!-- Empty block -->", nil } -
Access store if needed
type CustomRenderer struct { store cmsstore.StoreInterface } -
Return HTML comments for debugging
return "<!-- Custom block rendered successfully -->", nil
Adding Built-in Block Types (Internal)
If you're contributing to the cmsstore package itself:
- Create a new folder:
blocks/[block-type]/ - Create
renderer.gowith your block renderer implementation - Implement the
BlockRendererinterface:type BlockRenderer interface { Render(ctx context.Context, block cmsstore.BlockInterface) (string, error) } - Register the renderer in
frontend/block_renderer.go:registry.Register(cmsstore.BLOCK_TYPE_[TYPE], [type].New[Type]Renderer(f)) - Add any assets to the
assets/subfolder
Interface Requirements
Each renderer should depend on the FrontendStore interface rather than the concrete frontend struct to maintain loose coupling:
type FrontendStore interface {
MenuFindByID(ctx context.Context, id string) (cmsstore.MenuInterface, error)
MenuItemList(ctx context.Context, query cmsstore.MenuItemQueryInterface) ([]cmsstore.MenuItemInterface, error)
MenusEnabled() bool
PageFindByID(ctx context.Context, id string) (cmsstore.PageInterface, error)
Logger() *slog.Logger
}
Architecture Pattern
All renderers follow a consistent pattern:
- Renderer Struct: Each renderer has its own struct (e.g.,
HTMLRenderer,MenuRenderer) - Constructor Function:
New[Type]Renderer()creates renderer instances - Interface Implementation: All implement the
BlockRendererinterface - Delegation: Main
frontend.godelegates to specialized renderers for consistency
Example Pattern:
// In frontend/blocks/[type]/renderer.go
type [Type]Renderer struct { ... }
func New[Type]Renderer(store FrontendStore) *[Type]Renderer { ... }
func (r *[Type]Renderer) Render(ctx context.Context, block cmsstore.BlockInterface) (string, error) { ... }
// In frontend/block_renderer.go
registry.Register(cmsstore.BLOCK_TYPE_[TYPE], [type].New[Type]Renderer(f))
Click to show internal directories.
Click to hide internal directories.