forked from BlogEngine/BlogEngine.NET
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPost.cs
More file actions
2317 lines (2055 loc) · 75 KB
/
Copy pathPost.cs
File metadata and controls
2317 lines (2055 loc) · 75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
namespace BlogEngine.Core
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Net.Mail;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using BlogEngine.Core.Data.Models;
using BlogEngine.Core.Providers;
/// <summary>
/// A post is an entry on the blog - a blog post.
/// </summary>
[Serializable]
public class Post : BusinessBase<Post, Guid>, IComparable<Post>, IPublishable
{
#region Constants and Fields
/// <summary>
/// The sync root.
/// </summary>
private static readonly object SyncRoot = new object();
/// <summary>
/// The categories.
/// </summary>
private readonly StateList<Category> categories;
/// <summary>
/// All comments, including deleted
/// </summary>
private readonly List<Comment> allcomments;
/// <summary>
/// The notification emails.
/// </summary>
private readonly StateList<string> notificationEmails;
/// <summary>
/// The post tags.
/// </summary>
private readonly StateList<string> tags;
/// <summary>
/// The posts.
/// </summary>
private static Dictionary<Guid, List<Post>> posts = new Dictionary<Guid, List<Post>>();
/// <summary>
/// The deleted posts.
/// </summary>
private static Dictionary<Guid, List<Post>> deletedposts = new Dictionary<Guid, List<Post>>();
/// <summary>
/// The author.
/// </summary>
private string author;
/// <summary>
/// The content.
/// </summary>
private string content;
/// <summary>
/// The description - UNIFIED METADATA: This serves as the primary meta description.
/// Part of the unified SEO/GEO metadata model. Used for meta description tags,
/// Open Graph og:description, Twitter Card description, and Schema.org description.
/// </summary>
/// <remarks>
/// BACKWARD COMPATIBILITY: Legacy property maintained for existing code.
/// In the unified model, this is the authoritative source for all description metadata.
/// SemanticSummary provides an alternative AI-optimized description when set.
/// </remarks>
private string description;
/// <summary>
/// Whether the post is comments enabled.
/// </summary>
private bool hasCommentsEnabled;
/// <summary>
/// The nested comments.
/// </summary>
private List<Comment> nestedComments;
/// <summary>
/// Whether the post is published.
/// </summary>
private bool isPublished;
/// <summary>
/// Whether the post is deleted.
/// </summary>
private bool isDeleted;
/// <summary>
/// The raters.
/// </summary>
private int raters;
/// <summary>
/// The rating.
/// </summary>
private float rating;
/// <summary>
/// The slug of the post.
/// </summary>
private string slug;
/// <summary>
/// The title.
/// </summary>
private string title;
#region Unified SEO/GEO Metadata Fields
/// <summary>
/// UNIFIED METADATA: Canonical URL for the post.
/// </summary>
/// <remarks>
/// Part of the unified SEO/GEO metadata model.
/// - SEO: Prevents duplicate content penalties, establishes primary URL identity
/// - GEO: Helps AI systems identify the authoritative source for this content
/// Used in: canonical link tag, og:url, Schema.org url property
/// </remarks>
private string canonicalUrl;
/// <summary>
/// UNIFIED METADATA: Schema.org type (e.g., "BlogPosting", "Article", "NewsArticle").
/// </summary>
/// <remarks>
/// Part of the unified SEO/GEO metadata model.
/// - SEO: Helps search engines understand content type for rich snippets
/// - GEO: Critical for AI systems to properly classify and understand content
/// Defaults to "BlogPosting" if not specified.
/// </remarks>
private string schemaType;
/// <summary>
/// UNIFIED METADATA: Comma-separated key entities mentioned in the post.
/// </summary>
/// <remarks>
/// Part of the unified SEO/GEO metadata model (primarily GEO-focused).
/// - GEO: Named entities (people, organizations, locations, concepts) for AI extraction
/// - SEO: Can enhance semantic understanding in advanced search systems
/// Example: "Machine Learning, Neural Networks, TensorFlow"
/// Used in: Custom metadata tags, Schema.org mentions/about properties
/// </remarks>
private string keyEntities;
/// <summary>
/// UNIFIED METADATA: Semantic summary optimized for AI systems.
/// </summary>
/// <remarks>
/// Part of the unified SEO/GEO metadata model (primarily GEO-focused).
/// - GEO: Concise, semantically rich summary emphasizing key concepts and relationships
/// - SEO: Alternative to traditional description for AI-powered search
/// Unlike Description, this focuses on semantic meaning and entity relationships.
/// Falls back to Description if not set. Used in: AI-specific meta tags, structured data
/// </remarks>
private string semanticSummary;
/// <summary>
/// UNIFIED METADATA: Main Subject Line (MSL) for content classification.
/// </summary>
/// <remarks>
/// Part of the unified SEO/GEO metadata model (primarily GEO-focused).
/// - GEO: Primary subject/topic for AI topic modeling and categorization
/// - SEO: Can be used for content clustering and site organization
/// Should be a single, clear phrase (e.g., "Cloud Computing Best Practices")
/// Used in: Custom classification tags, Schema.org about property
/// </remarks>
private string contentMSL;
/// <summary>
/// UNIFIED METADATA: Meta keywords for search engines and AI systems.
/// </summary>
/// <remarks>
/// Part of the unified SEO/GEO metadata model.
/// - SEO: Traditional keyword meta tag (lower weight in modern search)
/// - GEO: Still valuable for AI systems and specialized search
/// CONSOLIDATED: This field unifies all keyword storage. Legacy 'keywords' usage
/// should map to this field for backward compatibility.
/// Used in: meta keywords tag, Schema.org keywords property
/// </remarks>
private string metaKeywords;
/// <summary>
/// UNIFIED METADATA: Meta robots directives (e.g., "index, follow", "noindex").
/// </summary>
/// <remarks>
/// Part of the unified SEO/GEO metadata model.
/// - SEO: Controls search engine crawling and indexing behavior
/// - GEO: Can include/exclude content from AI training data and generative results
/// Common values: "index, follow", "noindex, follow", "index, nofollow", "noindex, nofollow"
/// Extended for GEO: "noai", "noimageai" directives may be added
/// Used in: meta robots tag, HTTP headers, robots.txt references
/// </remarks>
private string metaRobots;
/// <summary>
/// UNIFIED METADATA: Open Graph data (JSON serialized).
/// </summary>
/// <remarks>
/// Part of the unified SEO/GEO metadata model.
/// - SEO: Rich social media previews on Facebook, LinkedIn, etc.
/// - GEO: Provides structured context for AI systems analyzing social shares
/// USAGE PATTERN: This field is for CUSTOM OVERRIDES only.
/// By default, SeoMetadataManager generates OG tags from other metadata properties.
/// Store JSON here only when you need to override the default OG tag generation.
/// Example: {"og:video": "https://...", "og:video:type": "video/mp4"}
/// </remarks>
private string openGraphData;
/// <summary>
/// UNIFIED METADATA: Breadcrumb label for navigation structure.
/// </summary>
/// <remarks>
/// Part of the unified SEO/GEO metadata model.
/// - SEO: Used in Schema.org BreadcrumbList for rich snippets
/// - GEO: Helps AI understand page hierarchy and site structure
/// Falls back to post title if not set.
/// Used in: Schema.org BreadcrumbList structured data
/// </remarks>
private string breadcrumbLabel;
/// <summary>
/// UNIFIED METADATA: Schema.org organization name for this post.
/// </summary>
/// <remarks>
/// Part of the unified SEO/GEO metadata model.
/// - SEO: Organization attribution in structured data for rich snippets
/// - GEO: Helps AI systems understand organizational context and authority
/// Can override blog-level organization for posts from guest authors or partners.
/// Used in: Schema.org publisher/organization properties
/// </remarks>
private string schemaOrganization;
#endregion
#endregion
#region Constructors and Destructors
/// <summary>
/// Initializes a new instance of the <see cref = "Post" /> class.
/// The default contstructor assign default values.
/// </summary>
public Post()
{
this.Id = Guid.NewGuid();
this.allcomments = new List<Comment>();
this.categories = new StateList<Category>();
this.tags = new StateList<string>();
this.notificationEmails = new StateList<string>();
this.DateCreated = new DateTime();
this.isPublished = true;
this.hasCommentsEnabled = true;
}
static Post()
{
Blog.Saved += (s, e) =>
{
if (e.Action == SaveAction.Delete)
{
Blog blog = s as Blog;
if (blog != null)
{
RefreshPostLists(blog);
}
}
};
}
#endregion
#region Events
/// <summary>
/// Occurs before a new comment is added.
/// </summary>
public static event EventHandler<CancelEventArgs> AddingComment;
/// <summary>
/// Occurs when a comment is added.
/// </summary>
public static event EventHandler<EventArgs> CommentAdded;
/// <summary>
/// Occurs when a comment has been removed.
/// </summary>
public static event EventHandler<EventArgs> CommentRemoved;
/// <summary>
/// Occurs when a comment has been purged.
/// </summary>
public static event EventHandler<EventArgs> CommentPurged;
/// <summary>
/// Occurs when a comment has been restored.
/// </summary>
public static event EventHandler<EventArgs> CommentRestored;
/// <summary>
/// Occurs when a comment is updated.
/// </summary>
public static event EventHandler<EventArgs> CommentUpdated;
/// <summary>
/// Occurs when a visitor rates the post.
/// </summary>
public static event EventHandler<EventArgs> Rated;
/// <summary>
/// Occurs before comment is removed.
/// </summary>
public static event EventHandler<CancelEventArgs> RemovingComment;
/// <summary>
/// Occurs before comment is purged.
/// </summary>
public static event EventHandler<CancelEventArgs> PurgingComment;
/// <summary>
/// Occurs before comment is restored.
/// </summary>
public static event EventHandler<CancelEventArgs> RestoringComment;
/// <summary>
/// Occurs when the post is being served to the output stream.
/// </summary>
public static event EventHandler<ServingEventArgs> Serving;
/// <summary>
/// Occurs when the post is being published.
/// </summary>
public static event EventHandler<CancelEventArgs> Publishing;
/// <summary>
/// Occurs when a post is published.
/// </summary>
public static event EventHandler<EventArgs> Published;
/// <summary>
/// Occurs before a new comment is updated.
/// </summary>
public static event EventHandler<CancelEventArgs> UpdatingComment;
#endregion
#region Post Properties
/// <summary>
/// Gets a sorted collection of all undeleted posts in the blog.
/// Sorted by date.
/// </summary>
public static List<Post> Posts
{
get
{
Blog blog = Blog.CurrentInstance;
List<Post> blogPosts;
if (!posts.TryGetValue(blog.BlogId, out blogPosts))
{
lock (SyncRoot)
{
if (!posts.TryGetValue(blog.BlogId, out blogPosts))
{
posts[blog.Id] = blogPosts = BlogService.FillPosts().Where(p => p.IsDeleted == false).ToList();
blogPosts.TrimExcess();
AddRelations(blogPosts);
}
}
}
return blogPosts;
}
}
/// <summary>
/// Gets a sorted collection of all undeleted posts across all blogs.
/// Sorted by date.
/// </summary>
public static List<Post> AllBlogPosts
{
get
{
List<Blog> blogs = Blog.Blogs.Where(b => b.IsActive).ToList();
Guid originalBlogInstanceIdOverride = Blog.InstanceIdOverride;
List<Post> postsAcrossAllBlogs = new List<Post>();
// Posts are not loaded for a blog instance until that blog
// instance is first accessed. For blog instances where the
// posts have not yet been loaded, using InstanceIdOverride to
// temporarily switch the blog CurrentInstance blog so the Posts
// for that blog instance can be loaded.
//
for (int i = 0; i < blogs.Count; i++)
{
List<Post> blogPosts;
if (!posts.TryGetValue(blogs[i].Id, out blogPosts))
{
// temporarily override the Current BlogId to the
// blog Id we need posts to be loaded for.
Blog.InstanceIdOverride = blogs[i].Id;
blogPosts = Posts;
Blog.InstanceIdOverride = originalBlogInstanceIdOverride;
}
postsAcrossAllBlogs.AddRange(blogPosts);
}
postsAcrossAllBlogs.Sort();
// do not call AddRelations(). that will change the Next/Previous properties
// to point to posts in other blogs, which leads to the Next / Previous
// posts pointing to posts in other blog instances when viewing a single post
// (in post.aspx). If Next/Previous is needed for the posts returned
// here in AllBlogPosts, would be better to create new properties
// (e.g. AllBlogsNextPost, AllBlogsPreviousPost).
// AddRelations(postsAcrossAllBlogs);
return postsAcrossAllBlogs;
}
}
/// <summary>
/// Gets a sorted collection of all undeleted posts, taking into account the
/// current blog instance's Site Aggregation status in determining if posts
/// from just the current instance or all instances should be returned.
/// Sorted by date.
/// </summary>
/// <remarks>
/// This logic could be put into the normal 'Posts' property, however
/// there are times when a Site Aggregation blog instance may just need
/// its own posts. So ApplicablePosts can be called when data across
/// all blog instances may be needed, and Posts can be called when data
/// for just the current blog instance is needed.
/// </remarks>
public static List<Post> ApplicablePosts
{
get
{
if (Blog.CurrentInstance.IsSiteAggregation)
return AllBlogPosts;
else
return Posts;
}
}
/// <summary>
/// Gets a sorted collection of all deleted posts in the blog.
/// Sorted by date.
/// </summary>
public static List<Post> DeletedPosts
{
get
{
Blog blog = Blog.CurrentInstance;
List<Post> blogPosts;
if (!deletedposts.TryGetValue(blog.Id, out blogPosts))
{
lock (SyncRoot)
{
if (!deletedposts.TryGetValue(blog.Id, out blogPosts))
{
blogPosts = BlogService.FillPosts().Where(p => p.IsDeleted == true).ToList();
deletedposts[blog.Id] = blogPosts;
}
}
}
return blogPosts;
}
}
/// <summary>
/// Gets or sets the Author or the post.
/// </summary>
public string Author
{
get
{
return this.author;
}
set
{
base.SetValue("Author", value, ref this.author);
}
}
/// <summary>
/// Gets AuthorProfile.
/// </summary>
public AuthorProfile AuthorProfile
{
get
{
return AuthorProfile.GetProfile(this.Author);
}
}
/// <summary>
/// Gets an unsorted List of categories.
/// </summary>
public StateList<Category> Categories
{
get
{
return this.categories;
}
}
/// <summary>
/// Gets or sets the Content or the post.
/// </summary>
public string Content
{
get
{
return this.content;
}
set
{
base.SetValue("Content", value, ref this.content);
// This is commented out only because I can't find any reference to
// this cache item anywhere in the project. So it seems pretty obscure
// if it's supposed to be used by plugins or something else.
//if (base.SetValue("Content", value, ref this.content))
//{
// Blog.CurrentInstance.Cache.Remove("content_" + this.Id);
//}
}
}
/// <summary>
/// Gets or sets the Description of the post.
/// </summary>
/// <remarks>
/// UNIFIED METADATA: This is the primary meta description in the unified SEO/GEO model.
/// - SEO: Used in meta description tags for search engine results
/// - GEO: Provides context for AI summarization and content understanding
/// BACKWARD COMPATIBILITY: Legacy property maintained. This is the authoritative source
/// for all description metadata unless SemanticSummary provides an AI-optimized alternative.
/// Used by MetadataBuilder and SeoMetadataManager for og:description, twitter:description,
/// and Schema.org description properties.
/// </remarks>
public string Description
{
get
{
return this.description;
}
set
{
base.SetValue("Description", value, ref this.description);
}
}
/// <summary>
/// Gets if the Post have been changed.
/// </summary>
public override bool IsChanged
{
get
{
if (base.IsChanged)
{
return true;
}
if (this.Categories.IsChanged || this.Tags.IsChanged || this.NotificationEmails.IsChanged)
{
return true;
}
return false;
}
}
/// <summary>
/// Gets the next post relative to this one based on time.
/// <remarks>
/// If this post is the newest, then it returns null.
/// </remarks>
/// </summary>
public Post Next { get; private set; }
/// <summary>
/// Gets a collection of email addresses that is signed up for
/// comment notification on the specific post.
/// </summary>
public StateList<string> NotificationEmails
{
get
{
return this.notificationEmails;
}
}
/// <summary>
/// Gets the absolute permanent link to the post.
/// </summary>
public Uri PermaLink
{
get
{
return new Uri($"{Blog.AbsoluteWebRoot}post.aspx?id={Id}");
//return new Uri(string.Format("{0}post/{1}", this.Blog.AbsoluteWebRoot, this.Slug));
}
}
/// <summary>
/// Gets the previous post relative to this one based on time.
/// <remarks>
/// If this post is the oldest, then it returns null.
/// </remarks>
/// </summary>
public Post Previous { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether or not the post is published.
/// </summary>
public bool IsPublished
{
get
{
return this.isPublished;
}
set
{
base.SetValue("IsPublished", value, ref this.isPublished);
}
}
/// <summary>
/// Gets or sets a value indicating whether or not the post is deleted.
/// </summary>
public bool IsDeleted
{
get
{
return this.isDeleted;
}
set
{
base.SetValue("IsDeleted", value, ref this.isDeleted);
}
}
/// <summary>
/// Gets or sets the number of raters or the object.
/// </summary>
public int Raters
{
get
{
return this.raters;
}
set
{
base.SetValue("Raters", value, ref this.raters);
}
}
/// <summary>
/// Gets or sets the rating or the post.
/// </summary>
public float Rating
{
get
{
return this.rating;
}
set
{
base.SetValue("Rating", value, ref this.rating);
}
}
/// <summary>
/// Gets the absolute link to the post.
/// </summary>
public Uri AbsoluteLink
{
get
{
return new Uri(this.Blog.AbsoluteWebRootAuthority + this.RelativeLink);
}
}
/// <summary>
/// Gets a relative-to-the-site-root path to the post.
/// Only for in-site use.
/// </summary>
public string RelativeLink
{
get
{
// taking into account aggregated posts
var settings = BlogSettings.GetInstanceSettings(Blog);
var ext = string.IsNullOrEmpty(BlogConfig.FileExtension) ? ".aspx" : BlogConfig.FileExtension;
var theslug = Utils.RemoveIllegalCharacters(this.Slug);
if (!settings.RemoveExtensionsFromUrls)
theslug += ext;
var BlogUrl = "";
if (this.BlogId != Blog.CurrentInstance.Id)
{
// point it to child blog
BlogUrl = this.Blog.Name + "/";
}
return settings.TimeStampPostLinks
? string.Format("{0}{1}post/{2}{3}", Blog.RelativeWebRoot, BlogUrl, DateCreated.ToString("yyyy/MM/dd/", CultureInfo.InvariantCulture), theslug)
: string.Format("{0}{1}post/{2}", Utils.RelativeWebRoot, BlogUrl, theslug);
}
}
/// <summary>
/// Returns a relative link if possible if the hostname of this blog instance matches the
/// hostname of the site aggregation blog. If the hostname is different, then the
/// absolute link is returned.
/// </summary>
public string RelativeOrAbsoluteLink
{
get
{
return Blog.DoesHostnameDifferFromSiteAggregationBlog ? AbsoluteLink.ToString() : RelativeLink;
}
}
/// <summary>
/// Gets or sets the Slug of the Post.
/// A Slug is the relative URL used by the posts.
/// </summary>
public string Slug
{
get
{
return string.IsNullOrEmpty(this.slug) ? GetUniqueSlug(this.title, this.Id) : this.slug;
}
set
{
base.SetValue("Slug", value, ref this.slug);
}
}
/// <summary>
/// Gets an unsorted collection of tags.
/// </summary>
public StateList<string> Tags
{
get
{
return this.tags;
}
}
/// <summary>
/// Gets or sets the Title or the post.
/// </summary>
public string Title
{
get
{
return this.title;
}
set
{
base.SetValue("Title", value, ref this.title);
}
}
/// <summary>
/// Gets the trackback link to the post.
/// </summary>
public Uri TrackbackLink
{
get
{
return new Uri($"{Blog.AbsoluteWebRoot}trackback.axd?id={Id}");
}
}
/// <summary>
/// Gets a value indicating whether or not the post is visible or not.
/// </summary>
public bool IsVisible
{
get
{
if (this.IsDeleted)
return false;
else if (this.IsPublished && this.DateCreated <= BlogSettings.Instance.FromUtc())
return true;
else if (Security.IsAuthorizedTo(Rights.ViewUnpublishedPosts))
return true;
return false;
}
}
/// <summary>
/// Gets a value indicating whether a post is available to visitors not logged into the blog.
/// </summary>
public bool IsVisibleToPublic
{
get
{
return (this.IsPublished && this.IsDeleted == false &&
this.DateCreated <= BlogSettings.Instance.FromUtc());
}
}
/// <summary>
/// URL of the first image in the post, if any.
/// If there's no first image, returns an empty string.
/// </summary>
public string FirstImgSrc
{
get
{
string srcValue = null;
if (!string.IsNullOrEmpty(content))
{
Match match = Regex.Match(content, @"<img\s+?.*?src=('|"")(.*?)\1.*?>", RegexOptions.Multiline | RegexOptions.IgnoreCase);
if (match.Success)
{
srcValue = match.Groups[2].Value;
}
}
return srcValue ?? string.Empty;
}
}
#endregion
#region SEO & GEO (Generative Engine Optimization) Properties
/// <summary>
/// Gets or sets the canonical URL for this post.
/// </summary>
/// <remarks>
/// UNIFIED METADATA: Part of the unified SEO/GEO model.
/// The canonical URL is used to prevent duplicate content penalties in search engines
/// and to establish the primary URL identity for this specific post when duplicated elsewhere.
/// Also helps AI systems identify the authoritative source.
/// </remarks>
public string CanonicalUrl
{
get { return this.canonicalUrl; }
set { base.SetValue("CanonicalUrl", value, ref this.canonicalUrl); }
}
/// <summary>
/// Gets or sets the Schema.org type for structured data markup.
/// </summary>
/// <remarks>
/// UNIFIED METADATA: Part of the unified SEO/GEO model.
/// Specifies the schema type for this post (e.g., "BlogPosting", "Article", "NewsArticle").
/// Used in structured data to help search engines and AI systems understand the content type.
/// Defaults to "BlogPosting" if not specified.
/// </remarks>
public string SchemaType
{
get { return this.schemaType; }
set { base.SetValue("SchemaType", value, ref this.schemaType); }
}
/// <summary>
/// Gets or sets comma-separated key entities mentioned in the post.
/// </summary>
/// <remarks>
/// UNIFIED METADATA: Part of the unified SEO/GEO model (GEO-focused).
/// A comma-separated list of named entities (people, organizations, locations, concepts)
/// mentioned in the post content. Used by AI systems for entity extraction and semantic understanding.
/// Example: "Machine Learning, Neural Networks, TensorFlow"
/// </remarks>
public string KeyEntities
{
get { return this.keyEntities; }
set { base.SetValue("KeyEntities", value, ref this.keyEntities); }
}
/// <summary>
/// Gets or sets a semantic summary optimized for AI systems.
/// </summary>
/// <remarks>
/// UNIFIED METADATA: Part of the unified SEO/GEO model (GEO-focused).
/// A concise, semantically rich summary that emphasizes key concepts and relationships
/// for better understanding by generative AI systems. Unlike traditional meta descriptions,
/// this should highlight semantic meaning and entity relationships.
/// </remarks>
public string SemanticSummary
{
get { return this.semanticSummary; }
set { base.SetValue("SemanticSummary", value, ref this.semanticSummary); }
}
/// <summary>
/// Gets or sets the Main Subject Line (MSL) for content classification.
/// </summary>
/// <remarks>
/// UNIFIED METADATA: Part of the unified SEO/GEO model (GEO-focused).
/// The primary subject or main topic of the post content, used for topic modeling
/// and content categorization by AI systems. Should be a single, clear phrase representing
/// the post's main subject (e.g., "Cloud Computing Best Practices").
/// </remarks>
public string ContentMSL
{
get { return this.contentMSL; }
set { base.SetValue("ContentMSL", value, ref this.contentMSL); }
}
/// <summary>
/// Gets or sets comma-separated meta keywords for search engines and AI systems.
/// </summary>
/// <remarks>
/// UNIFIED METADATA: Part of the unified SEO/GEO model.
/// Keywords that describe the post content for search engines and AI-powered search systems.
/// Use comma-separated values. Modern search engines may give less weight to this field,
/// but it remains useful for AI systems and specialized search applications.
///
/// CONSOLIDATED FIELD: This is the unified storage for all keyword metadata in the Post model.
///
/// BACKWARD COMPATIBILITY: This property provides explicit keyword metadata control.
/// Falls back to Tags collection if not explicitly set. MetadataBuilder automatically
/// uses this field when set, or falls back to Tags when empty.
///
/// Usage: Set this when you need fine-grained control over SEO keywords that differs
/// from your taxonomy Tags. Leave empty to use Tags as the default keyword source.
/// </remarks>
public string MetaKeywords
{
get
{
// If MetaKeywords not explicitly set, fall back to Tags for backward compatibility
if (string.IsNullOrEmpty(this.metaKeywords) && this.Tags != null && this.Tags.Count > 0)
{
return string.Join(", ", this.Tags);
}
return this.metaKeywords;
}
set { base.SetValue("MetaKeywords", value, ref this.metaKeywords); }
}
/// <summary>
/// Gets or sets meta robots directives for search engine crawling behavior.
/// </summary>
/// <remarks>
/// UNIFIED METADATA: Part of the unified SEO/GEO model.
/// Controls how search engine crawlers and AI indexers should treat this post.
/// Common values: "index, follow", "noindex, follow", "index, nofollow", "noindex, nofollow".
/// Extended GEO directives: "noai", "noimageai" to opt-out of AI training/generation.
/// Used to include/exclude content from search results and AI training data.
/// </remarks>
public string MetaRobots
{
get { return this.metaRobots; }
set { base.SetValue("MetaRobots", value, ref this.metaRobots); }
}
/// <summary>
/// Gets or sets Open Graph metadata (JSON serialized).
/// </summary>
/// <remarks>
/// UNIFIED METADATA: Part of the unified SEO/GEO model.
/// JSON-formatted Open Graph metadata for social media sharing and rich previews.
/// Includes properties like og:title, og:description, og:image, og:type, etc.
/// Improves how posts appear when shared on social media and messaging platforms.
/// USAGE PATTERN: This field is for CUSTOM OVERRIDES only. By default, SeoMetadataManager
/// generates OG tags from other metadata properties. Use this only when you need to override
/// default OG tag generation with custom values.
/// </remarks>
public string OpenGraphData
{