~evanj/evanjon.es

0a9e51eaf882824dcd89a1b7765bb165a8e89592 — Evan M Jones 1 year, 4 months ago 147b6d6
WIP(*): Refactor.
M evanjon.es.go => evanjon.es.go +20 -13
@@ 11,8 11,9 @@ import (
	"git.sr.ht/~evanj/evanjon.es/internal/c"
	"git.sr.ht/~evanj/evanjon.es/internal/c/list"
	"git.sr.ht/~evanj/evanjon.es/internal/c/search"
	"git.sr.ht/~evanj/evanjon.es/internal/s/cms"
	api "git.sr.ht/~evanj/evanjon.es/pkg/cms"

	// "git.sr.ht/~evanj/evanjon.es/internal/s/cms"
	"git.sr.ht/~evanj/evanjon.es/pkg/cms"
)

//go:generate go get git.sr.ht/~evanj/embed


@@ 43,17 44,23 @@ func New(out io.Writer) *App {
		l.Fatal("invalid space id")
	}

	// Transformer, match our data types.
	// // Transformer, match our data types.
	// cms := cms.New(
	// 	log.New(out, "[evanjon.es:cms] ", 0),
	// 	// API impl.
	// 	api.New(
	// 		os.Getenv("CMS_USER"),
	// 		os.Getenv("CMS_PASS"),
	// 		os.Getenv("CMS_URL"),
	// 		space,
	// 	),
	// 	os.Getenv("CACHE_SECRET"),
	// )
	cms := cms.New(
		log.New(out, "[evanjon.es:cms] ", 0),
		// API impl.
		api.New(
			os.Getenv("CMS_USER"),
			os.Getenv("CMS_PASS"),
			os.Getenv("CMS_URL"),
			space,
		),
		os.Getenv("CACHE_SECRET"),
		os.Getenv("CMS_USER"),
		os.Getenv("CMS_PASS"),
		os.Getenv("CMS_URL"),
		space,
	)

	return &App{


@@ 78,7 85,7 @@ func New(out io.Writer) *App {
			typeMeta,
		),

		cacheRouter: cms,
		// cacheRouter: cms,

		// rssRouter: rss.New(
		// 	e,

M internal/c/list/list.go => internal/c/list/list.go +3 -3
@@ 6,8 6,8 @@ import (
	"net/http"

	"git.sr.ht/~evanj/evanjon.es/internal/c"
	"git.sr.ht/~evanj/evanjon.es/internal/s/cms"
	"git.sr.ht/~evanj/evanjon.es/internal/s/tmpl"
	"git.sr.ht/~evanj/evanjon.es/pkg/cms"
)

var ListHTML = tmpl.MustParse("html/list.html")


@@ 21,7 21,7 @@ type ListEndpoint struct {

type cmsProvider interface {
	List(ctx context.Context, typeID int, order, field string) ([]cms.Content, error)
	FindMeta(ctx context.Context, typeID int, field, query string) (cms.Meta, error)
	Find(ctx context.Context, typeID int, field, query string) (*cms.Content, error)
}

func New(base *c.Endpoint, log *log.Logger, cms cmsProvider, typePage, typePost, typeMeta int) *ListEndpoint {


@@ 45,7 45,7 @@ func (e *ListEndpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
		return
	}

	a, err := e.cms.FindMeta(ctx, e.typeMeta, "slug", "aside")
	a, err := e.cms.Find(ctx, e.typeMeta, "slug", "aside")
	if err != nil {
		e.log.Println(err)
		w.WriteHeader(http.StatusNotFound)

M internal/c/search/search.go => internal/c/search/search.go +9 -11
@@ 4,15 4,14 @@ import (
	"context"
	"log"
	"net/http"
	"strconv"

	"git.sr.ht/~evanj/evanjon.es/internal/c"
	"git.sr.ht/~evanj/evanjon.es/internal/s/cms"
	"git.sr.ht/~evanj/evanjon.es/internal/s/tmpl"
	"git.sr.ht/~evanj/evanjon.es/pkg/cms"
)

var (
	ItemHTML = tmpl.MustParse("html/item.html")
)
var ItemHTML = tmpl.MustParse("html/item.html")

type SearchEndpoint struct {
	*c.Endpoint


@@ 22,8 21,7 @@ type SearchEndpoint struct {
}

type cmsProvider interface {
	Find(ctx context.Context, typeID int, field, query string) (cms.Content, error)
	FindMeta(ctx context.Context, typeID int, field, query string) (cms.Meta, error)
	Find(ctx context.Context, typeID int, field, query string) (*cms.Content, error)
}

func New(base *c.Endpoint, log *log.Logger, cms cmsProvider, typePage, typePost, typeMeta int) *SearchEndpoint {


@@ 35,8 33,8 @@ func New(base *c.Endpoint, log *log.Logger, cms cmsProvider, typePage, typePost,
	}
}

func (e *SearchEndpoint) search(ctx context.Context, slug string) (c cms.Content, err error) {
	do := func(ctx context.Context, typ int, slug string, ch chan cms.Content, ech chan error) {
func (e *SearchEndpoint) search(ctx context.Context, slug string) (c *cms.Content, err error) {
	do := func(ctx context.Context, typ int, slug string, ch chan *cms.Content, ech chan error) {
		c, err := e.cms.Find(ctx, typ, "slug", slug)
		if err != nil {
			ech <- err


@@ 45,7 43,7 @@ func (e *SearchEndpoint) search(ctx context.Context, slug string) (c cms.Content
		ch <- c
	}

	contentCh := make(chan cms.Content)
	contentCh := make(chan *cms.Content)
	errCh := make(chan error)

	go do(ctx, e.typePost, slug, contentCh, errCh)


@@ 87,7 85,7 @@ func (e *SearchEndpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
		return
	}

	a, err := e.cms.FindMeta(ctx, e.typeMeta, "slug", "aside")
	a, err := e.cms.Find(ctx, e.typeMeta, "slug", "aside")
	if err != nil {
		e.log.Println(err)
		w.WriteHeader(http.StatusNotFound)


@@ 98,6 96,6 @@ func (e *SearchEndpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	e.HTML(w, r, ItemHTML, map[string]interface{}{
		"Item":     c,
		"Aside":    a,
		"ShowDate": c.Typ != e.typePage,
		"ShowDate": c.ContentParentTypeID != strconv.Itoa(e.typePage),
	})
}

D internal/s/cms/cms.go => internal/s/cms/cms.go +0 -265
@@ 1,265 0,0 @@
package cms

import (
	"context"
	"errors"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"strconv"
	"sync"
	"time"

	orig "git.sr.ht/~evanj/evanjon.es/pkg/cms"
)

var (
	ErrNoValue  = errors.New("content does not have all required fields")
	ErrBadValue = errors.New("content has malformed field")
)

type CMS struct {
	provider    provider
	log         *log.Logger
	cacheSecret string
	cache       sync.Map
}

type Content struct {
	Name, Slug, Short string
	Desc              template.HTML
	Date              time.Time
	HasPrev, HasNext  bool
	Prev, Next        *Content
	Typ               int
}

func (c Content) PrettyDate() string {
	return c.Date.Format("Jan 2, 2006")
}

type Meta struct {
	String1, String2, Desc string
	Nav                    []Nav
}

type Nav struct {
	Name, Slug string
}

type provider interface {
	Find(ctx context.Context, typeID int, field, query string) (*orig.Content, error)
	List(ctx context.Context, typeID int, order, field string) ([]orig.Content, error)
}

func New(log *log.Logger, cms provider, cacheSecret string) *CMS {
	return &CMS{
		cms,
		log,
		cacheSecret,
		sync.Map{},
	}
}

func (cms *CMS) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if cms.cacheSecret != r.FormValue("secret") {
		w.WriteHeader(http.StatusForbidden)
		w.Write([]byte("you cannot do this"))
		cms.log.Println("cache has failed to clear, no valid secret")
		return
	}

	cms.breakcache()

	w.WriteHeader(http.StatusOK)
	w.Write([]byte("cache has been cleared"))

	cms.log.Println("cache has been cleared")
}

func (cms *CMS) breakcache() {
	cms.cache.Range(func(key, val interface{}) bool {
		cms.cache.Delete(key)
		return true
	})
}

func (cms *CMS) FindMeta(ctx context.Context, typeID int, field, query string) (c Meta, err error) {
	key := fmt.Sprintf("meta::%d::%s::%s", typeID, field, query)

	cached, ok := cms.cache.Load(key)
	if ok {
		item, ok := cached.(Meta)
		if ok {
			return item, nil
		}
	}

	orig, err := cms.provider.Find(ctx, typeID, field, query)
	if err != nil {
		return c, err
	}

	string1, ok := orig.Val("string1")
	if !ok {
		return c, fmt.Errorf("string1: %w", ErrNoValue)
	}

	string2, ok := orig.Val("string2")
	if !ok {
		return c, fmt.Errorf("string2: %w", ErrNoValue)
	}

	desc, ok := orig.Val("desc")
	if !ok {
		return c, fmt.Errorf("desc: %w", ErrNoValue)
	}

	refList, ok := orig.List("nav")
	if !ok {
		return c, fmt.Errorf("nav: %w", ErrNoValue)
	}

	var nav []Nav
	for _, item := range refList {
		name, ok := item.Val("name")
		if !ok {
			return c, fmt.Errorf("name: %w", ErrNoValue)
		}

		slug, ok := item.Val("slug")
		if !ok {
			return c, fmt.Errorf("slug: %w", ErrNoValue)
		}

		nav = append(nav, Nav{name, slug})
	}

	c = Meta{string1, string2, desc, nav}
	cms.cache.Store(key, c)
	return c, nil
}

func (cms *CMS) Find(ctx context.Context, typeID int, field, query string) (c Content, err error) {
	key := fmt.Sprintf("find::%d::%s::%s", typeID, field, query)

	cached, ok := cms.cache.Load(key)
	if ok {
		item, ok := cached.(Content)
		if ok {
			return item, nil
		}
	}

	orig, err := cms.provider.Find(ctx, typeID, field, query)
	if err != nil {
		return c, err
	}

	c, err = transform(orig)
	if err != nil {
		return c, err
	}

	cms.cache.Store(key, c)
	return c, nil
}

func (cms *CMS) List(ctx context.Context, typeID int, order, field string) ([]Content, error) {
	key := fmt.Sprintf("list::%d::%s::%s", typeID, order, field)

	cached, ok := cms.cache.Load(key)
	if ok {
		list, ok := cached.([]Content)
		if ok {
			return list, nil
		}
	}

	orig, err := cms.provider.List(ctx, typeID, order, field)
	if err != nil {
		return nil, err
	}

	list, err := transformList(orig)
	if err != nil {
		return nil, err
	}

	cms.cache.Store(key, list)
	return list, nil
}

func transformList(in []orig.Content) (out []Content, err error) {
	for _, orig := range in {
		item, err := transform(&orig)
		if err != nil {
			return out, err
		}
		out = append(out, item)
	}

	return out, nil
}

func transform(orig *orig.Content) (out Content, err error) {
	name, ok := orig.Val("name")
	if !ok {
		return out, fmt.Errorf("name: %w", ErrNoValue)
	}

	slug, ok := orig.Val("slug")
	if !ok {
		return out, fmt.Errorf("slug: %w", ErrNoValue)
	}

	short, ok := orig.Val("short")
	if !ok {
		return out, fmt.Errorf("short: %w", ErrNoValue)
	}

	descStr, ok := orig.Val("desc")
	if !ok {
		return out, fmt.Errorf("desc: %w", ErrNoValue)
	}
	desc := template.HTML(descStr)

	dateStr, ok := orig.Val("date")
	if !ok {
		return out, fmt.Errorf("date: %w", ErrNoValue)
	}

	date, err := time.Parse("2006-01-02", dateStr)
	if err != nil {
		return out, fmt.Errorf("date: %w", ErrBadValue)
	}

	prev, hasPrev := getref(orig, "prev")
	next, hasNext := getref(orig, "next")

	typ, err := strconv.Atoi(orig.ContentParentTypeID)
	if err != nil {
		return out, fmt.Errorf("type: %w", ErrBadValue)
	}

	out = Content{
		name, slug, short,
		desc,
		date,
		hasPrev, hasNext,
		prev, next,
		typ,
	}

	return out, nil
}

func getref(c *orig.Content, key string) (*Content, bool) {
	ref, ok := c.Ref(key)
	if !ok {
		return nil, false
	}
	next, err := transform(ref)
	return &next, err == nil
}

D internal/s/cms/cms_test.go => internal/s/cms/cms_test.go +0 -1
@@ 1,1 0,0 @@
package cms_test

M internal/s/tmpl/html/_aside.html => internal/s/tmpl/html/_aside.html +8 -4
@@ 1,16 1,20 @@
{{with .Aside.Must}}
<aside class="sidebar">
  <div class="container sidebar-sticky">
    <div class="sidebar-about">
      <a href="/">
        <h1>{{.Aside.String1}}</h1>
        <h1>{{.Val "string1"}}</h1>
      </a>
      <p class="lead">{{.Aside.Desc}}</p>
      <p class="lead">{{.Val "desc"}}</p>
      <nav>
        <ul class="sidebar-nav">
          {{range .Aside.Nav}}
          <li><a href="{{.Slug}}">{{.Name}}</a></li>
          {{range .List "nav"}}
          {{with .Must}}
          <li><a href='{{.Val "slug"}}'>{{.Val "name"}}</a></li>
          {{end}}
          {{end}}
        </ul>
      </nav>
  </div>
</aside>
{{end}}

M internal/s/tmpl/html/item.html => internal/s/tmpl/html/item.html +27 -20
@@ 1,35 1,42 @@
{{with .Item.Must}}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  {{ template "html/_head.html" $ }}
  <title>{{.Item.Name}}</title>
  <meta name="description" content="{{.Item.Short}}" />
  <title>{{.Val "name"}}</title>
</head>
<body class="theme-base-08">
  {{ template "html/_styles.html" $ }}
  {{ template "html/_aside.html" $ }}
  <main class="content container">
    <div class="post">
      <h1>{{.Item.Name}}</h1>
      {{if .ShowDate}}
      <time class="post-date">{{.Item.PrettyDate}}</time>
      <h1>{{.Val "name"}}</h1>
      {{if $.ShowDate}}
      <time>{{.Val "date" | date}}</time>
      {{end}}
      <article>{{.Item.Desc}}</article>
      <article>{{.Val "desc" | html }}</article>
    </div>
    {{if .Item.HasPrev}}
    <div>
      <a href="{{.Item.Prev.Slug}}">
        ⬅️ {{.Item.Prev.Name}}
      </a>
    </div>
    {{end}}
    {{if .Item.HasNext}}
    <div>
      <a href="{{.Item.Next.Slug}}">
        ➡️ {{.Item.Next.Name}}
      </a>
    </div>
    {{end}}
    <table>
      <tr>
        {{with .Ref "prev"}}
        <td width="50%">
          <div>
            <a href='/{{.Must.Val "slug"}}'>Prev</a>
            <div>{{.Must.Val "name"}}</div>
          </div>
        </td>
        {{end}}
        {{with .Ref "next"}}
        <td width="50%">
          <div>
            <a href='/{{.Must.Val "slug"}}'>Next</a>
            <div>{{.Must.Val "name"}}</div>
          </div>
        </td>
        {{end}}
      </tr>
    </table>
  </main>
</body>
</html>
{{end}}

M internal/s/tmpl/html/list.html => internal/s/tmpl/html/list.html +18 -16
@@ 1,27 1,29 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  {{ template "html/_head.html" $ }}
  <title>TODO</title>
  {{template "html/_head.html" $}}
  <title>{{.Aside.Must.Val "string3"}}</title>
  <meta name="description" content="TODO" />
</head>
<body class="theme-base-08">
  {{ template "html/_styles.html" $ }}
  {{ template "html/_aside.html" $ }}
  {{template "html/_styles.html" $}}
  {{template "html/_aside.html" $}}
  <main class="content container">
    <div class="posts">
      {{ range .List }}
      <article class="post">
        <h1 class="post-title">
          <a href="/{{.Slug}}">{{.Name}}</a>
        </h1>
        <time class="post-date">{{.PrettyDate}}</time>
        <p>{{.Short}}</p>
        <div class="read-more-link">
          <a href="/{{.Slug}}">Read More…</a>
        </div>
      </article>
      {{ end }}
      {{range .List}}
        {{with .Must}}
        <article class="post">
          <h1 class="post-title">
            <a href='/{{.Val "slug"}}'>{{.Val "name"}}</a>
          </h1>
          <time class="post-date">{{.Val "date" | date}}</time>
          <p>{{.Val "short"}}</p>
          <div class="read-more-link">
            <a href='/{{.Val "slug"}}'>Read More…</a>
          </div>
        </article>
        {{end}}
      {{end}}
    </div>
  </main>
</body>

M internal/s/tmpl/tmpl.go => internal/s/tmpl/tmpl.go +9 -1
@@ 4,6 4,7 @@ import (
	"html/template"
	"path/filepath"
	"sync"
	"time"

	"github.com/tdewolff/minify/v2"
	"github.com/tdewolff/minify/v2/css"


@@ 45,7 46,14 @@ func MustParse(name string) *template.Template {
				panic(err.Error())
			}

			all = template.Must(all.New(key).Parse(val))
			all = template.Must(all.New(key).Parse(val)).
				Funcs(template.FuncMap(map[string]interface{}{
					"html": func(val string) template.HTML { return template.HTML(val) },
					"date": func(val string) (string, error) {
						date, err := time.Parse("2006-01-02", val)
						return date.Format("Jan 2, 2006"), err
					},
				}))
		}
	})


M internal/s/tmpl/tmpls_embed.go => internal/s/tmpl/tmpls_embed.go +3 -3
@@ 24,15 24,15 @@ func init() {

	tmpls["css/syntax.css"] = tostring("LmhsbCB7IGJhY2tncm91bmQtY29sb3I6ICNmZmZmY2MgfQogLyp7IGJhY2tncm91bmQ6ICNmMGYzZjM7IH0qLwouYyB7IGNvbG9yOiAjOTk5OyB9IC8qIENvbW1lbnQgKi8KLmVyciB7IGNvbG9yOiAjQUEwMDAwOyBiYWNrZ3JvdW5kLWNvbG9yOiAjRkZBQUFBIH0gLyogRXJyb3IgKi8KLmsgeyBjb2xvcjogIzAwNjY5OTsgfSAvKiBLZXl3b3JkICovCi5vIHsgY29sb3I6ICM1NTU1NTUgfSAvKiBPcGVyYXRvciAqLwouY20geyBjb2xvcjogIzAwOTlGRjsgZm9udC1zdHlsZTogaXRhbGljIH0gLyogQ29tbWVudC5NdWx0aWxpbmUgKi8KLmNwIHsgY29sb3I6ICMwMDk5OTkgfSAvKiBDb21tZW50LlByZXByb2MgKi8KLmMxIHsgY29sb3I6ICM5OTk7IH0gLyogQ29tbWVudC5TaW5nbGUgKi8KLmNzIHsgY29sb3I6ICM5OTk7IH0gLyogQ29tbWVudC5TcGVjaWFsICovCi5nZCB7IGJhY2tncm91bmQtY29sb3I6ICNGRkNDQ0M7IGJvcmRlcjogMXB4IHNvbGlkICNDQzAwMDAgfSAvKiBHZW5lcmljLkRlbGV0ZWQgKi8KLmdlIHsgZm9udC1zdHlsZTogaXRhbGljIH0gLyogR2VuZXJpYy5FbXBoICovCi5nciB7IGNvbG9yOiAjRkYwMDAwIH0gLyogR2VuZXJpYy5FcnJvciAqLwouZ2ggeyBjb2xvcjogIzAwMzMwMDsgfSAvKiBHZW5lcmljLkhlYWRpbmcgKi8KLmdpIHsgYmFja2dyb3VuZC1jb2xvcjogI0NDRkZDQzsgYm9yZGVyOiAxcHggc29saWQgIzAwQ0MwMCB9IC8qIEdlbmVyaWMuSW5zZXJ0ZWQgKi8KLmdvIHsgY29sb3I6ICNBQUFBQUEgfSAvKiBHZW5lcmljLk91dHB1dCAqLwouZ3AgeyBjb2xvcjogIzAwMDA5OTsgfSAvKiBHZW5lcmljLlByb21wdCAqLwouZ3MgeyB9IC8qIEdlbmVyaWMuU3Ryb25nICovCi5ndSB7IGNvbG9yOiAjMDAzMzAwOyB9IC8qIEdlbmVyaWMuU3ViaGVhZGluZyAqLwouZ3QgeyBjb2xvcjogIzk5Q0M2NiB9IC8qIEdlbmVyaWMuVHJhY2ViYWNrICovCi5rYyB7IGNvbG9yOiAjMDA2Njk5OyB9IC8qIEtleXdvcmQuQ29uc3RhbnQgKi8KLmtkIHsgY29sb3I6ICMwMDY2OTk7IH0gLyogS2V5d29yZC5EZWNsYXJhdGlvbiAqLwoua24geyBjb2xvcjogIzAwNjY5OTsgfSAvKiBLZXl3b3JkLk5hbWVzcGFjZSAqLwoua3AgeyBjb2xvcjogIzAwNjY5OSB9IC8qIEtleXdvcmQuUHNldWRvICovCi5rciB7IGNvbG9yOiAjMDA2Njk5OyB9IC8qIEtleXdvcmQuUmVzZXJ2ZWQgKi8KLmt0IHsgY29sb3I6ICMwMDc3ODg7IH0gLyogS2V5d29yZC5UeXBlICovCi5tIHsgY29sb3I6ICNGRjY2MDAgfSAvKiBMaXRlcmFsLk51bWJlciAqLwoucyB7IGNvbG9yOiAjZDQ0OTUwIH0gLyogTGl0ZXJhbC5TdHJpbmcgKi8KLm5hIHsgY29sb3I6ICM0ZjlmY2YgfSAvKiBOYW1lLkF0dHJpYnV0ZSAqLwoubmIgeyBjb2xvcjogIzMzNjY2NiB9IC8qIE5hbWUuQnVpbHRpbiAqLwoubmMgeyBjb2xvcjogIzAwQUE4ODsgfSAvKiBOYW1lLkNsYXNzICovCi5ubyB7IGNvbG9yOiAjMzM2NjAwIH0gLyogTmFtZS5Db25zdGFudCAqLwoubmQgeyBjb2xvcjogIzk5OTlGRiB9IC8qIE5hbWUuRGVjb3JhdG9yICovCi5uaSB7IGNvbG9yOiAjOTk5OTk5OyB9IC8qIE5hbWUuRW50aXR5ICovCi5uZSB7IGNvbG9yOiAjQ0MwMDAwOyB9IC8qIE5hbWUuRXhjZXB0aW9uICovCi5uZiB7IGNvbG9yOiAjQ0MwMEZGIH0gLyogTmFtZS5GdW5jdGlvbiAqLwoubmwgeyBjb2xvcjogIzk5OTlGRiB9IC8qIE5hbWUuTGFiZWwgKi8KLm5uIHsgY29sb3I6ICMwMENDRkY7IH0gLyogTmFtZS5OYW1lc3BhY2UgKi8KLm50IHsgY29sb3I6ICMyZjZmOWY7IH0gLyogTmFtZS5UYWcgKi8KLm52IHsgY29sb3I6ICMwMDMzMzMgfSAvKiBOYW1lLlZhcmlhYmxlICovCi5vdyB7IGNvbG9yOiAjMDAwMDAwOyB9IC8qIE9wZXJhdG9yLldvcmQgKi8KLncgeyBjb2xvcjogI2JiYmJiYiB9IC8qIFRleHQuV2hpdGVzcGFjZSAqLwoubWYgeyBjb2xvcjogI0ZGNjYwMCB9IC8qIExpdGVyYWwuTnVtYmVyLkZsb2F0ICovCi5taCB7IGNvbG9yOiAjRkY2NjAwIH0gLyogTGl0ZXJhbC5OdW1iZXIuSGV4ICovCi5taSB7IGNvbG9yOiAjRkY2NjAwIH0gLyogTGl0ZXJhbC5OdW1iZXIuSW50ZWdlciAqLwoubW8geyBjb2xvcjogI0ZGNjYwMCB9IC8qIExpdGVyYWwuTnVtYmVyLk9jdCAqLwouc2IgeyBjb2xvcjogI0NDMzMwMCB9IC8qIExpdGVyYWwuU3RyaW5nLkJhY2t0aWNrICovCi5zYyB7IGNvbG9yOiAjQ0MzMzAwIH0gLyogTGl0ZXJhbC5TdHJpbmcuQ2hhciAqLwouc2QgeyBjb2xvcjogI0NDMzMwMDsgZm9udC1zdHlsZTogaXRhbGljIH0gLyogTGl0ZXJhbC5TdHJpbmcuRG9jICovCi5zMiB7IGNvbG9yOiAjQ0MzMzAwIH0gLyogTGl0ZXJhbC5TdHJpbmcuRG91YmxlICovCi5zZSB7IGNvbG9yOiAjQ0MzMzAwOyB9IC8qIExpdGVyYWwuU3RyaW5nLkVzY2FwZSAqLwouc2ggeyBjb2xvcjogI0NDMzMwMCB9IC8qIExpdGVyYWwuU3RyaW5nLkhlcmVkb2MgKi8KLnNpIHsgY29sb3I6ICNBQTAwMDAgfSAvKiBMaXRlcmFsLlN0cmluZy5JbnRlcnBvbCAqLwouc3ggeyBjb2xvcjogI0NDMzMwMCB9IC8qIExpdGVyYWwuU3RyaW5nLk90aGVyICovCi5zciB7IGNvbG9yOiAjMzNBQUFBIH0gLyogTGl0ZXJhbC5TdHJpbmcuUmVnZXggKi8KLnMxIHsgY29sb3I6ICNDQzMzMDAgfSAvKiBMaXRlcmFsLlN0cmluZy5TaW5nbGUgKi8KLnNzIHsgY29sb3I6ICNGRkNDMzMgfSAvKiBMaXRlcmFsLlN0cmluZy5TeW1ib2wgKi8KLmJwIHsgY29sb3I6ICMzMzY2NjYgfSAvKiBOYW1lLkJ1aWx0aW4uUHNldWRvICovCi52YyB7IGNvbG9yOiAjMDAzMzMzIH0gLyogTmFtZS5WYXJpYWJsZS5DbGFzcyAqLwoudmcgeyBjb2xvcjogIzAwMzMzMyB9IC8qIE5hbWUuVmFyaWFibGUuR2xvYmFsICovCi52aSB7IGNvbG9yOiAjMDAzMzMzIH0gLyogTmFtZS5WYXJpYWJsZS5JbnN0YW5jZSAqLwouaWwgeyBjb2xvcjogI0ZGNjYwMCB9IC8qIExpdGVyYWwuTnVtYmVyLkludGVnZXIuTG9uZyAqLwoKLmNzcyAubywKLmNzcyAubyArIC5udCwKLmNzcyAubnQgKyAubnQgeyBjb2xvcjogIzk5OTsgfQo=")

	tmpls["html/_aside.html"] = tostring("PGFzaWRlIGNsYXNzPSJzaWRlYmFyIj4KICA8ZGl2IGNsYXNzPSJjb250YWluZXIgc2lkZWJhci1zdGlja3kiPgogICAgPGRpdiBjbGFzcz0ic2lkZWJhci1hYm91dCI+CiAgICAgIDxhIGhyZWY9Ii8iPgogICAgICAgIDxoMT57ey5Bc2lkZS5TdHJpbmcxfX08L2gxPgogICAgICA8L2E+CiAgICAgIDxwIGNsYXNzPSJsZWFkIj57ey5Bc2lkZS5EZXNjfX08L3A+CiAgICAgIDxuYXY+CiAgICAgICAgPHVsIGNsYXNzPSJzaWRlYmFyLW5hdiI+CiAgICAgICAgICB7e3JhbmdlIC5Bc2lkZS5OYXZ9fQogICAgICAgICAgPGxpPjxhIGhyZWY9Int7LlNsdWd9fSI+e3suTmFtZX19PC9hPjwvbGk+CiAgICAgICAgICB7e2VuZH19CiAgICAgICAgPC91bD4KICAgICAgPC9uYXY+CiAgPC9kaXY+CjwvYXNpZGU+Cg==")
	tmpls["html/_aside.html"] = tostring("e3t3aXRoIC5Bc2lkZS5NdXN0fX0KPGFzaWRlIGNsYXNzPSJzaWRlYmFyIj4KICA8ZGl2IGNsYXNzPSJjb250YWluZXIgc2lkZWJhci1zdGlja3kiPgogICAgPGRpdiBjbGFzcz0ic2lkZWJhci1hYm91dCI+CiAgICAgIDxhIGhyZWY9Ii8iPgogICAgICAgIDxoMT57ey5WYWwgInN0cmluZzEifX08L2gxPgogICAgICA8L2E+CiAgICAgIDxwIGNsYXNzPSJsZWFkIj57ey5WYWwgImRlc2MifX08L3A+CiAgICAgIDxuYXY+CiAgICAgICAgPHVsIGNsYXNzPSJzaWRlYmFyLW5hdiI+CiAgICAgICAgICB7e3JhbmdlIC5MaXN0ICJuYXYifX0KICAgICAgICAgIHt7d2l0aCAuTXVzdH19CiAgICAgICAgICA8bGk+PGEgaHJlZj0ne3suVmFsICJzbHVnIn19Jz57ey5WYWwgIm5hbWUifX08L2E+PC9saT4KICAgICAgICAgIHt7ZW5kfX0KICAgICAgICAgIHt7ZW5kfX0KICAgICAgICA8L3VsPgogICAgICA8L25hdj4KICA8L2Rpdj4KPC9hc2lkZT4Ke3tlbmR9fQo=")

	tmpls["html/_head.html"] = tostring("PG1ldGEgaHR0cC1lcXVpdj0iY29udGVudC10eXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiPgo8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEuMCI+CjxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iaHR0cHM6Ly9mb250cy5nb29nbGVhcGlzLmNvbS9jc3M/ZmFtaWx5PUFicmlsK0ZhdGZhY2V8UFQrU2Fuczo0MDAsNDAwaSw3MDAiPgo=")

	tmpls["html/_styles.html"] = tostring("PHN0eWxlPgogIHt7IHRlbXBsYXRlICJjc3MvcG9vbGUuY3NzIiB9fQogIHt7IHRlbXBsYXRlICJjc3Mvc3ludGF4LmNzcyIgfX0KICB7eyB0ZW1wbGF0ZSAiY3NzL2h5ZGUuY3NzIiB9fQogIHt7IHRlbXBsYXRlICJjc3MvbWFpbi5jc3MiIH19Cjwvc3R5bGU+Cg==")

	tmpls["html/item.html"] = tostring("PCFET0NUWVBFIGh0bWw+CjxodG1sIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIj4KPGhlYWQ+CiAge3sgdGVtcGxhdGUgImh0bWwvX2hlYWQuaHRtbCIgJCB9fQogIDx0aXRsZT57ey5JdGVtLk5hbWV9fTwvdGl0bGU+CiAgPG1ldGEgbmFtZT0iZGVzY3JpcHRpb24iIGNvbnRlbnQ9Int7Lkl0ZW0uU2hvcnR9fSIgLz4KPC9oZWFkPgo8Ym9keSBjbGFzcz0idGhlbWUtYmFzZS0wOCI+CiAge3sgdGVtcGxhdGUgImh0bWwvX3N0eWxlcy5odG1sIiAkIH19CiAge3sgdGVtcGxhdGUgImh0bWwvX2FzaWRlLmh0bWwiICQgfX0KICA8bWFpbiBjbGFzcz0iY29udGVudCBjb250YWluZXIiPgogICAgPGRpdiBjbGFzcz0icG9zdCI+CiAgICAgIDxoMT57ey5JdGVtLk5hbWV9fTwvaDE+CiAgICAgIHt7aWYgLlNob3dEYXRlfX0KICAgICAgPHRpbWUgY2xhc3M9InBvc3QtZGF0ZSI+e3suSXRlbS5QcmV0dHlEYXRlfX08L3RpbWU+CiAgICAgIHt7ZW5kfX0KICAgICAgPGFydGljbGU+e3suSXRlbS5EZXNjfX08L2FydGljbGU+CiAgICA8L2Rpdj4KICAgIHt7aWYgLkl0ZW0uSGFzUHJldn19CiAgICA8ZGl2PgogICAgICA8YSBocmVmPSJ7ey5JdGVtLlByZXYuU2x1Z319Ij4KICAgICAgICDirIXvuI8ge3suSXRlbS5QcmV2Lk5hbWV9fQogICAgICA8L2E+CiAgICA8L2Rpdj4KICAgIHt7ZW5kfX0KICAgIHt7aWYgLkl0ZW0uSGFzTmV4dH19CiAgICA8ZGl2PgogICAgICA8YSBocmVmPSJ7ey5JdGVtLk5leHQuU2x1Z319Ij4KICAgICAgICDinqHvuI8ge3suSXRlbS5OZXh0Lk5hbWV9fQogICAgICA8L2E+CiAgICA8L2Rpdj4KICAgIHt7ZW5kfX0KICA8L21haW4+CjwvYm9keT4KPC9odG1sPgo=")
	tmpls["html/item.html"] = tostring("e3t3aXRoIC5JdGVtLk11c3R9fQo8IURPQ1RZUEUgaHRtbD4KPGh0bWwgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGh0bWwiPgo8aGVhZD4KICB7eyB0ZW1wbGF0ZSAiaHRtbC9faGVhZC5odG1sIiAkIH19CiAgPHRpdGxlPnt7LlZhbCAibmFtZSJ9fTwvdGl0bGU+CjwvaGVhZD4KPGJvZHkgY2xhc3M9InRoZW1lLWJhc2UtMDgiPgogIHt7IHRlbXBsYXRlICJodG1sL19zdHlsZXMuaHRtbCIgJCB9fQogIHt7IHRlbXBsYXRlICJodG1sL19hc2lkZS5odG1sIiAkIH19CiAgPG1haW4gY2xhc3M9ImNvbnRlbnQgY29udGFpbmVyIj4KICAgIDxkaXYgY2xhc3M9InBvc3QiPgogICAgICA8aDE+e3suVmFsICJuYW1lIn19PC9oMT4KICAgICAge3tpZiAkLlNob3dEYXRlfX0KICAgICAgPHRpbWU+e3suVmFsICJkYXRlIiB8IGRhdGV9fTwvdGltZT4KICAgICAge3tlbmR9fQogICAgICA8YXJ0aWNsZT57ey5WYWwgImRlc2MiIHwgaHRtbCB9fTwvYXJ0aWNsZT4KICAgIDwvZGl2PgogICAgPHRhYmxlPgogICAgICA8dHI+CiAgICAgICAge3t3aXRoIC5SZWYgInByZXYifX0KICAgICAgICA8dGQgd2lkdGg9IjUwJSI+CiAgICAgICAgICA8ZGl2PgogICAgICAgICAgICA8YSBocmVmPScve3suTXVzdC5WYWwgInNsdWcifX0nPlByZXY8L2E+CiAgICAgICAgICAgIDxkaXY+e3suTXVzdC5WYWwgIm5hbWUifX08L2Rpdj4KICAgICAgICAgIDwvZGl2PgogICAgICAgIDwvdGQ+CiAgICAgICAge3tlbmR9fQogICAgICAgIHt7d2l0aCAuUmVmICJuZXh0In19CiAgICAgICAgPHRkIHdpZHRoPSI1MCUiPgogICAgICAgICAgPGRpdj4KICAgICAgICAgICAgPGEgaHJlZj0nL3t7Lk11c3QuVmFsICJzbHVnIn19Jz5OZXh0PC9hPgogICAgICAgICAgICA8ZGl2Pnt7Lk11c3QuVmFsICJuYW1lIn19PC9kaXY+CiAgICAgICAgICA8L2Rpdj4KICAgICAgICA8L3RkPgogICAgICAgIHt7ZW5kfX0KICAgICAgPC90cj4KICAgIDwvdGFibGU+CiAgPC9tYWluPgo8L2JvZHk+CjwvaHRtbD4Ke3tlbmR9fQo=")

	tmpls["html/list.html"] = tostring("PCFET0NUWVBFIGh0bWw+CjxodG1sIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIj4KPGhlYWQ+CiAge3sgdGVtcGxhdGUgImh0bWwvX2hlYWQuaHRtbCIgJCB9fQogIDx0aXRsZT5UT0RPPC90aXRsZT4KICA8bWV0YSBuYW1lPSJkZXNjcmlwdGlvbiIgY29udGVudD0iVE9ETyIgLz4KPC9oZWFkPgo8Ym9keSBjbGFzcz0idGhlbWUtYmFzZS0wOCI+CiAgPHN0eWxlPgogICAge3sgdGVtcGxhdGUgImNzcy9wb29sZS5jc3MiIH19CiAgICB7eyB0ZW1wbGF0ZSAiY3NzL3N5bnRheC5jc3MiIH19CiAgICB7eyB0ZW1wbGF0ZSAiY3NzL2h5ZGUuY3NzIiB9fQogIDwvc3R5bGU+CiAge3sgdGVtcGxhdGUgImh0bWwvX2FzaWRlLmh0bWwiICQgfX0KICA8bWFpbiBjbGFzcz0iY29udGVudCBjb250YWluZXIiPgogICAgPGRpdiBjbGFzcz0icG9zdHMiPgogICAgICB7eyByYW5nZSAuTGlzdCB9fQogICAgICA8YXJ0aWNsZSBjbGFzcz0icG9zdCI+CiAgICAgICAgPGgxIGNsYXNzPSJwb3N0LXRpdGxlIj4KICAgICAgICAgIDxhIGhyZWY9Ii97ey5TbHVnfX0iPnt7Lk5hbWV9fTwvYT4KICAgICAgICA8L2gxPgogICAgICAgIDx0aW1lIGNsYXNzPSJwb3N0LWRhdGUiPnt7LlByZXR0eURhdGV9fTwvdGltZT4KICAgICAgICA8cD57ey5TaG9ydH19PC9wPgogICAgICAgIDxkaXYgY2xhc3M9InJlYWQtbW9yZS1saW5rIj4KICAgICAgICAgIDxhIGhyZWY9Ii97ey5TbHVnfX0iPlJlYWQgTW9yZeKApjwvYT4KICAgICAgICA8L2Rpdj4KICAgICAgPC9hcnRpY2xlPgogICAgICB7eyBlbmQgfX0KICAgIDwvZGl2PgogIDwvbWFpbj4KPC9ib2R5Pgo8L2h0bWw+Cg==")
	tmpls["html/list.html"] = tostring("PCFET0NUWVBFIGh0bWw+CjxodG1sIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hodG1sIj4KPGhlYWQ+CiAge3t0ZW1wbGF0ZSAiaHRtbC9faGVhZC5odG1sIiAkfX0KICA8dGl0bGU+e3suQXNpZGUuTXVzdC5WYWwgInN0cmluZzMifX08L3RpdGxlPgogIDxtZXRhIG5hbWU9ImRlc2NyaXB0aW9uIiBjb250ZW50PSJUT0RPIiAvPgo8L2hlYWQ+Cjxib2R5IGNsYXNzPSJ0aGVtZS1iYXNlLTA4Ij4KICB7e3RlbXBsYXRlICJodG1sL19zdHlsZXMuaHRtbCIgJH19CiAge3t0ZW1wbGF0ZSAiaHRtbC9fYXNpZGUuaHRtbCIgJH19CiAgPG1haW4gY2xhc3M9ImNvbnRlbnQgY29udGFpbmVyIj4KICAgIDxkaXYgY2xhc3M9InBvc3RzIj4KICAgICAge3tyYW5nZSAuTGlzdH19CiAgICAgICAge3t3aXRoIC5NdXN0fX0KICAgICAgICA8YXJ0aWNsZSBjbGFzcz0icG9zdCI+CiAgICAgICAgICA8aDEgY2xhc3M9InBvc3QtdGl0bGUiPgogICAgICAgICAgICA8YSBocmVmPScve3suVmFsICJzbHVnIn19Jz57ey5WYWwgIm5hbWUifX08L2E+CiAgICAgICAgICA8L2gxPgogICAgICAgICAgPHRpbWUgY2xhc3M9InBvc3QtZGF0ZSI+e3suVmFsICJkYXRlIiB8IGRhdGV9fTwvdGltZT4KICAgICAgICAgIDxwPnt7LlZhbCAic2hvcnQifX08L3A+CiAgICAgICAgICA8ZGl2IGNsYXNzPSJyZWFkLW1vcmUtbGluayI+CiAgICAgICAgICAgIDxhIGhyZWY9Jy97ey5WYWwgInNsdWcifX0nPlJlYWQgTW9yZeKApjwvYT4KICAgICAgICAgIDwvZGl2PgogICAgICAgIDwvYXJ0aWNsZT4KICAgICAgICB7e2VuZH19CiAgICAgIHt7ZW5kfX0KICAgIDwvZGl2PgogIDwvbWFpbj4KPC9ib2R5Pgo8L2h0bWw+Cg==")

	tmpls["img/_code-24px.svg"] = tostring("PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0Ij48cGF0aCBkPSJNMCAwaDI0djI0SDBWMHoiIGZpbGw9Im5vbmUiLz48cGF0aCBkPSJNOS40IDE2LjZMNC44IDEybDQuNi00LjZMOCA2bC02IDYgNiA2IDEuNC0xLjR6bTUuMiAwbDQuNi00LjYtNC42LTQuNkwxNiA2bDYgNi02IDYtMS40LTEuNHoiLz48L3N2Zz4=")


M pkg/cms/cms.go => pkg/cms/cms.go +32 -0
@@ 5,6 5,7 @@ import (
	"encoding/json"
	"errors"
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
)


@@ 35,6 36,14 @@ func (c *Content) Val(key string) (string, bool) {
	return val.FieldValue, true
}

func (c *Content) HTML(key string) (template.HTML, bool) {
	val, ok := c.valueMap[key]
	if !ok {
		return template.HTML(""), false
	}
	return template.HTML(val.FieldValue), true
}

func (c *Content) Ref(key string) (*Content, bool) {
	val, ok := c.valueMap[key]
	if !ok || val.FieldReference == nil {


@@ 59,6 68,29 @@ func (c *Content) List(key string) ([]Content, bool) {
	return list, true
}

type ContentMust struct {
	c Content
}

func (c *Content) Must() ContentMust {
	return ContentMust{Content{c.ContentID, c.ContentParentTypeID, c.ContentValues, c.valueMap}}
}

func (cm ContentMust) Val(key string) string {
	val, _ := cm.c.Val(key)
	return val
}

func (cm ContentMust) Ref(key string) *Content {
	val, _ := cm.c.Ref(key)
	return val
}

func (cm ContentMust) List(key string) []Content {
	val, _ := cm.c.List(key)
	return val
}

type ContentList struct {
	ContentList []Content
	ContentMore bool