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