DEV Community

Cover image for Create a blog with content stored in GitHub (Examples in Golang)
Alex Awesome
Alex Awesome

Posted on

Create a blog with content stored in GitHub (Examples in Golang)

You have the cheapest and easiest way to start your own IT(or not) blog. You don't need a lot of space to store your photos, and you don't even need to use a database.

In this article, I suggest using GitHub to store and edit articles. We will use a familiar GitHub markdown to design articles.

I will use Golang examples. But we can rewrite this code in PHP if we want the cheapest or even free solution. We can get the simplest PHP hosting that can be free.

Let's get it started!

Preparations

First, I need to choose the Go web framework. It doesn't matter which one. I closed my eyes and pointed at the pkg.go.dev site. My finger was on the Gin Framework. Okay, this is a popular choice.

Install Gin using the following command:

go get -u github.com/gin-gonic/gin
Enter fullscreen mode Exit fullscreen mode

Then create a basic Gin web server:

package main

import (
    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    router.GET("/", getHome)
    router.GET("/article/:articleID", getArticle)
    router.Run(":8080")
}

func getHome(c *gin.Context) {
    c.String(200, "Welcome to the blog!")
}

func getArticle(c *gin.Context) {
    articleID := c.Param("articleID")
    c.String(200, "Displaying article with ID: %s", articleID)
}

Enter fullscreen mode Exit fullscreen mode

Fetch content for GitHub

To fetch articles from a GitHub repository, we'll need to use the GitHub API. You can use the google/go-github library:

go get github.com/google/go-github/v39/github
Enter fullscreen mode Exit fullscreen mode

Add a function to fetch articles from the GitHub repository:

package main

import (
    "context"
    "fmt"
    "os"

    "github.com/gin-gonic/gin"
    "github.com/google/go-github/v39/github"
    "golang.org/x/oauth2"
)

// ...

func getArticle(c *gin.Context) {
    articleID := c.Param("articleID")
    content, err := fetchArticle(articleID)
    if err != nil {
        c.String(500, "Error fetching article")
        return
    }
    c.String(200, "Article content:\n%s", content)
}

func fetchArticle(articleID string) (string, error) {
    ctx := context.Background()
    ts := oauth2.StaticTokenSource(
        &oauth2.Token{AccessToken: os.Getenv("GITHUB_TOKEN")},
    )
    tc := oauth2.NewClient(ctx, ts)
    client := github.NewClient(tc)

    fileContent, _, _, err := client.Repositories.GetContents(ctx, os.Getenv("GITHUB_USERNAME"), os.Getenv("GITHUB_REPO"), "path/to/articles/"+articleID+".md", &github.RepositoryContentGetOptions{})
    if err != nil {
        return "", err
    }

    content, err := fileContent.GetContent()
    if err != nil {
        return "", err
    }

    return content, nil
}
Enter fullscreen mode Exit fullscreen mode

We need to set up a GitHub access token with the repo scope and store it in the GITHUB_TOKEN environment variable. We also need to store the environment variables "GITHUB_USERNAME" and "GITHUB_REPO" with the values of the actual GitHub username and repository name.

This example serves as a starting point for creating a blog that fetches articles from a GitHub repository using Golang.

Then we need to build upon this example to handle templating, styling, and other web development tasks to create a complete website.

Render HTML

To render articles from Markdown to HTML, we can use the github.com/gomarkdown/markdown package. This package also supports syntax highlighting for code blocks. It's very important for us because we are trying to create a popular IT blog.

First, install the necessary packages:

go get -u github.com/gomarkdown/markdown
go get -u github.com/gomarkdown/markdown/parser
Enter fullscreen mode Exit fullscreen mode

Next, modify the fetchArticle function to convert the fetched Markdown content into HTML, and update the getArticle function to render the HTML content:

package main

import (
    "context"
    "fmt"
    "os"

    "github.com/gin-gonic/gin"
    "github.com/github.com/gomarkdown/markdown"
    "github.com/github.com/gomarkdown/markdown/parser"
    "github.com/google/go-github/v39/github"
    "golang.org/x/oauth2"
)

// ...

func getArticle(c *gin.Context) {
    articleID := c.Param("articleID")
    content, err := fetchArticle(articleID)
    if err != nil {
        c.String(404, "Article not found")
        return
    }
    c.Data(200, "text/html; charset=utf-8", []byte(content))
}

func fetchArticle(articleID string) (string, error) {
    ctx := context.Background()
    ts := oauth2.StaticTokenSource(
        &oauth2.Token{AccessToken: os.Getenv("GITHUB_TOKEN")},
    )
    tc := oauth2.NewClient(ctx, ts)
    client := github.NewClient(tc)

    fileContent, _, _, err := client.Repositories.GetContents(ctx, "YourGitHubUsername", "YourRepoName", "path/to/articles/"+articleID+".md", &github.RepositoryContentGetOptions{})
    if err != nil {
        return "", err
    }

    markdownContent, err := fileContent.GetContent()
    if err != nil {
        return "", err
    }

    // Parse and render the Markdown content
    extensions := parser.CommonExtensions | parser.AutoHeadingIDs
    mdParser := parser.NewWithExtensions(extensions)
    htmlContent := markdown.ToHTML([]byte(markdownContent), mdParser, nil)

    return string(htmlContent), nil
}

Enter fullscreen mode Exit fullscreen mode

Now, the fetchArticle fetches the Markdown content from the GitHub repository, convert it to HTML, and renders in the browser with syntax highlighting for code blocks.

So far this code only covers the basics of rendering Markdown to HTML. We need to add CSS.

Make it stylish

To render HTML we can use the html/template package from the Go standard library.

Create a new file called templates/base.html with the following content:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="{{.Description}}">
    <title>{{.Title}}</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <style>
        /* Add your custom CSS styles here */
    </style>
</head>
<body>
    <div class="container">
        {{.Content}}
    </div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

We use the Bootstrap CSS framework for styling to make it simpler.
You can replace it with your preferred CSS framework or custom styles.

Next, update the getArticle function to render the article content using the base.html template:

package main

import (
    "context"
    "fmt"
    "os"
    "html/template"

    "github.com/gin-gonic/gin"
    "github.com/github.com/gomarkdown/markdown"
    "github.com/github.com/gomarkdown/markdown/parser"
    "github.com/google/go-github/v39/github"
    "golang.org/x/oauth2"
)

// ...

func getArticle(c *gin.Context) {
    articleID := c.Param("articleID")
    content, err := fetchArticle(articleID)
    if err != nil {
        c.String(500, "Error fetching article")
        return
    }

    tmpl, err := template.ParseFiles("templates/base.html")
    if err != nil {
        c.String(500, "Error loading template")
        return
    }

    data := map[string]interface{}{
        "Title":       "Your Blog - " + articleID,
        "Description": "An article with the ID " + articleID,
        "Content":     template.HTML(content),
    }

    err = tmpl.Execute(c.Writer, data)
    if err != nil {
        c.String(500, "Error rendering template")
    }
}

// ...
Enter fullscreen mode Exit fullscreen mode

I also added the simplest way to render <title> and <description> tags.

Code-style

We want to copy the GitHub style of .md files for our blog design.
We will include the github-markdown-css package and a syntax highlighting library highlight.js.

First, update the templates/base.html file to include the necessary styles and scripts:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="{{.Description}}">
    <title>{{.Title}}</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/4.0.0/github-markdown.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/styles/github.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
    <script>hljs.highlightAll();</script>
    <style>
        .markdown-body {
            box-sizing: border-box;
            min-width: 200px;
            max-width: 980px;
            margin: 0 auto;
            padding: 45px;
        }

        @media (max-width: 767px) {
            .markdown-body {
                padding: 15px;
            }
        }
    </style>
</head>
<body>
    <div class="container markdown-body">
        {{.Content}}
    </div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

That's it. Quite simple, isn't it? Now our blog should display articles with GitHub-style CSS and proper syntax highlighting for code blocks.

Images

The last one is images. At this point, our blog is not displaying images because the image links have relative paths.

To host images for the articles on GitHub and include them in the content, we can follow these steps:

Create directory

Upload your images to a specific folder in your GitHub repository. In our case it will will be the images folder

Add images to the article

In the Markdown article, reference the image using a relative path to the image file in your GitHub repository:

![Image description](/images/your-image-file.jpg)
Enter fullscreen mode Exit fullscreen mode

Replace the image in the server-side

When fetching the article content in the fetchArticle function, replace the relative image paths with their corresponding raw.githubusercontent.com URLs:

// ...

func fetchArticle(articleID string) (string, error) {
    // Fetch article content from GitHub
    // ...

    // Replace relative image paths with raw.githubusercontent.com URLs
    rawImageBaseURL := "https://raw.githubusercontent.com/YourGitHubUsername/YourRepoName/main/images/"
    re := regexp.MustCompile(`!\[(.*?)\]\(/images/(.+?)\)`)
    markdownContent = re.ReplaceAllString(markdownContent, fmt.Sprintf(`![$1](%s$2)`, rawImageBaseURL))

    // Convert Markdown to HTML
    // ...

    return string(htmlContent), nil
}
Enter fullscreen mode Exit fullscreen mode

It will work only for public repositories. If you would like to have a private one your code will be a bit complicated.

You can create a getImage function for getting images from GitHub:


func getImage(c *gin.Context) {
    path := c.Param("path")
    if _, ok := imagesCache[path]; !ok {
        response, err := requestFromGithub("images/" + path)
        if err != nil {
            c.String(404, "Not found")
        }
        responseImg, _ := http.Get(*response.DownloadURL)
        buf := new(strings.Builder)
        io.Copy(buf, responseImg.Body)
        imagesCache[path] = buf.String()
    }
    c.String(200, imagesCache[path])
}


func requestFromGithub(path string) (*github.RepositoryContent, error) {
    ctx := context.Background()
    var ts = oauth2.StaticTokenSource(
        &oauth2.Token{AccessToken: os.Getenv("GITHUB_TOKEN")},
    )
    tc := oauth2.NewClient(ctx, ts)
    client := github.NewClient(tc)

    fileContent, _, _, err := client.Repositories.GetContents(
        ctx,
        os.Getenv("GITHUB_USER"),
        os.Getenv("GITHUB_REPO"),
        path,
        &github.RepositoryContentGetOptions{
            Ref: "main",
        },
    )

    return fileContent, err
}
Enter fullscreen mode Exit fullscreen mode

Add a route to the Gin

router.GET("/images/:path", getImage)
Enter fullscreen mode Exit fullscreen mode

And replace relative paths to images with absolute ones:


func fetchArticle(articleID string) (string, error) {
    htmlContentString, ok := articlesCache[articleID]
    if !ok {
        fileContent, err := requestFromGithub("articles/" + articleID + ".md")
        if err != nil {
            return "", err
        }

        markdownContent, err := fileContent.GetContent()
        if err != nil {
            return "", err
        }

        extensions := parser.CommonExtensions | parser.AutoHeadingIDs
        mdParser := parser.NewWithExtensions(extensions)
        htmlContent := markdown.ToHTML([]byte(markdownContent), mdParser, nil)

        htmlContentString = strings.Replace(
            string(htmlContent),
            "<img src=\"../images/",
            "<img src=\"/images/",
            -1,
        )
    }

    return htmlContentString, nil
}
Enter fullscreen mode Exit fullscreen mode

That's it for this article. We could improve the Social-Network appearance by adding something like this

 <!-- Open Graph Tags -->
    <meta property="og:title" content="{{.Title}}">
    <meta property="og:description" content="{{.Description}}">
    <meta property="og:type" content="article">
    <meta property="og:url" content="{{.URL}}">
    <meta property="og:image" content="{{.ImageURL}}">

    <!-- Twitter Card Tags -->
    <meta name="twitter:card" content="summary_large_image">
    <meta name="twitter:title" content="{{.Title}}">
    <meta name="twitter:description" content="{{.Description}}">
    <meta name="twitter:image" content="{{.ImageURL}}">
Enter fullscreen mode Exit fullscreen mode

We could also improve SEO-Shmeo things by storing title and description in some way.

But these things are superfluous for this article.


If you liked the article then subscribe to the
Telegram-channel

https://t.me/awesomeprog

or to my Twitter
https://twitter.com/shogenoff

Have a nice code!

Top comments (0)