Click to watch the companion tutorial video
Why Server-Side Rendering (SSR) Matters
In today's web development landscape dominated by client-side frameworks, server-side rendering offers compelling advantages:
- Instant Page Loads: HTML arrives ready-to-display
- SEO Superiority: Search engines easily crawl content
- Simplified Architecture: Single codebase manages both API and UI
- Resource Efficiency: Reduced client-side JavaScript overhead
GoFr, an opinionated Go framework, makes SSR implementation remarkably straightforward. Let's build a Dev.to article reader that demonstrates these principles in action.
Project Setup: Laying the Foundation
1. Initializing the GoFr Application (main.go
)
package main
import (
"encoding/json"
"gofr.dev/pkg/gofr"
"strconv"
)
type Article struct {
Title string `json:"title"`
URL string `json:"url"`
Description string `json:"description"`
}
type PageData struct {
Articles []Article
Tag string
Page int
}
func main() {
// Initialize GoFr application
app := gofr.New()
// Configure Dev.to API service
app.AddHTTPService("dev-articles", "https://dev.to/api")
// Register route handler
app.GET("/dev-articles", FetchArticles)
// Serve static files (CSS, templates)
app.AddStaticFiles("/", "./static")
// Start the server
app.Run()
}
Key Components Explained:
Service Configuration:
AddHTTPService
creates a pre-configured HTTP client for the Dev.to API, handling connection pooling and timeouts automatically.Route Handling:The
GET
method associates the/dev-articles
endpoint with ourFetchArticles
handler.Static Assets:
AddStaticFiles
serves CSS and templates from thestatic
directory, essential for our SSR approach.
Core Business Logic: The Article Handler
2. Implementing the FetchArticles Handler
func FetchArticles(ctx *gofr.Context) (any, error) {
// Get search parameters with defaults
tag := ctx.Param("tag")
if tag == "" {
tag = "go" // Default to Go articles
}
page, _ := strconv.Atoi(ctx.Param("page"))
if page < 1 {
page = 1 // Ensure minimum page number
}
// Fetch articles from Dev.to API
service := ctx.GetHTTPService("dev-articles")
resp, err := service.Get(ctx, "/articles", map[string]interface{}{
"tag": tag,
"page": page,
"per_page": 4, // Optimal for initial load
})
if err != nil {
return nil, err // Handle API errors
}
defer resp.Body.Close()
// Parse API response
var articles []Article
if err := json.NewDecoder(resp.Body).Decode(&articles); err != nil {
return nil, err // Handle parsing errors
}
// Render template with data
return gofr.Template{
Data: PageData{articles, tag, page},
Name: "devTo.html",
}, nil
}
Architectural Decisions:
-
Parameter Handling
- Default values ensure consistent behavior
- Type conversion guards against invalid inputs
-
per_page=4
balances content density and performance
-
Error Handling
- Automatic error propagation through GoFr's middleware
- Clean separation of concerns between API and rendering
-
Service Abstraction
- HTTP service configuration centralized in
main.go
- Easy to swap API endpoints or add caching later
- HTTP service configuration centralized in
Presentation Layer: HTML Templating
3. Template Implementation (static/devTo.html
)
</span>
lang="en">
charset="UTF-8">
Go Articles from Dev.to
rel="stylesheet" href="/style.css">
// Hybrid pagination: Client-side navigation with server-side validation
function updatePage(delta) {
const params = new URLSearchParams(window.location.search)
let page = parseInt(params.get('page') || {{.Page}}
let tag = params.get('tag') || '{{.Tag}}'
page = Math.max(1, page + delta)
window.location.href = `/dev-articles?tag=${tag}&page=${page}`
}
id="content">
📰 Latest Go Articles on Dev.to
method="GET" action="/dev-articles">
type="text" name="tag"
placeholder="Tag (e.g. go)"
value="{{.Tag}}" />
type="submit">Search
{{range .Articles}}
class="post">
href="{{.URL}}" target="_blank">{{.Title}}
class="description">{{.Description}}
{{end}}
class="pagination">
{{if gt .Page 1}}
onclick="updatePage(-1)">⬅️ Prev
{{else}}
disabled>⬅️ Prev
{{end}}
class="page-number">Page {{.Page}}
onclick="updatePage(1)">Next ➡️
Enter fullscreen mode
Exit fullscreen mode
Template Features:
Dynamic Content Binding
{{.Articles}} loop renders server-fetched content
{{.Tag}} and {{.Page}} maintain state between requests
Progressive Enhancement
Search form works without JavaScript
Pagination uses minimal client-side scripting
Security Best Practices
Auto-escaping prevents XSS vulnerabilities
target="_blank" with rel="noopener" (implicit in Go templates)
Styling: CSS Implementation (static/style.css)
#content {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
font-family: system-ui, sans-serif;
}
article.post {
background: #fff;
border-radius: 8px;
padding: 1.5rem;
margin: 1rem 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.pagination {
margin-top: 2rem;
display: flex;
gap: 1rem;
justify-content: center;
align-items: center;
}
button {
padding: 0.5rem 1rem;
background: #3182ce;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background: #a0aec0;
cursor: not-allowed;
}
.page-number {
font-weight: 500;
color: #4a5568;
}
Enter fullscreen mode
Exit fullscreen mode
Design Philosophy:
Modern Aesthetic
System font stack for native feel
Subtle shadows and rounded corners
Responsive Layout
Max-width constraint for readability
Flexible spacing using rem units
Accessibility Focus
High contrast colors
Clear button states (hover/disabled)
Running the Application
Install Dependencies
go mod init your-module-name
go get gofr.dev/pkg/gofr
Enter fullscreen mode
Exit fullscreen mode
Directory Structure
├── templates/
│ └── devTo.html # HTML templates
├── static/
│ └── style.css # Static assets
└── main.go # Application entrypoint
Enter fullscreen mode
Exit fullscreen mode
Start the Server
go run main.go
Enter fullscreen mode
Exit fullscreen mode
Access in Browser
Visit http://localhost:8000/dev-articles?tag=go&page=1
Why GoFr Shines in Production
Built-in Observability
Automatic request logging
Metrics endpoint at /metrics
Configuration Management
# configs/config.yml
services:
dev-articles:
url: https://dev.to/api
timeout: 3s
Enter fullscreen mode
Exit fullscreen mode
Horizontal Scaling
Stateless architecture
Native Kubernetes support
Error Handling
Centralized error recovery
Structured logging
Next Steps: From Prototype to Production
Add Caching Layer
// Redis integration example
cached, err := ctx.Redis.Get(ctx, "articles:"+tag+":"+page)
Enter fullscreen mode
Exit fullscreen mode
Implement Rate Limiting
app.UseMiddleware(ratelimit.New(100)) // 100 requests/minute
Enter fullscreen mode
Exit fullscreen mode
Add Health Checks
app.GET("/health", func(ctx *gofr.Context) (any, error) {
return "OK", nil
})
Enter fullscreen mode
Exit fullscreen mode
Error Boundaries
{{if .Error}}
class="error-alert">
⚠️ Failed to load articles: {{.Error}}
{{end}}
Enter fullscreen mode
Exit fullscreen mode
Conclusion: The Power of Simplicity
This project demonstrates how Go and GoFr enable building modern web applications with:✅ Full server-side rendering
✅ Clean architecture
✅ Production-ready features
✅ Minimal dependencies GitHub Repository:
https://github.com/coolwednesday/gofr-template-rendering-exampleReady to Go Further?
Explore GoFr's documentation for database integration
Implement user authentication using JWT middleware
Add server-side caching for API responses
The simplicity of Go combined with GoFr's powerful abstractions makes this stack ideal for projects ranging from small internal tools to large-scale content platforms.
Happy coding! 🚀