@feardread/fear 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/models/blog.js CHANGED
@@ -1,31 +1,961 @@
1
1
  const mongoose = require("mongoose");
2
+ const slugify = require("slugify");
2
3
 
3
- const blogSchema = new mongoose.Schema({
4
- title: { type: String, required: true },
5
- slug: { type: String, required: false },
6
- description: { type: String, required: true },
7
- section: { type:String, required: false, default: "Marketing" },
8
- category: { type: mongoose.Schema.Types.String, ref: "Category" },
9
- numViews: { type: Number, default: 0 },
10
- isLiked: { type: Boolean, default: false },
11
- isDisliked: { type: Boolean, default: false },
12
- likes: [{
4
+ const blogSchema = new mongoose.Schema(
5
+ {
6
+ // Basic Information
7
+ title: {
8
+ type: String,
9
+ required: [true, "Blog title is required"],
10
+ trim: true,
11
+ maxlength: [200, "Title cannot exceed 200 characters"],
12
+ index: true
13
+ },
14
+
15
+ slug: {
16
+ type: String,
17
+ unique: true,
18
+ lowercase: true,
19
+ index: true
20
+ },
21
+
22
+ subtitle: {
23
+ type: String,
24
+ trim: true,
25
+ maxlength: [250, "Subtitle cannot exceed 250 characters"]
26
+ },
27
+
28
+ excerpt: {
29
+ type: String,
30
+ trim: true,
31
+ maxlength: [500, "Excerpt cannot exceed 500 characters"]
32
+ },
33
+
34
+ content: {
35
+ type: String,
36
+ required: [true, "Blog content is required"]
37
+ },
38
+
39
+ contentHtml: {
40
+ type: String
41
+ },
42
+
43
+ images: [{
44
+ public_id: { type: String },
45
+ url: { type: String },
46
+ secure_url: { type: String },
47
+ alt: { type: String },
48
+ caption: { type: String },
49
+ order: { type: Number, default: 0 }
50
+ }],
51
+
52
+ video: {
53
+ url: { type: String },
54
+ embedCode: { type: String },
55
+ thumbnail: { type: String },
56
+ duration: { type: Number }
57
+ },
58
+
59
+ // Author Information
60
+ author: {
13
61
  type: mongoose.Schema.Types.ObjectId,
14
62
  ref: "User",
63
+ required: [true, "Author is required"],
64
+ index: true
65
+ },
66
+
67
+ authorName: {
68
+ type: String,
69
+ default: "Admin"
70
+ },
71
+
72
+ coAuthors: [{
73
+ type: mongoose.Schema.Types.ObjectId,
74
+ ref: "User"
15
75
  }],
76
+
77
+ // Classification
78
+ category: {
79
+ type: mongoose.Schema.Types.ObjectId,
80
+ ref: "Category",
81
+ required: [true, "Category is required"],
82
+ index: true
83
+ },
84
+
85
+ categories: [{
86
+ type: mongoose.Schema.Types.ObjectId,
87
+ ref: "Category"
88
+ }],
89
+
90
+ section: {
91
+ type: String,
92
+ default: "Marketing",
93
+ trim: true,
94
+ index: true
95
+ },
96
+
97
+ tags: [{
98
+ type: String,
99
+ trim: true,
100
+ lowercase: true,
101
+ index: true
102
+ }],
103
+
104
+ topics: [{
105
+ type: String,
106
+ trim: true
107
+ }],
108
+
109
+ // Publishing Status
110
+ status: {
111
+ type: String,
112
+ enum: ["draft", "published", "scheduled", "archived", "private"],
113
+ default: "draft",
114
+ index: true
115
+ },
116
+
117
+ published: {
118
+ type: Boolean,
119
+ default: false,
120
+ index: true
121
+ },
122
+
123
+ publishedAt: {
124
+ type: Date,
125
+ index: true
126
+ },
127
+
128
+ scheduledAt: {
129
+ type: Date,
130
+ index: true
131
+ },
132
+
133
+ archivedAt: {
134
+ type: Date
135
+ },
136
+
137
+ // Visibility and Access
138
+ visibility: {
139
+ type: String,
140
+ enum: ["public", "private", "password", "members-only"],
141
+ default: "public",
142
+ index: true
143
+ },
144
+
145
+ password: {
146
+ type: String,
147
+ select: false
148
+ },
149
+
150
+ accessLevel: {
151
+ type: String,
152
+ enum: ["free", "premium", "subscriber-only"],
153
+ default: "free"
154
+ },
155
+
156
+ // Featured and Priority
157
+ featured: {
158
+ type: Boolean,
159
+ default: false,
160
+ index: true
161
+ },
162
+
163
+ sticky: {
164
+ type: Boolean,
165
+ default: false,
166
+ index: true
167
+ },
168
+
169
+ trending: {
170
+ type: Boolean,
171
+ default: false,
172
+ index: true
173
+ },
174
+
175
+ priority: {
176
+ type: Number,
177
+ default: 0,
178
+ index: true
179
+ },
180
+
181
+ displayOrder: {
182
+ type: Number,
183
+ default: 0
184
+ },
185
+
186
+ // Engagement Metrics
187
+ views: {
188
+ type: Number,
189
+ default: 0,
190
+ index: true
191
+ },
192
+
193
+ numViews: {
194
+ type: Number,
195
+ default: 0
196
+ },
197
+
198
+ uniqueViews: {
199
+ type: Number,
200
+ default: 0
201
+ },
202
+
203
+ shares: {
204
+ type: Number,
205
+ default: 0
206
+ },
207
+
208
+ bookmarks: {
209
+ type: Number,
210
+ default: 0
211
+ },
212
+
213
+ // Likes and Dislikes
214
+ isLiked: {
215
+ type: Boolean,
216
+ default: false
217
+ },
218
+
219
+ isDisliked: {
220
+ type: Boolean,
221
+ default: false
222
+ },
223
+
224
+ likes: [{
225
+ type: mongoose.Schema.Types.ObjectId,
226
+ ref: "User"
227
+ }],
228
+
16
229
  dislikes: [{
17
230
  type: mongoose.Schema.Types.ObjectId,
18
- ref: "User",
231
+ ref: "User"
232
+ }],
233
+
234
+ likeCount: {
235
+ type: Number,
236
+ default: 0
237
+ },
238
+
239
+ dislikeCount: {
240
+ type: Number,
241
+ default: 0
242
+ },
243
+
244
+ // Comments
245
+ comments: [{
246
+ type: mongoose.Schema.Types.ObjectId,
247
+ ref: "Comment"
248
+ }],
249
+
250
+ commentCount: {
251
+ type: Number,
252
+ default: 0
253
+ },
254
+
255
+ allowComments: {
256
+ type: Boolean,
257
+ default: true
258
+ },
259
+
260
+ // Ratings
261
+ ratings: {
262
+ average: { type: Number, default: 0, min: 0, max: 5 },
263
+ count: { type: Number, default: 0 },
264
+ distribution: {
265
+ five: { type: Number, default: 0 },
266
+ four: { type: Number, default: 0 },
267
+ three: { type: Number, default: 0 },
268
+ two: { type: Number, default: 0 },
269
+ one: { type: Number, default: 0 }
270
+ }
271
+ },
272
+
273
+ // Reading Time
274
+ readingTime: {
275
+ type: Number, // in minutes
276
+ default: 0
277
+ },
278
+
279
+ wordCount: {
280
+ type: Number,
281
+ default: 0
282
+ },
283
+
284
+ // SEO
285
+ seo: {
286
+ metaTitle: {
287
+ type: String,
288
+ maxlength: [70, "Meta title cannot exceed 70 characters"]
289
+ },
290
+ metaDescription: {
291
+ type: String,
292
+ maxlength: [160, "Meta description cannot exceed 160 characters"]
293
+ },
294
+ metaKeywords: [{ type: String }],
295
+ focusKeyword: { type: String },
296
+ ogTitle: { type: String },
297
+ ogDescription: { type: String },
298
+ ogImage: {
299
+ public_id: { type: String },
300
+ url: { type: String }
301
+ },
302
+ twitterTitle: { type: String },
303
+ twitterDescription: { type: String },
304
+ twitterImage: {
305
+ public_id: { type: String },
306
+ url: { type: String }
307
+ },
308
+ canonicalUrl: { type: String },
309
+ noIndex: { type: Boolean, default: false },
310
+ noFollow: { type: Boolean, default: false }
311
+ },
312
+
313
+ // Related Content
314
+ relatedPosts: [{
315
+ type: mongoose.Schema.Types.ObjectId,
316
+ ref: "Blog"
19
317
  }],
20
- author: { type: String, default: "Admin" },
21
- images: [],
318
+
319
+ series: {
320
+ name: { type: String },
321
+ part: { type: Number },
322
+ totalParts: { type: Number }
323
+ },
324
+
325
+ // Content Structure
326
+ tableOfContents: [{
327
+ id: { type: String },
328
+ title: { type: String },
329
+ level: { type: Number },
330
+ children: [mongoose.Schema.Types.Mixed]
331
+ }],
332
+
333
+ // Format and Type
334
+ format: {
335
+ type: String,
336
+ enum: ["standard", "video", "audio", "gallery", "quote", "link"],
337
+ default: "standard"
338
+ },
339
+
340
+ postType: {
341
+ type: String,
342
+ enum: ["article", "tutorial", "news", "review", "interview", "case-study", "guide"],
343
+ default: "article"
344
+ },
345
+
346
+ // Language
347
+ language: {
348
+ type: String,
349
+ default: "en",
350
+ index: true
351
+ },
352
+
353
+ translations: [{
354
+ language: { type: String },
355
+ postId: {
356
+ type: mongoose.Schema.Types.ObjectId,
357
+ ref: "Blog"
358
+ }
359
+ }],
360
+
361
+ // Newsletter
362
+ includeInNewsletter: {
363
+ type: Boolean,
364
+ default: false
365
+ },
366
+
367
+ sentInNewsletter: {
368
+ type: Boolean,
369
+ default: false
370
+ },
371
+
372
+ newsletterSentAt: {
373
+ type: Date
374
+ },
375
+
376
+ // Analytics
377
+ analytics: {
378
+ averageTimeOnPage: { type: Number, default: 0 },
379
+ bounceRate: { type: Number, default: 0 },
380
+ clickThroughRate: { type: Number, default: 0 },
381
+ conversionRate: { type: Number, default: 0 },
382
+ socialShares: {
383
+ facebook: { type: Number, default: 0 },
384
+ twitter: { type: Number, default: 0 },
385
+ linkedin: { type: Number, default: 0 },
386
+ pinterest: { type: Number, default: 0 },
387
+ reddit: { type: Number, default: 0 }
388
+ }
389
+ },
390
+
391
+ // Custom Fields
392
+ customFields: {
393
+ type: Map,
394
+ of: mongoose.Schema.Types.Mixed
395
+ },
396
+
397
+ metadata: {
398
+ type: Map,
399
+ of: mongoose.Schema.Types.Mixed
400
+ },
401
+
402
+ // Workflow
403
+ workflow: {
404
+ status: {
405
+ type: String,
406
+ enum: ["draft", "in-review", "approved", "rejected", "published"],
407
+ default: "draft"
408
+ },
409
+ reviewedBy: {
410
+ type: mongoose.Schema.Types.ObjectId,
411
+ ref: "User"
412
+ },
413
+ reviewedAt: { type: Date },
414
+ approvedBy: {
415
+ type: mongoose.Schema.Types.ObjectId,
416
+ ref: "User"
417
+ },
418
+ approvedAt: { type: Date },
419
+ rejectionReason: { type: String }
420
+ },
421
+
422
+ // Version Control
423
+ version: {
424
+ type: Number,
425
+ default: 1
426
+ },
427
+
428
+ revisions: [{
429
+ version: { type: Number },
430
+ content: { type: String },
431
+ title: { type: String },
432
+ updatedBy: {
433
+ type: mongoose.Schema.Types.ObjectId,
434
+ ref: "User"
435
+ },
436
+ updatedAt: { type: Date, default: Date.now },
437
+ changeLog: { type: String }
438
+ }],
439
+
440
+ // Moderation
441
+ flagged: {
442
+ type: Boolean,
443
+ default: false
444
+ },
445
+
446
+ flagCount: {
447
+ type: Number,
448
+ default: 0
449
+ },
450
+
451
+ flagReasons: [{
452
+ reason: { type: String },
453
+ reportedBy: {
454
+ type: mongoose.Schema.Types.ObjectId,
455
+ ref: "User"
456
+ },
457
+ reportedAt: { type: Date, default: Date.now }
458
+ }],
459
+
460
+ // Admin Fields
461
+ createdBy: {
462
+ type: mongoose.Schema.Types.ObjectId,
463
+ ref: "User"
464
+ },
465
+
466
+ updatedBy: {
467
+ type: mongoose.Schema.Types.ObjectId,
468
+ ref: "User"
469
+ },
470
+
471
+ lastEditedBy: {
472
+ type: mongoose.Schema.Types.ObjectId,
473
+ ref: "User"
474
+ },
475
+
476
+ lastEditedAt: {
477
+ type: Date
478
+ },
479
+
480
+ notes: [{
481
+ content: { type: String },
482
+ createdBy: {
483
+ type: mongoose.Schema.Types.ObjectId,
484
+ ref: "User"
485
+ },
486
+ createdAt: { type: Date, default: Date.now }
487
+ }],
488
+
489
+ // Soft Delete
490
+ isDeleted: {
491
+ type: Boolean,
492
+ default: false,
493
+ index: true
494
+ },
495
+
496
+ deletedAt: {
497
+ type: Date
498
+ },
499
+
500
+ deletedBy: {
501
+ type: mongoose.Schema.Types.ObjectId,
502
+ ref: "User"
503
+ }
22
504
  },
23
505
  {
24
- toJSON: { virtuals: true },
25
- toObject: { virtuals: true },
26
506
  timestamps: true,
507
+ toJSON: { virtuals: true },
508
+ toObject: { virtuals: true }
27
509
  }
28
510
  );
29
511
 
30
- //Export the model
31
- module.exports = mongoose.model("Blog", blogSchema);
512
+ // Indexes for performance
513
+ blogSchema.index({ title: 1, status: 1 });
514
+ blogSchema.index({ author: 1, status: 1 });
515
+ blogSchema.index({ category: 1, published: 1 });
516
+ blogSchema.index({ featured: 1, published: 1 });
517
+ blogSchema.index({ tags: 1, published: 1 });
518
+ blogSchema.index({ publishedAt: -1 });
519
+ blogSchema.index({ views: -1 });
520
+ blogSchema.index({ likeCount: -1 });
521
+ blogSchema.index({ "ratings.average": -1 });
522
+ blogSchema.index({ slug: 1, status: 1 });
523
+ blogSchema.index({ createdAt: -1 });
524
+ blogSchema.index({ section: 1, published: 1 });
525
+
526
+ // Compound indexes
527
+ blogSchema.index({ published: 1, featured: 1, publishedAt: -1 });
528
+ blogSchema.index({ author: 1, published: 1, publishedAt: -1 });
529
+ blogSchema.index({ category: 1, published: 1, views: -1 });
530
+
531
+ // Text index for search
532
+ blogSchema.index({
533
+ title: "text",
534
+ subtitle: "text",
535
+ excerpt: "text",
536
+ content: "text",
537
+ tags: "text",
538
+ authorName: "text"
539
+ });
540
+
541
+ // Virtuals
542
+ blogSchema.virtual("url").get(function() {
543
+ return `/blog/${this.slug}`;
544
+ });
545
+
546
+ blogSchema.virtual("isPublished").get(function() {
547
+ return this.published && this.status === "published";
548
+ });
549
+
550
+ blogSchema.virtual("likeRatio").get(function() {
551
+ const total = this.likeCount + this.dislikeCount;
552
+ return total > 0 ? ((this.likeCount / total) * 100).toFixed(2) : 0;
553
+ });
554
+
555
+ blogSchema.virtual("engagementScore").get(function() {
556
+ return (
557
+ this.views * 1 +
558
+ this.likeCount * 5 +
559
+ this.commentCount * 10 +
560
+ this.shares * 15 +
561
+ this.bookmarks * 20
562
+ );
563
+ });
564
+
565
+ // Pre-save middleware
566
+ blogSchema.pre("save", async function(next) {
567
+ // Generate slug
568
+ if (this.isModified("title") && !this.slug) {
569
+ this.slug = slugify(this.title, {
570
+ lower: true,
571
+ strict: true,
572
+ remove: /[*+~.()'"!:@]/g
573
+ });
574
+
575
+ // Ensure unique slug
576
+ const slugRegEx = new RegExp(`^${this.slug}(-[0-9]*)?$`, "i");
577
+ const postsWithSlug = await this.constructor.find({ slug: slugRegEx });
578
+
579
+ if (postsWithSlug.length > 0) {
580
+ this.slug = `${this.slug}-${postsWithSlug.length}`;
581
+ }
582
+ }
583
+
584
+ // Calculate word count and reading time
585
+ if (this.isModified("content")) {
586
+ const words = this.content.trim().split(/\s+/).length;
587
+ this.wordCount = words;
588
+ this.readingTime = Math.ceil(words / 200); // Average reading speed: 200 words/min
589
+ }
590
+
591
+ // Sync like/dislike counts
592
+ if (this.isModified("likes")) {
593
+ this.likeCount = this.likes.length;
594
+ }
595
+
596
+ if (this.isModified("dislikes")) {
597
+ this.dislikeCount = this.dislikes.length;
598
+ }
599
+
600
+ // Sync comment count
601
+ if (this.isModified("comments")) {
602
+ this.commentCount = this.comments.length;
603
+ }
604
+
605
+ // Sync numViews with views
606
+ if (this.isModified("views")) {
607
+ this.numViews = this.views;
608
+ }
609
+
610
+ // Auto-publish if scheduled time has passed
611
+ if (this.status === "scheduled" && this.scheduledAt && this.scheduledAt <= new Date()) {
612
+ this.status = "published";
613
+ this.published = true;
614
+ this.publishedAt = new Date();
615
+ }
616
+
617
+ // Set published date on first publish
618
+ if (this.isModified("published") && this.published && !this.publishedAt) {
619
+ this.publishedAt = new Date();
620
+ }
621
+
622
+ // Set SEO defaults
623
+ if (!this.seo.metaTitle) {
624
+ this.seo.metaTitle = this.title.substring(0, 70);
625
+ }
626
+
627
+ if (!this.seo.metaDescription && this.excerpt) {
628
+ this.seo.metaDescription = this.excerpt.substring(0, 160);
629
+ }
630
+
631
+ next();
632
+ });
633
+
634
+ // Pre-update middleware
635
+ blogSchema.pre("findOneAndUpdate", function(next) {
636
+ const update = this.getUpdate();
637
+
638
+ if (update.title) {
639
+ update.slug = slugify(update.title, {
640
+ lower: true,
641
+ strict: true,
642
+ remove: /[*+~.()'"!:@]/g
643
+ });
644
+ }
645
+
646
+ if (update.content) {
647
+ const words = update.content.trim().split(/\s+/).length;
648
+ update.wordCount = words;
649
+ update.readingTime = Math.ceil(words / 200);
650
+ }
651
+
652
+ update.lastEditedAt = new Date();
653
+
654
+ next();
655
+ });
656
+
657
+ // Instance methods
658
+ blogSchema.methods = {
659
+ // Increment view count
660
+ incrementViews: async function() {
661
+ this.views += 1;
662
+ this.numViews += 1;
663
+ return this.save();
664
+ },
665
+
666
+ // Increment unique view count
667
+ incrementUniqueViews: async function() {
668
+ this.uniqueViews += 1;
669
+ return this.save();
670
+ },
671
+
672
+ // Toggle like
673
+ toggleLike: function(userId) {
674
+ const likeIndex = this.likes.indexOf(userId);
675
+ const dislikeIndex = this.dislikes.indexOf(userId);
676
+
677
+ // Remove from dislikes if exists
678
+ if (dislikeIndex > -1) {
679
+ this.dislikes.splice(dislikeIndex, 1);
680
+ this.dislikeCount -= 1;
681
+ this.isDisliked = false;
682
+ }
683
+
684
+ // Toggle like
685
+ if (likeIndex > -1) {
686
+ this.likes.splice(likeIndex, 1);
687
+ this.likeCount -= 1;
688
+ this.isLiked = false;
689
+ } else {
690
+ this.likes.push(userId);
691
+ this.likeCount += 1;
692
+ this.isLiked = true;
693
+ }
694
+
695
+ return this.save();
696
+ },
697
+
698
+ // Toggle dislike
699
+ toggleDislike: function(userId) {
700
+ const likeIndex = this.likes.indexOf(userId);
701
+ const dislikeIndex = this.dislikes.indexOf(userId);
702
+
703
+ // Remove from likes if exists
704
+ if (likeIndex > -1) {
705
+ this.likes.splice(likeIndex, 1);
706
+ this.likeCount -= 1;
707
+ this.isLiked = false;
708
+ }
709
+
710
+ // Toggle dislike
711
+ if (dislikeIndex > -1) {
712
+ this.dislikes.splice(dislikeIndex, 1);
713
+ this.dislikeCount -= 1;
714
+ this.isDisliked = false;
715
+ } else {
716
+ this.dislikes.push(userId);
717
+ this.dislikeCount += 1;
718
+ this.isDisliked = true;
719
+ }
720
+
721
+ return this.save();
722
+ },
723
+
724
+ // Publish post
725
+ publish: function() {
726
+ this.published = true;
727
+ this.status = "published";
728
+ this.publishedAt = new Date();
729
+ return this.save();
730
+ },
731
+
732
+ // Unpublish post
733
+ unpublish: function() {
734
+ this.published = false;
735
+ this.status = "draft";
736
+ return this.save();
737
+ },
738
+
739
+ // Schedule post
740
+ schedule: function(date) {
741
+ this.status = "scheduled";
742
+ this.scheduledAt = date;
743
+ return this.save();
744
+ },
745
+
746
+ // Archive post
747
+ archive: function() {
748
+ this.status = "archived";
749
+ this.archivedAt = new Date();
750
+ return this.save();
751
+ },
752
+
753
+ // Add to featured
754
+ addToFeatured: function() {
755
+ this.featured = true;
756
+ return this.save();
757
+ },
758
+
759
+ // Remove from featured
760
+ removeFromFeatured: function() {
761
+ this.featured = false;
762
+ return this.save();
763
+ },
764
+
765
+ // Create revision
766
+ createRevision: function(userId, changeLog) {
767
+ this.revisions.push({
768
+ version: this.version,
769
+ content: this.content,
770
+ title: this.title,
771
+ updatedBy: userId,
772
+ changeLog: changeLog || "Content updated"
773
+ });
774
+ this.version += 1;
775
+ return this.save();
776
+ },
777
+
778
+ // Soft delete
779
+ softDelete: function(userId) {
780
+ this.isDeleted = true;
781
+ this.deletedAt = new Date();
782
+ this.deletedBy = userId;
783
+ this.published = false;
784
+ this.status = "archived";
785
+ return this.save();
786
+ },
787
+
788
+ // Restore
789
+ restore: function() {
790
+ this.isDeleted = false;
791
+ this.deletedAt = null;
792
+ this.deletedBy = null;
793
+ this.status = "draft";
794
+ return this.save();
795
+ }
796
+ };
797
+
798
+ // Static methods
799
+ blogSchema.statics = {
800
+ // Get published posts
801
+ getPublished: function(limit = 10) {
802
+ return this.find({
803
+ published: true,
804
+ status: "published",
805
+ isDeleted: false
806
+ })
807
+ .sort({ publishedAt: -1 })
808
+ .limit(limit)
809
+ .populate("author", "name avatar email")
810
+ .populate("category", "title slug")
811
+ .exec();
812
+ },
813
+
814
+ // Get featured posts
815
+ getFeatured: function(limit = 5) {
816
+ return this.find({
817
+ featured: true,
818
+ published: true,
819
+ isDeleted: false
820
+ })
821
+ .sort({ priority: -1, publishedAt: -1 })
822
+ .limit(limit)
823
+ .populate("author", "name avatar")
824
+ .populate("category", "title slug")
825
+ .exec();
826
+ },
827
+
828
+ // Get trending posts
829
+ getTrending: function(limit = 10, days = 7) {
830
+ const dateLimit = new Date();
831
+ dateLimit.setDate(dateLimit.getDate() - days);
832
+
833
+ return this.find({
834
+ published: true,
835
+ isDeleted: false,
836
+ publishedAt: { $gte: dateLimit }
837
+ })
838
+ .sort({ views: -1, likeCount: -1 })
839
+ .limit(limit)
840
+ .populate("author", "name avatar")
841
+ .populate("category", "title slug")
842
+ .exec();
843
+ },
844
+
845
+ // Get popular posts
846
+ getPopular: function(limit = 10) {
847
+ return this.find({
848
+ published: true,
849
+ isDeleted: false
850
+ })
851
+ .sort({ views: -1, likeCount: -1, commentCount: -1 })
852
+ .limit(limit)
853
+ .populate("author", "name avatar")
854
+ .populate("category", "title slug")
855
+ .exec();
856
+ },
857
+
858
+ // Get by author
859
+ getByAuthor: function(authorId, limit = 10) {
860
+ return this.find({
861
+ author: authorId,
862
+ published: true,
863
+ isDeleted: false
864
+ })
865
+ .sort({ publishedAt: -1 })
866
+ .limit(limit)
867
+ .populate("category", "title slug")
868
+ .exec();
869
+ },
870
+
871
+ // Get by category
872
+ getByCategory: function(categoryId, limit = 10) {
873
+ return this.find({
874
+ category: categoryId,
875
+ published: true,
876
+ isDeleted: false
877
+ })
878
+ .sort({ publishedAt: -1 })
879
+ .limit(limit)
880
+ .populate("author", "name avatar")
881
+ .exec();
882
+ },
883
+
884
+ // Get by tags
885
+ getByTag: function(tag, limit = 10) {
886
+ return this.find({
887
+ tags: tag,
888
+ published: true,
889
+ isDeleted: false
890
+ })
891
+ .sort({ publishedAt: -1 })
892
+ .limit(limit)
893
+ .populate("author", "name avatar")
894
+ .populate("category", "title slug")
895
+ .exec();
896
+ },
897
+
898
+ // Search posts
899
+ searchPosts: function(query, options = {}) {
900
+ const searchQuery = {
901
+ $text: { $search: query },
902
+ published: true,
903
+ isDeleted: false
904
+ };
905
+
906
+ return this.find(searchQuery, { score: { $meta: "textScore" } })
907
+ .sort({ score: { $meta: "textScore" } })
908
+ .limit(options.limit || 20)
909
+ .populate("author", "name avatar")
910
+ .populate("category", "title slug")
911
+ .exec();
912
+ },
913
+
914
+ // Get related posts
915
+ getRelated: function(postId, tags, categoryId, limit = 5) {
916
+ return this.find({
917
+ _id: { $ne: postId },
918
+ $or: [
919
+ { tags: { $in: tags } },
920
+ { category: categoryId }
921
+ ],
922
+ published: true,
923
+ isDeleted: false
924
+ })
925
+ .sort({ publishedAt: -1 })
926
+ .limit(limit)
927
+ .populate("author", "name avatar")
928
+ .exec();
929
+ }
930
+ };
931
+
932
+ // Query helpers
933
+ blogSchema.query = {
934
+ published: function() {
935
+ return this.where({ published: true, status: "published", isDeleted: false });
936
+ },
937
+
938
+ featured: function() {
939
+ return this.where({ featured: true, published: true, isDeleted: false });
940
+ },
941
+
942
+ byAuthor: function(authorId) {
943
+ return this.where({ author: authorId, published: true, isDeleted: false });
944
+ },
945
+
946
+ byCategory: function(categoryId) {
947
+ return this.where({ category: categoryId, published: true, isDeleted: false });
948
+ },
949
+
950
+ byTag: function(tag) {
951
+ return this.where({ tags: tag, published: true, isDeleted: false });
952
+ },
953
+
954
+ recent: function(days = 30) {
955
+ const dateLimit = new Date();
956
+ dateLimit.setDate(dateLimit.getDate() - days);
957
+ return this.where({ publishedAt: { $gte: dateLimit }, published: true, isDeleted: false });
958
+ }
959
+ };
960
+
961
+ module.exports = mongoose.model("Blog", blogSchema);