Apr 9, 2023 7 min read

How I Built An Autonomous Newsletter Compiler Tool With GoLang, GitHub Actions, Listmonk, And Docker

I decided to build an autonomous newsletter compiler tool using my favorite programming language Go along with Listmonk and GitHub Actions. Let's walk through the steps and code to build this tool.

How I Built An Autonomous Newsletter Compiler Tool With GoLang, GitHub Actions, Listmonk, And Docker
Table of Contents

I have been running Opensource Geeks for quite some time and after I always compiled my newsletters manually a couple of times per month, I decided to build an autonomous newsletter compiler tool using my favorite programming language Go along with Listmonk and GitHub Actions. Let's walk through the steps and code to build this tool.

How Does This Autonomous Newsletter Tool Work?

Code Logic And Listmonk API

My blog RSS feed is queried using the gofeed module. From that data, the latest 5 articles are extracted, structured, and posted to an HTML template which is then posted to the Listmonk API to build and send the compiled newsletter to the specified subscribers' group.

GitHub Actions To Build The Go App And Send Newsletter Weekly

Once the Go App code is complete and tested, I scripted the CICD workflows to schedule the Go App execution to run weekly with cron in order to build, compile and send out the newsletter weekly.

Let's Code The Tool With Go Programming

The first step was to set up the GoLang project using go mod init but for this article, I am not going to walk through that whole process. If you want to use this tool and build out your own newsletter with it you can fork this project on GitHub and do just that.

I created three packages and called them listmonk.go, rss.go and loadenv.go. These three files are going to host the core code logic of the App. Let's start with the loadenv.go code. This code imports the godotenv module to ensure you can import and use.env files in your Go code.

loadenv.go

package loadenv

import (
	"log"

	"github.com/joho/godotenv"
)

func LoadEnv() {
	err := godotenv.Load(".env")
	if err != nil {
		log.Fatalf("Error occured. Err: %s", err)
	}
}

Next, I developed the rss.go code to query my blog's RSS feed, that data is structured and compiled to post the latest five articles of the blog to newsletter.html.

rss.go

package rss

import (
	"log"
	"os"

	"github.com/mmcdole/gofeed"
)

func Rss() {
	fp := gofeed.NewParser()
	feed, _ := fp.ParseURL("") // add your RSS feed url here

	f, err := os.Create("./newsletter.html")
	if err != nil {
		log.Fatal(err)
	}

	defer f.Close()

	f.WriteString("<h2>Check Out This Week's Articles: </h2><br> ") // replace this line with your preferred newsletter intro

	for i := 0; i < 5; i++ {
		f.WriteString("<p><a href='" + feed.Items[i].Link + "'>" + feed.Items[i].Title + "</a> - " + feed.Items[i].Description + "</p> \n")

	}

	f.WriteString("Thanks for being part of the Opensource Geeks community💻🐧") // replace this line with your preffered newsletter outro or conclusion
}

Next, I built the code to structure the data and post it to my newsletter platform Listmonk API where the newsletter is compiled and sent via the Listmonk API.

listmonk.go

package listmonk

import (
	"bytes"
	_ "embed"
	"encoding/json"
	"io/fs"
	"log"
	"net/http"
	"os"
	"time"

	"github.com/mmcdole/gofeed"
)

func Listmonk() {

	time.Sleep(30 * time.Second)

	fc, _ := fs.ReadFile(os.DirFS("."), "newsletter.html")
	newsletterName := "newsletter-" + time.Now().Format("01-02-2006")

	fp := gofeed.NewParser()
	feed, _ := fp.ParseURL("https://opensourcegeeks.net/rss/") // replace with your website or blog rss feed url
	newsletterSubject := feed.Items[0].Title

	type Payload struct {
		Name        string `json:"name"`
		Subject     string `json:"subject"`
		Lists       []int  `json:"lists"`
		FromEmail   string `json:"from_email"`
		ContentType string `json:"content_type"`
		Body        string `json:"body"`
		Messenger   string `json:"messenger"`
		Type        string `json:"type"`
		TemplateID  int    `json:"template_id"`
	}

	data := Payload{
		Name:        newsletterName,
		Subject:     newsletterSubject,
		Lists:       []int{3},
		FromEmail:   "Chad at Opensource Geeks <[email protected]>", // replace this with your name and email address
		ContentType: "html",
		Body:        string(fc),
		Messenger:   "email",
		Type:        "regular",
		TemplateID:  3, // replace with your listmonk template id
	}
	payloadBytes, err := json.Marshal(data)
	if err != nil {
		log.Fatalf("Error occured. Err: %s", err)
	}
	body := bytes.NewReader(payloadBytes)
    
    var api = os.Getenv("LISTMONK_API")

	req, err := http.NewRequest("POST", api, body) // replace this with your listmonk campaign api url
	if err != nil {
		log.Fatalf("Error occured. Err: %s", err)
	}

	var username = os.Getenv("USER")     // get listmonk username from .env file
	var password = os.Getenv("PASSWORD") // get listmonk password from .env

	req.SetBasicAuth(username, password)
	req.Header.Set("Content-Type", "application/json;charset=utf-8")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatalf("Error occured. Err: %s", err)
	}
	defer resp.Body.Close()

}

Once all our package code is completed, we are going to import all the local modules into our main.go file which is where our Go application executes all our code.

main.go

package main

import (
	_ "embed"
	"newsletter-builder/listmonk"
	"newsletter-builder/loadenv"
	"newsletter-builder/rss"
)

func main() {

	loadenv.LoadEnv()
	rss.Rss()
	listmonk.Listmonk()

}

Script Github Actions CICD Workflows To Compile Newsletter Weekly

Create the .github directory in your root repository and create the workflows and actions directories. Under the ./.github/actions/ directory create a directory called build and add action.yml file to it. This is the Github Actions composite template that will perform testing and auditing of our GoLang Application in the CICD workflows.

action.yml

name: "Build, Test & Audit Go App"
description: "Build, test and audit steps for Go App"

runs:
  using: "composite"
  steps:
    - name: Set up Go
      uses: actions/setup-go@v3
      with:
        go-version: 'stable'

    - name: Verify dependencies
      shell: bash
      run:  go mod verify

    - name: Install golint
      shell: bash
      run: go install golang.org/x/lint/golint@latest

    - name: Run vet & lint
      shell: bash
      run: |
        go vet .
        golint .
    - name: Build go app
      shell: bash
      run: go build -v .

    - name: Go unit test
      id: Unit-Test-Run
      shell: bash
      run: go test -v ./...

Once done we will create the send-newsletter.yml file under the ./.github/workflows/ directory where I scripted all the workflow steps. This will execute the Go app on a weekly basis using cron to compile and send out your newsletter.

name: Send Newsletter

# on:
#   workflow_dispatch

on:
  # workflow_dispatch:
  schedule:
    # Runs every Sunday at 18:30
    - cron: '5 19 * * Sun'


jobs:
  build-app:
    runs-on: self-hosted
    # if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Test & Build Go App
        uses: ./.github/actions/build

  send-newsletter:
    needs: [build-app]
    runs-on: self-hosted
    env:
      GITHUB_TOKEN: ${{ github.token }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Create .env
        run: |
          echo ${{ secrets.LISTMONK_USERNAME }} >> ./.env
          echo ${{ secrets.LISTMONK_PASSWORD}} >> ./.env
          echo ${{ secrets.LISTMONK_API}} >> ./.env

      - name: Set up Go
        uses: actions/setup-go@v3
        with:
          go-version: 'stable'

      - name: Verify dependencies
        run:  go mod verify

      - name: Install dependencies
        run:  go mod tidy

      - name: Send newsletter
        run: go run -v ./main.go

Once everything has been scripted and set up, you can commit to the repository and create your secrets using Github secrets to store your Listmonk API username and password. Navigate to your GitHub repository and select settings. Under the menu on the right-hand side navigate to Secrets and variables the drop-down menu and click on Actions. Select New repository secret to create your secrets for your Listmonk API username and password which will be used in one of the workflow steps to authenticate with your Listmonk API.

GitHub Actions Secrets
Listmonk Newsletter Campaign

How To Use This Tool For Your Blog Newsletter

That is probably quite a lot to take in. Let's dial it down on how to use this software to send out your newsletter. Step one would be to fork this repository and then clone it to your local machine. Replace all the sections where I left comments with your environment configuration and details and commit your changes. Once all those steps are complete have a look at your Github Actions section to ensure that your workflow is ready to execute weekly via cron.

If you do not use have Listmonk newsletter platform deployed in your environment to manage your newsletter you can follow the steps in this link to deploy it using Docker:

GitHub - chaddyc/auto-newsletter-listmonk: Autonomous newsletter builder tool for Listmonk and Ghost Blog CMS. This GoLang App compiles a newsletter from an RSS feed and posts it to Listmonk via API to create the new campaign.
Autonomous newsletter builder tool for Listmonk and Ghost Blog CMS. This GoLang App compiles a newsletter from an RSS feed and posts it to Listmonk via API to create the new campaign. - GitHub - c…
How To Install Listmonk With Docker
Want to run your own newsletter? Listmonk is a self-hosted free and open-source, high-performance mailing list and newsletter manager.

Conclusion

In conclusion, this Autonomous Newsletter Compiler tool that I built makes life so easy and fluent for my weekly newsletter. I am sure it might help a few other content creators and bloggers to automate their newsletter workloads. If you enjoyed this article consider signing up for our newsletter and don't forget to share it with people that would find it useful. Leave a comment below with a tutorial you would like us to cover.

Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to Opensource Geeks.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.