Initial "Bluez" default theme and Admin Setup page
This commit is contained in:
parent
0c243c849c
commit
456cad7a50
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
bin/
|
bin/
|
||||||
dist/
|
dist/
|
||||||
|
root/.private
|
||||||
|
|
10
core/admin.go
Normal file
10
core/admin.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupHandler is the initial blog setup route.
|
||||||
|
func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
b.RenderTemplate(w, r, "admin/setup", nil)
|
||||||
|
}
|
|
@ -10,6 +10,8 @@ import (
|
||||||
// Blog is the root application object that maintains the app configuration
|
// Blog is the root application object that maintains the app configuration
|
||||||
// and helper objects.
|
// and helper objects.
|
||||||
type Blog struct {
|
type Blog struct {
|
||||||
|
Debug bool
|
||||||
|
|
||||||
// DocumentRoot is the core static files root; UserRoot masks over it.
|
// DocumentRoot is the core static files root; UserRoot masks over it.
|
||||||
DocumentRoot string
|
DocumentRoot string
|
||||||
UserRoot string
|
UserRoot string
|
||||||
|
@ -27,6 +29,7 @@ func New(documentRoot, userRoot string) *Blog {
|
||||||
}
|
}
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
blog.r = r
|
blog.r = r
|
||||||
|
r.HandleFunc("/admin/setup", blog.SetupHandler)
|
||||||
r.HandleFunc("/", blog.PageHandler)
|
r.HandleFunc("/", blog.PageHandler)
|
||||||
r.NotFoundHandler = http.HandlerFunc(blog.PageHandler)
|
r.NotFoundHandler = http.HandlerFunc(blog.PageHandler)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -13,13 +14,64 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// Remove trailing slashes by redirecting them away.
|
// Remove trailing slashes by redirecting them away.
|
||||||
if len(path) > 1 && path[len(path)-1] == '/' {
|
if len(path) > 1 && path[len(path)-1] == '/' {
|
||||||
Redirect(w, strings.TrimRight(path, "/"))
|
b.Redirect(w, strings.TrimRight(path, "/"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restrict special paths.
|
||||||
|
if strings.HasPrefix(strings.ToLower(path), "/.") {
|
||||||
|
b.Forbidden(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search for a file that matches their URL.
|
// Search for a file that matches their URL.
|
||||||
|
filepath, err := b.ResolvePath(path)
|
||||||
|
if err != nil {
|
||||||
|
b.NotFound(w, r, "The page you were looking for was not found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is it a template file?
|
||||||
|
if strings.HasSuffix(filepath.URI, ".gohtml") || strings.HasSuffix(filepath.URI, ".html") {
|
||||||
|
b.RenderTemplate(w, r, filepath.URI, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.ServeFile(w, r, filepath.Absolute)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filepath represents a file discovered in the document roots, and maintains
|
||||||
|
// both its relative and absolute components.
|
||||||
|
type Filepath struct {
|
||||||
|
// Canonicalized URI version of the file resolved on disk,
|
||||||
|
// possible with a file extension injected.
|
||||||
|
// (i.e. "/about" -> "about.html")
|
||||||
|
URI string
|
||||||
|
Relative string // Relative path including document root (i.e. "root/about.html")
|
||||||
|
Absolute string // Absolute path on disk (i.e. "/opt/blog/root/about.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Filepath) String() string {
|
||||||
|
return f.Relative
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolvePath matches a filesystem path to a relative request URI.
|
||||||
|
//
|
||||||
|
// This checks the UserRoot first and then the DocumentRoot. This way the user
|
||||||
|
// may override templates from the core app's document root.
|
||||||
|
func (b *Blog) ResolvePath(path string) (Filepath, error) {
|
||||||
|
// Strip leading slashes.
|
||||||
|
if path[0] == '/' {
|
||||||
|
path = strings.TrimPrefix(path, "/")
|
||||||
|
}
|
||||||
|
|
||||||
log.Debug("Resolving filepath for URI: %s", path)
|
log.Debug("Resolving filepath for URI: %s", path)
|
||||||
for _, root := range []string{b.DocumentRoot, b.UserRoot} {
|
for _, root := range []string{b.DocumentRoot, b.UserRoot} {
|
||||||
|
if len(root) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the file path.
|
||||||
relPath := filepath.Join(root, path)
|
relPath := filepath.Join(root, path)
|
||||||
absPath, err := filepath.Abs(relPath)
|
absPath, err := filepath.Abs(relPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -31,26 +83,26 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// Found an exact hit?
|
// Found an exact hit?
|
||||||
if stat, err := os.Stat(absPath); !os.IsNotExist(err) && !stat.IsDir() {
|
if stat, err := os.Stat(absPath); !os.IsNotExist(err) && !stat.IsDir() {
|
||||||
log.Debug("Exact filepath found: %s", absPath)
|
log.Debug("Exact filepath found: %s", absPath)
|
||||||
http.ServeFile(w, r, absPath)
|
return Filepath{path, relPath, absPath}, nil
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try some supported suffixes.
|
// Try some supported suffixes.
|
||||||
suffixes := []string{
|
suffixes := []string{
|
||||||
|
".gohtml",
|
||||||
".html",
|
".html",
|
||||||
|
"/index.gohtml",
|
||||||
"/index.html",
|
"/index.html",
|
||||||
".md",
|
".md",
|
||||||
"/index.md",
|
"/index.md",
|
||||||
}
|
}
|
||||||
for _, suffix := range suffixes {
|
for _, suffix := range suffixes {
|
||||||
if stat, err := os.Stat(absPath + suffix); !os.IsNotExist(err) && !stat.IsDir() {
|
test := absPath + suffix
|
||||||
log.Debug("Filepath found via suffix %s: %s", suffix, absPath+suffix)
|
if stat, err := os.Stat(test); !os.IsNotExist(err) && !stat.IsDir() {
|
||||||
http.ServeFile(w, r, absPath+suffix)
|
log.Debug("Filepath found via suffix %s: %s", suffix, test)
|
||||||
return
|
return Filepath{path + suffix, relPath + suffix, test}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No file, must be a 404.
|
return Filepath{}, errors.New("not found")
|
||||||
http.NotFound(w, r)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,36 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
// Redirect sends an HTTP redirect response.
|
// Redirect sends an HTTP redirect response.
|
||||||
func Redirect(w http.ResponseWriter, location string) {
|
func (b *Blog) Redirect(w http.ResponseWriter, location string) {
|
||||||
w.Header().Set("Location", location)
|
w.Header().Set("Location", location)
|
||||||
w.WriteHeader(http.StatusFound)
|
w.WriteHeader(http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NotFound sends a 404 response.
|
||||||
|
func (b *Blog) NotFound(w http.ResponseWriter, r *http.Request, message ...string) {
|
||||||
|
if len(message) == 0 {
|
||||||
|
message = []string{"The page you were looking for was not found."}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
err := b.RenderTemplate(w, r, ".errors/404", map[string]interface{}{
|
||||||
|
"message": message[0],
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forbidden sends an HTTP 400 Forbidden response.
|
||||||
|
func (b *Blog) Forbidden(w http.ResponseWriter, r *http.Request, message ...string) {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
err := b.RenderTemplate(w, r, ".errors/403", nil)
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
61
core/templates.go
Normal file
61
core/templates.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultVars combines template variables with default, globally available vars.
|
||||||
|
func (b *Blog) DefaultVars(vars map[string]interface{}) map[string]interface{} {
|
||||||
|
defaults := map[string]interface{}{
|
||||||
|
"title": "Untitled Blog",
|
||||||
|
}
|
||||||
|
if vars == nil {
|
||||||
|
return defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range defaults {
|
||||||
|
vars[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return vars
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderTemplate responds with an HTML template.
|
||||||
|
func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path string, vars map[string]interface{}) error {
|
||||||
|
// Get the layout template.
|
||||||
|
layout, err := b.ResolvePath(".layout")
|
||||||
|
if err != nil {
|
||||||
|
log.Error("RenderTemplate(%s): layout template not found", path)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// And the template in question.
|
||||||
|
filepath, err := b.ResolvePath(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("RenderTemplate(%s): file not found", path)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the template files. The layout comes first because it's the wrapper
|
||||||
|
// and allows the filepath template to set the page title.
|
||||||
|
t, err := template.ParseFiles(layout.Absolute, filepath.Absolute)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject globally available variables.
|
||||||
|
vars = b.DefaultVars(vars)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; encoding=UTF-8")
|
||||||
|
err = t.ExecuteTemplate(w, "layout", vars)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Template parsing error: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("Parsed template")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
1
main.go
1
main.go
|
@ -34,5 +34,6 @@ func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
app := core.New(DocumentRoot, "")
|
app := core.New(DocumentRoot, "")
|
||||||
|
app.Debug = fDebug
|
||||||
app.ListenAndServe(fAddress)
|
app.ListenAndServe(fAddress)
|
||||||
}
|
}
|
||||||
|
|
3
root/.errors/403.gohtml
Normal file
3
root/.errors/403.gohtml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{{ define "content" }}
|
||||||
|
<h1>403 Forbidden</h1>
|
||||||
|
{{ end }}
|
6
root/.errors/404.gohtml
Normal file
6
root/.errors/404.gohtml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{{ define "title" }}Not Found{{ end }}
|
||||||
|
{{ define "content" }}
|
||||||
|
<h1>404 Not Found</h1>
|
||||||
|
|
||||||
|
{{ .message }}
|
||||||
|
{{ end }}
|
132
root/.layout.gohtml
Normal file
132
root/.layout.gohtml
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
{{ define "title" }}Untitled{{ end }}
|
||||||
|
{{ define "layout" }}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
|
||||||
|
<title>{{ template "title" or "Untitled" }} - {{ .title }}</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap core CSS -->
|
||||||
|
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="/bluez/theme.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav class="navbar navbar-expand-md fixed-top bluez-navbar">
|
||||||
|
<a href="#" class="navbar-brand">{{ .title }}</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||||
|
<ul class="navbar-nav mr-auto">
|
||||||
|
<li class="nav-item active">
|
||||||
|
<a href="/" class="nav-link">Home <span class="sr-only">(current)</span></a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/about" class="nav-link">About</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/archive" class="nav-link">Archive</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<form class="form-inline mt-2 mt-md-0">
|
||||||
|
<input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search">
|
||||||
|
<button class="btn btn-outline-light my-2 my-sm-0" type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="bluez-header">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="bluez-title">{{ .title }}</h1>
|
||||||
|
<p class="lead bluez-description">Just another web blog.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container mb-5">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-9">
|
||||||
|
{{ template "content" . }}
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">About</h4>
|
||||||
|
|
||||||
|
<p>Hello, world!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">Archives</h4>
|
||||||
|
|
||||||
|
<ul class="nav flex-column">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">March 2018</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">February 2018</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">January 2018</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">Elsewhere</h4>
|
||||||
|
|
||||||
|
<ul class="nav flex-column">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">Facebook</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">Twitter</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="bluez-footer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-8">
|
||||||
|
<ul class="nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">RSS</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">Random</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">Archive</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">Ask me anything</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">Back to top</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
Copyright 2017
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{ end }}
|
39
root/admin/setup.gohtml
Normal file
39
root/admin/setup.gohtml
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{{ define "title" }}Initial Setup{{ end }}
|
||||||
|
{{ define "content" }}
|
||||||
|
<h1>Initial Setup</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Welcome to your new web blog! To get started, you'll need to create a username
|
||||||
|
and password to be your <strong>admin user</strong>. You can create additional
|
||||||
|
users for your blog in a later step.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
It is not recommended to name this user "admin" because that would be very
|
||||||
|
predictable for an attacker to guess.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="POST" action="/admin/setup">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="setup-admin-username">Admin username:</label>
|
||||||
|
<input type="text" class="form-control" id="setup-admin-username" placeholder="Enter username">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="setup-admin-password1">Passphrase:</label>
|
||||||
|
<input type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="setup-admin-password1"
|
||||||
|
placeholder="correct horse battery staple"
|
||||||
|
aria-describedby="setup-password-help">
|
||||||
|
<small id="setup-password-help" class="form-text text-muted">
|
||||||
|
Choose an <a href="https://xkcd.com/936/" target="_blank">appropriately strong</a> password.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="setup-admin-password2">Confirm:</label>
|
||||||
|
<input type="password" class="form-control" id="setup-admin-password2" placeholder="correct horse battery staple">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">Continue</button>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
76
root/bluez/theme.css
Normal file
76
root/bluez/theme.css
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* Globals
|
||||||
|
*/
|
||||||
|
|
||||||
|
* {
|
||||||
|
transition-duration: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Verdana, Helvetica, Arial, sans-serif;
|
||||||
|
color: #555;
|
||||||
|
margin-top: 80px;
|
||||||
|
background-color: #DEF;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, .h1,
|
||||||
|
h2, .h2,
|
||||||
|
h3, .h3,
|
||||||
|
h4, .h4,
|
||||||
|
h5, .h5,
|
||||||
|
h6, .h6 {
|
||||||
|
font-family: "Trebuchet MS", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Bootstrap tweaks and overrides
|
||||||
|
*/
|
||||||
|
.form-group label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Top nav
|
||||||
|
*/
|
||||||
|
.bluez-navbar {
|
||||||
|
background-color: rgba(0, 75, 153, 0.8);
|
||||||
|
}
|
||||||
|
.bluez-navbar a {
|
||||||
|
color: #DDD;
|
||||||
|
}
|
||||||
|
.bluez-navbar a:hover, .bluez-navbar a:active {
|
||||||
|
color: #FFF !important;
|
||||||
|
}
|
||||||
|
.bluez-navbar .active a {
|
||||||
|
color: #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blog title and description */
|
||||||
|
.bluez-header {
|
||||||
|
padding-bottom: 1.25rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border-bottom: .05rem solid #CCC;
|
||||||
|
}
|
||||||
|
.bluez-title {
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.bluez-description {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page footer */
|
||||||
|
.bluez-footer {
|
||||||
|
padding: 2.5rem 0;
|
||||||
|
color: #999;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #EEF;
|
||||||
|
border-top: .05rem solid #CCC;
|
||||||
|
}
|
||||||
|
.bluez-footer p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
4
root/index.gohtml
Normal file
4
root/index.gohtml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{{ define "title" }}Welcome{{ end }}
|
||||||
|
{{ define "content" }}
|
||||||
|
<h1>Index</h1>
|
||||||
|
{{ end }}
|
|
@ -1,11 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Blog</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<h1>Index</h1>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Loading…
Reference in New Issue
Block a user