feat(seo): FAQPage structured data on blog posts
Extracts Q/A pairs from the post body (an <h3> ending in the Persian question mark ؟ followed by the next <p>) and emits FAQPage JSON-LD in <head>. Makes posts with FAQ sections eligible for FAQ rich results in Google. Non-question <h3> headings are ignored. Verified: post with 3 h3s emits exactly 2 Question entries (the plain heading excluded), valid escaped JSON. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,23 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@if (Model.Faqs.Any())
|
||||||
|
{
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@@context": "https://schema.org",
|
||||||
|
"@@type": "FAQPage",
|
||||||
|
"mainEntity": [
|
||||||
|
@for (int i = 0; i < Model.Faqs.Count; i++)
|
||||||
|
{
|
||||||
|
var f = Model.Faqs[i];
|
||||||
|
@:{ "@@type": "Question", "name": "@J(f.Q)", "acceptedAnswer": { "@@type": "Answer", "text": "@J(f.A)" } }@(i < Model.Faqs.Count - 1 ? "," : "")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* ─── Post Layout ──────────────────────────────────────────────── */
|
/* ─── Post Layout ──────────────────────────────────────────────── */
|
||||||
.post-layout{max-width:1100px;margin:0 auto;padding:5rem 2rem 3rem;display:grid;grid-template-columns:1fr 320px;gap:3rem;align-items:start}
|
.post-layout{max-width:1100px;margin:0 auto;padding:5rem 2rem 3rem;display:grid;grid-template-columns:1fr 320px;gap:3rem;align-items:start}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ public class PostModel : PageModel
|
|||||||
public BlogPost? Post { get; private set; }
|
public BlogPost? Post { get; private set; }
|
||||||
public List<CommentVm> Comments { get; private set; } = new();
|
public List<CommentVm> Comments { get; private set; } = new();
|
||||||
public bool CommentSent { get; private set; } = false;
|
public bool CommentSent { get; private set; } = false;
|
||||||
|
// (question, answer) pairs extracted from the post body for FAQPage JSON-LD
|
||||||
|
public List<(string Q, string A)> Faqs { get; private set; } = new();
|
||||||
|
|
||||||
// Comment form binding
|
// Comment form binding
|
||||||
[BindProperty]
|
[BindProperty]
|
||||||
@@ -45,11 +47,35 @@ public class PostModel : PageModel
|
|||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
Post = post;
|
Post = post;
|
||||||
|
Faqs = ExtractFaqs(post.Content);
|
||||||
await LoadCommentsAsync(post.Id);
|
await LoadCommentsAsync(post.Id);
|
||||||
await SetViewDataAsync(post);
|
await SetViewDataAsync(post);
|
||||||
return Page();
|
return Page();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pull FAQ pairs from the body: an <h3> whose text ends with the Persian
|
||||||
|
// question mark (؟) followed by the next <p>. Drives FAQPage rich results.
|
||||||
|
private static List<(string, string)> ExtractFaqs(string html)
|
||||||
|
{
|
||||||
|
var list = new List<(string, string)>();
|
||||||
|
if (string.IsNullOrEmpty(html)) return list;
|
||||||
|
var rx = new System.Text.RegularExpressions.Regex(
|
||||||
|
@"<h3[^>]*>(?<q>.*?)</h3>\s*<p[^>]*>(?<a>.*?)</p>",
|
||||||
|
System.Text.RegularExpressions.RegexOptions.Singleline |
|
||||||
|
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||||
|
foreach (System.Text.RegularExpressions.Match m in rx.Matches(html))
|
||||||
|
{
|
||||||
|
var q = Strip(m.Groups["q"].Value);
|
||||||
|
var a = Strip(m.Groups["a"].Value);
|
||||||
|
if (q.EndsWith("؟") && a.Length > 0) list.Add((q, a));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Strip(string s) =>
|
||||||
|
System.Net.WebUtility.HtmlDecode(
|
||||||
|
System.Text.RegularExpressions.Regex.Replace(s, "<[^>]*>", "")).Trim();
|
||||||
|
|
||||||
public async Task<IActionResult> OnPostAsync(string slug)
|
public async Task<IActionResult> OnPostAsync(string slug)
|
||||||
{
|
{
|
||||||
var post = await _db.BlogPosts
|
var post = await _db.BlogPosts
|
||||||
|
|||||||
Reference in New Issue
Block a user