generated from templates/go-generic
WIP: New Flow website built with Nanoc and Mage #1
11 changed files with 0 additions and 384 deletions
|
@ -1,7 +0,0 @@
|
||||||
FROM scratch
|
|
||||||
|
|
||||||
ADD landing /landing
|
|
||||||
|
|
||||||
USER 65534
|
|
||||||
|
|
||||||
ENTRYPOINT ["/landing"]
|
|
21
LICENSE
21
LICENSE
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2023 Dan Anglin
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
5
go.mod
5
go.mod
|
@ -1,5 +0,0 @@
|
||||||
module codeflow.dananglin.me.uk/flow/landing
|
|
||||||
|
|
||||||
go 1.21.0
|
|
||||||
|
|
||||||
require github.com/magefile/mage v1.15.0
|
|
2
go.sum
2
go.sum
|
@ -1,2 +0,0 @@
|
||||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
|
||||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
|
75
links.go
75
links.go
|
@ -1,75 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type linkParseError struct {
|
|
||||||
msg string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e linkParseError) Error() string {
|
|
||||||
return "link parse error: " + e.msg
|
|
||||||
}
|
|
||||||
|
|
||||||
type link struct {
|
|
||||||
Title string
|
|
||||||
URL string
|
|
||||||
Rel string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l link) String() string {
|
|
||||||
return fmt.Sprintf("Title: %s, URL: %s, Rel: %s", l.Title, l.URL, l.Rel)
|
|
||||||
}
|
|
||||||
|
|
||||||
type links []link
|
|
||||||
|
|
||||||
func (l *links) Set(value string) error {
|
|
||||||
linkAttributes := strings.Split(value, ",")
|
|
||||||
|
|
||||||
lenLinkAttributes := len(linkAttributes)
|
|
||||||
if lenLinkAttributes != 2 && lenLinkAttributes != 3 {
|
|
||||||
return linkParseError{msg: fmt.Sprintf("unexpected number of attributes found %s, want 2 or 3, got %d", value, lenLinkAttributes)}
|
|
||||||
}
|
|
||||||
|
|
||||||
var thisLink link
|
|
||||||
|
|
||||||
for _, attr := range linkAttributes {
|
|
||||||
split := strings.Split(attr, "=")
|
|
||||||
switch strings.ToLower(split[0]) {
|
|
||||||
case "title":
|
|
||||||
thisLink.Title = split[1]
|
|
||||||
case "url":
|
|
||||||
thisLink.URL = split[1]
|
|
||||||
case "rel":
|
|
||||||
thisLink.Rel = split[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if thisLink.Title == "" {
|
|
||||||
return linkParseError{msg: fmt.Sprintf("the title attribute is missing from %s", value)}
|
|
||||||
}
|
|
||||||
|
|
||||||
if thisLink.URL == "" {
|
|
||||||
return linkParseError{msg: fmt.Sprintf("the URL attribute is missing from %s", value)}
|
|
||||||
}
|
|
||||||
|
|
||||||
*l = append(*l, thisLink)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *links) String() string {
|
|
||||||
if len(*l) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var builder strings.Builder
|
|
||||||
|
|
||||||
for _, link := range *l {
|
|
||||||
fmt.Fprintln(&builder, link)
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.String()
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
//go:build mage
|
|
||||||
// +build mage
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/magefile/mage/sh"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Default = Build
|
|
||||||
|
|
||||||
var binary = "landing"
|
|
||||||
|
|
||||||
// Test run the go tests.
|
|
||||||
// To enable verbose mode set GO_TEST_VERBOSE=1.
|
|
||||||
// To enable coverage mode set GO_TEST_COVER=1.
|
|
||||||
func Test() error {
|
|
||||||
goTest := sh.RunCmd("go", "test")
|
|
||||||
|
|
||||||
args := []string{"./..."}
|
|
||||||
|
|
||||||
if os.Getenv("GO_TEST_VERBOSE") == "1" {
|
|
||||||
args = append(args, "-v")
|
|
||||||
}
|
|
||||||
|
|
||||||
if os.Getenv("GO_TEST_COVER") == "1" {
|
|
||||||
args = append(args, "-cover")
|
|
||||||
}
|
|
||||||
|
|
||||||
return goTest(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lint runs golangci-lint against the code.
|
|
||||||
func Lint() error {
|
|
||||||
return sh.RunV("golangci-lint", "run", "--color", "always")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build build the executable.
|
|
||||||
func Build() error {
|
|
||||||
os.Setenv("GOOS", "linux")
|
|
||||||
os.Setenv("GOARCH", "amd64")
|
|
||||||
os.Setenv("CGO_ENABLED", "0")
|
|
||||||
|
|
||||||
return sh.Run("go", "build", "-ldflags=-s -w", "-a", "-o", binary, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean clean the workspace.
|
|
||||||
func Clean() error {
|
|
||||||
if err := sh.Rm(binary); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sh.Run("go", "clean", "./..."); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
144
main.go
144
main.go
|
@ -1,144 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"html/template"
|
|
||||||
"io/fs"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed web/static/*
|
|
||||||
var staticFS embed.FS
|
|
||||||
|
|
||||||
//go:embed web/html/*
|
|
||||||
var htmlTemplates embed.FS
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var (
|
|
||||||
address string
|
|
||||||
profiles links
|
|
||||||
services links
|
|
||||||
)
|
|
||||||
|
|
||||||
flag.StringVar(&address, "address", "0.0.0.0:8080", "The address that the web server will listen on")
|
|
||||||
flag.Var(&services, "service", "A list of service links")
|
|
||||||
flag.Var(&profiles, "profile", "A list of profile links")
|
|
||||||
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
setupLogging()
|
|
||||||
|
|
||||||
mux, err := routes(services, profiles)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Unable to create the Mux", "error", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
server := http.Server{
|
|
||||||
Addr: address,
|
|
||||||
Handler: mux,
|
|
||||||
ReadHeaderTimeout: 1 * time.Second,
|
|
||||||
ReadTimeout: 5 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info("Starting web application.", "address", address)
|
|
||||||
|
|
||||||
err = server.ListenAndServe()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to run the web application", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func routes(services, profiles links) (*http.ServeMux, error) {
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
|
|
||||||
staticRootFS, err := fs.Sub(staticFS, "web/static")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to create the static root file system; %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileServer := http.FileServer(http.FS(staticRootFS))
|
|
||||||
mux.Handle("/static/", http.StripPrefix("/static", neuter(fileServer)))
|
|
||||||
|
|
||||||
mux.HandleFunc("/", landing(services, profiles))
|
|
||||||
|
|
||||||
return mux, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func neuter(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
if strings.HasSuffix(request.URL.Path, "/") {
|
|
||||||
notFound(writer)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTP(writer, request)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func landing(services, profiles links) func(http.ResponseWriter, *http.Request) {
|
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
if request.URL.Path != "/" {
|
|
||||||
notFound(writer)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.Method != http.MethodGet {
|
|
||||||
writer.Header().Set("Allow", http.MethodGet)
|
|
||||||
clientError(writer, http.StatusMethodNotAllowed)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpl, err := template.New("base").ParseFS(htmlTemplates, "web/html/base.tmpl.html")
|
|
||||||
if err != nil {
|
|
||||||
serverError(writer, fmt.Errorf("error parsing the HTML template; %w", err))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
links := struct {
|
|
||||||
Services links
|
|
||||||
Profiles links
|
|
||||||
}{
|
|
||||||
Services: services,
|
|
||||||
Profiles: profiles,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = tmpl.Execute(writer, &links); err != nil {
|
|
||||||
serverError(writer, fmt.Errorf("error rendering the HTML templates; %w", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func notFound(w http.ResponseWriter) {
|
|
||||||
clientError(w, http.StatusNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
func clientError(w http.ResponseWriter, status int) {
|
|
||||||
http.Error(w, http.StatusText(status), status)
|
|
||||||
}
|
|
||||||
|
|
||||||
func serverError(w http.ResponseWriter, err error) {
|
|
||||||
slog.Error(err.Error())
|
|
||||||
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupLogging() {
|
|
||||||
opts := slog.HandlerOptions{
|
|
||||||
AddSource: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, &opts))
|
|
||||||
|
|
||||||
slog.SetDefault(logger)
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
{{ define "base" }}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Dan Anglin | Flow Platform</title>
|
|
||||||
<link rel="stylesheet" href="/static/css/stylesheet.css">
|
|
||||||
<link rel="icon" type="image/x-icon" href="/static/img/favicon.ico">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h1>Flow Platform</h1>
|
|
||||||
{{ if gt (len .Services) 0 }}
|
|
||||||
<h2>Services</h2>
|
|
||||||
<div class="links">
|
|
||||||
{{- range .Services }}
|
|
||||||
<p><a href="{{ .URL }}"{{ if gt (len .Rel) 0 }} rel="{{ .Rel }}"{{ end }}>{{ .Title }}</a></p>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
{{ if gt (len .Profiles) 0 }}
|
|
||||||
<h2>My Profiles</h2>
|
|
||||||
<div class="links">
|
|
||||||
{{- range .Profiles }}
|
|
||||||
<p><a href="{{ .URL }}"{{ if gt (len .Rel) 0 }} rel="{{ .Rel }}"{{ end }}>{{ .Title }}</a></p>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
{{ end }}
|
|
|
@ -1,33 +0,0 @@
|
||||||
body {
|
|
||||||
background-color: #1e1c31;
|
|
||||||
text-align: center;
|
|
||||||
font-family: sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2{
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
body .links {
|
|
||||||
max-width: 500px;
|
|
||||||
margin: auto;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
body .links p a {
|
|
||||||
display: block;
|
|
||||||
background-color: #296ae2;
|
|
||||||
color: Thistle;
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 15px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
transition: 0.5s;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body .links p a:hover, a:active {
|
|
||||||
background-color: #719dee;
|
|
||||||
color: White;
|
|
||||||
transition: 0.5s;
|
|
||||||
}
|
|
BIN
web/static/gpg/public.asc
(Stored with Git LFS)
BIN
web/static/gpg/public.asc
(Stored with Git LFS)
Binary file not shown.
BIN
web/static/img/favicon.ico
(Stored with Git LFS)
BIN
web/static/img/favicon.ico
(Stored with Git LFS)
Binary file not shown.
Loading…
Reference in a new issue