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:
Dan Anglin 2023-08-24 13:15:55 +01:00
parent 439e50be9e
commit a0e3ee8a6f
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
14 changed files with 329 additions and 4 deletions

2
.gitattributes vendored Normal file
View 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
View file

@ -0,0 +1 @@
/landing

View file

@ -18,5 +18,8 @@ linters-settings:
linters:
enable-all: true
# disable:
disable:
- exhaustruct
- exhaustivestruct
- gomnd
fast: false

7
Dockerfile Normal file
View file

@ -0,0 +1,7 @@
FROM scratch
ADD landing /landing
USER 65534
ENTRYPOINT ["/landing"]

21
LICENSE Normal file
View 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
View 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
View 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
View 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()
}

View file

@ -11,7 +11,7 @@ import (
var Default = Build
var binary = "app"
var binary = "landing"
// Test run the go tests.
// To enable verbose mode set GO_TEST_VERBOSE=1.
@ -39,8 +39,7 @@ func Lint() error {
// Build build the executable.
func Build() error {
main := "main.go"
return sh.Run("go", "build", "-o", binary, main)
return sh.Run("go", "build", "-o", binary, ".")
}
// Clean clean the workspace.

140
main.go
View file

@ -1,4 +1,144 @@
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)
}

31
web/html/base.tmpl.html Normal file
View 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 }}

View 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

Binary file not shown.

BIN
web/static/img/favicon.ico (Stored with Git LFS) Normal file

Binary file not shown.