baseURL: Optional. If non-empty, this is the root of your URL prefix for your Camlistore server. Useful for when running behind a reverse proxy. Should not end in a slash. e.g. https://yourserver.example.com
-
flickr: Optional, and doesn't do anything yet. Support for continuous import from Flickr. Enter two child keys: appKey and appSecret, which you can get by filling out this form.
+
flickr: Optional, and doesn't do anything yet. Support for continuous import from Flickr. Should be an API key and secret formatted as key:secret. Get yours by filling out this form.
https: if "true", HTTPS is used
From a6aeff034c9151220bcb5c5bdab71b5d7fa61e36 Mon Sep 17 00:00:00 2001
From: mpl
Date: Mon, 28 Oct 2013 16:59:07 +0100
Subject: [PATCH 2/3] server/publish.go: use go html template for publishing
Change-Id: Ic1c55d35e52e407864d339927b453de58a64e03c
---
config/dev-server-config.json | 2 +
doc/publishing/README | 17 ++
pkg/publish/types.go | 89 +++++++
pkg/server/publish.go | 398 +++++++++++++++++------------
pkg/serverconfig/genconfig.go | 29 +--
server/camlistored/ui/blog.html | 6 +
server/camlistored/ui/gallery.html | 67 +++++
website/content/docs/server-config | 17 +-
8 files changed, 430 insertions(+), 195 deletions(-)
create mode 100644 doc/publishing/README
create mode 100644 pkg/publish/types.go
create mode 100644 server/camlistored/ui/blog.html
create mode 100644 server/camlistored/ui/gallery.html
diff --git a/config/dev-server-config.json b/config/dev-server-config.json
index de29d7187..ae9f50161 100644
--- a/config/dev-server-config.json
+++ b/config/dev-server-config.json
@@ -24,6 +24,7 @@
"blobRoot": "/bs-and-maybe-also-index/",
"searchRoot": "/my-search/",
"cache": "/cache/",
+ "goTemplate": "blog.html",
"devBootstrapPermanodeUsing": "/sighelper/"
}
},
@@ -38,6 +39,7 @@
"scaledImage": "lrucache",
"css": ["pics.css"],
"js": ["pics.js"],
+ "goTemplate": "gallery.html",
"devBootstrapPermanodeUsing": "/sighelper/"
}
},
diff --git a/doc/publishing/README b/doc/publishing/README
new file mode 100644
index 000000000..2d11b0b86
--- /dev/null
+++ b/doc/publishing/README
@@ -0,0 +1,17 @@
+Camlistore uses Go html templates (http://golang.org/pkg/text/template/) to publish pages.
+
+Resources for publishing, such as go templates, javascript and css files should be placed in server/camlistored/ui/, so they can be served directly when using the dev server or automatically embedded when using camlistored directly.
+
+You can then specify those resources through the configuration file. For example, there already is a go template (gallery.html), javascript file (pics.js) and css file (pics.css) that work together to provide publishing for image galleries. The dev server config (config/dev-server-config.json) already uses them. Here is how one would use them in the server config ($HOME/.config/camlistore/server-config.json):
+
+"publish": {
+ "/pics/": {
+ "rootPermanode": "sha1-6cbe9e1c35e854eab028cba43d099d35ceae0de8",
+ "style": "pics.css",
+ "js": "pics.js",
+ "goTemplate": "gallery.html"
+ }
+}
+
+If you want to provide your own (Go) template, see http://camlistore.org/pkg/publish for the data structures and functions available to the template.
+
diff --git a/pkg/publish/types.go b/pkg/publish/types.go
new file mode 100644
index 000000000..8e2f43d29
--- /dev/null
+++ b/pkg/publish/types.go
@@ -0,0 +1,89 @@
+/*
+Copyright 2013 The Camlistore Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package publish exposes the types and functions that can be used
+// from a Go template, for publishing.
+package publish
+
+import (
+ "html/template"
+
+ "camlistore.org/pkg/search"
+)
+
+// SubjectPage is the data structure used when serving a
+// publishing template. It contains the functions that can be called
+// from the template.
+type SubjectPage struct {
+ Header func() *PageHeader
+ File func() *PageFile
+ Members func() *PageMembers
+}
+
+// PageHeader contains the data available to the template,
+// and relevant to the page header.
+type PageHeader struct {
+ Title string // Page title.
+ CSSFiles []string // Available CSS files.
+ JSDeps []string // Dependencies (for e.g closure) that can/should be included as javascript files.
+ CamliClosure template.JS // Closure namespace defined in the provided js. e.g camlistore.GalleryPage from pics.js
+ Subject string // Subject of this page (i.e the object which is described and published).
+ Meta string // All the metadata describing the subject of this page.
+ ViewerIsOwner bool // Whether the viewer of the page is also the owner of the displayed subject. (localhost check for now.)
+}
+
+// PageFile contains the file related data available to the subject template,
+// if the page describes some file contents.
+type PageFile struct {
+ FileName string
+ Size int64
+ MIMEType string
+ IsImage bool
+ DownloadURL string
+ ThumbnailURL string
+ DomID string
+ Nav func() *Nav
+}
+
+// Nav holds links to the previous, next, and parent elements,
+// when displaying members.
+type Nav struct {
+ ParentPath string
+ PrevPath string
+ NextPath string
+}
+
+// PageMembers contains the data relevant to the members if the published subject
+// is a permanode with members.
+type PageMembers struct {
+ SubjectPath string // URL prefix path to the subject (i.e the permanode).
+ ZipName string // Name of the downloadable zip file which contains all the members.
+ Members []*search.DescribedBlob // List of the members.
+ Description func(*search.DescribedBlob) string // Returns the description of the given member.
+ Title func(*search.DescribedBlob) string // Returns the title for the given member.
+ Path func(*search.DescribedBlob) string // Returns the url prefix path to the given the member.
+ DomID func(*search.DescribedBlob) string // Returns the Dom ID of the given member.
+ FileInfo func(*search.DescribedBlob) *MemberFileInfo // Returns some file info if the given member is a file permanode.
+}
+
+// MemberFileInfo contains the file related data available for each member,
+// if the member is the permanode for a file.
+type MemberFileInfo struct {
+ FileName string
+ FileDomID string
+ FilePath string
+ FileThumbnailURL string
+}
diff --git a/pkg/server/publish.go b/pkg/server/publish.go
index f2583a276..908609d71 100644
--- a/pkg/server/publish.go
+++ b/pkg/server/publish.go
@@ -23,6 +23,8 @@ import (
"errors"
"fmt"
"html"
+ "html/template"
+ "io/ioutil"
"log"
"net/http"
"net/url"
@@ -40,7 +42,7 @@ import (
"camlistore.org/pkg/httputil"
"camlistore.org/pkg/jsonconfig"
"camlistore.org/pkg/jsonsign/signhandler"
- "camlistore.org/pkg/osutil"
+ "camlistore.org/pkg/publish"
"camlistore.org/pkg/schema"
"camlistore.org/pkg/search"
uistatic "camlistore.org/server/camlistored/ui"
@@ -55,7 +57,10 @@ type PublishHandler struct {
Cache blobserver.Storage // or nil
sc ScaledImage // cache of scaled images, optional
- JSFiles, CSSFiles []string
+ CSSFiles []string
+ // goTemplate is the go html template used for publishing.
+ goTemplate *template.Template
+ closureName string // Name of the closure object used to decorate the published page.
handlerFinder blobserver.FindHandlerByTyper
@@ -80,8 +85,9 @@ func newPublishFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Han
handlerFinder: ld,
}
ph.RootName = conf.RequiredString("rootName")
- ph.JSFiles = conf.OptionalList("js")
+ jsFiles := conf.OptionalList("js")
ph.CSSFiles = conf.OptionalList("css")
+ goTemplateFile := conf.RequiredString("goTemplate")
blobRoot := conf.RequiredString("blobRoot")
searchRoot := conf.RequiredString("searchRoot")
cachePrefix := conf.OptionalString("cache", "")
@@ -179,9 +185,47 @@ func newPublishFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Han
return nil, fmt.Errorf(`Invalid "sourceRoot" value of %q: %v"`, ph.sourceRoot, err)
}
+ ph.goTemplate, err = goTemplate(goTemplateFile)
+ if err != nil {
+ return nil, err
+ }
+ ph.setClosureName(jsFiles)
+
return ph, nil
}
+func goTemplate(templateFile string) (*template.Template, error) {
+ if filepath.Base(templateFile) != templateFile {
+ hint := fmt.Sprintf("The file should either be embedded or placed in %s.",
+ filepath.FromSlash("server/camlistored/ui"))
+ return nil, fmt.Errorf("Unsupported path %v for template. %s", templateFile, hint)
+ }
+ f, err := uistatic.Files.Open(templateFile)
+ if err != nil {
+ return nil, fmt.Errorf("Could not open template %v: %v", templateFile, err)
+ }
+ defer f.Close()
+ templateBytes, err := ioutil.ReadAll(f)
+ if err != nil {
+ return nil, fmt.Errorf("Could not read template %v: %v", templateFile, err)
+ }
+
+ return template.Must(template.New("subject").Parse(string(templateBytes))), nil
+}
+
+// setClosureName sets ph.closureName with the first found closure
+// namespace provided in jsFiles.
+func (ph *PublishHandler) setClosureName(jsFiles []string) {
+ for _, v := range jsFiles {
+ if ph.closureName == "" {
+ if cl := camliClosurePage(v); cl != "" {
+ ph.closureName = cl
+ break
+ }
+ }
+ }
+}
+
func (ph *PublishHandler) makeClosureHandler(root string) (http.Handler, error) {
return makeClosureHandler(root, "publish")
}
@@ -412,7 +456,7 @@ func (pr *publishRequest) serveHTTP() {
switch pr.SubresourceType() {
case "":
- pr.serveSubject()
+ pr.serveSubjectTemplate()
case "b":
// TODO: download a raw blob
case "f": // file download
@@ -472,12 +516,7 @@ var provCamliRx = regexp.MustCompile(`^goog\.(provide)\(['"]camlistore\.(.*)['"]
// and if yes, if it provides a page in the camlistore namespace.
// It returns that page name, or the empty string otherwise.
func camliClosurePage(filename string) string {
- camliRootPath, err := osutil.GoPackagePath("camlistore.org")
- if err != nil {
- return ""
- }
- fullpath := filepath.Join(camliRootPath, "server", "camlistored", "ui", filename)
- f, err := os.Open(fullpath)
+ f, err := uistatic.Files.Open(filename)
if err != nil {
return ""
}
@@ -517,37 +556,6 @@ func (pr *publishRequest) serveZip() {
zh.ServeHTTP(pr.rw, pr.req)
}
-// serveHeader serves the html header with the relevant title, javascript
-// and css includes. It is meant to be called from serveSubject.
-func (pr *publishRequest) serveHeader(title, camliClosurePage string) {
- pr.pf("\n\n\n %s\n", html.EscapeString(title))
- if camliClosurePage != "" && pr.ViewerIsOwner() {
- pr.pf(" \n", pr.staticPath("closure/goog/base.js"))
- pr.pf(" \n", pr.staticPath("deps.js"))
- pr.pf(" \n", pr.base+"?camli.mode=config&var=CAMLISTORE_CONFIG")
- pr.pf(" \n", pr.staticPath("base64.js"))
- pr.pf(" \n", pr.staticPath("Crypto.js"))
- pr.pf(" \n", pr.staticPath("SHA1.js"))
- pr.pf("\n", camliClosurePage)
- }
- for _, filename := range pr.ph.CSSFiles {
- pr.pf(" \n", pr.staticPath(filename))
- }
-}
-
-// serveMeta serves all the described meta data about the published items,
-// within the html header. It is meant to be called from serveSubject.
-func (pr *publishRequest) serveMeta(des map[string]*search.DescribedBlob) {
- pr.pf(" \n\n")
-}
-
-// TODO(mpl): use those everywhere else
const (
resSeparator = "/-"
digestPrefix = "h"
@@ -630,28 +638,115 @@ func (pr *publishRequest) parent() (parentPath string, parentBlobRef blob.Ref, e
return parentPath, parentBlobRef, nil
}
-// serveNav serves some navigation links (prev, next, up) if the
-// pr.subject is member of a collection (its parent has members).
-// It is meant to be called from serveFile.
-func (pr *publishRequest) serveNav() error {
+func (pr *publishRequest) cssFiles() []string {
+ files := []string{}
+ for _, filename := range pr.ph.CSSFiles {
+ files = append(files, pr.staticPath(filename))
+ }
+ return files
+}
+
+// jsDeps returns the list of paths that should be included
+// as javascript files in the published page to enable and use
+// additional javascript closure code.
+func (pr *publishRequest) jsDeps() []string {
+ var js []string
+ closureDeps := []string{
+ "closure/goog/base.js",
+ "deps.js",
+ // TODO(mpl): fix the deps generator and/or the SHA1.js etc files so they get into deps.js and we
+ // do not even have to include them here. detection fails for them because the provide statement
+ // is not at the beginning of the line.
+ // Not doing it right away because it might have consequences for the rest of the ui I suppose.
+ "base64.js",
+ "Crypto.js",
+ "SHA1.js",
+ }
+ for _, v := range closureDeps {
+ js = append(js, pr.staticPath(v))
+ }
+ js = append(js, pr.base+"?camli.mode=config&var=CAMLISTORE_CONFIG")
+ return js
+}
+
+// subjectHeader returns the PageHeader corresponding to the described subject.
+func (pr *publishRequest) subjectHeader(described map[string]*search.DescribedBlob) *publish.PageHeader {
+ subdes := described[pr.subject.String()]
+ header := &publish.PageHeader{
+ Title: html.EscapeString(subdes.Title()),
+ CSSFiles: pr.cssFiles(),
+ Meta: func() string {
+ jsonRes, _ := json.MarshalIndent(described, "", " ")
+ return string(jsonRes)
+ }(),
+ Subject: pr.subject.String(),
+ }
+ header.JSDeps = pr.jsDeps()
+ header.CamliClosure = template.JS("camlistore." + pr.ph.closureName)
+ if pr.ViewerIsOwner() {
+ header.ViewerIsOwner = true
+ }
+ return header
+}
+
+// subjectFile returns the relevant PageFile if the described subject is a file permanode.
+func (pr *publishRequest) subjectFile(described map[string]*search.DescribedBlob) (*publish.PageFile, error) {
+ subdes := described[pr.subject.String()]
+ contentRef, ok := subdes.ContentRef()
+ if !ok {
+ return nil, nil
+ }
+ fileDes, err := pr.dr.DescribeSync(contentRef)
+ if err != nil {
+ return nil, fmt.Errorf("Could not describe %v: %v", contentRef, err)
+ }
+ path := []blob.Ref{pr.subject, contentRef}
+ downloadURL := pr.SubresFileURL(path, fileDes.File.FileName)
+ thumbnailURL := ""
+ if fileDes.File.IsImage() {
+ thumbnailURL = pr.SubresThumbnailURL(path, fileDes.File.FileName, 600)
+ }
+ fileName := html.EscapeString(fileDes.File.FileName)
+ return &publish.PageFile{
+ FileName: fileName,
+ Size: fileDes.File.Size,
+ MIMEType: fileDes.File.MIMEType,
+ IsImage: fileDes.File.IsImage(),
+ DownloadURL: downloadURL,
+ ThumbnailURL: thumbnailURL,
+ DomID: contentRef.DomID(),
+ Nav: func() *publish.Nav {
+ nv, err := pr.fileNavigation()
+ if err != nil {
+ log.Print(err)
+ return nil
+ }
+ return nv
+ },
+ }, nil
+}
+
+func (pr *publishRequest) fileNavigation() (*publish.Nav, error) {
// first get the parent path and blob
parentPath, parentbr, err := pr.parent()
if err != nil {
- return fmt.Errorf("Errors building nav links for %s: %v", pr.subject, err)
+ return nil, fmt.Errorf("Could not get subject %v's parent's info: %v", pr.subject, err)
+ }
+ parentNav := strings.TrimSuffix(parentPath, resSeparator)
+ fileNav := &publish.Nav{
+ ParentPath: parentNav,
}
- parentNav := fmt.Sprintf("[up]", strings.TrimSuffix(parentPath, resSeparator))
// describe the parent so we get the siblings (members of the parent)
dr := pr.ph.Search.NewDescribeRequest()
dr.Describe(parentbr, 3)
parentRes, err := dr.Result()
if err != nil {
- return fmt.Errorf("Errors loading %s, permanode %s: %v, %#v", pr.req.URL, pr.subject, err, err)
+ return nil, fmt.Errorf("Could not \"deeply\" describe subject %v's parent %v: %v", pr.subject, parentbr, err)
}
members := parentRes[parentbr.String()].Members()
if len(members) == 0 {
- pr.pf("
",
- cref.DomID(),
- downloadURL)
- }
- if strings.Contains(pr.subjectBasePath, resSeparator) {
- // this permanode has a "parent" collection.
- // so we send a deep request on the parent in order to get some info
- // about the siblings and build some "prev" and "next" nav links.
- // TODO(mpl): nav links everywhere, not just when showing a permanode
- // with some content.
- err := pr.serveNav()
- if err != nil {
- pr.pf("
Error building navs links
")
- return err
- }
- }
- return nil
-}
-
-// serveMembers serves the relevant view when the subject in serveSubject
-// is a collection (permanode with members). It is meant to be called
-// from serveSubject.
-func (pr *publishRequest) serveMembers(title string, members []*search.DescribedBlob) {
zipName := ""
- if title == "" {
+ if title := subdes.Title(); title == "" {
zipName = "download.zip"
} else {
zipName = title + ".zip"
@@ -738,91 +792,95 @@ func (pr *publishRequest) serveMembers(title string, members []*search.Described
if !strings.Contains(subjectPath, "/-/") {
subjectPath += "/-"
}
- pr.pf("
\n")
- for _, member := range members {
- des := member.Description()
- if des != "" {
- des = " - " + des
- }
- var fileLink, thumbnail string
- if path, fileInfo, ok := member.PermanodeFile(); ok {
- fileLink = fmt.Sprintf("
\n")
+ return des
+ },
+ Title: func(member *search.DescribedBlob) string {
+ memberTitle := member.Title()
+ if memberTitle == "" {
+ memberTitle = member.BlobRef.DigestPrefix(10)
+ }
+ return html.EscapeString(memberTitle)
+ },
+ Path: func(member *search.DescribedBlob) string {
+ return pr.memberPath(member.BlobRef)
+ },
+ DomID: func(member *search.DescribedBlob) string {
+ return member.DomID()
+ },
+ FileInfo: func(member *search.DescribedBlob) *publish.MemberFileInfo {
+ if path, fileInfo, ok := member.PermanodeFile(); ok {
+ info := &publish.MemberFileInfo{
+ FileName: fileInfo.FileName,
+ FileDomID: path[len(path)-1].DomID(),
+ FilePath: html.EscapeString(pr.SubresFileURL(path, fileInfo.FileName)),
+ }
+ if fileInfo.IsImage() {
+ info.FileThumbnailURL = pr.SubresThumbnailURL(path, fileInfo.FileName, 200)
+ }
+ return info
+ }
+ return nil
+ },
+ }, nil
}
-func (pr *publishRequest) serveSubject() {
+// serveSubjectTemplate creates the funcs to generate the PageHeader, PageFile,
+// and pageMembers that can be used by the subject template, and serves the template.
+func (pr *publishRequest) serveSubjectTemplate() {
dr := pr.ph.Search.NewDescribeRequest()
dr.Describe(pr.subject, 3)
res, err := dr.Result()
if err != nil {
log.Printf("Errors loading %s, permanode %s: %v, %#v", pr.req.URL, pr.subject, err, err)
- pr.pf("
Errors loading.
")
+ http.Error(pr.rw, "Error loading describe request", http.StatusInternalServerError)
return
}
-
subdes := res[pr.subject.String()]
-
if subdes.CamliType == "file" {
pr.serveFileDownload(subdes)
return
}
- title := subdes.Title()
-
- // HTML header + Javascript
- var camliPage string
- // TODO(mpl): We are only using the first .js file, and expecting it to be
- // using closure. We want to be more customizable in the long run and enable
- // some sort of templating mechanism.
- if len(pr.ph.JSFiles) > 0 {
- camliPage = camliClosurePage(pr.ph.JSFiles[0])
+ headerFunc := func() *publish.PageHeader {
+ return pr.subjectHeader(res)
}
- pr.serveHeader(title, camliPage)
- pr.serveMeta(res)
- pr.pf("\n")
- if title != "" {
- pr.pf("
%s
\n", html.EscapeString(title))
- }
- defer pr.pf("\n\n")
-
- if cref, ok := subdes.ContentRef(); ok {
- err = pr.serveFile(cref)
+ fileFunc := func() *publish.PageFile {
+ file, err := pr.subjectFile(res)
if err != nil {
- log.Print(err)
- return
+ log.Printf("%v", err)
+ return nil
}
- } else {
- if members := subdes.Members(); len(members) > 0 {
- pr.serveMembers(title, members)
+ return file
+ }
+ membersFunc := func() *publish.PageMembers {
+ members, err := pr.subjectMembers(res)
+ if err != nil {
+ log.Printf("%v", err)
+ return nil
}
+ return members
+ }
+ page := &publish.SubjectPage{
+ Header: headerFunc,
+ File: fileFunc,
+ Members: membersFunc,
}
- if camliPage != "" && pr.ViewerIsOwner() {
- pr.pf("\n")
+ err = pr.ph.goTemplate.Execute(pr.rw, page)
+ if err != nil {
+ log.Printf("Error serving subject template: %v", err)
+ http.Error(pr.rw, "Error serving template", http.StatusInternalServerError)
+ return
}
}
diff --git a/pkg/serverconfig/genconfig.go b/pkg/serverconfig/genconfig.go
index 1f13de028..c67933921 100644
--- a/pkg/serverconfig/genconfig.go
+++ b/pkg/serverconfig/genconfig.go
@@ -61,7 +61,7 @@ func addPublishedConfig(prefixes jsonconfig.Obj,
return nil, fmt.Errorf("Wrong type for %s; was expecting map[string]interface{}, got %T", k, v)
}
rootName := strings.Replace(k, "/", "", -1) + "Root"
- rootPermanode, template, style := "", "", ""
+ rootPermanode, goTemplate, style, js := "", "", "", ""
for pk, pv := range p {
val, ok := pv.(string)
if !ok {
@@ -70,16 +70,18 @@ func addPublishedConfig(prefixes jsonconfig.Obj,
switch pk {
case "rootPermanode":
rootPermanode = val
- case "template":
- template = val
+ case "goTemplate":
+ goTemplate = val
case "style":
style = val
+ case "js":
+ js = val
default:
return nil, fmt.Errorf("Unexpected key %q in config for %s", pk, k)
}
}
- if rootPermanode == "" || template == "" {
- return nil, fmt.Errorf("Missing key in configuration for %s, need \"rootPermanode\" and \"template\"", k)
+ if rootPermanode == "" || goTemplate == "" {
+ return nil, fmt.Errorf("Missing key in configuration for %s, need \"rootPermanode\" and \"goTemplate\"", k)
}
ob := map[string]interface{}{}
ob["handler"] = "publish"
@@ -93,19 +95,10 @@ func addPublishedConfig(prefixes jsonconfig.Obj,
if sourceRoot != "" {
handlerArgs["sourceRoot"] = sourceRoot
}
- switch template {
- case "gallery":
- if style == "" {
- style = "pics.css"
- }
- handlerArgs["css"] = []interface{}{style}
- handlerArgs["js"] = []interface{}{"pics.js"}
- handlerArgs["scaledImage"] = "lrucache"
- case "blog":
- if style != "" {
- handlerArgs["css"] = []interface{}{style}
- }
- }
+ handlerArgs["goTemplate"] = goTemplate
+ handlerArgs["css"] = []interface{}{style}
+ handlerArgs["js"] = []interface{}{js}
+ handlerArgs["scaledImage"] = "lrucache"
ob["handlerArgs"] = handlerArgs
prefixes[k] = ob
pubPrefixes = append(pubPrefixes, k)
diff --git a/server/camlistored/ui/blog.html b/server/camlistored/ui/blog.html
new file mode 100644
index 000000000..f00297814
--- /dev/null
+++ b/server/camlistored/ui/blog.html
@@ -0,0 +1,6 @@
+
+
+
+ TODO
+
+
diff --git a/server/camlistored/ui/gallery.html b/server/camlistored/ui/gallery.html
new file mode 100644
index 000000000..85dbab549
--- /dev/null
+++ b/server/camlistored/ui/gallery.html
@@ -0,0 +1,67 @@
+
+
+{{if $header := call .Header}}
+
+ {{$header.Title}}
+ {{range $js := $header.JSDeps}}
+
+ {{end}}
+ {{if $header.CamliClosure}}
+
+ {{end}}
+ {{range $css := $header.CSSFiles}}
+
+ {{end}}
+
+
+
+
{{$header.Title}}
+ {{if $file := call .File}}
+
File: {{$file.FileName}}, {{$file.Size}} bytes, type {{$file.MIMEType}}
+ {{end}}
+ {{end}}
+ {{if $header.CamliClosure}}
+ {{if $header.ViewerIsOwner}}
+
+ {{end}}
+ {{end}}
+{{end}}
+
+
diff --git a/website/content/docs/server-config b/website/content/docs/server-config
index 18cc8b651..ae9ced053 100644
--- a/website/content/docs/server-config
+++ b/website/content/docs/server-config
@@ -65,18 +65,21 @@ to pkg/genconfig welcome.
config, as used by devcam server.
Publishing options
-
Although limited, publishing can be configured through the publish key. There is only support for an image gallery view (even though it will display thumbnails for other kinds of items), which is not really customizable. Here is an example of a value if one wanted to publish some items under /pics/:
+
Camlistore uses Go html templates to publish pages, and publishing can be configured through the publish key. There is already support for an image gallery view, which can be enabled similarly to the example below (obviously, the rootPermanode will be different).