Prerender index page

This commit is contained in:
Ken-Håvard Lieng 2018-12-17 14:41:24 +01:00
parent fc643483be
commit 6fedb23363
41 changed files with 5442 additions and 118 deletions

View file

@ -1,26 +0,0 @@
// Generated by egon.
// 🚫Edit at your own risk.
package server
import (
"io"
)
func IndexTemplate(w io.Writer, cssPath string, inlineScript string, scripts []string) error {
io.WriteString(w, "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><meta name=\"theme-color\" content=\"#f0f0f0\"><title>Dispatch</title><meta name=\"description\" content=\"Web-based IRC client.\"><link rel=\"preload\" href=\"/init\" as=\"fetch\" crossorigin><script>")
io.WriteString(w, inlineScript )
io.WriteString(w, "</script><link rel=\"preload\" href=\"/font/fontello.woff2?48901973\" as=\"font\" type=\"font/woff2\" crossorigin><link rel=\"preload\" href=\"/font/RobotoMono-Regular.woff2\" as=\"font\" type=\"font/woff2\" crossorigin><link rel=\"preload\" href=\"/font/Montserrat-Regular.woff2\" as=\"font\" type=\"font/woff2\" crossorigin><link rel=\"preload\" href=\"/font/Montserrat-Bold.woff2\" as=\"font\" type=\"font/woff2\" crossorigin><link rel=\"preload\" href=\"/font/RobotoMono-Bold.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>")
if cssPath != "" {
io.WriteString(w, "<link href=\"/")
io.WriteString(w, cssPath )
io.WriteString(w, "\" rel=\"stylesheet\">")
}
io.WriteString(w, "<link rel=\"manifest\" href=\"/manifest.json\"></head><body><div id=\"root\"></div>")
for _, script := range scripts {
io.WriteString(w, "<script src=\"/")
io.WriteString(w, script )
io.WriteString(w, "\"></script>")
}
io.WriteString(w, "<noscript>This page needs JavaScript enabled to function.</noscript></body></html>")
return nil
}

View file

@ -1,5 +1,12 @@
<%! cssPath string, inlineScript string, scripts []string %>
package server
type indexTemplateData struct {
InlineScript string
CSSPath string
Scripts []string
}
const indexTemplate = `
<!DOCTYPE html>
<html lang="en">
@ -13,9 +20,9 @@
<link rel="preload" href="/init" as="fetch" crossorigin>
<script>
<%== inlineScript %>
</script>
{{if .InlineScript}}
<script>{{.InlineScript}}</script>
{{end}}
<link rel="preload" href="/font/fontello.woff2?48901973" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/font/RobotoMono-Regular.woff2" as="font" type="font/woff2" crossorigin>
@ -23,9 +30,9 @@
<link rel="preload" href="/font/Montserrat-Bold.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/font/RobotoMono-Bold.woff2" as="font" type="font/woff2" crossorigin>
<% if cssPath != "" { %>
<link href="/<%== cssPath %>" rel="stylesheet">
<% } %>
{{if .CSSPath}}
<link href="/{{.CSSPath}}" rel="stylesheet">
{{end}}
<link rel="manifest" href="/manifest.json">
</head>
@ -33,11 +40,11 @@
<body>
<div id="root"></div>
<% for _, script := range scripts { %>
<script src="/<%== script %>"></script>
<% } %>
{{range .Scripts}}
<script src="/{{.}}"></script>
{{end}}
<noscript>This page needs JavaScript enabled to function.</noscript>
</body>
</html>
</html>`

View file

@ -12,18 +12,23 @@ import (
"path/filepath"
"strconv"
"strings"
"text/template"
"time"
"github.com/dsnet/compress/brotli"
"github.com/khlieng/dispatch/assets"
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/html"
)
const longCacheControl = "public, max-age=31536000, immutable"
const disabledCacheControl = "no-cache, no-store, must-revalidate"
type File struct {
Asset string
GzipAsset []byte
Data []byte
Length string
GzipData []byte
GzipLength string
Hash string
ContentType string
CacheControl string
@ -45,9 +50,8 @@ func newH2PushAsset(name string) h2PushAsset {
var (
files = map[string]*File{}
indexStylesheet string
indexScripts []string
inlineScript string
indexPage []byte
indexPageLen string
inlineScriptSha256 string
serviceWorker []byte
@ -75,20 +79,22 @@ func (d *Dispatch) initFileServer() {
cfg := d.Config()
if cfg.Dev {
indexScripts = []string{"boot.js", "main.js"}
renderIndexPage(indexTemplateData{
Scripts: []string{"boot.js", "main.js"},
})
} else {
bootloader := decompressedAsset(findAssetName("boot*.js"))
runtime := decompressedAsset(findAssetName("runtime*.js"))
inlineScript = string(bootloader) + string(runtime)
inlineScript := string(bootloader) + string(runtime)
hash := sha256.New()
hash.Write(bootloader)
hash.Write(runtime)
inlineScriptSha256 = base64.StdEncoding.EncodeToString(hash.Sum(nil))
indexStylesheet = findAssetName("main*.css")
indexScripts = []string{
indexStylesheet := findAssetName("main*.css")
indexScripts := []string{
findAssetName("vendors*.js"),
findAssetName("main*.js"),
}
@ -120,27 +126,33 @@ func (d *Dispatch) initFileServer() {
}
file := &File{
Asset: asset,
ContentType: contentTypes[filepath.Ext(assetName)],
CacheControl: longCacheControl,
Compressed: strings.HasSuffix(asset, ".br"),
}
if file.Compressed {
data, err := assets.Asset(file.Asset)
if err != nil {
log.Fatal(err)
}
data, err := assets.Asset(asset)
fatalErr(err)
file.Data = data
file.Length = strconv.Itoa(len(data))
file.GzipAsset = gzipAsset(data)
if file.Compressed {
file.GzipData = gzipAsset(data)
file.GzipLength = strconv.Itoa(len(file.GzipData))
}
files["/"+assetName] = file
}
renderIndexPage(indexTemplateData{
CSSPath: indexStylesheet,
InlineScript: inlineScript,
Scripts: indexScripts,
})
serviceWorker = decompressedAsset("sw.js")
hash.Reset()
IndexTemplate(hash, indexStylesheet, inlineScript, indexScripts)
hash.Write(indexPage)
indexHash := base64.StdEncoding.EncodeToString(hash.Sum(nil))
serviceWorker = append(serviceWorker, []byte(`
@ -165,6 +177,27 @@ workbox.routing.registerNavigationRoute('/');`)...)
}
}
func renderIndexPage(data indexTemplateData) {
tmpl, err := template.New("").Parse(indexTemplate)
fatalErr(err)
m := minify.New()
m.AddFunc("text/html", html.Minify)
buf := &bytes.Buffer{}
gzw, err := gzip.NewWriterLevel(buf, gzip.BestCompression)
fatalErr(err)
mw := m.Writer("text/html", gzw)
fatalErr(tmpl.Execute(mw, data))
fatalErr(mw.Close())
fatalErr(gzw.Close())
indexPage = buf.Bytes()
indexPageLen = strconv.Itoa(len(indexPage))
}
func findAssetName(glob string) string {
for _, assetName := range assets.AssetNames() {
assetName = strings.TrimSuffix(assetName, ".br")
@ -178,9 +211,7 @@ func findAssetName(glob string) string {
func decompressAsset(data []byte) []byte {
br, err := brotli.NewReader(bytes.NewReader(data), nil)
if err != nil {
log.Fatal(err)
}
fatalErr(err)
buf := &bytes.Buffer{}
io.Copy(buf, br)
@ -189,23 +220,17 @@ func decompressAsset(data []byte) []byte {
func decompressedAsset(name string) []byte {
asset, err := assets.Asset(name + ".br")
if err != nil {
log.Fatal(err)
}
fatalErr(err)
return decompressAsset(asset)
}
func gzipAsset(data []byte) []byte {
br, err := brotli.NewReader(bytes.NewReader(data), nil)
if err != nil {
log.Fatal(err)
}
fatalErr(err)
buf := &bytes.Buffer{}
gzw, err := gzip.NewWriterLevel(buf, gzip.BestCompression)
if err != nil {
log.Fatal(err)
}
fatalErr(err)
io.Copy(gzw, br)
gzw.Close()
@ -215,30 +240,20 @@ func gzipAsset(data []byte) []byte {
func (d *Dispatch) serveFiles(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
d.serveIndex(w, r)
return
}
if file, ok := files[r.URL.Path]; ok {
} else if file, ok := files[r.URL.Path]; ok {
d.serveFile(w, r, file)
return
}
if r.URL.Path == "/sw.js" {
} else if r.URL.Path == "/sw.js" {
w.Header().Set("Cache-Control", disabledCacheControl)
w.Header().Set("Content-Type", "text/javascript")
w.Header().Set("Content-Length", strconv.Itoa(len(serviceWorker)))
w.Write(serviceWorker)
return
}
if r.URL.Path == "/robots.txt" {
} else if r.URL.Path == "/robots.txt" {
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Content-Length", strconv.Itoa(len(robots)))
w.Write(robots)
return
} else {
d.serveIndex(w, r)
}
d.serveIndex(w, r)
}
func (d *Dispatch) serveIndex(w http.ResponseWriter, r *http.Request) {
@ -310,12 +325,10 @@ func (d *Dispatch) serveIndex(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
w.Header().Set("Content-Encoding", "gzip")
gzw := getGzipWriter(w)
IndexTemplate(gzw, indexStylesheet, inlineScript, indexScripts)
putGzipWriter(gzw)
w.Header().Set("Content-Length", indexPageLen)
w.Write(indexPage)
} else {
IndexTemplate(w, indexStylesheet, inlineScript, indexScripts)
serveDecompressed(w, indexPage)
}
}
@ -331,38 +344,39 @@ func setPushCookie(w http.ResponseWriter, r *http.Request) {
}
func (d *Dispatch) serveFile(w http.ResponseWriter, r *http.Request, file *File) {
data, err := assets.Asset(file.Asset)
w.Header().Set("Cache-Control", file.CacheControl)
w.Header().Set("Content-Type", file.ContentType)
if file.Compressed && strings.Contains(r.Header.Get("Accept-Encoding"), "br") {
w.Header().Set("Content-Encoding", "br")
w.Header().Set("Content-Length", file.Length)
w.Write(file.Data)
} else if file.Compressed && strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
w.Header().Set("Content-Encoding", "gzip")
w.Header().Set("Content-Length", file.GzipLength)
w.Write(file.GzipData)
} else if !file.Compressed {
w.Header().Set("Content-Length", file.Length)
w.Write(file.Data)
} else {
serveDecompressed(w, file.GzipData)
}
}
func serveDecompressed(w http.ResponseWriter, asset []byte) {
gzr, err := gzip.NewReader(bytes.NewReader(asset))
buf, err := ioutil.ReadAll(gzr)
if err != nil {
http.Error(w, "", http.StatusInternalServerError)
return
}
if file.CacheControl != "" {
w.Header().Set("Cache-Control", file.CacheControl)
}
w.Header().Set("Content-Length", strconv.Itoa(len(buf)))
w.Write(buf)
}
w.Header().Set("Content-Type", file.ContentType)
if file.Compressed && strings.Contains(r.Header.Get("Accept-Encoding"), "br") {
w.Header().Set("Content-Encoding", "br")
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
w.Write(data)
} else if file.Compressed && strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
w.Header().Set("Content-Encoding", "gzip")
w.Header().Set("Content-Length", strconv.Itoa(len(file.GzipAsset)))
w.Write(file.GzipAsset)
} else if !file.Compressed {
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
w.Write(data)
} else {
gzr, err := gzip.NewReader(bytes.NewReader(file.GzipAsset))
buf, err := ioutil.ReadAll(gzr)
if err != nil {
http.Error(w, "", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Length", strconv.Itoa(len(buf)))
w.Write(buf)
func fatalErr(err error) {
if err != nil {
log.Fatal(err)
}
}