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