Use more writers and streaming when possible

This commit is contained in:
Jan-Lukas Else 2022-02-11 16:19:10 +01:00
parent 5771a945cf
commit 68daab64e3
15 changed files with 130 additions and 171 deletions

View File

@ -113,7 +113,7 @@ func (a *goBlog) toASNote(p *post) *asNote {
as.Type = "Note"
}
// Content
as.Content = string(a.postHtml(p, true))
as.Content = a.postHtml(p, true)
// Attachments
if images := p.Parameters[a.cfg.Micropub.PhotoParam]; len(images) > 0 {
for _, image := range images {

View File

@ -12,6 +12,7 @@ import (
"sync/atomic"
"time"
"go.goblog.app/app/pkgs/bufferpool"
"golang.org/x/sync/singleflight"
)
@ -126,7 +127,10 @@ func (a *goBlog) checkLinks(w io.Writer, posts ...*post) error {
func (a *goBlog) allLinks(posts ...*post) (allLinks []*stringPair, err error) {
for _, p := range posts {
links, err := allLinksFromHTMLString(string(a.postHtml(p, true)), a.fullPostURL(p))
contentBuf := bufferpool.Get()
a.postHtmlToWriter(contentBuf, p, true)
links, err := allLinksFromHTML(contentBuf, a.fullPostURL(p))
bufferpool.Put(contentBuf)
if err != nil {
return nil, err
}

View File

@ -45,7 +45,7 @@ func (a *goBlog) serveEditorPreview(w http.ResponseWriter, r *http.Request) {
continue
}
// Create preview
preview, err := a.createMarkdownPreview(blog, message)
preview, err := a.createMarkdownPreview(blog, string(message))
if err != nil {
preview = []byte(err.Error())
}
@ -57,12 +57,10 @@ func (a *goBlog) serveEditorPreview(w http.ResponseWriter, r *http.Request) {
}
}
func (a *goBlog) createMarkdownPreview(blog string, markdown []byte) (rendered []byte, err error) {
func (a *goBlog) createMarkdownPreview(blog string, markdown string) (rendered []byte, err error) {
p := &post{
Content: string(markdown),
Blog: blog,
Path: "/editor/preview",
Published: localNowString(),
Content: markdown,
}
err = a.computeExtraPostParameters(p)
if err != nil {

View File

@ -8,6 +8,7 @@ import (
"github.com/araddon/dateparse"
"github.com/jlelse/feeds"
"go.goblog.app/app/pkgs/bufferpool"
"go.goblog.app/app/pkgs/contenttype"
)
@ -38,16 +39,18 @@ func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r
},
}
for _, p := range posts {
content, _ := a.min.MinifyString(contenttype.HTML, a.feedHtml(p))
buf := bufferpool.Get()
a.feedHtml(buf, p)
feed.Add(&feeds.Item{
Title: p.RenderedTitle,
Link: &feeds.Link{Href: a.fullPostURL(p)},
Description: a.postSummary(p),
Id: p.Path,
Content: content,
Content: buf.String(),
Created: timeNoErr(dateparse.ParseLocal(p.Published)),
Updated: timeNoErr(dateparse.ParseLocal(p.Updated)),
})
bufferpool.Put(buf)
}
var err error
var feedBuffer bytes.Buffer

View File

@ -2,7 +2,6 @@ package main
import (
"bytes"
"html/template"
"io"
marktag "git.jlel.se/jlelse/goldmark-mark"
@ -94,19 +93,6 @@ func (a *goBlog) renderMarkdownToWriter(w io.Writer, source string, absoluteLink
return err
}
func (a *goBlog) renderMarkdownAsHTML(source string, absoluteLinks bool) (rendered template.HTML, err error) {
b, err := a.renderMarkdown(source, absoluteLinks)
if err != nil {
return "", err
}
return template.HTML(b), nil
}
func (a *goBlog) safeRenderMarkdownAsHTML(source string) template.HTML {
h, _ := a.renderMarkdownAsHTML(source, false)
return h
}
func (a *goBlog) renderText(s string) string {
if s == "" {
return ""

View File

@ -79,11 +79,6 @@ func Test_markdown(t *testing.T) {
assert.Equal(t, "Tests", app.renderMdTitle("Test's"))
assert.Equal(t, "😂", app.renderMdTitle(":joy:"))
assert.Equal(t, "<b></b>", app.renderMdTitle("<b></b>"))
// Template func
renderedText = string(app.safeRenderMarkdownAsHTML("[Relative](/relative)"))
assert.Contains(t, renderedText, `href="/relative"`)
})
}

View File

@ -10,6 +10,7 @@ import (
"regexp"
"strconv"
"strings"
"time"
"github.com/spf13/cast"
"github.com/thoas/go-funk"
@ -397,7 +398,7 @@ func (a *goBlog) computeExtraPostParameters(p *post) error {
}
if p.Published == "" && p.Section != "" {
// Has no published date, but section -> published now
p.Published = localNowString()
p.Published = time.Now().Local().Format(time.RFC3339)
}
// Add images not in content
images := p.Parameters[a.cfg.Micropub.PhotoParam]

View File

@ -4,12 +4,10 @@ import (
"context"
"errors"
"fmt"
"html/template"
"net/http"
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/go-chi/chi/v5"
@ -31,8 +29,6 @@ type post struct {
// Not persisted
Slug string
RenderedTitle string
renderCache map[bool]template.HTML
renderMutex sync.RWMutex
}
type postStatus string

View File

@ -2,13 +2,13 @@ package main
import (
"fmt"
"html/template"
"log"
"io"
"strings"
"time"
gogeouri "git.jlel.se/jlelse/go-geouri"
"github.com/araddon/dateparse"
"go.goblog.app/app/pkgs/bufferpool"
"gopkg.in/yaml.v3"
)
@ -34,69 +34,56 @@ func (p *post) firstParameter(parameter string) (result string) {
return
}
func (a *goBlog) postHtml(p *post, absolute bool) template.HTML {
p.renderMutex.RLock()
// Check cache
if r, ok := p.renderCache[absolute]; ok && r != "" {
p.renderMutex.RUnlock()
return r
}
p.renderMutex.RUnlock()
// No cache, build it...
p.renderMutex.Lock()
defer p.renderMutex.Unlock()
// Build HTML
var htmlBuilder strings.Builder
// Add audio to the top
for _, a := range p.Parameters[a.cfg.Micropub.AudioParam] {
htmlBuilder.WriteString(`<audio controls preload=none><source src="`)
htmlBuilder.WriteString(a)
htmlBuilder.WriteString(`"/></audio>`)
}
// Render markdown
htmlContent, err := a.renderMarkdown(p.Content, absolute)
if err != nil {
log.Fatal(err)
return ""
}
htmlBuilder.Write(htmlContent)
// Add bookmark links to the bottom
for _, l := range p.Parameters[a.cfg.Micropub.BookmarkParam] {
htmlBuilder.WriteString(`<p><a class=u-bookmark-of href="`)
htmlBuilder.WriteString(l)
htmlBuilder.WriteString(`" target=_blank rel=noopener>`)
htmlBuilder.WriteString(l)
htmlBuilder.WriteString(`</a></p>`)
}
// Cache
html := template.HTML(htmlBuilder.String())
if p.renderCache == nil {
p.renderCache = map[bool]template.HTML{}
}
p.renderCache[absolute] = html
return html
func (a *goBlog) postHtml(p *post, absolute bool) (res string) {
buf := bufferpool.Get()
a.postHtmlToWriter(buf, p, absolute)
res = buf.String()
bufferpool.Put(buf)
return
}
func (a *goBlog) feedHtml(p *post) string {
var htmlBuilder strings.Builder
func (a *goBlog) postHtmlToWriter(w io.Writer, p *post, absolute bool) {
// Build HTML
hb := newHtmlBuilder(w)
// Add audio to the top
for _, a := range p.Parameters[a.cfg.Micropub.AudioParam] {
hb.writeElementOpen("audio", "controls", "preload", "none")
hb.writeElementOpen("source", "src", a)
hb.writeElementClose("source")
hb.writeElementClose("audio")
}
// Render markdown
_ = a.renderMarkdownToWriter(w, p.Content, absolute)
// Add bookmark links to the bottom
for _, l := range p.Parameters[a.cfg.Micropub.BookmarkParam] {
hb.writeElementOpen("p")
hb.writeElementOpen("a", "class", "u-bookmark-of", "href", l, "target", "_blank", "rel", "noopener noreferrer")
hb.writeEscaped(l)
hb.writeElementClose("a")
hb.writeElementClose("p")
}
}
func (a *goBlog) feedHtml(w io.Writer, p *post) {
hb := newHtmlBuilder(w)
// Add TTS audio to the top
for _, a := range p.Parameters[ttsParameter] {
htmlBuilder.WriteString(`<audio controls preload=none><source src="`)
htmlBuilder.WriteString(a)
htmlBuilder.WriteString(`"/></audio>`)
hb.writeElementOpen("audio", "controls", "preload", "none")
hb.writeElementOpen("source", "src", a)
hb.writeElementClose("source")
hb.writeElementClose("audio")
}
// Add post HTML
htmlBuilder.WriteString(string(a.postHtml(p, true)))
a.postHtmlToWriter(hb, p, true)
// Add link to interactions and comments
blogConfig := a.cfg.Blogs[defaultIfEmpty(p.Blog, a.cfg.DefaultBlog)]
if cc := blogConfig.Comments; cc != nil && cc.Enabled {
htmlBuilder.WriteString(`<p><a href="`)
htmlBuilder.WriteString(a.getFullAddress(p.Path))
htmlBuilder.WriteString(`#interactions">`)
htmlBuilder.WriteString(a.ts.GetTemplateStringVariant(blogConfig.Lang, "interactions"))
htmlBuilder.WriteString(`</a></p>`)
hb.writeElementOpen("p")
hb.writeElementOpen("a", "href", a.getFullAddress(p.Path)+"#interactions")
hb.writeEscaped(a.ts.GetTemplateStringVariant(blogConfig.Lang, "interactions"))
hb.writeElementClose("a")
hb.writeElementClose("p")
}
return htmlBuilder.String()
}
const summaryDivider = "<!--more-->"
@ -106,11 +93,12 @@ func (a *goBlog) postSummary(p *post) (summary string) {
if summary != "" {
return
}
html := string(a.postHtml(p, false))
if splitted := strings.Split(html, summaryDivider); len(splitted) > 1 {
summary = htmlText(splitted[0])
if splitted := strings.Split(p.Content, summaryDivider); len(splitted) > 1 {
rendered, _ := a.renderMarkdown(splitted[0], false)
summary = htmlText(string(rendered))
} else {
summary = strings.Split(htmlText(html), "\n\n")[0]
rendered, _ := a.renderMarkdown(splitted[0], false)
summary = strings.Split(htmlText(string(rendered)), "\n\n")[0]
}
return
}

2
tts.go
View File

@ -65,7 +65,7 @@ func (a *goBlog) createPostTTSAudio(p *post) error {
parts = append(parts, a.renderMdTitle(title))
}
// Add body split into paragraphs because of 5000 character limit
parts = append(parts, strings.Split(htmlText(string(a.postHtml(p, false))), "\n\n")...)
parts = append(parts, strings.Split(htmlText(a.postHtml(p, false)), "\n\n")...)
// Create TTS audio for each part
partsBuffers := make([]io.Reader, len(parts))

64
ui.go
View File

@ -2,7 +2,6 @@ package main
import (
"fmt"
"strings"
"time"
"github.com/hacdias/indieauth"
@ -20,7 +19,7 @@ func (a *goBlog) renderEditorPreview(hb *htmlBuilder, bc *configBlog, p *post) {
a.renderPostMeta(hb, p, bc, "preview")
if p.Content != "" {
hb.writeElementOpen("div")
hb.writeHtml(a.postHtml(p, true))
a.postHtmlToWriter(hb, p, true)
hb.writeElementClose("div")
}
a.renderPostTax(hb, p, bc)
@ -859,14 +858,11 @@ func (a *goBlog) renderPost(hb *htmlBuilder, rd *renderData) {
if !ok {
return
}
postHtml := a.postHtml(p, false)
a.renderBase(
hb, rd,
func(hb *htmlBuilder) {
a.renderTitleTag(hb, rd.Blog, p.RenderedTitle)
if strings.Contains(string(postHtml), "c-chroma") {
hb.writeElementOpen("link", "rel", "stylesheet", "href", a.assetFileName("css/chroma.css"))
}
a.renderPostHeadMeta(hb, p, rd.Canonical)
if su := a.shortPostURL(p); su != "" {
hb.writeElementOpen("link", "rel", "shortlink", "href", su)
@ -924,7 +920,7 @@ func (a *goBlog) renderPost(hb *htmlBuilder, rd *renderData) {
if p.Content != "" {
// Content
hb.writeElementOpen("div", "class", "e-content")
hb.writeHtml(postHtml)
a.postHtmlToWriter(hb, p, false)
hb.writeElementClose("div")
}
// GPS Track
@ -1027,7 +1023,7 @@ func (a *goBlog) renderStaticHome(hb *htmlBuilder, rd *renderData) {
if p.Content != "" {
// Content
hb.writeElementOpen("div", "class", "e-content")
hb.writeHtml(a.postHtml(p, false))
a.postHtmlToWriter(hb, p, false)
hb.writeElementClose("div")
}
// Author
@ -1142,7 +1138,7 @@ func (a *goBlog) renderEditorFiles(hb *htmlBuilder, rd *renderData) {
usesString := a.ts.GetTemplateStringVariant(rd.Blog.Lang, "fileuses")
for i, f := range ef.files {
hb.writeElementOpen("option", "value", f.Name)
hb.writeEscaped(fmt.Sprintf("%s (%s), %s, ~%d %s", f.Name, isoDateFormat(f.Time.String()), mBytesString(f.Size), ef.uses[i], usesString))
hb.writeEscaped(fmt.Sprintf("%s (%s), %s, ~%d %s", f.Name, f.Time.Local().Format(isoDateFormat), mBytesString(f.Size), ef.uses[i], usesString))
hb.writeElementClose("option")
}
hb.writeElementClose("select")
@ -1305,6 +1301,7 @@ func (a *goBlog) renderWebmentionAdmin(hb *htmlBuilder, rd *renderData) {
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "webmentions"))
hb.writeElementClose("h1")
// Notifications
tdLocale := matchTimeDiffLocale(rd.Blog.Lang)
for _, m := range wrd.mentions {
hb.writeElementOpen("div", "id", fmt.Sprintf("mention-%d", m.ID), "class", "p")
hb.writeElementOpen("p")
@ -1330,7 +1327,7 @@ func (a *goBlog) renderWebmentionAdmin(hb *htmlBuilder, rd *renderData) {
hb.writeElementOpen("br")
// Date
hb.writeEscaped("Created: ")
hb.writeEscaped(unixToLocalDateString(m.Created))
hb.writeEscaped(timediff.TimeDiff(time.Unix(m.Created, 0), timediff.WithLocale(tdLocale)))
hb.writeElementOpen("br")
hb.writeElementOpen("br")
// Author
@ -1447,36 +1444,24 @@ func (a *goBlog) renderEditor(hb *htmlBuilder, rd *renderData) {
hb.writeElementOpen("h2")
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "posts"))
hb.writeElementClose("h2")
// Template
postsListLink := func(path, title string) {
hb.writeElementOpen("p")
hb.writeElementOpen("a", "href", rd.Blog.getRelativePath(path))
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, title))
hb.writeElementClose("a")
hb.writeElementClose("p")
}
// Drafts
hb.writeElementOpen("p")
hb.writeElementOpen("a", "href", rd.Blog.getRelativePath("/editor/drafts"))
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "drafts"))
hb.writeElementClose("a")
hb.writeElementClose("p")
postsListLink("/editor/drafts", "drafts")
// Private
hb.writeElementOpen("p")
hb.writeElementOpen("a", "href", rd.Blog.getRelativePath("/editor/private"))
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "privateposts"))
hb.writeElementClose("a")
hb.writeElementClose("p")
postsListLink("/editor/private", "privateposts")
// Unlisted
hb.writeElementOpen("p")
hb.writeElementOpen("a", "href", rd.Blog.getRelativePath("/editor/unlisted"))
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "unlistedposts"))
hb.writeElementClose("a")
hb.writeElementClose("p")
postsListLink("/editor/unlisted", "unlistedposts")
// Scheduled
hb.writeElementOpen("p")
hb.writeElementOpen("a", "href", rd.Blog.getRelativePath("/editor/scheduled"))
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "scheduledposts"))
hb.writeElementClose("a")
hb.writeElementClose("p")
postsListLink("/editor/scheduled", "scheduledposts")
// Deleted
hb.writeElementOpen("p")
hb.writeElementOpen("a", "href", rd.Blog.getRelativePath("/editor/deleted"))
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "deletedposts"))
hb.writeElementClose("a")
hb.writeElementClose("p")
postsListLink("/editor/deleted", "deletedposts")
// Upload
hb.writeElementOpen("h2")
@ -1524,15 +1509,10 @@ func (a *goBlog) renderEditor(hb *htmlBuilder, rd *renderData) {
hb.writeElementClose("main")
// Scripts
// Editor preview
hb.writeElementOpen("script", "src", a.assetFileName("js/mdpreview.js"), "defer", "")
hb.writeElementClose("script")
// Geohelper
hb.writeElementOpen("script", "src", a.assetFileName("js/geohelper.js"), "defer", "")
hb.writeElementClose("script")
// Formcache
hb.writeElementOpen("script", "src", a.assetFileName("js/formcache.js"), "defer", "")
for _, script := range []string{"js/mdpreview.js", "js/geohelper.js", "js/formcache.js"} {
hb.writeElementOpen("script", "src", a.assetFileName(script), "defer", "")
hb.writeElementClose("script")
}
},
)
}

View File

@ -1,6 +1,9 @@
package main
import "fmt"
import (
"fmt"
"time"
)
type summaryTyp string
@ -46,7 +49,7 @@ func (a *goBlog) renderSummary(hb *htmlBuilder, bc *configBlog, p *post, typ sum
if typ != photoSummary && a.showFull(p) {
// Show full content
hb.writeElementOpen("div", "class", "e-content")
hb.write(string(a.postHtml(p, false)))
a.postHtmlToWriter(hb, p, false)
hb.writeElementClose("div")
} else {
// Show summary
@ -114,12 +117,12 @@ func (a *goBlog) renderPostMeta(hb *htmlBuilder, p *post, b *configBlog, typ str
hb.writeElementOpen("div", "class", "p")
}
// Published time
if published := p.Published; published != "" {
if published := toLocalTime(p.Published); !published.IsZero() {
hb.writeElementOpen("div")
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "publishedon"))
hb.write(" ")
hb.writeElementOpen("time", "class", "dt-published", "datetime", dateFormat(published, "2006-01-02T15:04:05Z07:00"))
hb.writeEscaped(isoDateFormat(published))
hb.writeElementOpen("time", "class", "dt-published", "datetime", published.Format(time.RFC3339))
hb.writeEscaped(published.Format(isoDateFormat))
hb.writeElementClose("time")
// Section
if p.Section != "" {
@ -133,12 +136,12 @@ func (a *goBlog) renderPostMeta(hb *htmlBuilder, p *post, b *configBlog, typ str
hb.writeElementClose("div")
}
// Updated time
if updated := p.Updated; updated != "" {
if updated := toLocalTime(p.Updated); !updated.IsZero() {
hb.writeElementOpen("div")
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "updatedon"))
hb.write(" ")
hb.writeElementOpen("time", "class", "dt-updated", "datetime", dateFormat(updated, "2006-01-02T15:04:05Z07:00"))
hb.writeEscaped(isoDateFormat(updated))
hb.writeElementOpen("time", "class", "dt-updated", "datetime", updated.Format(time.RFC3339))
hb.writeEscaped(updated.Format(isoDateFormat))
hb.writeElementClose("time")
hb.writeElementClose("div")
}
@ -335,11 +338,11 @@ func (a *goBlog) renderPostHeadMeta(hb *htmlBuilder, p *post, canonical string)
hb.writeElementOpen("meta", "property", "og:description", "content", summary)
hb.writeElementOpen("meta", "property", "twitter:description", "content", summary)
}
if p.Published != "" {
hb.writeElementOpen("meta", "itemprop", "datePublished", "content", dateFormat(p.Published, "2006-01-02T15:04:05-07:00"))
if published := toLocalTime(p.Published); !published.IsZero() {
hb.writeElementOpen("meta", "itemprop", "datePublished", "content", published.Format(time.RFC3339))
}
if p.Updated != "" {
hb.writeElementOpen("meta", "itemprop", "dateModified", "content", dateFormat(p.Updated, "2006-01-02T15:04:05-07:00"))
if updated := toLocalTime(p.Updated); !updated.IsZero() {
hb.writeElementOpen("meta", "itemprop", "dateModified", "content", updated.Format(time.RFC3339))
}
for _, img := range a.photoLinks(p) {
hb.writeElementOpen("meta", "itemprop", "image", "content", img)

View File

@ -2,7 +2,6 @@ package main
import (
"fmt"
"html/template"
"io"
textTemplate "text/template"
)
@ -33,10 +32,6 @@ func (h *htmlBuilder) write(s string) {
_, _ = h.WriteString(s)
}
func (h *htmlBuilder) writeHtml(s template.HTML) {
h.write(string(s))
}
func (h *htmlBuilder) writeEscaped(s string) {
textTemplate.HTMLEscape(h, []byte(s))
}

View File

@ -14,6 +14,7 @@ import (
"path/filepath"
"sort"
"strings"
"sync"
"time"
"unicode"
@ -164,25 +165,18 @@ func toUTC(s string) (string, error) {
return d.UTC().Format(time.RFC3339), nil
}
func dateFormat(date string, format string) string {
func toLocalTime(date string) time.Time {
if date == "" {
return time.Time{}
}
d, err := dateparse.ParseLocal(date)
if err != nil {
return ""
return time.Time{}
}
return d.Local().Format(format)
return d.Local()
}
func isoDateFormat(date string) string {
return dateFormat(date, "2006-01-02")
}
func unixToLocalDateString(unix int64) string {
return time.Unix(unix, 0).Local().Format(time.RFC3339)
}
func localNowString() string {
return time.Now().Local().Format(time.RFC3339)
}
const isoDateFormat = "2006-01-02"
func utcNowString() string {
return time.Now().UTC().Format(time.RFC3339)
@ -366,7 +360,17 @@ func (valueOnlyContext) Err() error {
return nil
}
var timeDiffLocaleMap = map[string]tdl.Locale{}
var timeDiffLocaleMutex sync.RWMutex
func matchTimeDiffLocale(lang string) tdl.Locale {
timeDiffLocaleMutex.RLock()
if locale, ok := timeDiffLocaleMap[lang]; ok {
return locale
}
timeDiffLocaleMutex.RUnlock()
timeDiffLocaleMutex.Lock()
defer timeDiffLocaleMutex.Unlock()
supportedLangs := []string{"en", "de", "es", "hi", "pt", "ru", "zh-CN"}
supportedTags := []language.Tag{}
for _, lang := range supportedLangs {
@ -374,5 +378,7 @@ func matchTimeDiffLocale(lang string) tdl.Locale {
}
matcher := language.NewMatcher(supportedTags)
_, idx, _ := matcher.Match(language.Make(lang))
return tdl.Locale(supportedLangs[idx])
locale := tdl.Locale(supportedLangs[idx])
timeDiffLocaleMap[lang] = locale
return locale
}

View File

@ -14,6 +14,7 @@ import (
"github.com/carlmjohnson/requests"
"github.com/thoas/go-funk"
"github.com/tomnomnom/linkheader"
"go.goblog.app/app/pkgs/bufferpool"
)
const postParamWebmention = "webmention"
@ -32,7 +33,10 @@ func (a *goBlog) sendWebmentions(p *post) error {
return nil
}
links := []string{}
contentLinks, err := allLinksFromHTML(strings.NewReader(string(a.postHtml(p, false))), a.fullPostURL(p))
contentBuf := bufferpool.Get()
a.postHtmlToWriter(contentBuf, p, false)
contentLinks, err := allLinksFromHTML(contentBuf, a.fullPostURL(p))
bufferpool.Put(contentBuf)
if err != nil {
return err
}