generator

package
v2.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 12, 2026 License: GPL-3.0 Imports: 14 Imported by: 0

Documentation

Overview

Package generator provides functionality for generating static HTML blog sites from markdown files.

The generator uses the functional options pattern for configuration. Required parameters are passed as positional arguments, while optional parameters are configured via option functions.

Basic Usage

Create a generator and generate a blog:

import (
	"context"
	"os"
	"github.com/harrydayexe/GoBlog/v2/pkg/generator"
	"github.com/harrydayexe/GoBlog/v2/pkg/config"
	"github.com/harrydayexe/GoBlog/v2/pkg/templates"
)

fsys := os.DirFS("posts/")
renderer, err := generator.NewTemplateRenderer(templates.Default)
if err != nil {
	log.Fatal(err)
}
gen := generator.New(fsys, renderer)

// Generate the blog
blog, err := gen.Generate(context.Background())
if err != nil {
	log.Fatal(err)
}

In raw output mode no templates are applied, so the renderer may be nil:

gen := generator.New(fsys, nil, config.WithRawOutput())

Custom Template Functions

Additional template functions can be registered via config.WithFuncs and passed to NewTemplateRenderer. Once registered, the function is available in every template as a regular call:

import (
	"html/template"
	"strings"
)

renderer, err := generator.NewTemplateRenderer(
	templates.Default,
	config.WithFuncs(template.FuncMap{
		"upper": strings.ToUpper,
	}),
)

In a template:

<h1>{{upper .Post.Title}}</h1>

The built-in helpers (formatDate, shortDate, year) remain available unless intentionally replaced. Registering a function whose name matches a built-in silently replaces that built-in — useful for custom date formats but a potential footgun if done accidentally. See config.WithFuncs for the full list of reserved names.

Custom Template Data

Arbitrary key-value data from the calling application can be injected into every rendered page via config.WithCustomData. The data is accessible in templates as {{.Custom.key}}:

gen := generator.New(fsys, renderer,
	config.WithCustomData(map[string]any{
		"author":      "Jane Smith",
		"analyticsID": "UA-12345",
	}),
)

In a template:

{{with .Custom}}
    <meta name="author" content="{{.author}}">
{{end}}

.Custom is nil when no WithCustomData option is supplied; templates should guard access with {{with .Custom}} or {{if .Custom}} to avoid nil-map errors. Multiple WithCustomData calls merge their maps; later values overwrite earlier ones for duplicate keys.

Page Path

Every rendered page receives a Path field on its template data containing the URL path for that page. It is accessible in templates as {{.Path}}:

<link rel="canonical" href="https://example.com{{.Path}}">
<meta property="og:url" content="https://example.com{{.Path}}">

By default (clean-URL mode) the value includes BlogRoot and no .html suffix:

  • Index page: / (or /blog/ when BlogRoot = "/blog/")
  • Post page: /posts/<slug>
  • Tag page: /tags/<tag>
  • Tags index page: /tags

When config.WithHTMLPaths is applied (automatically set by the goblog generate CLI), paths include the .html extension to match the files written to disk:

  • Index page: /index.html (or /blog.html when BlogRoot = "/blog/")
  • Post page: /posts/<slug>.html
  • Tag page: /tags/<tag>.html
  • Tags index page: /tags.html

The pkg/server package accepts both clean URLs and .html URLs via its built-in StripHTMLExtension middleware, so the server always uses clean-URL paths regardless of the generating option.

Output

The Generator returns all generated content in memory via GeneratedBlog. Callers are responsible for I/O operations such as writing files to disk or serving content via HTTP.

See the examples_test.go file for more usage examples.

Concurrency

The Generator is safe for concurrent use once created, though Generate operations should not be run concurrently on the same Generator instance.

Example

Example demonstrates basic usage of the generator package.

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/harrydayexe/GoBlog/v2/pkg/config"
	"github.com/harrydayexe/GoBlog/v2/pkg/generator"
)

func main() {
	// Create a generator with posts from testdata directory
	fsys := os.DirFS("testdata")
	gen := generator.New(fsys, nil, config.WithRawOutput())

	// Generate the blog
	ctx := context.Background()
	blog, err := gen.Generate(ctx)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Printf("Generated %d post(s)\n", len(blog.Posts))
}
Output:
Generated 3 post(s)
Example (RawOutput)

Example_rawOutput demonstrates using raw output mode.

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/harrydayexe/GoBlog/v2/pkg/config"
	"github.com/harrydayexe/GoBlog/v2/pkg/generator"
)

func main() {
	// Raw output mode generates HTML without templates
	fsys := os.DirFS("testdata")
	gen := generator.New(fsys, nil, config.WithRawOutput())

	ctx := context.Background()
	blog, err := gen.Generate(ctx)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Println("Raw HTML mode enabled")
	fmt.Printf("Posts generated: %d\n", len(blog.Posts))
}
Output:
Raw HTML mode enabled
Posts generated: 3

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type GeneratedBlog

type GeneratedBlog struct {
	Posts     map[string][]byte // Posts maps a slug to raw HTML bytes for each post
	Index     []byte            // Index contains the raw HTML for the blog index page
	Tags      map[string][]byte // Tags maps each tag name to its tag page HTML
	TagsIndex []byte            // TagsIndex contains the raw HTML for the tags index page
}

GeneratedBlog contains all the HTML content for a complete static blog site.

It includes individual post pages, the main index page, tag pages, and tags index. All content is stored as raw HTML bytes ready to be written to files or served via HTTP.

Post slugs are derived from markdown filenames. Tag names are extracted from post front matter. For more info see pkg/models/Post.

Raw Output Mode

When the generator is configured with config.WithRawOutput(), the content in GeneratedBlog will contain only the parsed Markdown as HTML without any template wrapping:

  • Posts map contains clean HTML fragments for each post
  • Tags map will be empty (tag pages are not generated)
  • TagsIndex will be empty (tags index is not generated)
  • Index field will be empty or contain minimal content

This mode is useful for embedding blog content into existing applications, custom CMSs, or when you need to apply your own templates programmatically.

Disable Tags Mode

When the generator is configured with config.WithDisableTags(), the content in GeneratedBlog will omit all tag-related output while still applying full templates to posts and the index:

  • Posts map contains fully templated HTML pages (with the default templates, tag pills are not rendered)
  • Tags map will be empty (tag pages are not generated)
  • TagsIndex will be nil (tags index is not generated)
  • Index contains the complete templated index page (with the default templates, the Tags nav link is not rendered)

This mode is useful when the blog content is not taxonomy-driven or when a custom navigation structure is used in place of GoBlog's built-in tag pages.

Disable Reading Time Mode

When the generator is configured with config.WithDisableReadingTime(), the Post.ReadingTimeMinutes field is left at zero for all posts. The default templates guard the "· N min read" annotation with {{if .Post.ReadingTimeMinutes}}, so the annotation is simply omitted without any other changes to the output structure.

Example

ExampleGeneratedBlog demonstrates working with a GeneratedBlog.

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/harrydayexe/GoBlog/v2/pkg/config"
	"github.com/harrydayexe/GoBlog/v2/pkg/generator"
)

func main() {
	fsys := os.DirFS("testdata")
	gen := generator.New(fsys, nil, config.WithRawOutput())

	ctx := context.Background()
	blog, err := gen.Generate(ctx)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	// Access posts map and verify structure
	fmt.Printf("Total posts: %d\n", len(blog.Posts))
	fmt.Printf("Has Posts map: %t\n", blog.Posts != nil)
	fmt.Printf("Has Tags map: %t\n", blog.Tags != nil)
}
Output:
Total posts: 3
Has Posts map: true
Has Tags map: true

func NewEmptyGeneratedBlog added in v2.0.3

func NewEmptyGeneratedBlog() *GeneratedBlog

type Generator

type Generator struct {
	PostsDir fs.FS // The filesystem containing the input posts in markdown

	config.RawOutput
	config.DisableTags
	config.DisableReadingTime
	config.SiteTitle
	config.BlogRoot
	config.Environment
	config.CustomData
	config.HTMLPaths
	ParserConfig parser.Config // The config to use when parsing
	// contains filtered or unexported fields
}

Generator produces HTML output based on its input configuration. It reads markdown files from a configured filesystem and renders them as HTML using templates.

A Generator is safe for concurrent use after creation, but Generate operations should not be called concurrently on the same instance.

func New

func New(posts fs.FS, renderer *TemplateRenderer, opts ...config.GeneratorOption) *Generator

New creates a new Generator with the specified options. It returns an error if the configuration is invalid or if required resources cannot be initialized.

Optional config.GeneratorOption values control behavior: config.WithRawOutput, config.WithDisableTags, config.WithDisableReadingTime, config.WithSiteTitle, config.WithBlogRoot, config.WithEnvironment, config.WithCustomData. The template renderer is supplied as a positional argument, not an option.

Example

ExampleNew demonstrates creating a new generator.

package main

import (
	"fmt"
	"os"

	"github.com/harrydayexe/GoBlog/v2/pkg/generator"
)

func main() {
	// Create a generator with default configuration
	fsys := os.DirFS("testdata")
	gen := generator.New(fsys, nil)

	if gen != nil {
		fmt.Println("Generator created")
	}
}
Output:
Generator created

func (*Generator) DebugConfig added in v2.0.3

func (g *Generator) DebugConfig(ctx context.Context)

DebugConfig logs the current generator configuration at the debug level.

This method is useful for troubleshooting and verifying configuration settings during development or when diagnosing issues. The output includes all generator configuration details and respects the provided context for structured logging.

The log output will only appear if the logger is configured to show debug level messages.

Example

ExampleGenerator_DebugConfig demonstrates debugging generator configuration.

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/harrydayexe/GoBlog/v2/pkg/config"
	"github.com/harrydayexe/GoBlog/v2/pkg/generator"
)

func main() {
	fsys := os.DirFS("testdata")
	gen := generator.New(fsys, nil, config.WithRawOutput())

	ctx := context.Background()
	gen.DebugConfig(ctx)

	fmt.Println("Debug output logged")
}
Output:
Debug output logged

func (*Generator) Generate

func (g *Generator) Generate(ctx context.Context) (*GeneratedBlog, error)

Generate reads markdown post files from the configured filesystem and generates a complete static blog site as HTML.

It returns a GeneratedBlog containing all rendered HTML content including individual post pages, tag pages, and the index page. The returned content is in-memory only; callers are responsible for writing to disk or serving via HTTP as needed.

Output Mode Behavior

When RawOutput is disabled (default):

  • Posts contain fully templated HTML pages
  • Tags map contains rendered tag pages
  • Index contains the complete index page with template

When RawOutput is enabled (via config.WithRawOutput()):

  • Posts contain only the Markdown-to-HTML conversion without templates
  • Tags map will be empty (tag generation is skipped)
  • Index will be empty or minimal
  • Useful for custom integration scenarios

Generate respects the provided context and will return early with context.Canceled or context.DeadlineExceeded if the context is canceled or times out.

It returns an error if markdown files cannot be read, parsing fails, or template rendering encounters an error.

Example

ExampleGenerator_Generate demonstrates generating a blog.

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/harrydayexe/GoBlog/v2/pkg/config"
	"github.com/harrydayexe/GoBlog/v2/pkg/generator"
)

func main() {
	fsys := os.DirFS("testdata")
	gen := generator.New(fsys, nil, config.WithRawOutput())

	ctx := context.Background()
	blog, err := gen.Generate(ctx)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}

	fmt.Println("Blog generation complete")
	fmt.Printf("Total posts: %d\n", len(blog.Posts))
}
Output:
Blog generation complete
Total posts: 3

func (Generator) String added in v2.0.5

func (c Generator) String() string

type TemplateRenderer added in v2.0.5

type TemplateRenderer struct {
	// contains filtered or unexported fields
}

TemplateRenderer parses a tree of HTML templates from an fs.FS and renders each page type (post, index, tag, tags-index) into HTML.

Create a renderer once via NewTemplateRenderer; the resulting value is safe for concurrent use by multiple goroutines. The internal *template.Template is not exposed — callers customise rendering by supplying a different fs.FS rather than mutating the renderer.

func NewTemplateRenderer added in v2.0.5

func NewTemplateRenderer(templatesFS fs.FS, opts ...config.RendererOption) (*TemplateRenderer, error)

NewTemplateRenderer parses every *.tmpl file under templatesFS and returns a renderer ready to produce HTML for each page type.

templatesFS must contain the following top-level directories:

pages/    required — must contain post.tmpl, index.tmpl, tag.tmpl,
          and tags-index.tmpl
partials/ required — each file must {{define}} one named block;
          the default templates expect "head", "header", "footer",
          and "post-card"
layouts/  optional — loaded but not executed by any Render* method;
          pages are self-contained documents that inline partials directly

Each *.tmpl file is registered under its full glob path as the template name (e.g. "pages/post.tmpl", "partials/head.tmpl"). Pages reference partials by their defined name ({{template "head" .}}), and Render* methods invoke pages by their path.

Built-in helpers

The following helpers are available in every template's FuncMap:

formatDate(t time.Time) string   formats t as "January 2, 2006"
shortDate(t time.Time) string    formats t as "Jan 2, 2006"
year() int                       returns the current calendar year

Custom functions

Additional template functions can be registered via config.WithFuncs. User-supplied functions are merged into the FuncMap after the built-ins, so registering a function whose name matches a built-in (formatDate, shortDate, year) will silently replace that built-in. This enables intentional overrides (e.g. a custom date format) but will also silently suppress default template behaviour if done accidentally.

Multiple config.WithFuncs calls accumulate; later registrations overwrite earlier ones for the same key.

Example:

renderer, err := generator.NewTemplateRenderer(
    templates.Default,
    config.WithFuncs(template.FuncMap{
        "upper": strings.ToUpper,
    }),
)

Pass github.com/harrydayexe/GoBlog/v2/pkg/templates.Default to use the built-in templates, or any fs.FS (e.g. os.DirFS("./mytemplates")) for a custom theme. Returns an error if any template fails to parse.

func (*TemplateRenderer) RenderIndex added in v2.0.5

func (tr *TemplateRenderer) RenderIndex(data models.IndexPageData) ([]byte, error)

RenderIndex renders the index/homepage by executing pages/index.tmpl with the supplied models.IndexPageData. Returns the rendered HTML or any error from template execution.

func (*TemplateRenderer) RenderPost added in v2.0.5

func (tr *TemplateRenderer) RenderPost(data models.PostPageData) ([]byte, error)

RenderPost renders a single post page by executing pages/post.tmpl with the supplied models.PostPageData. Returns the rendered HTML or any error from template execution.

func (*TemplateRenderer) RenderTag added in v2.0.5

func (tr *TemplateRenderer) RenderTag(data models.TagPageData) ([]byte, error)

RenderTag renders a tag page by executing pages/tag.tmpl with the supplied models.TagPageData. Returns the rendered HTML or any error from template execution.

func (*TemplateRenderer) RenderTagsIndex added in v2.0.5

func (tr *TemplateRenderer) RenderTagsIndex(data models.TagsIndexPageData) ([]byte, error)

RenderTagsIndex renders the tags index page by executing pages/tags-index.tmpl with the supplied models.TagsIndexPageData. Returns the rendered HTML or any error from template execution.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL