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>
|
||||
|
||||
@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>
|
||||
/* ─── 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}
|
||||
|
||||
@@ -16,6 +16,8 @@ public class PostModel : PageModel
|
||||
public BlogPost? Post { get; private set; }
|
||||
public List<CommentVm> Comments { get; private set; } = new();
|
||||
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
|
||||
[BindProperty]
|
||||
@@ -45,11 +47,35 @@ public class PostModel : PageModel
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
Post = post;
|
||||
Faqs = ExtractFaqs(post.Content);
|
||||
await LoadCommentsAsync(post.Id);
|
||||
await SetViewDataAsync(post);
|
||||
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)
|
||||
{
|
||||
var post = await _db.BlogPosts
|
||||
|
||||
Reference in New Issue
Block a user