mirror of https://github.com/jlelse/GoBlog
Various changes (SRI for JS, HTTP server init, ect.)
This commit is contained in:
parent
7f8ea2d94b
commit
33684a7940
11
go.mod
11
go.mod
|
@ -8,7 +8,7 @@ require (
|
|||
github.com/andybalholm/cascadia v1.2.0 // indirect
|
||||
github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
github.com/caddyserver/certmagic v0.12.0
|
||||
github.com/caddyserver/certmagic v0.13.0
|
||||
github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca
|
||||
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
|
||||
github.com/dgraph-io/ristretto v0.0.4-0.20210311064603-e4f298c8aa88
|
||||
|
@ -31,12 +31,10 @@ require (
|
|||
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
|
||||
github.com/lestrrat-go/strftime v1.0.4 // indirect
|
||||
github.com/lib/pq v1.9.0 // indirect
|
||||
github.com/libdns/libdns v0.2.0 // indirect
|
||||
github.com/lopezator/migrator v0.3.0
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.6
|
||||
github.com/mholt/acmez v0.1.3 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.4
|
||||
github.com/microcosm-cc/bluemonday v1.0.5
|
||||
github.com/miekg/dns v1.1.41 // indirect
|
||||
github.com/mitchellh/go-server-timing v1.0.1
|
||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||
|
@ -60,10 +58,11 @@ require (
|
|||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect
|
||||
golang.org/x/mod v0.4.1 // indirect
|
||||
golang.org/x/net v0.0.0-20210326060303-6b1517762897 // indirect
|
||||
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 // indirect
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
|
||||
golang.org/x/text v0.3.5 // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
|
23
go.sum
23
go.sum
|
@ -43,8 +43,8 @@ github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBW
|
|||
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
|
||||
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
|
||||
github.com/caddyserver/certmagic v0.12.0 h1:1f7kxykaJkOVVpXJ8ZrC6RAO5F6+kKm9U7dBFbLNeug=
|
||||
github.com/caddyserver/certmagic v0.12.0/go.mod h1:tr26xh+9fY5dN0J6IPAlMj07qpog22PJKa7Nw7j835U=
|
||||
github.com/caddyserver/certmagic v0.13.0 h1:ky0rntZvIFiUKFdIikYxj31WN+Ts0Od6Wjz83iTzxfc=
|
||||
github.com/caddyserver/certmagic v0.13.0/go.mod h1:dNOzF4iOB7H9E51xTooMB90vs+2XNVtpnx0liQNsQY4=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
|
||||
|
@ -210,7 +210,6 @@ github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR7
|
|||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
|
||||
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/libdns/libdns v0.1.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||
github.com/libdns/libdns v0.2.0 h1:ewg3ByWrdUrxrje8ChPVMBNcotg7H9LQYg+u5De2RzI=
|
||||
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||
github.com/lopezator/migrator v0.3.0 h1:VW/rR+J8NYwPdkBxjrFdjwejpgvP59LbmANJxXuNbuk=
|
||||
|
@ -230,11 +229,10 @@ github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGw
|
|||
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mholt/acmez v0.1.1/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM=
|
||||
github.com/mholt/acmez v0.1.3 h1:J7MmNIk4Qf9b8mAGqAh4XkNeowv3f1zW816yf4zt7Qk=
|
||||
github.com/mholt/acmez v0.1.3/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM=
|
||||
github.com/microcosm-cc/bluemonday v1.0.4 h1:p0L+CTpo/PLFdkoPcJemLXG+fpMD7pYOoDEq1axMbGg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.4/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.5 h1:cF59UCKMmmUgqN1baLvqU/B1ZsMori+duLVTLpgiG3w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.5/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
|
||||
|
@ -419,8 +417,8 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs=
|
||||
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
|
||||
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c h1:KHUzaHIpjWVlVVNh65G3hhuj3KB1HnjY6Cq5cTvRQT8=
|
||||
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -455,8 +453,9 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210324051608-47abb6519492 h1:Paq34FxTluEPvVyayQqMPgHm+vTOrIifmcYxFBx9TLg=
|
||||
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 h1:rF3Ohx8DRyl8h2zw9qojyLHLhrJpEMgyPOImREEryf0=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
|
@ -464,8 +463,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
|
47
http.go
47
http.go
|
@ -4,6 +4,7 @@ import (
|
|||
"compress/flate"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
@ -74,30 +75,56 @@ func startServer() (err error) {
|
|||
log.Println("Tor failed:", torErr.Error())
|
||||
}()
|
||||
}
|
||||
// Start HTTP(s) server
|
||||
localAddress := ":" + strconv.Itoa(appConfig.Server.Port)
|
||||
// Start server
|
||||
s := &http.Server{
|
||||
Handler: finalHandler,
|
||||
ReadTimeout: 5 * time.Minute,
|
||||
WriteTimeout: 5 * time.Minute,
|
||||
}
|
||||
if appConfig.Server.PublicHTTPS {
|
||||
// Configure
|
||||
certmagic.Default.Storage = &certmagic.FileStorage{Path: "data/https"}
|
||||
certmagic.DefaultACME.Agreed = true
|
||||
certmagic.DefaultACME.Email = appConfig.Server.LetsEncryptMail
|
||||
certmagic.DefaultACME.CA = certmagic.LetsEncryptProductionCA
|
||||
// Start HTTP server for TLS verification and redirect
|
||||
httpServer := &http.Server{
|
||||
Addr: ":http",
|
||||
Handler: http.HandlerFunc(redirectToHttps),
|
||||
ReadTimeout: 5 * time.Minute,
|
||||
WriteTimeout: 5 * time.Minute,
|
||||
}
|
||||
go func() {
|
||||
if err := httpServer.ListenAndServe(); err != nil {
|
||||
log.Println("Failed to start HTTP server:", err.Error())
|
||||
}
|
||||
}()
|
||||
// Start HTTPS
|
||||
s.Addr = ":https"
|
||||
hosts := []string{appConfig.Server.publicHostname}
|
||||
if appConfig.Server.shortPublicHostname != "" {
|
||||
hosts = append(hosts, appConfig.Server.shortPublicHostname)
|
||||
}
|
||||
err = certmagic.HTTPS(hosts, finalHandler)
|
||||
} else {
|
||||
s := &http.Server{
|
||||
Addr: localAddress,
|
||||
Handler: finalHandler,
|
||||
ReadTimeout: 5 * time.Minute,
|
||||
WriteTimeout: 5 * time.Minute,
|
||||
listener, e := certmagic.Listen(hosts)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
err = s.Serve(listener)
|
||||
} else {
|
||||
s.Addr = ":" + strconv.Itoa(appConfig.Server.Port)
|
||||
err = s.ListenAndServe()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func redirectToHttps(w http.ResponseWriter, r *http.Request) {
|
||||
requestHost, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
requestHost = r.Host
|
||||
}
|
||||
w.Header().Set("Connection", "close")
|
||||
http.Redirect(w, r, fmt.Sprintf("https://%s%s", requestHost, r.URL.RequestURI()), http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
func reloadRouter() error {
|
||||
h, err := buildDynamicRouter()
|
||||
if err != nil {
|
||||
|
|
|
@ -116,8 +116,9 @@ func initRendering() error {
|
|||
}
|
||||
return d.Before(b)
|
||||
},
|
||||
"asset": assetFileName,
|
||||
"string": getTemplateStringVariant,
|
||||
"asset": assetFileName,
|
||||
"assetsri": assetSRI,
|
||||
"string": getTemplateStringVariant,
|
||||
"include": func(templateName string, data ...interface{}) (template.HTML, error) {
|
||||
if len(data) == 0 || len(data) > 2 {
|
||||
return "", errors.New("wrong argument count")
|
||||
|
|
|
@ -2,7 +2,10 @@ package main
|
|||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -18,6 +21,7 @@ var assetFiles map[string]*assetFile = map[string]*assetFile{}
|
|||
|
||||
type assetFile struct {
|
||||
contentType string
|
||||
sri string
|
||||
body []byte
|
||||
}
|
||||
|
||||
|
@ -41,37 +45,41 @@ func initTemplateAssets() (err error) {
|
|||
}
|
||||
|
||||
func compileAsset(name string) (string, error) {
|
||||
originalContent, err := os.ReadFile(name)
|
||||
content, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ext := path.Ext(name)
|
||||
var compiledContent []byte
|
||||
compiledExt := ext
|
||||
switch ext {
|
||||
case ".js":
|
||||
compiledContent, err = minifier.Bytes("application/javascript", originalContent)
|
||||
content, err = minifier.Bytes("application/javascript", content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
case ".css":
|
||||
compiledContent, err = minifier.Bytes("text/css", originalContent)
|
||||
content, err = minifier.Bytes("text/css", content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
default:
|
||||
// Just copy the file
|
||||
compiledContent = originalContent
|
||||
// Do nothing
|
||||
}
|
||||
sha := sha1.New()
|
||||
if _, err := sha.Write(compiledContent); err != nil {
|
||||
// Hashes
|
||||
sha1Hash := sha1.New()
|
||||
sha512Hash := sha512.New()
|
||||
if _, err := io.MultiWriter(sha1Hash, sha512Hash).Write(content); err != nil {
|
||||
return "", err
|
||||
}
|
||||
hash := fmt.Sprintf("%x", sha.Sum(nil))
|
||||
compiledFileName := hash + compiledExt
|
||||
// File name
|
||||
compiledFileName := fmt.Sprintf("%x", sha1Hash.Sum(nil)) + compiledExt
|
||||
// SRI
|
||||
sriHash := fmt.Sprintf("sha512-%s", base64.StdEncoding.EncodeToString(sha512Hash.Sum(nil)))
|
||||
// Create struct
|
||||
assetFiles[compiledFileName] = &assetFile{
|
||||
contentType: mime.TypeByExtension(compiledExt),
|
||||
body: compiledContent,
|
||||
sri: sriHash,
|
||||
body: content,
|
||||
}
|
||||
return compiledFileName, err
|
||||
}
|
||||
|
@ -81,6 +89,10 @@ func assetFileName(fileName string) string {
|
|||
return "/" + assetFileNames[fileName]
|
||||
}
|
||||
|
||||
func assetSRI(fileName string) string {
|
||||
return assetFiles[assetFileNames[fileName]].sri
|
||||
}
|
||||
|
||||
func allAssetPaths() []string {
|
||||
var paths []string
|
||||
for _, name := range assetFileNames {
|
||||
|
|
|
@ -2,7 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
@ -21,14 +21,14 @@ func initTemplateStrings() error {
|
|||
}
|
||||
for _, variant := range variants {
|
||||
variantStrings := map[string]string{}
|
||||
fileContent, err := os.ReadFile(path.Join(stringsDir, variant+variantFileExt))
|
||||
f, err := os.Open(filepath.Join(stringsDir, variant+variantFileExt))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
err = yaml.Unmarshal(fileContent, variantStrings)
|
||||
err = yaml.NewDecoder(f).Decode(variantStrings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<div class="p flex" id="post-actions">
|
||||
<a href="https://www.addtoany.com/share#url={{ absolute .Data.Path }}{{ with title .Data }}&title={{ . }}{{ end }}" target="_blank" rel="nofollow noopener noreferrer" class="button">{{ string .Blog.Lang "share" }}</a>
|
||||
<a id="translateBtn" href="https://translate.google.com/translate?u={{ absolute .Data.Path }}" target="_blank" rel="nofollow noopener noreferrer" class="button">{{ string .Blog.Lang "translate" }}</a>
|
||||
<script defer src="{{ asset "js/translate.js" }}"></script>
|
||||
<script defer src="{{ asset "js/translate.js" }}" integrity="{{ assetsri "js/translate.js" }}"></script>
|
||||
<button id="speakBtn" class="hide" data-speak="{{ string .Blog.Lang "speak" }}" data-stopspeak="{{ string .Blog.Lang "stopspeak" }}"></button>
|
||||
<script defer src="{{ asset "js/speak.js" }}"></script>
|
||||
<script defer src="{{ asset "js/speak.js" }}" integrity="{{ assetsri "js/speak.js" }}"></script>
|
||||
</div>
|
||||
{{ end }}
|
Loading…
Reference in New Issue