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