generated from templates/go-generic
feat: add a landing page for the Flow Platform
Add a landing page for the Flow Platform. The landing page is a linktree-styled static page made with Go, HTML and CSS.
This commit is contained in:
parent
439e50be9e
commit
a0e3ee8a6f
14 changed files with 329 additions and 4 deletions
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
web/static/gpg/public.asc filter=lfs diff=lfs merge=lfs -text
|
||||||
|
web/static/img/favicon.ico filter=lfs diff=lfs merge=lfs -text
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -0,0 +1 @@
|
||||||
|
/landing
|
|
@ -18,5 +18,8 @@ linters-settings:
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
enable-all: true
|
enable-all: true
|
||||||
# disable:
|
disable:
|
||||||
|
- exhaustruct
|
||||||
|
- exhaustivestruct
|
||||||
|
- gomnd
|
||||||
fast: false
|
fast: false
|
||||||
|
|
7
Dockerfile
Normal file
7
Dockerfile
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
ADD landing /landing
|
||||||
|
|
||||||
|
USER 65534
|
||||||
|
|
||||||
|
ENTRYPOINT ["/landing"]
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
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
Normal file
5
go.mod
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module codeflow.dananglin.me.uk/flow/landing
|
||||||
|
|
||||||
|
go 1.21.0
|
||||||
|
|
||||||
|
require github.com/magefile/mage v1.15.0
|
2
go.sum
Normal file
2
go.sum
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
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
Normal file
75
links.go
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
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()
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ import (
|
||||||
|
|
||||||
var Default = Build
|
var Default = Build
|
||||||
|
|
||||||
var binary = "app"
|
var binary = "landing"
|
||||||
|
|
||||||
// Test run the go tests.
|
// Test run the go tests.
|
||||||
// To enable verbose mode set GO_TEST_VERBOSE=1.
|
// To enable verbose mode set GO_TEST_VERBOSE=1.
|
||||||
|
@ -39,8 +39,7 @@ func Lint() error {
|
||||||
|
|
||||||
// Build build the executable.
|
// Build build the executable.
|
||||||
func Build() error {
|
func Build() error {
|
||||||
main := "main.go"
|
return sh.Run("go", "build", "-o", binary, ".")
|
||||||
return sh.Run("go", "build", "-o", binary, main)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean clean the workspace.
|
// Clean clean the workspace.
|
||||||
|
|
140
main.go
140
main.go
|
@ -1,4 +1,144 @@
|
||||||
package main
|
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() {
|
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)
|
||||||
}
|
}
|
||||||
|
|
31
web/html/base.tmpl.html
Normal file
31
web/html/base.tmpl.html
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{{ 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 }}
|
33
web/static/css/stylesheet.css
Normal file
33
web/static/css/stylesheet.css
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
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)
Normal file
BIN
web/static/gpg/public.asc
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
web/static/img/favicon.ico
(Stored with Git LFS)
Normal file
BIN
web/static/img/favicon.ico
(Stored with Git LFS)
Normal file
Binary file not shown.
Loading…
Reference in a new issue