Compare commits

...

6 Commits

Author SHA1 Message Date
Brian Picciano
c1c1bb2c4c Implement cache and logger middlewares for gemini 2023-01-23 22:31:19 +01:00
Brian Picciano
024f514886 Add BlogHTTPURL preprocess function 2023-01-23 21:44:10 +01:00
Brian Picciano
26dbc6691d Fix page navigation in posts/drafts manage pages 2023-01-23 21:33:45 +01:00
Brian Picciano
57719f29b9 Small fixes to index pages 2023-01-23 18:55:10 +01:00
Brian Picciano
c4520f2c84 Automatically bridge gemini links to a gateway on http site 2023-01-23 16:02:35 +01:00
Brian Picciano
aba69d4329 Small formatting fixes on gmi site 2023-01-23 15:41:41 +01:00
20 changed files with 293 additions and 66 deletions

View File

@ -43,6 +43,7 @@
export MEDIOCRE_BLOG_HTTP_PUBLIC_URL="$MEDIOCRE_BLOG_ML_PUBLIC_URL"
export MEDIOCRE_BLOG_HTTP_LISTEN_PROTO="tcp"
export MEDIOCRE_BLOG_HTTP_LISTEN_ADDR=":4000"
export MEDIOCRE_BLOG_HTTP_GEMINI_GATEWAY_URL="https://nightfall.city/x/"
# http auth
# (password is "bar". This should definitely be changed for prod.)

49
src/cache/cache.go vendored Normal file
View File

@ -0,0 +1,49 @@
// Package cache implements a simple LRU cache which can be completely purged
// whenever needed.
package cache
import (
lru "github.com/hashicorp/golang-lru"
)
// Cache describes an in-memory cache for arbitrary objects, which has a Purge
// method for clearing everything at once.
type Cache interface {
Get(key string) interface{}
Set(key string, value interface{})
Purge()
}
type cache struct {
cache *lru.Cache
}
// New instantiates and returns a new Cache which can hold up to the given
// number of entries.
func New(size int) Cache {
c, err := lru.New(size)
// instantiating the lru cache can't realistically fail
if err != nil {
panic(err)
}
return &cache{c}
}
func (c *cache) Get(key string) interface{} {
value, ok := c.cache.Get(key)
if !ok {
return nil
}
return value
}
func (c *cache) Set(key string, value interface{}) {
c.cache.Add(key, value)
}
func (c *cache) Purge() {
c.cache.Purge()
}

View File

@ -104,6 +104,12 @@ published_posts:
Edgy.
=> / Here's a link within the site
=> gemini://mediocregopher.com And here's a link to a gemini capsule
=> https://mediocregopher.com And here's a link to an https site
#### Side-note
Did you know that the terms "cyberspace" and "matrix" are attributable to a book from 1984 called _Neuromancer_?
@ -117,8 +123,6 @@ published_posts:
This has been a great post.
=> / Here's a link outa here!
- id: empty-markdown-test
title: Empty Markdown Test
description:

View File

@ -7,6 +7,7 @@ import (
"syscall"
"time"
"github.com/mediocregopher/blog.mediocregopher.com/srv/cache"
cfgpkg "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
"github.com/mediocregopher/blog.mediocregopher.com/srv/gmi"
"github.com/mediocregopher/blog.mediocregopher.com/srv/http"
@ -102,7 +103,10 @@ func main() {
postAssetStore := post.NewAssetStore(postSQLDB)
postDraftStore := post.NewDraftStore(postSQLDB)
cache := cache.New(5000)
httpParams.Logger = logger.WithNamespace("http")
httpParams.Cache = cache
httpParams.PowManager = powMgr
httpParams.PostStore = postStore
httpParams.PostAssetStore = postAssetStore
@ -124,6 +128,7 @@ func main() {
}()
gmiParams.Logger = logger.WithNamespace("gmi")
gmiParams.Cache = cache
gmiParams.PostStore = postStore
gmiParams.PostAssetStore = postAssetStore
gmiParams.HTTPPublicURL = httpParams.PublicURL

87
src/gmi/cache.go Normal file
View File

@ -0,0 +1,87 @@
package gmi
import (
"bytes"
"context"
"io"
"sync"
"git.sr.ht/~adnano/go-gemini"
"github.com/mediocregopher/blog.mediocregopher.com/srv/cache"
)
type cacheRW struct {
gemini.ResponseWriter
status gemini.Status
mediaType string
buf []byte
}
func (c *cacheRW) SetMediaType(mediaType string) {
c.mediaType = mediaType
c.ResponseWriter.SetMediaType(mediaType)
}
func (c *cacheRW) WriteHeader(status gemini.Status, meta string) {
c.status = status
c.ResponseWriter.WriteHeader(status, meta)
}
func (c *cacheRW) Write(b []byte) (int, error) {
c.buf = append(c.buf, b...)
return c.ResponseWriter.Write(b)
}
func cacheMiddleware(cache cache.Cache) func(h gemini.Handler) gemini.Handler {
type entry struct {
mediaType string
body []byte
}
pool := sync.Pool{
New: func() interface{} { return new(bytes.Reader) },
}
return func(h gemini.Handler) gemini.Handler {
return gemini.HandlerFunc(func(
ctx context.Context,
rw gemini.ResponseWriter,
r *gemini.Request,
) {
id := r.URL.String()
if value := cache.Get(id); value != nil {
entry := value.(entry)
if entry.mediaType != "" {
rw.SetMediaType(entry.mediaType)
}
reader := pool.Get().(*bytes.Reader)
defer pool.Put(reader)
reader.Reset(entry.body)
io.Copy(rw, reader)
return
}
cacheRW := &cacheRW{
ResponseWriter: rw,
status: gemini.StatusSuccess,
}
h.ServeGemini(ctx, cacheRW, r)
if cacheRW.status == gemini.StatusSuccess {
cache.Set(id, entry{
mediaType: cacheRW.mediaType,
body: cacheRW.buf,
})
}
})
}
}

View File

@ -5,8 +5,9 @@ import (
"errors"
"fmt"
"io"
"log"
"net/url"
"path"
"path/filepath"
"regexp"
"strings"
)
@ -25,7 +26,10 @@ var linkRegexp = regexp.MustCompile(`^=>\s+(\S+)\s*(.*?)\s*$`)
// GemtextToMarkdown reads a gemtext formatted body from the Reader and writes
// the markdown version of that body to the Writer.
func GemtextToMarkdown(dst io.Writer, src io.Reader) error {
//
// gmiGateway, if given, is used for all `gemini://` links. The `gemini://`
// prefix will be stripped, and replaced with the given URL.
func GemtextToMarkdown(dst io.Writer, src io.Reader, gmiGateway *url.URL) error {
bufSrc := bufio.NewReader(src)
@ -40,7 +44,20 @@ func GemtextToMarkdown(dst io.Writer, src io.Reader) error {
if match := linkRegexp.FindStringSubmatch(line); len(match) > 0 {
isImg := hasImgExt(match[1])
u, err := url.Parse(match[1])
if err != nil {
return fmt.Errorf("link to invalid url %q: %w", match[1], err)
}
if u.Scheme == "gemini" && gmiGateway != nil {
newU := *gmiGateway
newU.Path = filepath.Join(newU.Path, u.Host, u.Path)
newU.RawQuery = u.RawQuery
u = &newU
}
isImg := hasImgExt(u.Path)
descr := match[2]
@ -52,9 +69,7 @@ func GemtextToMarkdown(dst io.Writer, src io.Reader) error {
descr = "Link"
}
log.Printf("descr:%q", descr)
line = fmt.Sprintf("[%s](%s)\n", descr, match[1])
line = fmt.Sprintf("[%s](%s)\n", descr, u.String())
if isImg {
line = "!" + line

View File

@ -2,6 +2,7 @@ package gmi
import (
"bytes"
"net/url"
"strconv"
"testing"
@ -10,6 +11,8 @@ import (
func TestGemtextToMarkdown(t *testing.T) {
gmiGateway, _ := url.Parse("https://gateway.com/x/")
tests := []struct {
in, exp string
}{
@ -37,13 +40,25 @@ func TestGemtextToMarkdown(t *testing.T) {
in: "=> img.png description is here ",
exp: "![description is here](img.png)\n",
},
{
in: "=> gemini://somewhere.com/foo Somewhere",
exp: "[Somewhere](https://gateway.com/x/somewhere.com/foo)\n",
},
{
in: "=> gemini://somewhere.com:420/foo Somewhere",
exp: "[Somewhere](https://gateway.com/x/somewhere.com:420/foo)\n",
},
{
in: "=> gemini://somewhere.com:420/foo?bar=baz Somewhere",
exp: "[Somewhere](https://gateway.com/x/somewhere.com:420/foo?bar=baz)\n",
},
}
for i, test := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
got := new(bytes.Buffer)
err := GemtextToMarkdown(got, bytes.NewBufferString(test.in))
err := GemtextToMarkdown(got, bytes.NewBufferString(test.in), gmiGateway)
assert.NoError(t, err)
assert.Equal(t, test.exp, got.String())
})

View File

@ -13,6 +13,7 @@ import (
"git.sr.ht/~adnano/go-gemini"
"git.sr.ht/~adnano/go-gemini/certificate"
"github.com/mediocregopher/blog.mediocregopher.com/srv/cache"
"github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
@ -23,6 +24,7 @@ import (
// unless otherwise noted.
type Params struct {
Logger *mlog.Logger
Cache cache.Cache
PostStore post.Store
PostAssetStore post.AssetStore
@ -108,10 +110,11 @@ func New(params Params) (API, error) {
go func() {
ctx := mctx.WithAnnotator(context.Background(), &a.params)
err := a.srv.ListenAndServe(context.Background())
err := a.srv.ListenAndServe(ctx)
if err != nil && !errors.Is(err, context.Canceled) {
ctx := mctx.WithAnnotator(context.Background(), &a.params)
a.params.Logger.Fatal(ctx, "serving gemini server", err)
}
}()
@ -123,6 +126,26 @@ func (a *api) Shutdown(ctx context.Context) error {
return a.srv.Shutdown(ctx)
}
func (a *api) logReqMiddleware(h gemini.Handler) gemini.Handler {
type logCtxKey string
return gemini.HandlerFunc(func(
ctx context.Context,
rw gemini.ResponseWriter,
r *gemini.Request,
) {
ctx = mctx.Annotate(ctx,
logCtxKey("url"), r.URL.String(),
)
h.ServeGemini(ctx, rw, r)
a.params.Logger.Info(ctx, "handled gemini request")
})
}
func indexMiddleware(h gemini.Handler) gemini.Handler {
return gemini.HandlerFunc(func(
@ -212,8 +235,8 @@ func (a *api) handler() (gemini.Handler, error) {
h = mux
h = indexMiddleware(h)
// TODO logging
// TODO caching
h = a.logReqMiddleware(h)
h = cacheMiddleware(a.params.Cache)(h)
return h, nil
}

View File

@ -134,11 +134,12 @@ func (r renderer) Add(a, b int) int { return a + b }
func (a *api) tplHandler() (gemini.Handler, error) {
blogURL := func(path string, abs bool) string {
blogURL := func(base *url.URL, path string, abs bool) string {
// filepath.Join strips trailing slash, but we want to keep it
trailingSlash := strings.HasSuffix(path, "/")
path = filepath.Join("/", a.params.PublicURL.Path, path)
path = filepath.Join("/", base.Path, path)
if trailingSlash && path != "/" {
path += "/"
@ -148,27 +149,29 @@ func (a *api) tplHandler() (gemini.Handler, error) {
return path
}
u := *a.params.PublicURL
u := *base
u.Path = path
return u.String()
}
preprocessFuncs := post.PreprocessFunctions{
BlogURL: func(path string) string {
return blogURL(path, false)
return blogURL(a.params.PublicURL, path, false)
},
BlogHTTPURL: func(path string) string {
return blogURL(a.params.HTTPPublicURL, path, true)
},
AssetURL: func(id string) string {
path := filepath.Join("assets", id)
return blogURL(path, false)
return blogURL(a.params.PublicURL, path, false)
},
PostURL: func(id string) string {
path := filepath.Join("posts", id) + ".gmi"
return blogURL(path, false)
return blogURL(a.params.PublicURL, path, false)
},
StaticURL: func(path string) string {
httpPublicURL := *a.params.HTTPPublicURL
httpPublicURL.Path = filepath.Join(httpPublicURL.Path, "/static", path)
return httpPublicURL.String()
path = filepath.Join("static", path)
return blogURL(a.params.HTTPPublicURL, path, true)
},
Image: func(args ...string) (string, error) {
@ -181,7 +184,8 @@ func (a *api) tplHandler() (gemini.Handler, error) {
descr = args[1]
}
path := blogURL(filepath.Join("assets", id), false)
path := filepath.Join("assets", id)
path = blogURL(a.params.PublicURL, path, false)
return fmt.Sprintf("\n=> %s %s", path, descr), nil
},
@ -193,11 +197,11 @@ func (a *api) tplHandler() (gemini.Handler, error) {
allTpls.Funcs(template.FuncMap{
"BlogURLAbs": func(path string) string {
return blogURL(path, true)
return blogURL(a.params.PublicURL, path, true)
},
"PostURLAbs": func(id string) string {
path := filepath.Join("posts", id) + ".gmi"
return blogURL(path, true)
return blogURL(a.params.PublicURL, path, true)
},
})

View File

@ -1,7 +1,6 @@
# mediocregopher's lil web corner
This here's my little corner of the web, where I publish posts about projects
I'm working on and things that interest me (which you can follow, if you like).
This here's my little corner of the web, where I publish posts about projects I'm working on and things that interest me (which you can follow, if you like).
=> {{ BlogURL "posts/" }} Browse all posts
@ -22,7 +21,7 @@ Feel free to hmu on any of these if you'd like to get in touch.
* Mastodon: @mediocregopher@social.cryptic.io
* Email: mediocregopher@gmail.com
* Email: me@mediocregopher.com
## Dev
@ -48,8 +47,6 @@ Feel free to hmu on any of these if you'd like to get in touch.
=> https://news.cryptic.io/ Cryptic News aggregates interesting blogs.
--------------------------------------------------------------------------------
I'm not affiliated with these, but they're worth listing.
=> https://search.marginalia.nu/ Marginalia reminds me of the old internet.

View File

@ -23,7 +23,7 @@ much as the quality!
{{ if $getPostsRes.HasMore -}}
=> {{ BlogURL "posts" }}/?page={{ .Add $page 1 }} Next page
{{ end }}
--------------------------------------------------------------------------------
================================================================================
=> {{ BlogURL "feed.xml" }} Subscribe to the RSS feed for updates

View File

@ -9,15 +9,14 @@
{{ .PostBody $post }}
--------------------------------------------------------------------------------
================================================================================
Published {{ $post.PublishedAt.Format "2006-01-02" }}
Published {{ $post.PublishedAt.Format "2006-01-02" }} by mediocregopher
{{- if $post.Series }}
This post is part of a series!
{{ $seriesNextPrev := .GetPostSeriesNextPrevious $post -}}
{{ if or $seriesNextPrev.Next $seriesNextPrev.Previous }}
This post is part of a series!
{{ if $seriesNextPrev.Next -}}
=> {{ BlogURL "posts" }}/{{ $seriesNextPrev.Next.ID }}.gmi Next in the series: {{ $seriesNextPrev.Next.Title }}
@ -27,6 +26,7 @@ This post is part of a series!
=> {{ BlogURL "posts" }}/{{ $seriesNextPrev.Previous.ID }}.gmi Prevous in the series: {{ $seriesNextPrev.Previous.Title }}
{{ end -}}
{{ end -}}
{{ end }}
=> {{ BlogURL "posts/" }} Browse all posts

View File

@ -16,6 +16,9 @@ import (
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
)
// TODO I don't think Set/GetRequestLogger are necessary? Seems sufficient to
// just annotate the request's context
type loggerCtxKey int
// SetRequestLogger sets the given Logger onto the given Request's Context,

View File

@ -15,7 +15,7 @@ import (
"strings"
"time"
lru "github.com/hashicorp/golang-lru"
"github.com/mediocregopher/blog.mediocregopher.com/srv/cache"
"github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
"github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
"github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist"
@ -33,6 +33,7 @@ var staticFS embed.FS
type Params struct {
Logger *mlog.Logger
PowManager pow.Manager
Cache cache.Cache
PostStore post.Store
PostAssetStore post.AssetStore
@ -56,12 +57,17 @@ type Params struct {
// AuthRatelimit indicates how much time must pass between subsequent auth
// attempts.
AuthRatelimit time.Duration
// GeminiGatewayURL will be used to translate links for `gemini://` into
// `http(s)://`. See gmi.GemtextToMarkdown.
GeminiGatewayURL *url.URL
}
// SetupCfg implement the cfg.Cfger interface.
func (p *Params) SetupCfg(cfg *cfg.Cfg) {
publicURLStr := cfg.String("http-public-url", "http://localhost:4000", "URL this service is accessible at")
geminiGatewayURLStr := cfg.String("http-gemini-gateway-url", "", "Optional URL to prefix to all gemini:// links, to make them accessible over https")
cfg.StringVar(&p.ListenProto, "http-listen-proto", "tcp", "Protocol to listen for HTTP requests with")
cfg.StringVar(&p.ListenAddr, "http-listen-addr", ":4000", "Address/unix socket path to listen for HTTP requests on")
@ -87,6 +93,12 @@ func (p *Params) SetupCfg(cfg *cfg.Cfg) {
return fmt.Errorf("parsing -http-public-url: %w", err)
}
if *geminiGatewayURLStr != "" {
if p.GeminiGatewayURL, err = url.Parse(*geminiGatewayURLStr); err != nil {
return fmt.Errorf("parsing -http-gemini-gateway-url: %w", err)
}
}
return nil
})
}
@ -179,13 +191,6 @@ func (a *api) apiHandler() http.Handler {
func (a *api) blogHandler() http.Handler {
cache, err := lru.New(5000)
// instantiating the lru cache can't realistically fail
if err != nil {
panic(err)
}
mux := http.NewServeMux()
mux.Handle("/posts/", http.StripPrefix("/posts",
@ -233,11 +238,11 @@ func (a *api) blogHandler() http.Handler {
readOnlyMiddlewares := []middleware{
logReqMiddleware, // only log GETs on cache miss
cacheMiddleware(cache),
cacheMiddleware(a.params.Cache, a.params.PublicURL),
}
readWriteMiddlewares := []middleware{
purgeCacheOnOKMiddleware(cache),
purgeCacheOnOKMiddleware(a.params.Cache),
authMiddleware(a.auther),
}

View File

@ -4,11 +4,12 @@ import (
"bytes"
"net"
"net/http"
"net/url"
"path/filepath"
"sync"
"time"
lru "github.com/hashicorp/golang-lru"
"github.com/mediocregopher/blog.mediocregopher.com/srv/cache"
"github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
@ -86,6 +87,9 @@ func (rw *wrappedResponseWriter) WriteHeader(statusCode int) {
}
func logReqMiddleware(h http.Handler) http.Handler {
type logCtxKey string
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
wrw := newWrappedResponseWriter(rw)
@ -94,8 +98,6 @@ func logReqMiddleware(h http.Handler) http.Handler {
h.ServeHTTP(wrw, r)
took := time.Since(started)
type logCtxKey string
ctx := r.Context()
ctx = mctx.Annotate(ctx,
logCtxKey("took"), took.String(),
@ -139,7 +141,7 @@ func (rw *cacheResponseWriter) Write(b []byte) (int, error) {
return rw.wrappedResponseWriter.Write(b)
}
func cacheMiddleware(cache *lru.Cache) middleware {
func cacheMiddleware(cache cache.Cache, publicURL *url.URL) middleware {
type entry struct {
body []byte
@ -153,11 +155,14 @@ func cacheMiddleware(cache *lru.Cache) middleware {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
id := r.URL.RequestURI()
// r.URL doesn't have Scheme or Host populated, better to add the
// public url to the key to make sure there's no possiblity of
// collision with other protocols using the cache.
id := publicURL.String() + "|" + r.URL.String()
if val, ok := cache.Get(id); ok {
if value := cache.Get(id); value != nil {
entry := val.(entry)
entry := value.(entry)
reader := pool.Get().(*bytes.Reader)
defer pool.Put(reader)
@ -174,7 +179,7 @@ func cacheMiddleware(cache *lru.Cache) middleware {
h.ServeHTTP(cacheRW, r)
if cacheRW.statusCode == 200 {
cache.Add(id, entry{
cache.Set(id, entry{
body: cacheRW.buf.Bytes(),
createdAt: time.Now(),
})
@ -183,7 +188,7 @@ func cacheMiddleware(cache *lru.Cache) middleware {
}
}
func purgeCacheOnOKMiddleware(cache *lru.Cache) middleware {
func purgeCacheOnOKMiddleware(cache cache.Cache) middleware {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {

View File

@ -72,6 +72,9 @@ func (a *api) postToPostTplPayload(storedPost post.StoredPost) (postTplPayload,
BlogURL: func(path string) string {
return a.blogURL(path, false)
},
BlogHTTPURL: func(path string) string {
return a.blogURL(path, false)
},
AssetURL: func(id string) string {
return a.assetURL(id, false)
},
@ -96,7 +99,11 @@ func (a *api) postToPostTplPayload(storedPost post.StoredPost) (postTplPayload,
prevBodyBuf := bodyBuf
bodyBuf = new(bytes.Buffer)
if err := gmi.GemtextToMarkdown(bodyBuf, prevBodyBuf); err != nil {
err := gmi.GemtextToMarkdown(
bodyBuf, prevBodyBuf, a.params.GeminiGatewayURL,
)
if err != nil {
return postTplPayload{}, fmt.Errorf("converting gemtext to markdown: %w", err)
}
}

View File

@ -12,7 +12,7 @@
{{ if ge .Payload.PrevPage 0 }}
<p>
<a href="?p={{ .Payload.PrevPage}}">&lt; &lt; Previous Page</a>
<a href="?method=manage&p={{ .Payload.PrevPage}}">&lt; &lt; Previous Page</a>
</p>
{{ end }}
@ -41,7 +41,7 @@
{{ if ge .Payload.NextPage 0 }}
<p>
<a href="?p={{ .Payload.NextPage}}">Next Page &gt; &gt;</a>
<a href="?method=manage&p={{ .Payload.NextPage}}">Next Page &gt; &gt;</a>
</p>
{{ end }}

View File

@ -13,7 +13,7 @@
<ul>
<li>Matrix: <a href="https://matrix.to/#/@mediocregopher:waffle.farm">@mediocregopher:waffle.farm</a></li>
<li>Mastodon: <a href="https://social.cryptic.io/@mediocregopher">@mediocregopher@social.cryptic.io</a></li>
<li>Email: <a href="mailto:mediocregopher@gmail.com">mediocregopher@gmail.com</a></li>
<li>Email: <a href="mailto:me@mediocregopher.com">me@mediocregopher.com</a></li>
</ul>
<h2>Dev</h2>

View File

@ -8,7 +8,7 @@
{{ if ge .Payload.PrevPage 0 }}
<p>
<a href="?p={{ .Payload.PrevPage}}">&lt; &lt; Previous Page</a>
<a href="?method=manage&p={{ .Payload.PrevPage}}">&lt; &lt; Previous Page</a>
</p>
{{ end }}
@ -38,7 +38,7 @@
{{ if ge .Payload.NextPage 0 }}
<p>
<a href="?p={{ .Payload.NextPage}}">Next Page &gt; &gt;</a>
<a href="?method=manage&p={{ .Payload.NextPage}}">Next Page &gt; &gt;</a>
</p>
{{ end }}

View File

@ -17,6 +17,12 @@ type PreprocessFunctions struct {
// The given path should not have a leading slash.
BlogURL func(path string) string
// BlogURL returns the given string, rooted to the base URL of the blog's
// HTTP server (which may or may not include path components itself).
//
// The given path should not have a leading slash.
BlogHTTPURL func(path string) string
// AssetURL returns the URL of the asset with the given ID.
AssetURL func(id string) string
@ -39,11 +45,12 @@ type PreprocessFunctions struct {
func (funcs PreprocessFunctions) ToFuncsMap() template.FuncMap {
return template.FuncMap{
"BlogURL": funcs.BlogURL,
"AssetURL": funcs.AssetURL,
"PostURL": funcs.PostURL,
"StaticURL": funcs.StaticURL,
"Image": funcs.Image,
"BlogURL": funcs.BlogURL,
"BlogHTTPURL": funcs.BlogHTTPURL,
"AssetURL": funcs.AssetURL,
"PostURL": funcs.PostURL,
"StaticURL": funcs.StaticURL,
"Image": funcs.Image,
}
}