freek-dev-blog
freek.dev Blog Posts
API
All post operations use the freek.dev Blog Posts API. No browser automation needed.
Base URL
https://freek.dev/api
Authentication
Bearer token in the Authorization header.
Token location: .secrets/blog-freek-dev.md
Authorization: Bearer <token>
Endpoints
List posts — GET /api/posts
Query parameters (all optional):
published(0 or 1): filter by published statusoriginal_content(0 or 1): filter by original contenttag(string): filter by tag namesearch(string): search by title
Returns paginated results.
Get a post — GET /api/posts/{id}
Create a post — POST /api/posts
Body (JSON):
title(string, required)text(string, required, markdown)publish_date(ISO 8601 date, nullable)published(boolean, default false)original_content(boolean, default false)external_url(URL string, nullable)tags(array of strings)send_automated_tweet(boolean, default false)author_twitter_handle(string, nullable)series_slug(string, nullable)
Returns 201 with the created post.
Update a post — PUT /api/posts/{id}
Same fields as create, all optional. Only send fields you want to change.
Delete a post — DELETE /api/posts/{id}
Returns 204 No Content.
Response Format
All responses are wrapped in a data key:
{
"data": {
"id": 1,
"title": "...",
"slug": "...",
"text": "...",
"html": "...",
"publish_date": "2026-03-02T14:00:00+00:00",
"published": true,
"original_content": false,
"external_url": null,
"series_slug": null,
"author_twitter_handle": null,
"send_automated_tweet": false,
"tags": ["laravel", "php"],
"url": "https://freek.dev/1-post-slug",
"preview_url": "https://freek.dev/1-post-slug?preview_secret=abc123",
"created_at": "2026-03-02T12:00:00+00:00",
"updated_at": "2026-03-02T12:00:00+00:00"
}
}
API Usage Examples
Create a link post:
curl -X POST https://freek.dev/api/posts \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Article title here",
"text": "Short summary of the linked content.",
"external_url": "https://example.com/article",
"send_automated_tweet": true
}'
Schedule a post (set publish_date + published):
curl -X PUT https://freek.dev/api/posts/3040 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"publish_date": "2026-03-03T13:30:00+00:00",
"published": true
}'
Update post text:
curl -X PUT https://freek.dev/api/posts/3040 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "Updated markdown content here."}'
Creating a Link Post
90% of posts are link posts (external content with a summary).
Steps
- Create the post via
POST /api/postswith:title: Use the original article title or a clear varianttext: 2 sentences max summarizing the linked content. No links in the text.external_url: The full URL being linkedoriginal_content: false (it's a link post)send_automated_tweet: true
- Before scheduling, ALWAYS inspect the existing unpublished queue (
GET /api/posts?published=0) and look atpublish_datevalues. Do not assume the next slot is today. - Schedule sequentially after the last already-scheduled post, usually one post per day, around 14:30 CET/CEST unless the existing queue clearly follows a different pattern.
- To schedule, set
publish_dateto the chosen future time. Do NOT setpublished: true(the blog auto-publishes when the date arrives). - Always confirm by displaying the created title, full summary text, and preview URL back to the user (so Freek can review what was written)
Link Post Text Guidelines
- Keep it to 2 sentences max
- Summarize what's interesting about the linked content
- No links in the summary text (the external URL field handles linking)
- No code blocks unless essential
- Write in third person ("They've released..." not "I found...")
- Never use em dashes in summaries or titles. Use commas, periods, or "and" instead.
Creating an Original Content Post
For posts written by Freek (package announcements, tutorials, opinions).
Steps
- Create the post via
POST /api/postswith:title: The post titletext: Full markdown content (see write-freek-dev-blogpost skill for writing style)original_content: truesend_automated_tweet: true (usually)tags: Relevant tags as array of strings
- Share the
preview_urlfrom the response for review - Schedule when Freek approves
Creating a YouTube Video Post
When Freek shares a YouTube link for the blog.
Steps
- Get the video ID from the YouTube URL (e.g.
dQw4w9WgXcQfromyoutube.com/watch?v=dQw4w9WgXcQ) - Get the video title via oembed API:
youtube.com/oembed?url=VIDEO_URL&format=json - Create the post via
POST /api/postswith:title: Use the video titletext: Summary text followed by a blank line and the YouTube iframe embed:Summary text here. <iframe width="560" height="315" src="https://www.youtube.com/embed/VIDEO_ID" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>external_url: null (the embed IS the content)original_content: falseauthor_twitter_handle: Set if knownsend_automated_tweet: true
- Always confirm by displaying the title, full summary text, and preview URL
YouTube Embed Notes
- If the original URL has a timestamp (
t=24orstart=24), add?start=24to the embed src - Use
width="560" height="315"as standard dimensions
Creating a Tweet Embed Post
When Freek wants to embed a Twitter/X thread on the blog.
Steps
- Gather the tweet IDs from the thread
- Create the post via
POST /api/postswith:title: Descriptive title (e.g. "A Twitter thread about laravel-permission v7")text: Minimal blockquotes withdata-conversation="none":<blockquote class="twitter-tweet" data-conversation="none"> <a href="https://twitter.com/USERNAME/status/TWEET_ID"></a> </blockquote> <blockquote class="twitter-tweet" data-conversation="none"> <a href="https://twitter.com/USERNAME/status/TWEET_ID_2"></a> </blockquote> <script async src="https://platform.twitter.com/widgets.js"></script>external_url: nulloriginal_content: true (it's Freek's own content)author_twitter_handle: Tweet author (e.g.freekmurze)send_automated_tweet: false (the tweets already exist)
- Confirm with the preview URL so Freek can verify the embeds render
Tweet Embed Notes
data-conversation="none"prevents showing the parent tweet above each reply- Use ONE
<script>tag at the end, not per-blockquote - Twitter's widget.js renders the blockquotes client-side into rich embeds
Scheduling Convention
Standard scheduling: sequential daily at ~14:30 CET (13:30 UTC in winter, 12:30 UTC in summer).
⚠️ CRITICAL: NEVER set published: true for scheduled posts. Setting published: true publishes IMMEDIATELY and triggers the automated tweet. The blog auto-publishes posts when their publish_date arrives.
To schedule a new post: Create with published: false (or omit, it defaults to false) and set publish_date to the desired future date.
To schedule an existing post: Update with published: false and publish_date:
PUT /api/posts/{id}
{"published": false, "publish_date": "2026-03-03T13:30:00+00:00"}
Example Link Post
Title: SQL performance improvements: automatic detection & regression testing
Text:
The final part of Oh Dear's series on SQL performance. Mattias introduces phpunit-query-count-assertions, a package that catches N+1 queries, duplicate queries, and missing indexes in your test suite.
External url: https://ohdear.app/news-and-updates/...
More from freekmurze/dotfiles
php-guidelines-from-spatie
Describes PHP and Laravel guidelines provided by Spatie. These rules result in more maintainable, and readable code.
144context7-auto-research
Automatically fetches up-to-date documentation from Context7 when users ask about libraries, frameworks, APIs, or need code examples. Triggers proactively without explicit user request.
27react-native-best-practices
Provides React Native performance optimization guidelines for FPS, TTI, bundle size, memory leaks, re-renders, and animations. Applies to tasks involving Hermes optimization, JS thread blocking, bridge overhead, FlashList, native modules, or debugging jank and frame drops.
24agent-browser
Automates browser interactions for web testing, form filling, screenshots, and data extraction. Use when the user needs to navigate websites, interact with web pages, fill forms, take screenshots, test web applications, or extract information from web pages.
24copy-editing
When the user wants to edit, review, or improve existing marketing copy. Also use when the user mentions 'edit this copy,' 'review my copy,' 'copy feedback,' 'proofread,' 'polish this,' 'make this better,' or 'copy sweep.' This skill provides a systematic approach to editing marketing copy through multiple focused passes.
24fix-github-issue
Fix GitHub issues using gh CLI. Use when asked to fix, resolve, or address a GitHub issue. Creates fixes on separate branches, runs tests locally, and creates PRs when tests pass. Requires gh CLI authenticated and repo cloned locally.
22