Compose daily blog posts in a multi-post single-file format from multiple data sources including Mastodon and Linkding...
This skill enables composing and iteratively updating daily blog posts by automatically fetching content from multiple sources (Mastodon posts and Linkding bookmarks) and organizing them into a special multi-post single-file format. Unlike traditional blog posts, this format allows multiple posts for a single day to be composed in one Markdown file, with each post having its own metadata and publish time.
The skill handles:
Daily blog posts use a special format where multiple posts for one day live in a single file (YYYY-MM-DD.md):
8<--- { "title": "Miscellanea for 2025-05-12", "time": "23:59:00-07:00", "type": "miscellanea", "slug": "miscellanea", "tags": ["miscellanea"] }
- Hello world!
- First bullet point about something
- Another observation with a [link](https://example.com)
8<--- { "title": "Adventures in Vibe Coding", "slug": "vibe-coding", "tags": ["coding", "ai"], "time": "15:00:00-07:00" }
This is a focused post about a specific topic...
More paragraphs of prose...
Each post starts with a divider: 8<--- followed by JSON metadata:
{
"title": "Post Title",
"time": "HH:MM:SS-07:00",
"slug": "post-slug",
"tags": ["tag1", "tag2"],
"type": "miscellanea", // optional
"draft": false // optional, defaults to false
}
Required fields:
title - Post titletime - Time with timezone (affects publication order)slug - URL-friendly slugtags - Array of tagsOptional fields:
type - Post type (e.g., "miscellanea")draft - Whether post is draft (separate from future-dating)Critical concept: Posts with times in the future are effectively drafts and won't be fully published until that time arrives.
This is why:
"23:59:00-07:00" (11:59 PM) - they're the last thing published each dayThis means you can freely revise posts throughout the day since future-dated posts are effectively drafts until their time passes.
1. Miscellanea Post (always included):
2. Focused Posts (0-3 per day, created as themes emerge):
When a user first requests to create or update daily posts, check if the skill is configured:
cd /path/to/daily-blog-post-composer
# Check if config exists
if [ ! -f "./config/config.json" ]; then
echo "First-time setup required."
./scripts/setup.sh
fi
If configuration doesn't exist:
scripts/setup.sh with their inputsMastodon Access Token:
Linkding API Token:
The primary workflow for composing and updating daily posts is designed to be run multiple times throughout the day. Each run fetches fresh data and intelligently merges it with existing content.
By default, use today's date. The user might also specify:
from datetime import datetime, timedelta
target_date = datetime.now()
# Or for yesterday:
target_date = datetime.now() - timedelta(days=1)
date_str = target_date.strftime("%Y-%m-%d")
Detecting the blog directory:
Check if the current working directory contains content/posts/ - if so, you're in the blog directory.
if [ -d "content/posts" ]; then
echo "In blog directory"
fi
File path:
content/posts/{YYYY}/{YYYY-MM-DD}.md
Check if file exists:
cd /path/to/blog-directory
target_file="content/posts/2025/2025-12-15.md"
if [ -f "$target_file" ]; then
echo "File exists - will merge with existing content"
else
echo "Starting fresh daily post"
fi
Always fetch a rolling 24-hour window of source data. This keeps it simple - no need to track "what's already been fetched."
cd /path/to/daily-blog-post-composer
# For current 24-hour window (now minus 24 hours to now)
./scripts/fetch-sources.sh
# For specific date's full day (00:00 to 23:59)
./scripts/fetch-sources.sh --date YYYY-MM-DD
# For custom range
./scripts/fetch-sources.sh --start YYYY-MM-DD --end YYYY-MM-DD
This fetches:
Output files are saved to data/latest/:
mastodon.md - Formatted Mastodon postslinkding.md - Formatted bookmarksVerify fetched data:
cd /path/to/daily-blog-post-composer
./scripts/prepare-sources.py
# Read the source files
cat data/latest/mastodon.md
cat data/latest/linkding.md
Check for configured style reference:
# Check if blog archive URL is configured
cd /path/to/daily-blog-post-composer
cat config/config.json
If the config contains a blog archive URL, or if you can detect existing daily posts in the blog directory, fetch and review 1-2 recent daily posts to understand the user's writing style and voice.
Look for daily posts in the blog:
# Check recent daily posts
ls -lt content/posts/2025/*.md | head -5
Read a couple recent daily post files to understand:
Voice & Tone:
Miscellanea structure:
Focused post style:
When composing, aim to match this voice rather than using a generic blog style.
If the target file exists, read and parse it to understand what's already covered:
# Read existing file
cat content/posts/2025/2025-12-15.md
Parse the file to extract:
Track what's covered to avoid duplication:
https://masto.hackers.town/@user/12345)Now intelligently decide where new content should go:
For each new Mastodon post or bookmark:
Is it already covered?
Does it fit an existing focused post theme?
Does it cluster with other items to form a new theme?
Otherwise:
Content placement priorities:
Miscellanea as staging area:
Theme extraction strategy (hybrid approach):
Scan miscellanea bullets (both new and existing)
Identify clusters:
Maximum focused posts per day: 3-5
Create new focused post:
Be sparing with user input:
Post timing strategy:
"23:59:00-07:00""14:23:00-07:00")Revising existing focused posts:
File structure:
8<--- { "title": "First Focused Post Title", "slug": "first-post", "tags": ["tag1", "tag2"], "time": "09:00:00-07:00" }
Content of first focused post...
8<--- { "title": "Second Focused Post Title", "slug": "second-post", "tags": ["tag3", "tag4"], "time": "15:00:00-07:00" }
Content of second focused post...
8<--- { "title": "Miscellanea for YYYY-MM-DD", "time": "23:59:00-07:00", "type": "miscellanea", "slug": "miscellanea", "tags": ["miscellanea"] }
- Hello world!
- First bullet point
- Second bullet point with [a link](https://example.com)
- Image from Mastodon: 
Post order:
Miscellanea formatting:
Saving the file:
# Ensure year directory exists
mkdir -p content/posts/2025
# Write to file
# content/posts/2025/2025-12-15.md
File path format:
content/posts/{YYYY}/{YYYY-MM-DD}.md
Where:
{YYYY} = 4-digit year{YYYY-MM-DD} = Target date (e.g., 2025-12-15)Before finalizing (especially at end of day):
Structure check:
Content verification:
Prose polish:
Draft management:
"draft": true if desiredReview 1-2 recent daily posts from the blog directory (if available) to understand the user's writing style. Look at recent files in content/posts/2025/*.md to see how they typically write both miscellanea bullets and focused posts. If no recent daily posts exist, fall back to checking the configured blog archive URL or use a conversational blog post style.
Key style elements:
Structure:
Example:
8<--- { "title": "Adventures in Home Lab Monitoring", "slug": "homelab-monitoring", "tags": ["homelab", "grafana", "docker"], "time": "14:00:00-07:00" }
I [decided](https://masto.hackers.town/@user/12345) to set up proper monitoring for my home lab this week. Nothing groundbreaking, but it's been a while since I ran Grafana and Prometheus on my own hardware.
Turns out there's this handy all-in-one docker-compose setup that runs on Synology NAS. It fired up with minimal fuss...

The whole setup took about an hour from idea to working dashboard. Pretty satisfying for barely having to think about it.
Structure:
Using blockquotes for excerpts: When sharing a particularly good quote from an article, essay, or post, use blockquotes to distinguish the quoted text from your own commentary:
> for the actual quoted text (indented appropriately)Example:
8<--- { "title": "Miscellanea for 2025-12-15", "time": "23:59:00-07:00", "type": "miscellanea", "slug": "miscellanea", "tags": ["miscellanea"] }
- Hello world!
- [This article about home automation](https://example.com/article) explores the tension between convenience and complexity:
> The promise of smart homes was simplicity, but we've traded one kind of complexity for another. Instead of manual switches, we now debug YAML files and restart servers.
Seems relevant after my recent smart home adventures.
- Posted [some thoughts on Mastodon](https://masto.hackers.town/@user/12346) about technology longevity. Our 15-year-old solar inverter died this week.
- Millie's [take on software completion](https://example.com/post):
> We need to normalize declaring software as finished. Not everything needs continuous updates to function. Most software works as it is written.
- Miss Biscuits discovered a new perch: 
- Been thinking about writing more regularly. This daily format seems to be working well so far.
Mastodon posts:
I [posted about](https://masto.hackers.town/@user/12345) solar panels...Bookmarks:
[This article about home automation](https://example.com) explores...Images:
To update the Go CLI binaries to the latest releases:
cd /path/to/daily-blog-post-composer
./scripts/download-binaries.sh
To update API credentials or change settings:
cd /path/to/daily-blog-post-composer
./scripts/setup.sh
All scripts automatically detect the current platform and use the appropriate binary:
bin/darwin-arm64/bin/darwin-amd64/bin/linux-amd64/Platform detection is handled automatically via uname commands.
setup.sh - First-time configuration for API credentialsfetch-sources.sh - Fetch data from all configured sources (rolling 24-hour window)prepare-sources.py - Verify fetched data and prepare for compositiondownload-binaries.sh - Update Go CLI binaries to latest releasesPre-compiled Go CLI binaries organized by platform:
mastodon-to-markdown - Fetch Mastodon posts as markdownlinkding-to-markdown - Fetch Linkding bookmarks as markdownconfig.json - User configuration with API credentials (created by setup.sh)latest/ - Most recently fetched source datamastodon.md and linkding.md after fetchingIf setup fails:
If platform detection fails:
# Check current platform
uname -s # Should show: Darwin or Linux
uname -m # Should show: arm64, x86_64, etc.
# Verify binary exists
ls -la bin/darwin-arm64/ # Or appropriate platform directory
If fetched data is empty:
--verbose flag for debuggingIf parsing existing file fails:
8<--- followed by space and valid JSON