Building a Go Blog Engine with Fiber and Google Cloud Storage
In this post, I’ll walk you through the process of building a custom blog engine using Go, the Fiber web framework, and Google Cloud Storage. This project combines several powerful technologies to create a fast, efficient, and scalable blogging platform.
The Tech Stack
- Go: Our primary programming language, known for its simplicity and efficiency.
- Fiber: A Go web framework inspired by Express.js, offering fast HTTP APIs.
- Google Cloud Storage: For storing our blog posts as Markdown files.
- Firestore: For storing and retrieving post metrics like read count.
- gomarkdown: A Markdown parser and HTML renderer for Go.
- Docker: For containerizing our application, ensuring consistency across different environments.
Key Features of Our Blog Engine
- Markdown file storage in Google Cloud Storage
- On-the-fly Markdown to HTML conversion
- Custom HTML templates for rendering posts
- Error handling with custom error pages
- Logging for debugging and monitoring
- Pagination for the home page
- Read count tracking for posts
- RSS feed generation
The Development Process
1. Setting Up the Project
We started by initializing a Go module and setting up our main.go file. We installed necessary dependencies including Fiber, the Google Cloud Storage client, Firestore client, and the Markdown processor.
import (
"cloud.google.com/go/firestore"
"cloud.google.com/go/storage"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html/v2"
"github.com/gomarkdown/markdown"
// ... other imports
)
2. Connecting to Google Cloud Storage and Firestore
We implemented functionality to read Markdown files from a specified GCS bucket and interact with Firestore for post metrics. This involved setting up authentication and creating clients for both services.
bucketClient, err = storage.NewClient(ctx)
if err != nil {
log.Fatalf("Failed to create storage client: %v", err)
}
firestoreClient, err = firestore.NewClient(ctx, projectID)
if err != nil {
log.Fatalf("Failed to create Firestore client: %v", err)
}
3. Parsing Markdown and Frontmatter
We created a system to parse the frontmatter (metadata) of our Markdown files, separating it from the main content. This allows us to extract information like the post’s title, author, and date.
type PostMetadata struct {
Title string `yaml:"title"`
Author string `yaml:"author"`
Date string `yaml:"date"`
Slug string
ReadCount int
}
// In the readPost function:
var post Post
if err := yaml.Unmarshal([]byte(parts[1]), &post); err != nil {
return Post{}, fmt.Errorf("error parsing metadata: %v", err)
}
4. Rendering Markdown to HTML
Using the gomarkdown library, we implemented functionality to convert Markdown content to HTML. This step involved some troubleshooting to ensure proper rendering of code blocks and other Markdown features.
extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock | parser.FencedCode
p := parser.NewWithExtensions(extensions)
md := markdown.Parse([]byte(post.Content), p)
htmlFlags := mdhtml.CommonFlags | mdhtml.HrefTargetBlank
opts := mdhtml.RendererOptions{Flags: htmlFlags}
renderer := mdhtml.NewRenderer(opts)
html := markdown.Render(md, renderer)
5. Creating HTML Templates
We designed HTML templates for our blog posts and error pages, focusing on clean, readable designs. These templates are rendered using Fiber’s template engine.
engine := html.New("./views", ".html")
engine.AddFunc("add", func(a, b int) int {
return a + b
})
// ... other custom functions
app := fiber.New(fiber.Config{
Views: engine,
})
6. Implementing Error Handling and Logging
We added robust error handling throughout the application, including a custom error page to display user-friendly error messages. We also implemented detailed logging for debugging and monitoring.
app.Use(func(c *fiber.Ctx) error {
log.Printf("Received request: %s %s", c.Method(), c.Path())
return c.Next()
})
// In error cases:
log.Printf("Error reading post: %v", err)
return c.Status(fiber.StatusNotFound).SendString(fmt.Sprintf("Post not found: %s", err))
7. Implementing Pagination
We added pagination to the home page to manage the display of multiple blog posts efficiently.
const postsPerPage = 10
type PaginatedResponse struct {
Posts []Post
CurrentPage int
TotalPages int
HasNext bool
HasPrev bool
}
// In handleHome function:
totalPages := int(math.Ceil(float64(totalPosts) / float64(postsPerPage)))
paginatedPosts := posts[startIndex:endIndex]
8. Adding Read Count Tracking
We implemented a feature to track and display the number of times each post has been read using Firestore.
func incrementReadCount(slug string) error {
ctx := context.Background()
_, err := firestoreClient.Collection("post_metrics").Doc(slug).Set(ctx, map[string]interface{}{
"readCount": firestore.Increment(1),
"lastRead": time.Now(),
}, firestore.MergeAll)
return err
}
9. Implementing RSS Feed
We added an RSS feed generator to allow readers to subscribe to the blog.
func handleRSS(c *fiber.Ctx) error {
posts, err := listPosts()
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Error generating RSS feed")
}
feed := &feeds.Feed{
Title: "My Blog",
Link: &feeds.Link{Href: "https://yourblog.com"},
Description: "Latest posts from My Blog",
Author: &feeds.Author{Name: "Your Name"},
Created: time.Now(),
}
// ... populate feed with posts
rss, err := feed.ToRss()
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Error generating RSS feed")
}
c.Type("application/rss+xml")
return c.SendString(rss)
}
Challenges and Solutions
During development, we encountered several challenges:
Markdown Rendering Issues: Initially, we faced problems with code blocks not rendering correctly. We solved this by configuring the Markdown renderer with appropriate options and implementing a custom function to add language classes to code blocks.
Error Handling: We improved our error handling by implementing detailed logging throughout the application and returning appropriate HTTP status codes.
Google Cloud Storage Integration: Setting up authentication and correctly reading files from GCS required careful configuration and error checking. We implemented robust error handling and logging to diagnose and resolve issues.
Pagination Implementation: Implementing pagination required careful calculation of page numbers and post ranges. We created a custom
PaginatedResponsestruct to manage this data efficiently.Read Count Tracking: Integrating Firestore for read count tracking involved setting up a new client and implementing atomic increment operations to ensure accurate counts.
Future Improvements
While our blog engine is functional, there’s always room for improvement. Some ideas for future enhancements include:
- Implementing a cache to reduce GCS read operations
- Adding categories or tags for posts
- Implementing a search functionality
- Adding support for comments
- Implementing user authentication for an admin panel
- Adding support for scheduling posts
Conclusion
Building a custom blog engine from scratch was a challenging but rewarding experience. It provided deep insights into web development with Go, working with cloud storage and databases, and the intricacies of Markdown processing.
This project serves as a solid foundation for a personal blog or can be extended into a more feature-rich content management system. The combination of Go’s performance, Fiber’s simplicity, Google Cloud Storage’s scalability, and Firestore’s real-time capabilities makes for a powerful and flexible blogging platform.
Remember, the joy of software development lies not just in the end result, but in the learning process and challenges overcome along the way. Happy coding!