# Themes

Page: https://microblog.dev/api/themes/
API base URL: https://micro.blog

Endpoints for browsing and editing a blog's custom themes, plug-in templates, built-in theme templates, and custom CSS. Send an app token in the `Authorization` header. Reading needs the `read` scope and changing anything needs `write`; the post scopes (`create`, `update`, `delete`, `media`) don't grant access. Full-access app tokens work for both.

Notes:
- Only the owner of a blog, theme, or template can use it here. Being a member of a team blog doesn't grant access. Unknown IDs, IDs that aren't numbers, and IDs that belong to someone else all return `404`.
- The discovery, built-in template, and CSS endpoints take an optional `site_id` query parameter and use the user's default blog when it's left out. A `site_id` for a blog the user doesn't own returns `404` with `{"error": "Blog not found."}`.
- `POST` bodies are JSON, sent with `Content-Type: application/json`, up to 2 MiB. Other content types return `415`, larger bodies return `413`, malformed JSON returns `400`, and so does a body with fields the endpoint doesn't accept.
- Errors from these endpoints are JSON objects such as `{"error": "Theme not found."}`. A missing token returns `401` with the text `Token required`, and a token without the needed scope returns `403` with the text `Token missing required scope`.
- Every endpoint allows cross-origin requests and answers `OPTIONS` preflight requests with `204`. Responses are sent with `Cache-Control: no-store`. Writes signed in with a browser cookie instead of a token must come from a Micro.blog page.

## Endpoints
### GET /themes

URL: `https://micro.blog/themes`

Returns the blog, the user's custom themes, and the plug-ins installed on that blog. Start here to find the `theme_id` for the template endpoints.




Query parameters:
- `site_id` (integer, optional): Blog to describe. Defaults to the user's default blog. `themes` is the same for every blog; `site` and `plugins` change with it.
  Example: `5678`



Header parameters:
- `Authorization` (string, required): Bearer token for authentication. Format: `Bearer {token}`.
  Example: `Bearer ABCDEF1234567890ABCD`




Example request:

```sh
curl -X GET "https://micro.blog/themes?site_id=5678" \
  --header "Authorization: Bearer ABCDEF1234567890ABCD"
```

Response:
- Status: 200
- Content-Type: `application/json`
- Description: `site.root_theme_id` is the custom theme the blog uses, or `null` when it uses a built-in theme. `site.built_in_theme` names the built-in theme the blog is set to, such as `marfa`. `themes` lists every custom theme the user owns, sorted by title. `plugins` lists the plug-ins installed on this blog, including disabled ones. Theme `site_id` is `null` for custom themes and is the blog's ID for plug-ins. Empty text fields are `""`.
Example response:

```json
{
  "site": {
    "id": 5678,
    "url": "https://example.com/",
    "root_theme_id": 4321,
    "built_in_theme": "marfa"
  },
  "themes": [
    {
      "id": 4321,
      "site_id": null,
      "title": "Custom Marfa",
      "description": "",
      "version": "",
      "author_username": "",
      "github_url": "",
      "engine": "hugo",
      "is_plugin": false,
      "is_disabled": false,
      "is_card": false,
      "updated_at": "2026-10-20T09:30:00+00:00"
    }
  ],
  "plugins": [
    {
      "id": 4322,
      "site_id": 5678,
      "title": "Example Plug-in",
      "description": "Adds a reading list page.",
      "version": "1.2",
      "author_username": "example",
      "github_url": "https://github.com/example/example-plugin",
      "engine": "hugo",
      "is_plugin": true,
      "is_disabled": false,
      "is_card": false,
      "updated_at": "2026-10-18T14:05:00+00:00"
    }
  ]
}
```

### GET /themes/defaults/templates

URL: `https://micro.blog/themes/defaults/templates`

Lists the read-only templates a blog falls back to when its custom theme doesn't override them. These come from the blog's built-in theme and from Micro.blog's `blank` theme, with the built-in theme winning when both have the same path. When the blog has an enabled plug-in that is a full theme, only the `blank` templates are listed.




Query parameters:
- `site_id` (integer, optional): Blog whose built-in theme to list. Defaults to the user's default blog.
  Example: `5678`



Header parameters:
- `Authorization` (string, required): Bearer token for authentication. Format: `Bearer {token}`.
  Example: `Bearer ABCDEF1234567890ABCD`




Example request:

```sh
curl -X GET "https://micro.blog/themes/defaults/templates" \
  --header "Authorization: Bearer ABCDEF1234567890ABCD"
```

Response:
- Status: 200
- Content-Type: `application/json`
- Description: Returns templates sorted by path, without their contents. Built-in templates have no ID, so `id`, `theme_id`, and `updated_at` are `null`. `read_only` is always `true` and `source` names the built-in theme the file comes from. `is_text` is `false` for images, fonts, and other files that can't be read as text. Copy a built-in template into a custom theme by reading it and creating a template with the same `path`.
Example response:

```json
{
  "templates": [
    {
      "id": null,
      "theme_id": null,
      "path": "config.json",
      "is_text": true,
      "read_only": true,
      "updated_at": null,
      "source": "marfa"
    },
    {
      "id": null,
      "theme_id": null,
      "path": "layouts/index.html",
      "is_text": true,
      "read_only": true,
      "updated_at": null,
      "source": "marfa"
    },
    {
      "id": null,
      "theme_id": null,
      "path": "layouts/robots.txt",
      "is_text": true,
      "read_only": true,
      "updated_at": null,
      "source": "blank"
    }
  ]
}
```

### GET /themes/defaults/template

URL: `https://micro.blog/themes/defaults/template`

Returns one built-in template with its contents.




Query parameters:
- `path` (string, required): Template path exactly as listed by `GET /themes/defaults/templates`. Missing or empty returns `400`; a path not in that list returns `404`.
  Example: `layouts/index.html`
- `site_id` (integer, optional): Blog whose built-in theme to read from. Defaults to the user's default blog.
  Example: `5678`



Header parameters:
- `Authorization` (string, required): Bearer token for authentication. Format: `Bearer {token}`.
  Example: `Bearer ABCDEF1234567890ABCD`




Example request:

```sh
curl -G "https://micro.blog/themes/defaults/template" \
  --header "Authorization: Bearer ABCDEF1234567890ABCD" \
  --data-urlencode "path=layouts/index.html"
```

Response:
- Status: 200
- Content-Type: `application/json`
- Description: Returns the template with `text`. Returns `422` for a file that isn't text, such as an image.
Example response:

```json
{
  "id": null,
  "theme_id": null,
  "path": "layouts/index.html",
  "is_text": true,
  "read_only": true,
  "updated_at": null,
  "source": "marfa",
  "text": "{{ define \"main\" }}\n  <div class=\"posts\">\n  ...\n{{ end }}\n"
}
```

### GET /themes/css

URL: `https://micro.blog/themes/css`

Returns the blog's custom CSS, the same CSS as Edit CSS on the Design page.




Query parameters:
- `site_id` (integer, optional): Blog to read. Defaults to the user's default blog.
  Example: `5678`



Header parameters:
- `Authorization` (string, required): Bearer token for authentication. Format: `Bearer {token}`.
  Example: `Bearer ABCDEF1234567890ABCD`




Example request:

```sh
curl -X GET "https://micro.blog/themes/css?site_id=5678" \
  --header "Authorization: Bearer ABCDEF1234567890ABCD"
```

Response:
- Status: 200
- Content-Type: `application/json`
- Description: Returns the blog ID and its CSS. `css` is `""` when the blog has none.
Example response:

```json
{
  "site_id": 5678,
  "css": "body {\n  font-family: Georgia, serif;\n}\n"
}
```

### POST /themes/css

URL: `https://micro.blog/themes/css`

Replaces the blog's custom CSS and uploads it to the blog as `/custom.css` straight away, without rebuilding the blog.




Query parameters:
- `site_id` (integer, optional): Blog to change. Defaults to the user's default blog. Send it in the query string; the JSON body only accepts `css`.
  Example: `5678`



Header parameters:
- `Authorization` (string, required): Bearer token for authentication. Format: `Bearer {token}`.
  Example: `Bearer ABCDEF1234567890ABCD`
- `Content-Type` (string, required): Must be `application/json`.
  Example: `application/json`



Body parameters:
- `css` (string, required): The complete CSS, replacing what's there. Send `""` to clear it. Anything other than a string returns `400`.
  Example: `body { font-family: Georgia, serif; }`


Example request:

```sh
curl -X POST "https://micro.blog/themes/css?site_id=5678" \
  --header "Authorization: Bearer ABCDEF1234567890ABCD" \
  --header "Content-Type: application/json" \
  --data '{"css": "body { font-family: Georgia, serif; }"}'
```

Response:
- Status: 200
- Content-Type: `application/json`
- Description: Returns the blog ID and the saved CSS.
Example response:

```json
{
  "site_id": 5678,
  "css": "body { font-family: Georgia, serif; }"
}
```

### GET /themes/{theme_id}

URL: `https://micro.blog/themes/{theme_id}`

Returns one of the user's custom themes or plug-ins.


Path parameters:
- `theme_id` (integer, required): Theme or plug-in ID from `GET /themes`.
  Example: `4321`





Header parameters:
- `Authorization` (string, required): Bearer token for authentication. Format: `Bearer {token}`.
  Example: `Bearer ABCDEF1234567890ABCD`




Example request:

```sh
curl -X GET "https://micro.blog/themes/4321" \
  --header "Authorization: Bearer ABCDEF1234567890ABCD"
```

Response:
- Status: 200
- Content-Type: `application/json`
- Description: Returns the theme with the same fields as the entries in `GET /themes`.
Example response:

```json
{
  "id": 4321,
  "site_id": null,
  "title": "Custom Marfa",
  "description": "",
  "version": "",
  "author_username": "",
  "github_url": "",
  "engine": "hugo",
  "is_plugin": false,
  "is_disabled": false,
  "is_card": false,
  "updated_at": "2026-10-20T09:30:00+00:00"
}
```

### GET /themes/{theme_id}/templates

URL: `https://micro.blog/themes/{theme_id}/templates`

Lists the templates in a custom theme or plug-in, without their contents.


Path parameters:
- `theme_id` (integer, required): Theme or plug-in ID from `GET /themes`.
  Example: `4321`





Header parameters:
- `Authorization` (string, required): Bearer token for authentication. Format: `Bearer {token}`.
  Example: `Bearer ABCDEF1234567890ABCD`




Example request:

```sh
curl -X GET "https://micro.blog/themes/4321/templates" \
  --header "Authorization: Bearer ABCDEF1234567890ABCD"
```

Response:
- Status: 200
- Content-Type: `application/json`
- Description: Returns templates sorted by path. `is_text` is `false` for images, fonts, and other files that can't be read or edited here. `read_only` is always `false` for theme templates.
Example response:

```json
{
  "templates": [
    {
      "id": 98765,
      "theme_id": 4321,
      "path": "layouts/index.html",
      "is_text": true,
      "read_only": false,
      "updated_at": "2026-10-20T09:30:00+00:00"
    },
    {
      "id": 98766,
      "theme_id": 4321,
      "path": "static/images/header.png",
      "is_text": false,
      "read_only": false,
      "updated_at": "2026-10-12T16:45:00+00:00"
    }
  ]
}
```

### GET /themes/{theme_id}/templates/{id}

URL: `https://micro.blog/themes/{theme_id}/templates/{id}`

Returns one template with its contents.


Path parameters:
- `theme_id` (integer, required): Theme or plug-in ID.
  Example: `4321`
- `id` (integer, required): Template ID from `GET /themes/{theme_id}/templates`. A template from a different theme returns `404`.
  Example: `98765`





Header parameters:
- `Authorization` (string, required): Bearer token for authentication. Format: `Bearer {token}`.
  Example: `Bearer ABCDEF1234567890ABCD`




Example request:

```sh
curl -X GET "https://micro.blog/themes/4321/templates/98765" \
  --header "Authorization: Bearer ABCDEF1234567890ABCD"
```

Response:
- Status: 200
- Content-Type: `application/json`
- Description: Returns the template with `text`. Returns `422` when `is_text` is `false`.
Example response:

```json
{
  "id": 98765,
  "theme_id": 4321,
  "path": "layouts/index.html",
  "is_text": true,
  "read_only": false,
  "updated_at": "2026-10-20T09:30:00+00:00",
  "text": "{{ define \"main\" }}\n  <h1>Hello</h1>\n{{ end }}\n"
}
```

### POST /themes/{theme_id}/templates

URL: `https://micro.blog/themes/{theme_id}/templates`

Adds a text template to a custom theme or plug-in, then rebuilds every one of the user's blogs that uses the theme.

Notes:
- Only text files can be created: paths ending in `.html`, `.txt`, `.md`, `.markdown`, `.css`, `.scss`, `.json`, `.coffee`, `.svg`, `.toml`, `.map`, `.yml`, `.xml`, `.js`, `.ts`, or `.webmanifest`, and files named `LICENSE` or `README`. Other paths return `422`. Hugo's `readFile` and `readDir` are removed from the saved text, so the returned `text` can differ from what you sent.


Path parameters:
- `theme_id` (integer, required): Theme or plug-in ID.
  Example: `4321`





Header parameters:
- `Authorization` (string, required): Bearer token for authentication. Format: `Bearer {token}`.
  Example: `Bearer ABCDEF1234567890ABCD`
- `Content-Type` (string, required): Must be `application/json`.
  Example: `application/json`



Body parameters:
- `path` (string, required): Relative path inside the theme, at most 250 characters, using letters, digits, spaces, `.`, `-`, `_`, and `/`. A leading `/`, empty segments, `.`, and `..` return `400`. A path that already exists in the theme returns `409`.
  Example: `layouts/partials/head.html`
- `text` (string, required): Template contents. May be `""`.
  Example: `<link rel="stylesheet" href="/custom.css">`


Example request:

```sh
curl -X POST "https://micro.blog/themes/4321/templates" \
  --header "Authorization: Bearer ABCDEF1234567890ABCD" \
  --header "Content-Type: application/json" \
  --data '{"path": "layouts/partials/head.html", "text": "<link rel=\"stylesheet\" href=\"/custom.css\">"}'
```

Response:
- Status: 201
- Content-Type: `application/json`
- Description: Returns the new template with `text`, and the IDs of the blogs being rebuilt in `publishing_site_ids`. The `Location` header holds the template's path, such as `/themes/4321/templates/98767`.
Example response:

```json
{
  "template": {
    "id": 98767,
    "theme_id": 4321,
    "path": "layouts/partials/head.html",
    "is_text": true,
    "read_only": false,
    "updated_at": "2026-10-28T10:15:00+00:00",
    "text": "<link rel=\"stylesheet\" href=\"/custom.css\">"
  },
  "publishing_site_ids": [5678]
}
```

### POST /themes/{theme_id}/templates/{id}

URL: `https://micro.blog/themes/{theme_id}/templates/{id}`

Renames a template, replaces its contents, or both, then rebuilds every one of the user's blogs that uses the theme. Fields you leave out keep their current values.

Notes:
- The same path and text rules apply as when creating a template. Templates that aren't text, and renames to a path that isn't text, return `422`. Renaming to a path another template in the theme already uses returns `409`.


Path parameters:
- `theme_id` (integer, required): Theme or plug-in ID.
  Example: `4321`
- `id` (integer, required): Template ID.
  Example: `98765`





Header parameters:
- `Authorization` (string, required): Bearer token for authentication. Format: `Bearer {token}`.
  Example: `Bearer ABCDEF1234567890ABCD`
- `Content-Type` (string, required): Must be `application/json`.
  Example: `application/json`



Body parameters:
- `path` (string, optional): New relative path for the template. Send `path`, `text`, or both.
  Example: `layouts/index.html`
- `text` (string, optional): New contents, replacing the old ones. May be `""`.
  Example: `{{ define "main" }}<h1>Hello again</h1>{{ end }}`


Example request:

```sh
curl -X POST "https://micro.blog/themes/4321/templates/98765" \
  --header "Authorization: Bearer ABCDEF1234567890ABCD" \
  --header "Content-Type: application/json" \
  --data '{"text": "{{ define \"main\" }}<h1>Hello again</h1>{{ end }}"}'
```

Response:
- Status: 200
- Content-Type: `application/json`
- Description: Returns the updated template with `text`, and the IDs of the blogs being rebuilt in `publishing_site_ids`.
Example response:

```json
{
  "template": {
    "id": 98765,
    "theme_id": 4321,
    "path": "layouts/index.html",
    "is_text": true,
    "read_only": false,
    "updated_at": "2026-10-28T10:20:00+00:00",
    "text": "{{ define \"main\" }}<h1>Hello again</h1>{{ end }}"
  },
  "publishing_site_ids": [5678]
}
```

### DELETE /themes/{theme_id}/templates/{id}

URL: `https://micro.blog/themes/{theme_id}/templates/{id}`

Deletes a template from a custom theme or plug-in, then rebuilds every one of the user's blogs that uses the theme. Binary files such as images can be deleted too.


Path parameters:
- `theme_id` (integer, required): Theme or plug-in ID.
  Example: `4321`
- `id` (integer, required): Template ID.
  Example: `98766`





Header parameters:
- `Authorization` (string, required): Bearer token for authentication. Format: `Bearer {token}`.
  Example: `Bearer ABCDEF1234567890ABCD`




Example request:

```sh
curl -X DELETE "https://micro.blog/themes/4321/templates/98766" \
  --header "Authorization: Bearer ABCDEF1234567890ABCD"
```

Response:
- Status: 200
- Content-Type: `application/json`
- Description: Confirms the deletion and returns the IDs of the blogs being rebuilt in `publishing_site_ids`.
Example response:

```json
{
  "deleted": true,
  "publishing_site_ids": [5678]
}
```

