Skip to content

Fix post previews rendering raw markdown#474

Merged
compscidr merged 1 commit intomainfrom
fix/markdown-preview-rendering
Mar 3, 2026
Merged

Fix post previews rendering raw markdown#474
compscidr merged 1 commit intomainfrom
fix/markdown-preview-rendering

Conversation

@compscidr
Copy link
Collaborator

Summary

  • Post previews were displaying raw markdown syntax ([text](url), fenced code blocks, **bold**, etc.) because PreviewContent() simply truncated the raw content string
  • Add PlainTextPreview() that strips markdown to clean text (used for <meta description> tag)
  • Add HTMLPreview() that preserves links as clickable <a> tags and renders inline/fenced code as <code> elements, with visible-character-aware truncation so HTML tags don't consume preview length

Test plan

  • go test ./... passes
  • Footer "Most Recent Post" shows clean preview with clickable links and styled code
  • /posts page shows clean previews
  • /tag/:name pages show clean previews
  • Full post pages still render markdown normally via Showdown.js
  • Meta description in page source contains plain text (no HTML)

🤖 Generated with Claude Code

Post previews (footer, /posts, /tag pages) were displaying raw markdown
syntax like [text](url), ```code blocks```, **bold**, etc. because
PreviewContent() simply truncated the raw markdown string.

Add PlainTextPreview() that strips markdown to clean text (used for meta
description), and HTMLPreview() that preserves links as clickable <a>
tags and renders code as inline <code> elements. HTMLPreview truncates
by visible character count so HTML tags don't eat into preview length.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 3, 2026 05:20
@codecov
Copy link

codecov bot commented Mar 3, 2026

Codecov Report

❌ Patch coverage is 67.27273% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 43.23%. Comparing base (c5bb0b6) to head (a72df25).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
blog/post.go 67.27% 12 Missing and 6 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #474      +/-   ##
==========================================
+ Coverage   41.92%   43.23%   +1.30%     
==========================================
  Files           6        6              
  Lines        1016     1071      +55     
==========================================
+ Hits          426      463      +37     
- Misses        561      573      +12     
- Partials       29       35       +6     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@compscidr compscidr merged commit f0720e6 into main Mar 3, 2026
8 of 9 checks passed
@compscidr compscidr deleted the fix/markdown-preview-rendering branch March 3, 2026 05:24
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates how post previews are generated so list pages and meta descriptions no longer show raw markdown syntax, introducing dedicated preview helpers on Post.

Changes:

  • Add Post.PlainTextPreview() to strip markdown into plain text (used for <meta name="description">).
  • Add Post.HTMLPreview() to render a limited subset of markdown (links/code) with visible-character-aware truncation.
  • Update post listing templates (posts/tag/footer) to use HTMLPreview instead of PreviewContent.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
templates/tag.html Switch tag page previews to HTMLPreview.
templates/posts.html Switch /posts previews to HTMLPreview.
templates/header.html Use PlainTextPreview for meta description.
templates/footer.html Switch “Most Recent Post” preview to HTMLPreview (and adjust length).
blog/post.go Add markdown-stripping preview helpers and HTML-aware truncation logic.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +80 to +86
func (p Post) HTMLPreview(length int) template.HTML {
s := p.Content
s = reFencedCode.ReplaceAllString(s, "<code>$1</code>")
s = reInlineCode.ReplaceAllString(s, "<code>$1</code>")
s = reImages.ReplaceAllString(s, "$1")
s = reLinksWithURL.ReplaceAllString(s, `<a href="$2">$1</a>`)
s = reHeadings.ReplaceAllString(s, "")
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTMLPreview returns template.HTML built from p.Content via regex replacements without HTML-escaping/sanitization. This disables Go template auto-escaping and allows raw HTML in post content (or crafted link text/URLs) to be rendered/executed in list pages; href is also inserted unescaped and could allow attribute injection or javascript: URLs. Prefer escaping all user content first and only allowing a very small, sanitized subset of tags/attributes (e.g., escape code/link text, validate/escape href, and/or run the output through an allowlist HTML sanitizer before casting to template.HTML).

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +91
s = reFencedCode.ReplaceAllString(s, "<code>$1</code>")
s = reInlineCode.ReplaceAllString(s, "<code>$1</code>")
s = reImages.ReplaceAllString(s, "$1")
s = reLinksWithURL.ReplaceAllString(s, `<a href="$2">$1</a>`)
s = reHeadings.ReplaceAllString(s, "")
s = reBold.ReplaceAllString(s, "$1")
s = reItalic.ReplaceAllString(s, "$1")
s = reUBold.ReplaceAllString(s, "$1")
s = reUItalic.ReplaceAllString(s, "$1")
s = reHR.ReplaceAllString(s, "")
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The markdown-stripping pipeline wraps fenced/inline code in <code>...</code> and then runs bold/italic underscore regexes afterwards. That will also transform characters inside code blocks (e.g., *, _, **) and can corrupt the rendered code preview. Consider extracting code spans/blocks to placeholders before running emphasis/heading/whitespace cleanup, then reinserting them at the end (or otherwise ensuring later regexes do not run inside code).

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +76
runes := bytes.Runes([]byte(s))
if len(runes) > length {
return string(runes[:length]) + "..."
}
return s
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PlainTextPreview appends "..." after truncating to length runes, so the returned string can exceed the requested length (e.g., length+3). For meta description usage, consider truncating to length-3 when adding an ellipsis (or make ellipsis optional) so callers can enforce a strict maximum length.

Suggested change
runes := bytes.Runes([]byte(s))
if len(runes) > length {
return string(runes[:length]) + "..."
}
return s
// Convert to runes so length is based on Unicode code points.
runes := bytes.Runes([]byte(s))
if length <= 0 {
return ""
}
if len(runes) > length {
// Ensure the returned string never exceeds the requested length.
// For very small lengths, just truncate without adding an ellipsis.
if length <= 3 {
return string(runes[:length])
}
return string(runes[:length-3]) + "..."
}
return string(runes)

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +126
// PlainTextPreview strips markdown syntax and returns clean plain text for previews
func (p Post) PlainTextPreview(length int) string {
s := p.Content
s = reFencedCode.ReplaceAllString(s, "$1")
s = reInlineCode.ReplaceAllString(s, "$1")
s = reImages.ReplaceAllString(s, "$1")
s = reLinks.ReplaceAllString(s, "$1")
s = reHeadings.ReplaceAllString(s, "")
s = reBold.ReplaceAllString(s, "$1")
s = reItalic.ReplaceAllString(s, "$1")
s = reUBold.ReplaceAllString(s, "$1")
s = reUItalic.ReplaceAllString(s, "$1")
s = reHR.ReplaceAllString(s, "")
s = reWhitespace.ReplaceAllString(s, " ")
s = strings.TrimSpace(s)

runes := bytes.Runes([]byte(s))
if len(runes) > length {
return string(runes[:length]) + "..."
}
return s
}

// HTMLPreview strips markdown syntax but preserves links as HTML hyperlinks
func (p Post) HTMLPreview(length int) template.HTML {
s := p.Content
s = reFencedCode.ReplaceAllString(s, "<code>$1</code>")
s = reInlineCode.ReplaceAllString(s, "<code>$1</code>")
s = reImages.ReplaceAllString(s, "$1")
s = reLinksWithURL.ReplaceAllString(s, `<a href="$2">$1</a>`)
s = reHeadings.ReplaceAllString(s, "")
s = reBold.ReplaceAllString(s, "$1")
s = reItalic.ReplaceAllString(s, "$1")
s = reUBold.ReplaceAllString(s, "$1")
s = reUItalic.ReplaceAllString(s, "$1")
s = reHR.ReplaceAllString(s, "")
s = reWhitespace.ReplaceAllString(s, " ")
s = strings.TrimSpace(s)

// Truncate by visible character count, skipping HTML tags
runes := []rune(s)
visible := 0
cutoff := len(runes)
inTag := false
for i, r := range runes {
if r == '<' {
inTag = true
} else if r == '>' {
inTag = false
} else if !inTag {
visible++
if visible > length {
cutoff = i
break
}
}
}

if cutoff < len(runes) {
result := string(runes[:cutoff])
if strings.Count(result, "<a ") > strings.Count(result, "</a>") {
result += "</a>"
}
if strings.Count(result, "<code>") > strings.Count(result, "</code>") {
result += "</code>"
}
result += "..."
return template.HTML(result)
}
return template.HTML(s)
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New preview behavior is introduced via PlainTextPreview/HTMLPreview, but existing unit tests only cover PreviewContent. Since there is already test coverage for this file, please add tests for markdown stripping/link rendering/truncation (including edge cases like code spans containing */_ and potentially unsafe URLs/HTML) to prevent regressions.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants