---
title: BlogPost
tags: [data-model, mongoose]
aliases: [Blog Post, Article, IBlogPost]
---

# BlogPost

Editorial content for the marketplace's blog. Each post has a title, an auto-generated slug, rich `content`, optional cover image, gallery, and embedded videos (YouTube / Vimeo / Aparat / other). Carries publication workflow (`draft` / `published` / `archived`), denormalised author info, SEO metadata, and counters for views, likes, and comments. Two pre-save hooks handle slug generation and `publishedAt` stamping.

> [!note] Source
> `backend/src/models/BlogPost.ts:39` — schema definition
> `backend/src/models/BlogPost.ts:182` — model export

## Schema

| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `title` | String | yes | — | trim, maxlength 200 | — | Post title. |
| `slug` | String | no | (auto-generated) | lowercase, trim | unique, sparse | URL slug. |
| `description` | String | yes | — | maxlength 500 | — | Short summary. |
| `content` | String | yes | — | — | — | Full body (markdown / HTML). |
| `coverImage` | String | no | — | — | — | Hero image URL. |
| `images[]` | String[] | no | — | — | — | Gallery URLs. |
| `videos[].url` | String | yes | — | — | — | Video URL. |
| `videos[].title` | String | no | — | — | — | Video title. |
| `videos[].platform` | String | no | `youtube` | enum: `youtube` / `vimeo` / `aparat` / `other` | — | Platform. |
| `videos[].embedId` | String | no | — | — | — | Embed id (if applicable). |
| `author.id` | ObjectId → [[User]] | yes | — | — | — | Author user. |
| `author.name` | String | yes | — | — | — | Denormalised author name. |
| `author.avatar` | String | no | — | — | — | Avatar URL. |
| `category` | String | yes | `tutorial` | enum: `tutorial` / `news` / `guide` / `tips` / `announcement` / `other` | yes (compound) | Editorial category. |
| `tags[]` | String[] | no | — | trim | yes | Free-form tags. |
| `status` | String | no | `draft` | enum: `draft` / `published` / `archived` | yes (compound) | Workflow state. |
| `publishedAt` | Date | no | — | — | yes (compound) | Auto-set when status → `published`. |
| `views` | Number | no | `0` | — | — | View counter. |
| `likes` | Number | no | `0` | — | — | Like counter. |
| `comments` | Number | no | `0` | — | — | Comment counter. |
| `readTime` | Number | no | `5` | — | — | Estimated read time (minutes). |
| `featured` | Boolean | no | `false` | — | yes (compound) | Front-page promotion. |
| `seo.metaTitle` | String | no | — | — | — | SEO title. |
| `seo.metaDescription` | String | no | — | — | — | SEO description. |
| `seo.metaKeywords[]` | String[] | no | — | — | — | SEO keywords. |
| `createdAt` | Date | auto | — | — | — | Mongoose timestamp. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |

Virtuals are enabled in `toJSON` and `toObject` even though none are declared on the schema.

## Virtuals

None defined (but enabled in serialisation).

## Indexes

Defined at `backend/src/models/BlogPost.ts:148-151`. Plus the implicit unique sparse index on `slug`:

- `{ status: 1, publishedAt: -1 }` — published feed.
- `{ category: 1, status: 1 }` — category page.
- `{ tags: 1 }` — tag lookup.
- `{ featured: 1, status: 1 }` — featured posts.

## Pre/Post Hooks

| Hook | Behaviour |
| --- | --- |
| `pre('save')` (`backend/src/models/BlogPost.ts:154`) | Auto-generates `slug` from the title (English letters only) plus a timestamp suffix; falls back to `post-<timestamp>` for non-Latin titles. |
| `pre('save')` (`backend/src/models/BlogPost.ts:175`) | When `status` is modified to `published` and `publishedAt` is empty, sets `publishedAt = new Date()`. |

## Instance Methods

None defined.

## Static Methods

None defined.

## Relationships

- **References**: [[User]] (`author.id`).
- **Referenced by**: none.

## State Transitions

```mermaid
stateDiagram-v2
    [*] --> draft
    draft --> published : author publishes
    published --> archived : admin archives
    published --> draft : unpublish
    archived --> published : restore
    archived --> [*]
```

## Common Queries

```ts
// Public feed
BlogPost.find({ status: 'published' }).sort({ publishedAt: -1 }).limit(20);

// By slug (detail page)
BlogPost.findOne({ slug, status: 'published' });

// Featured carousel
BlogPost.find({ featured: true, status: 'published' }).sort({ publishedAt: -1 });

// Tag search
BlogPost.find({ tags: tag, status: 'published' });

// Increment views atomically
BlogPost.updateOne({ _id }, { $inc: { views: 1 } });
```

Related: [[User]].
