diff --git a/scraper/internal/server/ui.go b/scraper/internal/server/ui.go
index 47aa398..57b6852 100644
--- a/scraper/internal/server/ui.go
+++ b/scraper/internal/server/ui.go
@@ -1896,7 +1896,7 @@ const chapterTmpl = `
hx-swap="innerHTML"
title="Back to chapter list"
aria-label="Back to chapter list"
- class="flex-shrink-0 px-2 py-1.5 rounded-lg text-zinc-400 hover:text-amber-400 transition-colors text-base leading-none no-underline">
+ class="flex-shrink-0 px-2 py-1.5 rounded-lg text-zinc-400 hover:text-amber-400 transition-colors text-xl leading-none no-underline">
☰
@@ -1909,7 +1909,6 @@ const chapterTmpl = `
title="Jump to chapter"
class="flex-1 min-w-0 flex items-center justify-center gap-1.5 overflow-hidden bg-transparent border-none cursor-pointer py-1 px-1">
{{.Title}}
- ▼
@@ -2801,8 +2800,12 @@ func (s *Server) handleChapter(w http.ResponseWriter, r *http.Request) {
return
}
+ // Strip the first heading line so it isn't rendered as a duplicate
+ // inside the article (the template already renders an explicit ).
+ rawForHTML := stripFirstHeadingLine(raw)
+
var htmlBuf bytes.Buffer
- if err := md.Convert([]byte(raw), &htmlBuf); err != nil {
+ if err := md.Convert([]byte(rawForHTML), &htmlBuf); err != nil {
http.Error(w, "markdown render error: "+err.Error(), http.StatusInternalServerError)
return
}
@@ -2898,6 +2901,28 @@ func adjacentChapters(chapters []writer.ChapterInfo, n int) (prev, next int) {
return
}
+// stripFirstHeadingLine removes the first non-empty line if it is a markdown
+// heading (starts with one or more "#"). This prevents the heading from being
+// rendered as a duplicate inside the article when the template already
+// renders an explicit title above the article.
+func stripFirstHeadingLine(src string) string {
+ lines := strings.SplitN(src, "\n", -1)
+ for i, line := range lines {
+ trimmed := strings.TrimSpace(line)
+ if trimmed == "" {
+ continue
+ }
+ if strings.HasPrefix(trimmed, "#") {
+ // Remove this line and return the rest.
+ rest := strings.Join(append(lines[:i], lines[i+1:]...), "\n")
+ return strings.TrimLeft(rest, "\n")
+ }
+ // First non-empty line is not a heading — nothing to strip.
+ break
+ }
+ return src
+}
+
// firstHeading returns the text of the first non-empty line, stripping a
// leading "# " markdown heading marker. Falls back to fallback.
func firstHeading(md, fallback string) string {
diff --git a/scraper/scraper b/scraper/scraper
new file mode 100755
index 0000000..9e8e1a6
Binary files /dev/null and b/scraper/scraper differ