Create a blog with content stored in GitHub (Examples in Golang)
Published: 2023-03-31 18:18:50

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:

```shell
go get -u github.com/gin-gonic/gin
```

Then create a basic Gin web server:

```go
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)
}

```

## 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:

```shell
go get github.com/google/go-github/v39/github
```

Add a function to fetch articles from the GitHub repository:

```go
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
}
```

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:

```shell
go get -u github.com/gomarkdown/markdown
go get -u github.com/gomarkdown/markdown/parser
```

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

```go
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
}

```
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:

```html
<!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>
```

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:

```go
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")
	}
}

// ...
```

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:

```html
<!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>
```

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)
```

### 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:

```go
// ...

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
}
```

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:

```go

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
}
```

Add a route to the Gin

```go
router.GET("/images/:path", getImage)
```

And replace relative paths to images with absolute ones:

```go

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
}
```

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

```html
 <!-- 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}}">
```

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](https://t.me/awesomeprog)

or to my Twitter
[https://twitter.com/shogenoff](https://twitter.com/shogenoff)

Have a nice code!