Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions blog/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package blog

import (
"bytes"
"html/template"
"regexp"
"strings"
"time"
)

Expand Down Expand Up @@ -36,6 +38,93 @@ func (p Post) PreviewContent(length int) string {
return string(runes)
}

var (
reFencedCode = regexp.MustCompile("(?s)```[^\n]*\n(.*?)```")
reInlineCode = regexp.MustCompile("`([^`]+)`")
reImages = regexp.MustCompile(`!\[([^\]]*)\]\([^)]+\)`)
reLinks = regexp.MustCompile(`\[([^\]]+)\]\([^)]+\)`)
reLinksWithURL = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
reHeadings = regexp.MustCompile(`(?m)^#{1,6}\s+`)
reBold = regexp.MustCompile(`\*\*(.+?)\*\*`)
reItalic = regexp.MustCompile(`\*(.+?)\*`)
reUBold = regexp.MustCompile(`__(.+?)__`)
reUItalic = regexp.MustCompile(`_(.+?)_`)
reHR = regexp.MustCompile(`(?m)^[-*_]{3,}\s*$`)
reWhitespace = regexp.MustCompile(`\s+`)
)

// 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
Comment on lines +72 to +76
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.
}

// 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, "")
Comment on lines +80 to +86
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.
s = reBold.ReplaceAllString(s, "$1")
s = reItalic.ReplaceAllString(s, "$1")
s = reUBold.ReplaceAllString(s, "$1")
s = reUItalic.ReplaceAllString(s, "$1")
s = reHR.ReplaceAllString(s, "")
Comment on lines +82 to +91
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.
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)
}
Comment on lines +56 to +126
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.

// Permalink returns the link to the post relative to root
func (p Post) Permalink() string {
return p.CreatedAt.Format("/posts/2006/01/02/") + p.Slug
Expand Down
2 changes: 1 addition & 1 deletion templates/footer.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ <h3 class="text-left"><a href="{{ .recent.Permalink }}" title="{{ .recent.Title
{{ end }}
</p>
<p class="text-justify">
{{ .recent.PreviewContent 600 }} ...
{{ .recent.HTMLPreview 300 }}
</p>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion templates/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<meta name="robots" content="index, follow" />
<meta property="og:site_name" content="{{ .settings.site_title.Value }}">
{{ if .post }}
<meta name="description" content="{{ .post.PreviewContent 155 }}">
<meta name="description" content="{{ .post.PlainTextPreview 155 }}">
<meta property="og:title" content="{{ .settings.site_title.Value }}: {{ .post.Title }}">
<meta property="og:type" content="article">
<meta property="og:url" content="https://www.jasonernst.com{{ .post.Permalink }}">
Expand Down
2 changes: 1 addition & 1 deletion templates/posts.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ <h3 class="text-left"><a href="{{ .Permalink }}" title="{{ .Title }}">{{ .Title
{{ end }}
</p>
<p class="text-justify">
{{ .PreviewContent 200 }} ...
{{ .HTMLPreview 200 }}
</p>

</div>
Expand Down
2 changes: 1 addition & 1 deletion templates/tag.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ <h1 class="text-left">Posts with Tag: {{.tag}}</h1>
<h2 class="text-left"><a href="{{ .Permalink }}">{{ .Title }}</a></h2>
<p class="blog-post-meta text-left">{{ .CreatedAt.Format "Jan 02, 2006 15:04:05 UTC" }}</p>
<p class="text-left">
{{ .PreviewContent 100 }} ...
{{ .HTMLPreview 100 }}
</p>

</div>
Expand Down
Loading