Extract Blog Thumbnails for Archive Page

This commit is contained in:
Noah 2019-06-05 16:18:38 -07:00
parent c556f862e5
commit 7376947e8a
5 changed files with 125 additions and 19 deletions

View File

@ -15,8 +15,9 @@ func UpdateIndex(p *Post) error {
// Index caches high level metadata about the blog's contents for fast access. // Index caches high level metadata about the blog's contents for fast access.
type Index struct { type Index struct {
Posts map[int]Post `json:"posts"` Posts map[int]Post `json:"posts"`
Fragments map[string]int `json:"fragments"` Fragments map[string]int `json:"fragments"`
Thumbnails map[int]string `json:"thumbnails"`
} }
// GetIndex loads the index, or rebuilds it first if it doesn't exist. // GetIndex loads the index, or rebuilds it first if it doesn't exist.
@ -33,8 +34,9 @@ func GetIndex() (*Index, error) {
// RebuildIndex builds the index from scratch. // RebuildIndex builds the index from scratch.
func RebuildIndex() (*Index, error) { func RebuildIndex() (*Index, error) {
idx := &Index{ idx := &Index{
Posts: map[int]Post{}, Posts: map[int]Post{},
Fragments: map[string]int{}, Fragments: map[string]int{},
Thumbnails: map[int]string{},
} }
entries, _ := DB.List("blog/posts") entries, _ := DB.List("blog/posts")
for _, doc := range entries { for _, doc := range entries {
@ -65,6 +67,12 @@ func (idx *Index) Update(p *Post) error {
Updated: p.Updated, Updated: p.Updated,
} }
idx.Fragments[p.Fragment] = p.ID idx.Fragments[p.Fragment] = p.ID
// Find a thumbnail image if possible.
if thumb, ok := p.ExtractThumbnail(); ok {
idx.Thumbnails[p.ID] = thumb
}
err := DB.Commit("blog/index", idx) err := DB.Commit("blog/index", idx)
return err return err
} }

View File

@ -18,6 +18,12 @@ var DB *jsondb.DB
var log *golog.Logger var log *golog.Logger
// Regexp used to parse a thumbnail image from a blog post. Looks for the first
// URI component ending with an image extension.
var (
ThumbnailImageRegexp = regexp.MustCompile(`['"(]([a-zA-Z0-9-_:/?.=&]+\.(?:jpe?g|png|gif))['")]`)
)
func init() { func init() {
log = golog.GetLogger("blog") log = golog.GetLogger("blog")
} }
@ -191,6 +197,16 @@ func (p *Post) Delete() error {
return idx.Delete(p) return idx.Delete(p)
} }
// ExtractThumbnail searches and returns a thumbnail image to represent the
// post. This will be the first image embedded in the post, or nothing.
func (p *Post) ExtractThumbnail() (string, bool) {
result := ThumbnailImageRegexp.FindStringSubmatch(p.Body)
if len(result) < 2 {
return "", false
}
return result[1], true
}
// getNextID gets the next blog post ID. // getNextID gets the next blog post ID.
func (p *Post) nextID() int { func (p *Post) nextID() int {
// Highest ID seen so far. // Highest ID seen so far.

View File

@ -0,0 +1,63 @@
package posts_test
import (
"testing"
"github.com/kirsle/blog/models/posts"
)
func TestThumbnailRegexp(t *testing.T) {
type testCase struct {
Text string
Expect string
ExpectFail bool
}
var tests = []testCase{
{
Text: "Hello world",
ExpectFail: true,
},
{
Text: "Some text.\n\n![An image](/static/photos/Image-1.jpg)\n" +
"![Another image](/static/photos/Image-2.jpg)",
Expect: "/static/photos/Image-1.jpg",
},
{
Text: `<a href="/static/photos/12Abc456.jpg" target="_blank">` +
`<img src="/static/photos/34Xyz123.jpg"></a>`,
Expect: "/static/photos/12Abc456.jpg",
},
{
Text: `A markdown image: ![With text](/test1.gif) and an HTML ` +
`image: <img src="/test2.png">`,
Expect: "/test1.gif",
},
{
Text: `<a href="https://google.com/"><img src="https://example.com/logo.gif?query=string.jpg"></a>`,
Expect: "https://example.com/logo.gif?query=string.jpg",
},
}
for _, test := range tests {
p := &posts.Post{
Body: test.Text,
}
result, ok := p.ExtractThumbnail()
if !ok && !test.ExpectFail {
t.Errorf("Text: %s\nExpected to fail, but did not!\nGot: %s",
test.Text,
result,
)
continue
}
if result != test.Expect {
t.Errorf("Text: %s\nExpect: %s\nGot: %s",
test.Text,
test.Expect,
result,
)
}
}
}

View File

@ -3,22 +3,40 @@
<h1>Archive</h1> <h1>Archive</h1>
{{ $thumbs := .Data.Thumbnails }}
{{ range .Data.Archive }} {{ range .Data.Archive }}
<h3>{{ .Date.Format "January, 2006" }}</h3> <div class="card mb-4">
<div class="card-header">
<h3>{{ .Date.Format "January, 2006" }}</h3>
</div>
<div class="card-body">
<ul class="list-unstyled"> <div class="row">
{{ range .Posts }} {{ range .Posts }}
<li class="list-item"> {{ $thumb := index $thumbs .ID }}
<a href="/{{ .Fragment }}">{{ .Title }}</a> <div class="col-12 col-sm-6 col-md-4 col-lg-3 mb-4">
<small class="blog-meta"> <div class="card bg-secondary"
{{ .Created.Format "Jan 02 2006" }} style="height: auto; min-height: 150px;
{{ if ne .Privacy "public" }} {{ if $thumb }}background-image: url({{ $thumb }}); background-size: cover{{ end }}
<span class="blog-{{ .Privacy }}">[{{ .Privacy }}]</span> "
title="Tags: {{ range .Tags }}#{{ . }} {{ end }}">
<span class="p-1" style="background-color: RGBA(255, 255, 255, 0.8)">
<a href="/{{ .Fragment }}">{{ .Title }}</a><br>
<small class="blog-meta">
{{ .Created.Format "Jan 02 2006" }}
{{ if ne .Privacy "public" }}
<span class="blog-{{ .Privacy }}">[{{ .Privacy }}]</span>
{{ end }}
</small>
</span>
</div>
</div>
{{ end }} {{ end }}
</small> </div>
</li>
{{ end }} </div>
</ul> </div>
{{ end }} {{ end }}
{{ end }} {{ end }}

View File

@ -5,8 +5,8 @@ import (
"sort" "sort"
"time" "time"
"github.com/kirsle/blog/src/middleware/auth"
"github.com/kirsle/blog/models/posts" "github.com/kirsle/blog/models/posts"
"github.com/kirsle/blog/src/middleware/auth"
"github.com/kirsle/blog/src/render" "github.com/kirsle/blog/src/render"
"github.com/kirsle/blog/src/responses" "github.com/kirsle/blog/src/responses"
"github.com/kirsle/blog/src/types" "github.com/kirsle/blog/src/types"
@ -54,7 +54,8 @@ func archiveHandler(w http.ResponseWriter, r *http.Request) {
} }
v := map[string]interface{}{ v := map[string]interface{}{
"Archive": result, "Archive": result,
"Thumbnails": idx.Thumbnails,
} }
render.Template(w, r, "blog/archive", v) render.Template(w, r, "blog/archive", v)
} }