Building a Content Management System (CMS) with Go

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Setting Up the Project
  4. Creating the Database Schema
  5. Implementing CRUD Operations
  6. Building the User Interface
  7. Conclusion

Introduction

In this tutorial, we will learn how to build a Content Management System (CMS) using Go. A CMS is a web application that allows users to create, manage, and publish content on the internet. By the end of this tutorial, you will have a functional CMS that can perform basic CRUD (Create, Read, Update, Delete) operations on content.

Prerequisites

To follow along with this tutorial, you should have a basic understanding of the Go programming language. Familiarity with web development concepts like HTML and CSS will also be helpful. Additionally, make sure you have Go installed on your machine.

Setting Up the Project

First, let’s set up our project structure and initialize a Go module. Open your terminal and create a new directory for your project:

$ mkdir cms
$ cd cms

Next, initialize the Go module:

$ go mod init github.com/your-username/cms

This will create a go.mod file that will track the dependencies for our project.

Creating the Database Schema

Our CMS will require a database to store the content. We will be using SQLite as our database engine. Install the necessary SQLite driver by running the following command:

$ go get github.com/mattn/go-sqlite3

Now, let’s create a file called database.go and define the database schema:

package main

import (
	"database/sql"
	"fmt"

	_ "github.com/mattn/go-sqlite3"
)

func main() {
	db, err := sql.Open("sqlite3", "./cms.db")
	if err != nil {
		fmt.Println(err)
		return
	}

	defer db.Close()

	// Create the content table
	_, err = db.Exec(`
		CREATE TABLE IF NOT EXISTS content (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			title TEXT,
			body TEXT
		)`)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println("Database schema created successfully.")
}

In this code, we opened a connection to the SQLite database using the sql.Open function. Then, we created the content table with an id column, title column, and body column. Finally, we printed a success message to indicate that the schema creation was successful.

To run this code and create the database schema, execute the following command:

$ go run database.go

Implementing CRUD Operations

Now that we have the database schema set up, let’s implement the CRUD operations for our CMS. Create a new file called crud.go and add the following code:

package main

import (
	"database/sql"
	"fmt"

	_ "github.com/mattn/go-sqlite3"
)

type Content struct {
	ID    int
	Title string
	Body  string
}

func createContent(db *sql.DB, content Content) error {
	_, err := db.Exec("INSERT INTO content (title, body) VALUES (?, ?)", content.Title, content.Body)
	return err
}

func getContent(db *sql.DB, id int) (Content, error) {
	var content Content

	row := db.QueryRow("SELECT * FROM content WHERE id = ?", id)
	err := row.Scan(&content.ID, &content.Title, &content.Body)
	if err != nil {
		return content, err
	}

	return content, nil
}

func updateContent(db *sql.DB, id int, newContent Content) error {
	_, err := db.Exec("UPDATE content SET title = ?, body = ? WHERE id = ?", newContent.Title, newContent.Body, id)
	return err
}

func deleteContent(db *sql.DB, id int) error {
	_, err := db.Exec("DELETE FROM content WHERE id = ?", id)
	return err
}

func main() {
	db, err := sql.Open("sqlite3", "./cms.db")
	if err != nil {
		fmt.Println(err)
		return
	}

	defer db.Close()

	// Test the CRUD operations
	err = createContent(db, Content{Title: "First Post", Body: "This is the first post"})
	if err != nil {
		fmt.Println(err)
		return
	}

	content, err := getContent(db, 1)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println("Retrieved content:", content)

	err = updateContent(db, 1, Content{Title: "Updated Post", Body: "This post has been updated"})
	if err != nil {
		fmt.Println(err)
		return
	}

	content, err = getContent(db, 1)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println("Updated content:", content)

	err = deleteContent(db, 1)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println("Content deleted successfully.")
}

In this code, we defined the Content struct to represent the content stored in the database. Then, we implemented four functions: createContent to create new content, getContent to fetch content by its ID, updateContent to update existing content, and deleteContent to remove content from the database.

In the main function, we tested these CRUD operations by creating a new content, retrieving it, updating it, and deleting it.

To run this code, execute the following command:

$ go run crud.go

Building the User Interface

Now that we have the backend functionality in place, let’s build a simple user interface to interact with our CMS. Create a new file called main.go and add the following code:

package main

import (
	"database/sql"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"strconv"

	_ "github.com/mattn/go-sqlite3"
)

type Content struct {
	ID    int
	Title string
	Body  string
}

func createContent(db *sql.DB, content Content) error {
	_, err := db.Exec("INSERT INTO content (title, body) VALUES (?, ?)", content.Title, content.Body)
	return err
}

func getContent(db *sql.DB, id int) (Content, error) {
	var content Content

	row := db.QueryRow("SELECT * FROM content WHERE id = ?", id)
	err := row.Scan(&content.ID, &content.Title, &content.Body)
	if err != nil {
		return content, err
	}

	return content, nil
}

func updateContent(db *sql.DB, id int, newContent Content) error {
	_, err := db.Exec("UPDATE content SET title = ?, body = ? WHERE id = ?", newContent.Title, newContent.Body, id)
	return err
}

func deleteContent(db *sql.DB, id int) error {
	_, err := db.Exec("DELETE FROM content WHERE id = ?", id)
	return err
}

func mainHandler(w http.ResponseWriter, r *http.Request) {
	db, err := sql.Open("sqlite3", "./cms.db")
	if err != nil {
		log.Println(err)
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	defer db.Close()

	if r.Method == "GET" {
		id, err := strconv.Atoi(r.URL.Query().Get("id"))
		if err != nil {
			http.Error(w, "Bad Request", http.StatusBadRequest)
			return
		}

		content, err := getContent(db, id)
		if err != nil {
			log.Println(err)
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			return
		}

		tmpl := template.Must(template.ParseFiles("template.html"))
		err = tmpl.Execute(w, content)
		if err != nil {
			log.Println(err)
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			return
		}
	}

	if r.Method == "POST" {
		title := r.FormValue("title")
		body := r.FormValue("body")

		content := Content{Title: title, Body: body}

		err := createContent(db, content)
		if err != nil {
			log.Println(err)
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			return
		}

		http.Redirect(w, r, "/", http.StatusSeeOther)
	}
}

func deleteHandler(w http.ResponseWriter, r *http.Request) {
	db, err := sql.Open("sqlite3", "./cms.db")
	if err != nil {
		log.Println(err)
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	defer db.Close()

	if r.Method == "POST" {
		idStr := r.FormValue("id")

		id, err := strconv.Atoi(idStr)
		if err != nil {
			http.Error(w, "Bad Request", http.StatusBadRequest)
			return
		}

		err = deleteContent(db, id)
		if err != nil {
			log.Println(err)
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			return
		}

		http.Redirect(w, r, "/", http.StatusSeeOther)
	}
}

func main() {
	http.HandleFunc("/", mainHandler)
	http.HandleFunc("/delete", deleteHandler)

	log.Fatal(http.ListenAndServe(":8080", nil))
}

In this code, we defined two HTTP request handlers: mainHandler and deleteHandler. The mainHandler handles both GET and POST requests on the root path (“/”). For GET requests, it retrieves the content by its ID, renders an HTML template (template.html), and sends it as the response. For POST requests, it extracts the form values for title and body, creates a new content, and redirects the user back to the root path.

The deleteHandler handles POST requests on the “/delete” path. It extracts the ID of the content to be deleted from the form, deletes the content from the database, and redirects the user back to the root path.

To complete the user interface, create a file called template.html and add the following HTML code:

<!DOCTYPE html>
<html>
<head>
  <title>CMS</title>
</head>
<body>
  <h1>CMS</h1>

  {{ if .ID }}
    <h2>{{ .Title }}</h2>
    <p>{{ .Body }}</p>

    <form action="/delete" method="post">
      <input type="hidden" name="id" value="{{ .ID }}">
      <button type="submit">Delete</button>
    </form>
  {{ else }}
    <form action="/" method="post">
      <label for="title">Title</label>
      <input type="text" name="title" required><br>

      <label for="body">Body</label>
      <textarea name="body" required></textarea><br>

      <button type="submit">Create</button>
    </form>
  {{ end }}
</body>
</html>

This HTML template conditionally renders the content if it exists, displaying the title, body, and a delete button. If there is no content, it displays a form to create new content.

To run the CMS, execute the following command:

$ go run main.go

Open your web browser and visit http://localhost:8080 to start using the CMS.

Conclusion

In this tutorial, you have learned how to build a simple Content Management System (CMS) using Go. We covered the basic steps of setting up the project, creating the database schema, implementing CRUD operations, and building a user interface. You can now extend this CMS by adding features like user authentication, pagination, and search functionality.

Remember to handle errors properly and perform input validation to ensure the security and stability of your CMS. Happy coding!