AI-generated article: This walkthrough was generated by AI to document this site’s publishing workflow and test its Buffer integration.
This site uses Hugo and GitHub Pages. After a new blog post deploys, a GitHub Actions workflow sends an announcement to Buffer for sharing on LinkedIn, Mastodon, and Bluesky. Automatic queueing is the goal; the initial testing phase uses drafts to check the text and target accounts before anything is shared. Draft mode remains available as an optional review step.
The implementation is in this repository. If you also use Hugo and GitHub Pages, you can reuse it with a few changes.
Start with a site that already deploys from GitHub Actions when you push to main, and connect your social accounts to Buffer. The script expects one LinkedIn, one Mastodon, and one Bluesky channel. You will need Hugo and Node.js 22 locally to run the tests.
From the root of your site’s repository, copy these files. The URL uses a specific commit so you get the version described here:
for file in \
layouts/index.blogmanifest.json \
scripts/buffer.mjs \
tests/buffer.test.mjs \
.github/workflows/buffer.yml
do
mkdir -p "$(dirname "$file")"
curl --fail --location \
"https://raw.githubusercontent.com/svrnm/svrnm.com/028156e82b8d4aabfd32cd0f2b66890ea0cfa0bf/$file" \
--output "$file"
done
Replace https://svrnm.com in the script and tests with your site’s origin, without a trailing slash. The script validates URLs against that origin before sending anything. If your blog lives in content/posts/, change the template’s section filter from "blog" to "posts" and the Hugo test’s writePost('blog/...') paths to writePost('posts/...'). This site uses content/blog/; videos and external publications live elsewhere, so they stay out of the list even when their category is blog.
Hugo needs to produce that list during the build. Add the following to hugo.yaml, merging it with any existing output configuration. Keep the output formats your site already uses:
outputFormats:
BlogManifest:
mediaType: application/json
baseName: blog-manifest
isPlainText: true
notAlternative: true
outputs:
home:
- HTML
- RSS
- JSON
- BlogManifest
The template generates public/blog-manifest.json. Each entry has a title, canonical URL, announcement text, and a skip flag. Because Hugo builds the list, its usual rules for drafts, future dates, and expired posts apply.
In your existing deployment workflow, upload the file after the Hugo build:
- name: Upload blog manifest
uses: actions/upload-artifact@v7.0.2
with:
name: blog-manifest
path: public/blog-manifest.json
if-no-files-found: error
Then edit the workflows value in the copied buffer.yml to match the name of your deployment workflow. In this repository, it is Deploy Hugo site to GitHub Pages. Adjust its main branch filter too if you deploy from another branch.
The Buffer workflow waits for a successful push deployment and downloads the manifest from that exact run. That means the link is live before Buffer receives the announcement. A manual run of the Buffer workflow only checks saved post statuses; it does not submit new announcements.
Next, create a personal API key under Buffer’s API settings. Give it accountRead, postsRead, and postsWrite permissions, and add it to your repository’s Actions secrets as BUFFER_API_KEY. Buffer’s personal keys expire after at most a year, so this secret will eventually need replacing. Buffer’s key setup guide covers the options.
For the initial test, leave the repository variable BUFFER_MODE unset: the default is draft. The script calls Buffer’s createPost mutation with saveToDraft: true, so you can inspect the results without sending them to your followers. For automatic sharing, set BUFFER_MODE to queue. New announcements then use addToQueue with saveToDraft: false and publish in the next available slots in your Buffer schedule. You can switch back to draft whenever you want to review announcements manually. Buffer’s API reference documents these settings.
Channel discovery can find more than one account for each service. During setup on this site, it found both personal channels and OpenTelemetry’s channels, so the workflow stopped and listed their names and IDs. To select the intended accounts, set BUFFER_ORGANIZATION_ID to the organization ID and BUFFER_CHANNEL_IDS to the three channel IDs, separated by commas. These belong in GitHub’s Actions variables. If there is only one matching channel per service, discovery works without them.
Before pushing, run:
node --test tests/buffer.test.mjs
hugo --minify
Inspect public/blog-manifest.json as well. It should contain your public blog posts, with URLs pointing to your site. Push the setup first, before adding the post you want to test. On the first successful run, the workflow records the existing posts as a baseline and sends nothing to Buffer. The initial baseline for this site contained twelve posts.
That record lives in buffer-state.json on a separate buffer-state Git branch. Subsequent runs save a Buffer post ID for each URL and channel. Editing a post at the same URL does not announce it again. An attempt is recorded before the API call, so a timeout leaves an unconfirmed entry for manual inspection in Buffer before retrying. Otherwise a lost response could turn into a duplicate announcement. The operation notes explain how to reconcile those entries.
Now add a new blog post and push it. After the site deploys, check the Buffer workflow’s summary for three post IDs, then open Buffer and read the drafts. Pushing an edit to that post should leave you with the same three drafts.
The default announcement is the title and URL. For a different introduction, add this to the post’s front matter:
social:
text: "A short introduction for the people reading this on social media."
The script appends the URL. social.skip: true suppresses an announcement. The same text goes to all three channels, so keep it short enough for Bluesky.
After checking the first drafts and confirming that edits do not create duplicates, set BUFFER_MODE to queue in the repository’s Actions variables. From then on, new posts are scheduled automatically. Changing modes leaves any drafts from the test phase alone; you can publish or delete those in Buffer.
Every six hours, the workflow checks the saved IDs and records their status. When Buffer publishes an announcement, the record can include its publication time and social post URL. Publishing errors show up as a failed Actions run, both for posts scheduled manually during testing and for announcements queued automatically.
This post is the first test of submitting an announcement. The setup and baseline runs have passed. Publishing this page should create three drafts for review before automatic queueing is enabled.