--- 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-` 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]].