feat(seo): FAQPage structured data on blog posts
CI/CD / CI · dotnet build (push) Successful in 16m5s
CI/CD / Deploy · drsousan (push) Successful in 29s

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:
soroush.asadi
2026-06-26 01:02:44 +03:30
parent f3701c5893
commit 1c9d8cdc1b
2 changed files with 43 additions and 0 deletions
+17
View File
@@ -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}
+26
View File
@@ -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