Documentation Index Fetch the complete documentation index at: https://docs.go-mizu.dev/llms.txt
Use this file to discover all available pages before exploring further.
In this tutorial, youβll build a blog website with server-rendered pages, layouts, forms, and static assets.
What Weβll Build
A blog website with:
Homepage listing posts
Individual post pages
Create post form
Shared layout and navigation
CSS styling
Prerequisites
Go 1.22 or later
Basic HTML/CSS knowledge
Completed the First API tutorial (helpful but not required)
Step 1: Create the Project
mkdir blog && cd blog
go mod init blog
go get github.com/go-mizu/mizu
Create the directory structure:
mkdir -p templates/{layouts,partials}
mkdir -p static/css
Step 2: Create the Layout
Create templates/layouts/main.html:
<! DOCTYPE html >
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > {{.Title}} | My Blog </ title >
< link rel = "stylesheet" href = "/static/css/style.css" >
</ head >
< body >
< nav >
< div class = "container" >
< a href = "/" class = "logo" > My Blog </ a >
< div class = "nav-links" >
< a href = "/" > Home </ a >
< a href = "/posts/new" > New Post </ a >
</ div >
</ div >
</ nav >
< main class = "container" >
{{template "content" .}}
</ main >
< footer >
< div class = "container" >
< p > © 2024 My Blog. Built with Mizu. </ p >
</ div >
</ footer >
</ body >
</ html >
Step 3: Create Page Templates
Create templates/home.html:
{{define "content"}}
< h1 > Welcome to My Blog </ h1 >
{{if .Posts}}
< div class = "posts" >
{{range .Posts}}
< article class = "post-card" >
< h2 >< a href = "/posts/{{.ID}}" > {{.Title}} </ a ></ h2 >
< p class = "meta" > {{.CreatedAt.Format "January 2, 2006"}} </ p >
< p > {{.Excerpt}} </ p >
< a href = "/posts/{{.ID}}" class = "read-more" > Read more β </ a >
</ article >
{{end}}
</ div >
{{else}}
< p > No posts yet. < a href = "/posts/new" > Create your first post! </ a ></ p >
{{end}}
{{end}}
Create templates/post.html:
{{define "content"}}
< article class = "post" >
< h1 > {{.Post.Title}} </ h1 >
< p class = "meta" > {{.Post.CreatedAt.Format "January 2, 2006"}} </ p >
< div class = "content" >
{{.Post.Content}}
</ div >
< a href = "/" class = "back" > β Back to posts </ a >
</ article >
{{end}}
Create templates/new-post.html:
{{define "content"}}
< h1 > Create New Post </ h1 >
{{if .Error}}
< div class = "error" > {{.Error}} </ div >
{{end}}
< form method = "POST" action = "/posts" class = "post-form" >
< div class = "form-group" >
< label for = "title" > Title </ label >
< input type = "text" id = "title" name = "title" value = "{{.Title}}" required >
</ div >
< div class = "form-group" >
< label for = "content" > Content </ label >
< textarea id = "content" name = "content" rows = "10" required > {{.Content}} </ textarea >
</ div >
< button type = "submit" > Publish Post </ button >
</ form >
{{end}}
Step 4: Add CSS
Create static/css/style.css:
* {
margin : 0 ;
padding : 0 ;
box-sizing : border-box ;
}
body {
font-family : -apple-system , BlinkMacSystemFont, 'Segoe UI' , Roboto, sans-serif ;
line-height : 1.6 ;
color : #333 ;
background : #f5f5f5 ;
}
.container {
max-width : 800 px ;
margin : 0 auto ;
padding : 0 20 px ;
}
/* Navigation */
nav {
background : #2c3e50 ;
padding : 1 rem 0 ;
}
nav .container {
display : flex ;
justify-content : space-between ;
align-items : center ;
}
nav .logo {
color : white ;
font-size : 1.5 rem ;
font-weight : bold ;
text-decoration : none ;
}
nav .nav-links a {
color : #ecf0f1 ;
text-decoration : none ;
margin-left : 20 px ;
}
nav .nav-links a :hover {
color : #3498db ;
}
/* Main content */
main {
padding : 2 rem 0 ;
min-height : calc ( 100 vh - 150 px );
}
h1 {
margin-bottom : 1.5 rem ;
color : #2c3e50 ;
}
/* Post cards */
.posts {
display : grid ;
gap : 1.5 rem ;
}
.post-card {
background : white ;
padding : 1.5 rem ;
border-radius : 8 px ;
box-shadow : 0 2 px 4 px rgba ( 0 , 0 , 0 , 0.1 );
}
.post-card h2 {
margin-bottom : 0.5 rem ;
}
.post-card h2 a {
color : #2c3e50 ;
text-decoration : none ;
}
.post-card h2 a :hover {
color : #3498db ;
}
.meta {
color : #7f8c8d ;
font-size : 0.9 rem ;
margin-bottom : 0.5 rem ;
}
.read-more {
color : #3498db ;
text-decoration : none ;
}
/* Single post */
.post {
background : white ;
padding : 2 rem ;
border-radius : 8 px ;
box-shadow : 0 2 px 4 px rgba ( 0 , 0 , 0 , 0.1 );
}
.post .content {
margin : 1.5 rem 0 ;
white-space : pre-wrap ;
}
.back {
color : #3498db ;
text-decoration : none ;
}
/* Form */
.post-form {
background : white ;
padding : 2 rem ;
border-radius : 8 px ;
box-shadow : 0 2 px 4 px rgba ( 0 , 0 , 0 , 0.1 );
}
.form-group {
margin-bottom : 1.5 rem ;
}
.form-group label {
display : block ;
margin-bottom : 0.5 rem ;
font-weight : 500 ;
}
.form-group input ,
.form-group textarea {
width : 100 % ;
padding : 0.75 rem ;
border : 1 px solid #ddd ;
border-radius : 4 px ;
font-size : 1 rem ;
}
.form-group textarea {
resize : vertical ;
}
button {
background : #3498db ;
color : white ;
padding : 0.75 rem 1.5 rem ;
border : none ;
border-radius : 4 px ;
font-size : 1 rem ;
cursor : pointer ;
}
button :hover {
background : #2980b9 ;
}
.error {
background : #fee ;
color : #c00 ;
padding : 1 rem ;
border-radius : 4 px ;
margin-bottom : 1 rem ;
}
/* Footer */
footer {
background : #2c3e50 ;
color : #ecf0f1 ;
padding : 1 rem 0 ;
text-align : center ;
}
Step 5: Create the Server
Create main.go:
package main
import (
" fmt "
" html/template "
" net/http "
" sync "
" time "
" github.com/go-mizu/mizu "
)
// Post model
type Post struct {
ID string
Title string
Content string
Excerpt string
CreatedAt time . Time
}
// Storage
var (
posts = make ( map [ string ] * Post )
postsMu sync . RWMutex
nextID = 1
)
// Templates
var templates * template . Template
func init () {
templates = template . Must ( template . ParseGlob ( "templates/*.html" ))
template . Must ( templates . ParseGlob ( "templates/layouts/*.html" ))
}
// Render helper
func render ( c * mizu . Ctx , name string , data map [ string ] any ) error {
c . Header (). Set ( "Content-Type" , "text/html; charset=utf-8" )
// Clone template to add layout
t := template . Must ( templates . Clone ())
template . Must ( t . ParseFiles ( "templates/layouts/main.html" ))
return t . ExecuteTemplate ( c . Writer (), "main.html" , data )
}
// Handlers
func homePage ( c * mizu . Ctx ) error {
postsMu . RLock ()
defer postsMu . RUnlock ()
list := make ([] * Post , 0 , len ( posts ))
for _ , p := range posts {
list = append ( list , p )
}
return render ( c , "home" , map [ string ] any {
"Title" : "Home" ,
"Posts" : list ,
})
}
func showPost ( c * mizu . Ctx ) error {
id := c . Param ( "id" )
postsMu . RLock ()
post , ok := posts [ id ]
postsMu . RUnlock ()
if ! ok {
return c . Text ( 404 , "Post not found" )
}
return render ( c , "post" , map [ string ] any {
"Title" : post . Title ,
"Post" : post ,
})
}
func newPostForm ( c * mizu . Ctx ) error {
return render ( c , "new-post" , map [ string ] any {
"Title" : "New Post" ,
})
}
func createPost ( c * mizu . Ctx ) error {
title := c . FormValue ( "title" )
content := c . FormValue ( "content" )
if title == "" || content == "" {
return render ( c , "new-post" , map [ string ] any {
"Title" : "New Post" ,
"Error" : "Title and content are required" ,
"Title" : title ,
"Content" : content ,
})
}
postsMu . Lock ()
id := fmt . Sprintf ( " %d " , nextID )
nextID ++
excerpt := content
if len ( excerpt ) > 150 {
excerpt = excerpt [: 150 ] + "..."
}
post := & Post {
ID : id ,
Title : title ,
Content : content ,
Excerpt : excerpt ,
CreatedAt : time . Now (),
}
posts [ id ] = post
postsMu . Unlock ()
return c . Redirect ( 302 , "/posts/" + id )
}
func main () {
app := mizu . New ()
// Static files
app . Static ( "/static/" , http . Dir ( "static" ))
// Routes
app . Get ( "/" , homePage )
app . Get ( "/posts/new" , newPostForm )
app . Post ( "/posts" , createPost )
app . Get ( "/posts/{id}" , showPost )
fmt . Println ( "Server running at http://localhost:3000" )
app . Listen ( ":3000" )
}
Step 6: Run and Test
Open http://localhost:3000 in your browser.
Try:
Click βNew Postβ to create a post
Fill in the form and submit
View the post
Go back to see it on the homepage
Step 7: Add Sample Data
Add some initial posts in main():
func main () {
// Add sample posts
posts [ "1" ] = & Post {
ID : "1" ,
Title : "Getting Started with Mizu" ,
Content : "Mizu is a lightweight web framework for Go..." ,
Excerpt : "Mizu is a lightweight web framework for Go..." ,
CreatedAt : time . Now (). Add ( - 24 * time . Hour ),
}
posts [ "2" ] = & Post {
ID : "2" ,
Title : "Building APIs with Go" ,
Content : "Learn how to build REST APIs using Go and Mizu..." ,
Excerpt : "Learn how to build REST APIs using Go and Mizu..." ,
CreatedAt : time . Now (). Add ( - 48 * time . Hour ),
}
nextID = 3
app := mizu . New ()
// ... rest of setup
}
What You Learned
Using Go templates with layouts
Serving static files (CSS, images)
Handling form submissions
Redirecting after form submission
Template composition
Project Structure
blog/
βββ main.go
βββ go.mod
βββ go.sum
βββ templates/
β βββ layouts/
β β βββ main.html
β βββ home.html
β βββ post.html
β βββ new-post.html
βββ static/
βββ css/
βββ style.css
Next Steps
Build Full-Stack App Add a React frontend.
View Engine Advanced template features.