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
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)
}
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
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
}
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
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
}
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>
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")
}
}
// ...
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>
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:
// ...
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:
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
router.GET("/images/:path", getImage)
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
}
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}}">
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
or to my Twitter
https://twitter.com/shogenoff
Have a nice code!
Top comments (0)