## List Posts

```
GET /api/v1/posts
```

### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | `number` | 50 | Posts per page (1-100) |
| `offset` | `number` | 0 | Posts to skip |
| `query` | `string` | – | Search by post title |
| `active` | `boolean` | – | Filter by published status |
| `tag` | `string` | – | Filter by tag |
| `categoryId` | `string` | – | Filter by blog category ID |
| `lang` | `string` | – | Locale code (e.g. `pl-PL`) to overlay translated fields onto the response. Only honoured when the store's translations tool is enabled. |

### Response

```json
{
  "data": [
    {
      "id": "0191abc0-1234-7def-8000-000000000001",
      "title": "Welcome to Our Store",
      "slug": "welcome-to-our-store",
      "image": "https://cdn.example.com/welcome.jpg",
      "tag": "news",
      "active": true,
      "publishedAt": "2024-06-01T12:00:00.000Z",
      "createdAt": "2024-05-28T09:00:00.000Z"
    }
  ],
  "meta": {
    "count": 12
  },
  "lang": "pl-PL"
}
```

---

## Get Post

```
GET /api/v1/posts/:idOrSlug
```

Returns a single blog post by UUID or slug, with full TipTap JSON content and SEO metadata.

| Parameter | Type | Description |
|-----------|------|-------------|
| `lang` | `string` | Locale code for translated field overlay and translated slug lookup |

When `?lang=` is provided and translations are enabled, the endpoint overlays translated `title`, `slug`, `content`, `seo.title`, and `seo.description` onto the response. The post can also be looked up by its **translated slug** – if no base slug matches, the endpoint tries the translated slug for the given locale.

---

## Create Post

```
POST /api/v1/posts
```

Content must be valid TipTap JSON with `type: "doc"`. The slug must be unique within the store.

External image URLs – both the featured `image` field and any `image` or `imageResize` nodes inside `content` – are automatically downloaded and re-hosted on the store's own CDN. The response will contain the re-hosted URLs, not the original URLs you submitted. Images that already belong to the store are left untouched. If an image cannot be fetched, the original URL is preserved.

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | `string` | Yes | Post title |
| `slug` | `string` | Yes | URL slug (lowercase, numbers, hyphens) |
| `content` | `object` | Yes | TipTap JSON content (`{ type: "doc", content: [...] }`) |
| `image` | `string` | No | Featured image URL |
| `tag` | `string` | No | Post tag/category |
| `active` | `boolean` | No | Visible on storefront |
| `publishedAt` | `string` | No | ISO 8601 publication date |
| `seo` | `object` | No | SEO metadata (see below) |
| `filters` | `object` | No | Key-value metadata filters |

### SEO Object

| Field | Type | Description |
|-------|------|-------------|
| `title` | `string` | Meta title |
| `description` | `string` | Meta description |
| `canonical` | `string` | Canonical URL |

```bash
curl -X POST \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Welcome to Our Store",
    "slug": "welcome-to-our-store",
    "content": {
      "type": "doc",
      "content": [
        {
          "type": "paragraph",
          "content": [
            { "type": "text", "text": "Hello and welcome!" }
          ]
        }
      ]
    },
    "tag": "news",
    "active": true,
    "seo": {
      "title": "Welcome | Our Store",
      "description": "Learn about our store and products."
    }
  }' \
  https://your-store.yns.store/api/v1/posts
```

---

## Update Post

```
PATCH /api/v1/posts/:idOrSlug
```

Partially updates a blog post. Only the provided fields are changed. Accepts the same fields as create (all optional). Image re-hosting applies to updates as well – any new external image URLs are re-hosted, while omitted fields are left unchanged.

---

## Delete Post

```
DELETE /api/v1/posts/:idOrSlug
```

Permanently deletes a blog post.

---

## Translations

```
PUT /api/v1/posts/:idOrSlug/translations/:locale
```

Upsert or delete a single locale's translated fields for a blog post. The post can be identified by UUID or base slug. The locale must be a supported locale code (e.g. `pl-PL`) and **cannot** be the store's base locale – base content is edited via `PATCH /api/v1/posts/:idOrSlug`.

### Request Body

| Field | Type | Description |
|-------|------|-------------|
| `title` | `string` | Translated post title |
| `slug` | `string` | Translated URL slug |
| `content` | `object` | Translated body (TipTap JSONContent) |
| `seoTitle` | `string` | Translated SEO title |
| `seoDescription` | `string` | Translated SEO description |

All fields are optional. The schema is strict – unknown fields are rejected. Sending an empty body (all fields omitted or blank) deletes the translation row, so the post falls back to its base-language content.

The locale does not need to be enabled in `settings.enabledLanguages` – you can stage translations before flipping the language on.

```bash
curl -X PUT \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Witamy w naszym sklepie",
    "slug": "witamy-w-naszym-sklepie",
    "seoTitle": "Witamy | Nasz sklep"
  }' \
  https://your-store.yns.store/api/v1/posts/welcome-to-our-store/translations/pl-PL
```

### Response

```json
{
  "ok": true,
  "postId": "0191abc0-1234-7def-8000-000000000001",
  "locale": "pl-PL",
  "deleted": false
}
```

### Errors

| Status | Meaning |
|--------|---------|
| `400` | Unsupported locale, or the locale is the store's base locale |
| `404` | No post matches the identifier |
| `409` | A translation with this slug already exists for the same locale |

See the [Localization page](/docs/api-reference/localization#writing-translations) for the full translation behaviour and conventions.

---

## Post Comments

### List Comments

```
GET /api/v1/posts/:idOrSlug/comments
```

Returns approved comments for a post. Pending comments are only visible in the admin interface.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | `number` | 20 | Comments per page (1-100) |
| `offset` | `number` | 0 | Comments to skip |

```json
{
  "data": [
    {
      "id": "0191abc0-1234-7def-8000-000000000001",
      "author": "Jane Doe",
      "content": "Great article, very helpful!",
      "createdAt": "2024-06-15T14:30:00.000Z"
    }
  ],
  "meta": {
    "count": 1,
    "offset": 0,
    "limit": 20
  }
}
```

### Create Comment

```
POST /api/v1/posts/:idOrSlug/comments
```

Submits a comment (pending approval by store owner). The `email` field is stored but not returned in responses.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `author` | `string` | Yes | Display name (1-100 chars) |
| `email` | `string` | Yes | Email address |
| `content` | `string` | Yes | Comment text (1-5000 chars) |

```bash
curl -X POST \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"author": "Jane", "email": "jane@example.com", "content": "Great article!"}' \
  https://your-store.yns.store/api/v1/posts/my-post/comments
```

### Response (201)

```json
{
  "id": "0191abc0-1234-7def-8000-000000000001",
  "author": "Jane",
  "content": "Great article!",
  "createdAt": "2024-06-15T14:30:00.000Z"
}