# freek.dev: a Laravel blog with a write API for AI tooling

> The source of freek.dev is a Laravel application that exposes its blog posts through a REST API at /api/posts, protected by Sanctum tokens and limited to admin users, so an AI tool can list, draft, update and delete posts. It deploys to Laravel Cloud on a push to main, and the environment file shows a full publishing stack with Twitter, OpenAI, Mailcoach and Mastodon integrations.

**spatie/freek.dev** — The sourcecode of freek.dev

- Repository: https://github.com/spatie/freek.dev
- Website: https://freek.dev/on-migrating-my-blog-from-wordpress-to-a-laravel-application
- Stars: 650 · Forks: 156
- Language: PHP
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/spatie-freek-dev

## A blog engine that lets a tool write its posts

freek.dev is the source code of a personal blog, written in PHP on Laravel with a Tailwind front end, and the part that makes it interesting to an engineer is not the blog itself but the API the README documents. The site exposes a REST API at /api/posts for programmatic blog post management, and the README gives the example use as an AI tool doing that management. That framing tells you the whole design goal. A blog that only serves rendered HTML is a dead end for automation, because a tool that wants to publish a post has to scrape the admin interface. Here the post is a resource with a schema, and a tool can create it. The repository is the public source of the site, published under the MIT licence by Spatie, with the author credit going to Freek Van der Herten and the contributors. The last push to the repository was on 2026-10-19, and the only tagged release is 0.0.1 from 2021-11-03, which tells you the versioning here tracks the application, not a published package.

## Access needs a Sanctum token and an admin account

The API is not open. It is protected by Sanctum token authentication and restricted to admin users, and every request carries a Bearer token in the Authorization header:

```http
Authorization: Bearer <token>
```

The token is created in tinker, and the README shows the two lines that do it:

```php
$user = User::where('admin', true)->first();
$token = $user->createToken('ai-tool');
$token->plainTextToken; // save this, it's only shown once
```

Two details matter for anyone automating this. The first is the admin lookup, since the token belongs to a user whose admin flag is set, and the second is that the plain text token is shown once, so it has to be captured at creation and stored in the tool's configuration rather than regenerated on demand. The token name in the example, ai-tool, suggests the intended usage, one integration with its own credential. The pair of conditions, a valid Sanctum token and an admin user, means the guard is doubled up, which is a sensible arrangement for an endpoint that can publish content and delete it.

## Five endpoints cover the whole lifecycle of a post

The surface is small and conventional REST. Listing is GET /api/posts, and it returns paginated results with four optional query parameters, published and original_content as 0 or 1 filters, tag to filter by tag name, and search to search posts by title. A single post is GET /api/posts/{id}. Creating is POST /api/posts, which returns 201 with the created post. Updating is PUT /api/posts/{id}, taking the same fields as create but with all of them optional, so a tool can send only the fields it wants to change. Deleting is DELETE /api/posts/{id}, which returns 204 No Content. The shape is conventional enough that any HTTP client can drive it, and the pagination on the list endpoint is the only detail that needs care, since a tool that expects the whole archive in one response will have to follow pages.

## The create payload mixes content with publishing decisions

The body of a create request is JSON, and the field list is where the design shows its editorial side. Two fields are required, title and text, and the text is markdown. Everything else is optional or defaulted. publish_date is an ISO 8601 date and nullable, published is a boolean that defaults to false, and original_content is a boolean that also defaults to false, which suggests a distinction between posts written here and posts whose text is quoted from elsewhere. external_url is a nullable URL, tags is an array of strings, and series_slug is a nullable string for grouping posts into a series. Then there are two fields with an editorial workflow attached: send_automated_tweet, a boolean defaulting to false, and author_twitter_handle, a nullable string. In other words the API does not merely store text, it can queue a tweet about the post. An automated tool that fills the payload blindly could publish something and then announce it, which is a good reason to read the field list before wiring it up.

## Every response is wrapped in a data key and can carry a preview secret

The response format is uniform, and the envelope is the thing to look at. All responses are wrapped in a data key, and the object inside it carries both the stored fields and some derived ones. The excerpt below is the beginning of that object as the README prints it:

```json
{
  "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,
```

The interesting parts are the fields that are not part of the write payload. The response includes html alongside the markdown text, so a caller does not have to render anything. It includes a url, the public address of the post, and a preview_url that carries a preview_secret query parameter, which is how an unpublished or not-yet-public post can be shared for review. The slug is generated rather than supplied, and the created_at and updated_at timestamps come back in ISO 8601 form. A tool that wants to show a human a draft therefore has a link to give them, which is a small detail that makes review workflows practical rather than theoretical.

## Front end tooling and deployment on a push to main

The site runs on Laravel Cloud and deploys automatically when code is pushed to main, so there is no release script to run and no tag to cut for a content change. The front end is a Vite project with a package.json that keeps the build to three scripts, dev, build and a prettier format command, and the dependency list explains what the site uses. Tailwind CSS 4 with the forms and typography plugins does the styling, shiki handles syntax highlighting for the code blocks in posts, tippy.js is there for tooltips, and puppeteer is a runtime dependency rather than a test-time one, which is unusual and suggests the site renders or screenshots something at request time. The laravel-vite-plugin wires the Vite build into the Laravel asset pipeline, and there is a vue-template-compiler in devDependencies even though the rest of the front end looks like Blade with Tailwind. The PHP side has the familiar Laravel layout, with app/, bootstrap/, config/, database/, lang/, resources/, routes/, tests/ and stubs/, plus a phpunit.xml, an artisan entry point, a boost.json, a .mcp.json and a uses.md describing what the project depends on.

## The environment file shows the publishing stack behind the posts

The .env.example is the most revealing file in the repository, because it lists every external service the blog talks to. The database is MySQL on 127.0.0.1 with a database named freekdev, the queue runs through Redis, and the cache and session drivers are files. Mail goes to the log driver with Mailtrap as the host. There is a Pusher configuration for broadcasting, a Google Analytics id, and a set of Twitter keys under the Twitter prefix, which lines up with the send_automated_tweet flag in the API. There is also an ANALYTICS_PROPERTY_ID, an OpenAI key and organisation, a Mastodon token, Mailcoach settings for a newsletter list, a Nova licence key, Oh Dear monitoring settings, a media library path generator salt, a Stripe key pair, and remote host, user and path variables that suggest deployment by file transfer. It is a lot of integrations for a personal blog, and that is the point of the repository: it is a working example of a Laravel site wired to a broad set of modern services, and the API is the seam that a tool plugs into.

## Conclusion

freek.dev suits anyone who wants a self-hosted Laravel blog with a machine-readable post API, since the create, update and delete endpoints with a JSON envelope are small enough to understand in an afternoon. It is not a framework to adopt, it is one person's site, so the value is in the example rather than the code, and the API is admin-only by design, which means an AI tool needs an admin user's Sanctum token. Before building on it, read the create payload in the README, because the field set, including send_automated_tweet and the preview secret in the response, encodes publishing decisions that a generic content pipeline would not expect.

## FAQ

### How do I authenticate against the freek.dev posts API?

Every request needs a Bearer token in the Authorization header, and the token comes from Laravel Sanctum. The README shows creating it in tinker for a user where the admin flag is true, and notes that the plain text token is only shown once, so it has to be saved at creation.

### Which endpoints does the freek.dev blog API expose?

There are five: GET /api/posts for a paginated list, GET /api/posts/{id} for one post, POST /api/posts to create, PUT /api/posts/{id} to update and DELETE /api/posts/{id} to delete. The list endpoint accepts published, original_content, tag and search as optional query parameters.

### What fields are required when creating a post through the API?

Only title and text, where the text is markdown. publish_date, published, original_content, external_url, tags, send_automated_tweet, author_twitter_handle and series_slug are optional, and the create call returns 201 with the created post.

### How is freek.dev deployed?

The site runs on Laravel Cloud and deploys automatically when code is pushed to main. The front end is built with Vite and Tailwind CSS 4, and the PHP side is a standard Laravel application with the usual app, config, database, resources and routes directories.

### Can a tool share an unpublished post for review through the API?

Yes. The response object includes a url for the public address and a preview_url that carries a preview_secret query parameter, so a draft can be shared with a reviewer before publication.

## Sources

- [License: MIT](https://github.com/spatie/freek.dev/blob/main/LICENSE)
- [Project website](https://freek.dev/on-migrating-my-blog-from-wordpress-to-a-laravel-application)
- [README](https://github.com/spatie/freek.dev/blob/main/README.md)
- [Releases](https://github.com/spatie/freek.dev/releases)
- [spatie/freek.dev on GitHub](https://github.com/spatie/freek.dev)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/spatie-freek-dev
