outputter

package
v2.0.5 Latest Latest
Warning

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

Go to latest
Published: Jan 19, 2026 License: GPL-3.0 Imports: 6 Imported by: 0

Documentation

Overview

Package outputter provides interfaces and implementations for handling generated blog content from the generator package.

The Outputter interface defines how generated blog content should be processed. Implementations can write to disk, serve via HTTP, store in databases, or perform any other output operation.

The package includes DirectoryWriter, a filesystem-based implementation that writes blog content as static HTML files to a specified directory.

Basic Usage

Create a DirectoryWriter and use it to write generated blog content:

postsFS := os.DirFS("posts/")
gen := generator.New(postsFS)
blog, err := gen.Generate(context.Background())
if err != nil {
    log.Fatal(err)
}

writer := outputter.NewDirectoryWriter("output/")
if err := writer.HandleGeneratedBlog(blog); err != nil {
    log.Fatal(err)
}

Directory Structure

DirectoryWriter creates the following structure:

output/
├── index.html           # Blog index page
├── post-slug-1.html     # Individual post pages
├── post-slug-2.html
└── tags/                # Tag pages (unless RawOutput is enabled)
    ├── tag-1.html
    └── tag-2.html

Configuration

DirectoryWriter supports the functional options pattern for optional configuration:

writer := outputter.NewDirectoryWriter("output/",
    config.WithRawOutput(true),
)

When RawOutput is enabled, the tags directory is not created.

Concurrency

Outputter implementations should be safe for concurrent use. DirectoryWriter is safe for concurrent calls to HandleGeneratedBlog, though concurrent writes to the same output directory may result in race conditions at the filesystem level.

Example

Example demonstrates basic usage of the outputter package.

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"path/filepath"

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

func main() {
	// Create a temporary directory for this example
	tempDir, err := os.MkdirTemp("", "blog-example")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(tempDir)

	// Create some sample blog content
	blog := &generator.GeneratedBlog{
		Posts: map[string][]byte{
			"hello-world": []byte("<h1>Hello World</h1><p>My first post</p>"),
		},
		Index: []byte("<h1>My Blog</h1><p>Welcome!</p>"),
		Tags: map[string][]byte{
			"intro": []byte("<h1>Intro Posts</h1>"),
		},
	}

	// Create a DirectoryWriter and write the blog
	writer := outputter.NewDirectoryWriter(tempDir)
	if err := writer.HandleGeneratedBlog(context.Background(), blog); err != nil {
		log.Fatal(err)
	}

	// Verify files were created
	files := []string{"index.html", "posts/hello-world.html", "tags/intro.html"}
	for _, file := range files {
		path := filepath.Join(tempDir, file)
		if _, err := os.Stat(path); err == nil {
			fmt.Printf("%s created\n", file)
		}
	}

}
Output:

index.html created
posts/hello-world.html created
tags/intro.html created
Example (FullWorkflow)

Example_fullWorkflow demonstrates a complete workflow of generating and outputting a blog. This example uses RawOutput mode since templates are not yet fully implemented in the generator.

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"path/filepath"

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

func main() {
	// Setup temporary directories
	postsDir, err := os.MkdirTemp("", "posts")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(postsDir)

	outputDir, err := os.MkdirTemp("", "output")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(outputDir)

	// Create a sample markdown post
	postContent := `---
title: Getting Started
description: A beginner's guide to getting started
tags: [tutorial, beginner]
date: 2024-01-01
---

# Getting Started

This is a simple blog post.
`
	postPath := filepath.Join(postsDir, "getting-started.md")
	if err := os.WriteFile(postPath, []byte(postContent), 0644); err != nil {
		log.Fatal(err)
	}

	// Generate the blog using os.DirFS to create a filesystem
	// Use RawOutput since templates are not fully implemented yet
	gen := generator.New(os.DirFS(postsDir), nil, config.WithRawOutput())
	blog, err := gen.Generate(context.Background())
	if err != nil {
		log.Fatal(err)
	}

	// Output the blog to disk with RawOutput to match generator settings
	writer := outputter.NewDirectoryWriter(outputDir, config.WithRawOutput())
	if err := writer.HandleGeneratedBlog(context.Background(), blog); err != nil {
		log.Fatal(err)
	}

	// Verify the output
	indexPath := filepath.Join(outputDir, "index.html")
	postPath = filepath.Join(outputDir, "posts", "getting-started.html")

	if _, err := os.Stat(indexPath); err == nil {
		fmt.Println("index.html created")
	}
	if _, err := os.Stat(postPath); err == nil {
		fmt.Println("posts/getting-started.html created")
	}

}
Output:

index.html created
posts/getting-started.html created
Example (MultipleWrites)

Example_multipleWrites demonstrates that DirectoryWriter is idempotent and can safely write to the same directory multiple times.

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"path/filepath"

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

func main() {
	tempDir, err := os.MkdirTemp("", "blog-multi")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(tempDir)

	writer := outputter.NewDirectoryWriter(tempDir)

	// First write
	blog1 := &generator.GeneratedBlog{
		Posts: map[string][]byte{"post": []byte("Version 1")},
		Index: []byte("Index V1"),
		Tags:  make(map[string][]byte),
	}
	if err := writer.HandleGeneratedBlog(context.Background(), blog1); err != nil {
		log.Fatal(err)
	}
	fmt.Println("First write completed")

	// Second write - updates the same files
	blog2 := &generator.GeneratedBlog{
		Posts: map[string][]byte{"post": []byte("Version 2")},
		Index: []byte("Index V2"),
		Tags:  make(map[string][]byte),
	}
	if err := writer.HandleGeneratedBlog(context.Background(), blog2); err != nil {
		log.Fatal(err)
	}
	fmt.Println("Second write completed")

	// Read final content
	content, _ := os.ReadFile(filepath.Join(tempDir, "posts", "post.html"))
	fmt.Printf("Final content: %s\n", string(content))

}
Output:

First write completed
Second write completed
Final content: Version 2
Example (RawOutputMode)

Example_rawOutputMode demonstrates using RawOutput mode to skip template wrapping and tag generation.

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"path/filepath"

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

func main() {
	tempDir, err := os.MkdirTemp("", "blog-raw")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(tempDir)

	// Create blog content
	blog := &generator.GeneratedBlog{
		Posts: map[string][]byte{
			"post": []byte("<article>Raw HTML content</article>"),
		},
		Index: []byte("<div>Index content</div>"),
		Tags: map[string][]byte{
			"tag": []byte("<div>Tag content</div>"),
		},
	}

	// Write with RawOutput enabled - tags directory won't be created
	writer := outputter.NewDirectoryWriter(tempDir, config.WithRawOutput())
	if err := writer.HandleGeneratedBlog(context.Background(), blog); err != nil {
		log.Fatal(err)
	}

	// Check what was created
	if _, err := os.Stat(filepath.Join(tempDir, "index.html")); err == nil {
		fmt.Println("index.html created")
	}
	if _, err := os.Stat(filepath.Join(tempDir, "posts", "post.html")); err == nil {
		fmt.Println("posts/post.html created")
	}
	if _, err := os.Stat(filepath.Join(tempDir, "tags")); os.IsNotExist(err) {
		fmt.Println("tags directory not created (as expected)")
	}

}
Output:

index.html created
posts/post.html created
tags directory not created (as expected)

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type DirectoryWriter

type DirectoryWriter struct {
	config.RawOutput
	// contains filtered or unexported fields
}

DirectoryWriter is an Outputter implementation that writes blog content as static HTML files to a filesystem directory.

It creates an index.html file, individual post HTML files, and (unless RawOutput is enabled) a tags subdirectory with tag pages and a tags index.

DirectoryWriter is safe for concurrent use, though concurrent writes to the same output directory may result in filesystem race conditions.

func NewDirectoryWriter

func NewDirectoryWriter(outputDir string, opts ...config.Option) DirectoryWriter

NewDirectoryWriter creates a new DirectoryWriter with the specified output directory and optional configuration.

The outputDir parameter specifies where blog files will be written. The directory will be created if it does not exist.

Optional configuration can be provided via functional options from the config package:

writer := NewDirectoryWriter("/var/www/blog",
    config.WithRawOutput(),
)

This is the recommended constructor for most use cases. Use NewDirectoryWriterWithConfig when you need to provide an explicit DirectoryWriterConfig struct.

Example

ExampleNewDirectoryWriter demonstrates creating a DirectoryWriter with default settings.

package main

import (
	"fmt"

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

func main() {
	writer := outputter.NewDirectoryWriter("/var/www/blog")

	fmt.Printf("Writer created with output path: %s\n", "/var/www/blog")
	_ = writer // Use the writer

}
Output:

Writer created with output path: /var/www/blog
Example (WithRawOutput)

ExampleNewDirectoryWriter_withRawOutput demonstrates creating a DirectoryWriter with the RawOutput option enabled.

package main

import (
	"fmt"

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

func main() {
	// Create writer with RawOutput option - this will skip creating
	// the tags directory
	writer := outputter.NewDirectoryWriter(
		"/var/www/blog",
		config.WithRawOutput(),
	)

	fmt.Printf("Writer created with RawOutput enabled\n")
	_ = writer // Use the writer

}
Output:

Writer created with RawOutput enabled

func (DirectoryWriter) HandleGeneratedBlog

func (dw DirectoryWriter) HandleGeneratedBlog(ctx context.Context, blog *generator.GeneratedBlog) error

HandleGeneratedBlog writes the generated blog content to the filesystem as static HTML files.

The method creates the following structure in the output directory:

  • index.html: the main blog index page
  • posts/{slug}.html: individual post files, one per post
  • tags/{tag}.html: tag pages (only if RawOutput is false)
  • tags/index.html: tags index page (only if RawOutput is false)

When RawOutput mode is enabled (via config.WithRawOutput()), the tags/ directory is not created and individual post files contain only raw HTML fragments without template wrappers. This is useful when you plan to wrap the content with your own templates or integrate it into an existing site structure.

All necessary directories are created automatically with permissions 0755. Files are written with permissions 0644.

Returns an error if:

  • The output directory cannot be created
  • Any file write operation fails (e.g., insufficient permissions)
  • The filesystem is full or read-only

If an error occurs partway through writing, some files may have been created successfully while others were not. The output directory may be in a partial state.

Example

ExampleDirectoryWriter_HandleGeneratedBlog demonstrates writing generated blog content to disk.

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"path/filepath"

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

func main() {
	tempDir, err := os.MkdirTemp("", "blog-handle")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(tempDir)

	// Create blog content
	blog := &generator.GeneratedBlog{
		Posts: map[string][]byte{
			"my-post": []byte("<h1>My Post</h1>"),
		},
		Index: []byte("<h1>Blog Index</h1>"),
		Tags: map[string][]byte{
			"tech": []byte("<h1>Tech Posts</h1>"),
		},
	}

	// Write to disk
	writer := outputter.NewDirectoryWriter(tempDir)
	if err := writer.HandleGeneratedBlog(context.Background(), blog); err != nil {
		log.Fatal(err)
	}

	// Verify index was created
	indexPath := filepath.Join(tempDir, "index.html")
	if _, err := os.Stat(indexPath); err == nil {
		fmt.Println("Blog written successfully")
	}

}
Output:

Blog written successfully

type Outputter

type Outputter interface {
	// HandleGeneratedBlog processes the generated blog content and performs
	// the implementation-specific output operation.
	//
	// The blog parameter contains all generated content including posts, tags,
	// and the index page as HTML byte slices.
	//
	// Returns an error if the output operation fails (e.g., filesystem errors,
	// network errors, permission issues).
	HandleGeneratedBlog(context.Context, *generator.GeneratedBlog) error
}

Outputter defines the interface for handling generated blog content.

Implementations process the GeneratedBlog structure from the generator package and perform output operations such as writing files to disk, serving content via HTTP, storing in a database, or any other desired output mechanism.

Implementations should be safe for concurrent use.

Jump to

Keyboard shortcuts

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