diff --git a/.air.toml b/.air.toml
deleted file mode 100644
index fc7bd09..0000000
--- a/.air.toml
+++ /dev/null
@@ -1,51 +0,0 @@
-root = "."
-testdata_dir = "testdata"
-tmp_dir = "tmp"
-
-[build]
- args_bin = []
- bin = "./tmp/webserver"
- cmd = "go build -o ./tmp personal-website/cmd/webserver"
- delay = 1000
- exclude_dir = ["assets", "tmp", "vendor", "testdata"]
- exclude_file = []
- exclude_regex = ["_test.go"]
- exclude_unchanged = false
- follow_symlink = false
- full_bin = ""
- include_dir = []
- include_ext = ["go", "tpl", "tmpl", "html", "css"]
- include_file = []
- kill_delay = "0s"
- log = "build-errors.log"
- poll = false
- poll_interval = 0
- post_cmd = []
- pre_cmd = []
- rerun = false
- rerun_delay = 500
- send_interrupt = false
- stop_on_error = false
-
-[color]
- app = ""
- build = "yellow"
- main = "magenta"
- runner = "green"
- watcher = "cyan"
-
-[log]
- main_only = false
- time = false
-
-[misc]
- clean_on_exit = false
-
-[proxy]
- app_port = 0
- enabled = false
- proxy_port = 0
-
-[screen]
- clear_on_rebuild = false
- keep_scroll = true
diff --git a/.gitignore b/.gitignore
index ac50a76..acbda05 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,25 +1,35 @@
-/*
+*
!README.md
+!LICENSE
-# go webserver code
-!cmd/
+# go
!internal/
-!.air.toml
!go.mod
!go.sum
!gomod2nix.toml
-cmd/webserver/public/js/
-
-# blogs
-!posts/
-# odin wasm code
-!client/
+# uploader
+!cmd/uploader
# nix code
-!nix/
+!nix/*
!flake.*
# docker
-Makefile
+!Makefile
+
+# services
+!services/**/default.nix
+!services/**/*.go
+!services/**/*.templ
+services/**/*_templ.go
+
+# webserver
+!.air.toml
+!services/webserver/public/**
+services/webserver/**/*.js
+!services/webserver/public/tailwind.config.js
+!posts/
+
+!*/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..71b07aa
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) Ethan Thoma
+
+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.
diff --git a/Makefile b/Makefile
index 07d9762..7413347 100644
--- a/Makefile
+++ b/Makefile
@@ -1,9 +1,65 @@
IMAGE_NAME = webserver
TAG = 0.1
+PORT = 8080
+SYSTEM = x86_64-linux
-run: build
- docker load < result
- docker run --rm --env-file ./.env -p 127.0.0.1:8080:8080 -t $(IMAGE_NAME):$(TAG)
+run:
+ nix run github:Mic92/nix-fast-build -- --flake '.#packages.$(SYSTEM).default'
+ nix run
-build:
+docker/build:
nix build .#container
+
+docker: docker/build
+ docker load < result
+ docker run \
+ --rm \
+ --env-file ./.env \
+ -p 127.0.0.1:$(PORT):$(PORT) \
+ -t $(IMAGE_NAME):$(TAG)
+
+live/templ:
+ templ generate \
+ --watch \
+ --proxy="http://localhost:3001" \
+ --open-browser=false \
+ -v \
+ --path=./services/webserver/
+
+live/server:
+ air \
+ --build.cmd "go build -o tmp/bin/main personal-website/services/webserver" \
+ --build.bin "tmp/bin/main" \
+ --build.delay "100" \
+ --build.include_dir "personal-website/services/webserver" \
+ --build.include_ext "go" \
+ --build.stop_on_error "false" \
+ --misc.clean_on_exit true \
+ --proxy.enabled true \
+ --proxy.proxy_port 3001 \
+ --proxy.app_port $(PORT)
+
+live/tailwind:
+ tailwindcss \
+ -c ./services/webserver/public/tailwind.config.js \
+ -i ./services/webserver/public/main.css \
+ -o ./public/main.css \
+ -m \
+ -w
+
+live/sync_assets:
+ rsync -a ./services/webserver/public/* ./public --exclude='*.css'
+ sleep 0.1
+ air \
+ --build.cmd "templ generate --notify-proxy" \
+ --build.bin "true" \
+ --build.delay "100" \
+ --build.exclude_dir "" \
+ --build.include_dir "public" \
+ --build.include_ext "js,css"
+
+live:
+ make live/templ & \
+ make live/server & \
+ make live/tailwind & \
+ make live/sync_assets
diff --git a/README.md b/README.md
index 00b6e75..a0a7122 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
@@ -18,15 +18,34 @@
| Tech | Stack |
|-------|----------|
-| Go | Backend |
+| GO | Backend |
| Htmx | Frontend |
| Turso | Database |
+We use [templ](https://github.com/a-h/templ) for templating and [tailwindcss](https://github.com/tailwindlabs/tailwindcss)
+for styles. So it is technically more like the GoTTTH stack...
+
## Building + Running
-The nix flake has three derivations:
+The nix flake has four derivations:
- .#default: this produces the webserver binary
- .#container: docker image containing the webserver binary
- .#uploader: simple CLI to upload my markdown blogs
+- .#blob: a WIP blob storage service I plan to use for my images
+
+## Developing
+
+The [make file](./Makefile) in root is setup for running air w/ livereload.
+It will run tailwindcss, templ, and air.
+
+> [!TIP]
+> You can also locally deply the docker image using `make docker`.
+
+The webserver assumes the port is set to ":8080". This should be set in your dotenv.
+The dotenv file should contain:
+- TURSO_DATABASE_URL
+- TURSO_AUTH_TOKEN
+- WEBSERVER_PORT
-You can run it test the webserver locally with docker with `make run`.
+> [!NOTE]
+> WEBSERVER_PORT will probably be moved to the flake config instead
diff --git a/client/main.odin b/client/main.odin
deleted file mode 100644
index 23acd0b..0000000
--- a/client/main.odin
+++ /dev/null
@@ -1,46 +0,0 @@
-//+build js
-package main
-
-import "base:runtime"
-import "vendor:wasm/js"
-
-HTML_Canvas_ID :: "wasm"
-
-Context :: struct {
- initialized: bool,
- accumulated_time: f64,
-}
-
-state: Context = {
- initialized = false,
- accumulated_time = 1,
-}
-
-main :: proc() {}
-
-run :: proc() {
- state.initialized = true
-}
-
-@(export)
-step :: proc(delta_time: f64) -> (keep_going: bool) {
- if !state.initialized {
- return true
- }
- state.accumulated_time += delta_time
-
- if state.accumulated_time > 1 {
- frame(state.accumulated_time)
- }
-
- for state.accumulated_time > 1 {
- state.accumulated_time -= 1
- }
-
- return true
-}
-
-frame :: proc(delta_time: f64) {}
-
-@(private = "file", fini)
-finish :: proc() {}
diff --git a/cmd/webserver/components/ascii/ascii.css b/cmd/webserver/components/ascii/ascii.css
deleted file mode 100644
index 328fef4..0000000
--- a/cmd/webserver/components/ascii/ascii.css
+++ /dev/null
@@ -1,14 +0,0 @@
-.ascii {
- display: block;
- width: 100%;
- line-height: 0;
- text-wrap: nowrap;
- white-space-collapse: preserve;
- text-align: center;
-
- & .ascii-char {
- display: inline-block;
- font-size: 5px;
- width: 3px;
- }
-}
diff --git a/cmd/webserver/components/ascii/ascii.go b/cmd/webserver/components/ascii/ascii.go
deleted file mode 100644
index ea67071..0000000
--- a/cmd/webserver/components/ascii/ascii.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package components
-
-import (
- "html/template"
-)
-
-type Props struct {
- Ascii [][]string
-}
-
-func (props Props) Component(t *template.Template) error {
- name := "ascii"
-
- filepath := "cmd/webserver/components/" + name + "/" + name + ".tmpl"
-
- if _, err := t.New(name + "-component").ParseFiles(filepath); err != nil {
- return err
- } else {
- return nil
- }
-}
diff --git a/cmd/webserver/components/ascii/ascii.tmpl b/cmd/webserver/components/ascii/ascii.tmpl
deleted file mode 100644
index b24c244..0000000
--- a/cmd/webserver/components/ascii/ascii.tmpl
+++ /dev/null
@@ -1,7 +0,0 @@
-{{ define "ascii" }}
-
- {{ range .Ascii }}
- {{ range $char := . }}{{ $char }}{{ end }}
- {{ end }}
-
-{{ end }}
diff --git a/cmd/webserver/components/footer/footer.css b/cmd/webserver/components/footer/footer.css
deleted file mode 100644
index 28b18e5..0000000
--- a/cmd/webserver/components/footer/footer.css
+++ /dev/null
@@ -1,10 +0,0 @@
-footer {
- background-color: var(--clr-text);
- color: var(--clr-base);
- border-block-start: var(--space-3xs) solid var(--clr-high);
- font-size: var(--fs-100);
- margin-block-start: var(--space-xl);
- padding-block: var(--space-s);
- text-align: center;
- width: 100%;
-}
diff --git a/cmd/webserver/components/footer/footer.go b/cmd/webserver/components/footer/footer.go
deleted file mode 100644
index b05e665..0000000
--- a/cmd/webserver/components/footer/footer.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package components
-
-import (
- "html/template"
-)
-
-type Props struct{}
-
-func (props Props) Component(t *template.Template) error {
- name := "footer"
-
- filepath := "cmd/webserver/components/" + name + "/" + name + ".tmpl"
-
- if _, err := t.New(name + "-component").ParseFiles(filepath); err != nil {
- return err
- } else {
- return nil
- }
-}
diff --git a/cmd/webserver/components/footer/footer.tmpl b/cmd/webserver/components/footer/footer.tmpl
deleted file mode 100644
index 743bf4c..0000000
--- a/cmd/webserver/components/footer/footer.tmpl
+++ /dev/null
@@ -1,5 +0,0 @@
-{{ define "footer" }}
-
-{{ end }}
diff --git a/cmd/webserver/components/header/header.css b/cmd/webserver/components/header/header.css
deleted file mode 100644
index d9b1c3b..0000000
--- a/cmd/webserver/components/header/header.css
+++ /dev/null
@@ -1,38 +0,0 @@
-header {
- align-items: center;
- display: flex;
- flex-direction: column;
- margin-block-end: var(--space-s-l);
- padding-block: var(--space-xs-s);
-
- & nav {
- width: 100%;
-
- & a.selected {
- font-weight: 700;
- text-decoration: underline;
- }
-
- & ul {
- display: flex;
- justify-content: space-evenly;
- list-style-type: none;
- text-align: center;
-
- & li {
- flex: 1;
-
- & a {
- display: block;
- padding-block: var(--space-2xs);
- padding-inline: var(--space-xs);
- }
- }
- }
- }
-
- .divider {
- border-block-start: var(--space-3xs) solid var(--clr-high);
- width: 100%;
- }
-}
diff --git a/cmd/webserver/components/header/header.go b/cmd/webserver/components/header/header.go
deleted file mode 100644
index c2f3b2e..0000000
--- a/cmd/webserver/components/header/header.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package components
-
-import (
- "html/template"
-
- ascii "personal-website/cmd/webserver/components/ascii"
- nav "personal-website/cmd/webserver/components/nav"
-)
-
-type Props struct {
- Ascii [][]string
- PageCurrent string
- Pages []string
-}
-
-func (props Props) Component(t *template.Template) error {
- name := "header"
-
- filepath := "cmd/webserver/components/" + name + "/" + name + ".tmpl"
-
- if err := (ascii.Props{
- Ascii: props.Ascii,
- }.Component(t)); err != nil {
- return err
- }
-
- if err := (nav.Props{
- PageCurrent: props.PageCurrent,
- Pages: props.Pages,
- }.Component(t)); err != nil {
- return err
- }
-
- if _, err := t.New(name + "-component").ParseFiles(filepath); err != nil {
- return err
- } else {
- return nil
- }
-}
diff --git a/cmd/webserver/components/header/header.tmpl b/cmd/webserver/components/header/header.tmpl
deleted file mode 100644
index eedc4d6..0000000
--- a/cmd/webserver/components/header/header.tmpl
+++ /dev/null
@@ -1,11 +0,0 @@
-{{ define "header" }}
-
- {{ template "ascii" . }}
- {{ template "nav" . }}
-
-
-{{ end }}
-
-{{ define "header-oob" }}
- {{ template "nav-oob" . }}
-{{ end }}
diff --git a/cmd/webserver/components/index.css b/cmd/webserver/components/index.css
deleted file mode 100644
index 050adcf..0000000
--- a/cmd/webserver/components/index.css
+++ /dev/null
@@ -1,5 +0,0 @@
-@import "./ascii/ascii.css";
-@import "./footer/footer.css";
-@import "./header/header.css";
-@import "./nav/nav.css";
-@import "./spacer/spacer.css";
diff --git a/cmd/webserver/components/nav/nav.css b/cmd/webserver/components/nav/nav.css
deleted file mode 100644
index f908eec..0000000
--- a/cmd/webserver/components/nav/nav.css
+++ /dev/null
@@ -1,25 +0,0 @@
-#nav {
- width: 100%;
-
- & a.selected {
- font-weight: 700;
- text-decoration: underline;
- }
-
- & ul {
- display: flex;
- justify-content: space-evenly;
- list-style-type: none;
- text-align: center;
-
- & li {
- flex: 1;
-
- & a {
- display: block;
- padding-block: var(--space-2xs);
- padding-inline: var(--space-xs);
- }
- }
- }
-}
diff --git a/cmd/webserver/components/nav/nav.go b/cmd/webserver/components/nav/nav.go
deleted file mode 100644
index 56afe3c..0000000
--- a/cmd/webserver/components/nav/nav.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package components
-
-import (
- "html/template"
-)
-
-type Props struct {
- PageCurrent string
- Pages []string
-}
-
-func (props Props) Component(t *template.Template) error {
- name := "nav"
-
- filepath := "cmd/webserver/components/" + name + "/" + name + ".tmpl"
-
- if _, err := t.New(name + "-component").ParseFiles(filepath); err != nil {
- return err
- } else {
- return nil
- }
-}
diff --git a/cmd/webserver/components/nav/nav.tmpl b/cmd/webserver/components/nav/nav.tmpl
deleted file mode 100644
index d26f9a0..0000000
--- a/cmd/webserver/components/nav/nav.tmpl
+++ /dev/null
@@ -1,24 +0,0 @@
-{{ define "nav" }}
-
-{{ end }}
-
-{{ define "nav-oob" }}
- {{ template "nav" . }}
-{{ end }}
-
-{{ define "nav-links" }}
- {{ range $name := .Pages }}
- {{ $name }}
- {{ end }}
-{{ end }}
diff --git a/cmd/webserver/components/spacer/spacer.css b/cmd/webserver/components/spacer/spacer.css
deleted file mode 100644
index 688a345..0000000
--- a/cmd/webserver/components/spacer/spacer.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.spacer {
- padding-inline: var(--space-2xs)
-}
diff --git a/cmd/webserver/components/spacer/spacer.go b/cmd/webserver/components/spacer/spacer.go
deleted file mode 100644
index 71cf87b..0000000
--- a/cmd/webserver/components/spacer/spacer.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package components
-
-import (
- "html/template"
-)
-
-type Props struct{}
-
-func (props Props) Component(t *template.Template) error {
- name := "spacer"
-
- filepath := "cmd/webserver/components/" + name + "/" + name + ".tmpl"
-
- if _, err := t.New(name + "-component").ParseFiles(filepath); err != nil {
- return err
- } else {
- return nil
- }
-}
diff --git a/cmd/webserver/components/spacer/spacer.tmpl b/cmd/webserver/components/spacer/spacer.tmpl
deleted file mode 100644
index 4d3e96a..0000000
--- a/cmd/webserver/components/spacer/spacer.tmpl
+++ /dev/null
@@ -1,3 +0,0 @@
-{{ define "spacer" }}
- -
-{{ end }}
diff --git a/cmd/webserver/layouts/base/base.css b/cmd/webserver/layouts/base/base.css
deleted file mode 100644
index 859c6bf..0000000
--- a/cmd/webserver/layouts/base/base.css
+++ /dev/null
@@ -1,103 +0,0 @@
-body {
- align-items: center;
- background-color: var(--clr-base);
- color: var(--clr-text);
- display: flex;
- flex-direction: column;
- font-family: Arial, sans-serif;
- font-size: var(--fs-300);
- line-height: 1.5;
- max-width: 100svw;
- overflow-x: hidden;
-
- --max-width: 640px;
- --min-width: 320px;
-
- & main {
- flex: 1 1 auto;
- }
-
- & main,
- & header {
- display: block;
- width: clamp(var(--min-width), 75%, var(--max-width));
- margin-inline: auto;
- padding-inline: var(--space-2xs-s);
- }
-}
-
-h1,
-h2,
-h3 {
- font-family: var(--ff-heading);
- font-weight: 600;
- letter-spacing: -0.01em;
-}
-
-h1 {
- font-size: var(--fs-600);
-}
-
-h2 {
- font-size: var(--fs-500);
-}
-
-h3 {
- font-size: var(--fs-400);
-}
-
-a {
- color: var(--clr-link);
-}
-
-.content {
- &>*+* {
- margin-block-start: var(--space-2xs-xs);
- }
-
- & h2,
- & h3 {
- margin-block-start: var(--space-l-xl);
- }
-
- & pre {
- background: --var(--clr-high);
- font-size: var(--fs-200);
- overflow-x: auto;
- padding: var(--space-xs);
-
- &>code {
- word-wrap: break-word;
- }
- }
-
- & ul,
- & ol {
- list-style-position: inside;
-
- &>li {
- margin-inline-start: var(--space-m);
- }
-
- &>*+li {
- margin-block-start: var(--space-3xs);
- }
- }
-
- & blockquote {
- border-inline-start: var(--space-3xs) solid var(--clr-high);
- padding-inline-start: var(--space-3xs);
- }
-}
-
-.post {
- display: flex;
-
- & a {
- align-self: center;
-
- & h2 {
- font-size: var(--fs-300);
- }
- }
-}
diff --git a/cmd/webserver/layouts/base/base.go b/cmd/webserver/layouts/base/base.go
deleted file mode 100644
index 1697a94..0000000
--- a/cmd/webserver/layouts/base/base.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package base
-
-import (
- "html/template"
-
- // Components
- footer "personal-website/cmd/webserver/components/footer"
- header "personal-website/cmd/webserver/components/header"
-)
-
-type Props struct {
- Ascii [][]string
- Pages []string
- PageCurrent string
-}
-
-func (props Props) Layout(t *template.Template) error {
- name := "base"
-
- filepath := "cmd/webserver/layouts/" + name + "/" + name + ".tmpl"
-
- if _, err := t.New(name + "-layout").ParseFiles(filepath); err != nil {
- return err
- }
-
- // Components
- if err := (footer.Props{}.Component(t)); err != nil {
- return err
- }
-
- if err := (header.Props{
- Ascii: props.Ascii,
- PageCurrent: props.PageCurrent,
- Pages: props.Pages,
- }.Component(t)); err != nil {
- return err
- }
-
- return nil
-}
diff --git a/cmd/webserver/layouts/base/base.tmpl b/cmd/webserver/layouts/base/base.tmpl
deleted file mode 100644
index 003c712..0000000
--- a/cmd/webserver/layouts/base/base.tmpl
+++ /dev/null
@@ -1,26 +0,0 @@
-{{ define "base-layout" }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ template "header" . }}
-
- {{ template "content" . }}
-
- {{ template "footer" . }}
-
-
-{{ end }}
diff --git a/cmd/webserver/layouts/index.css b/cmd/webserver/layouts/index.css
deleted file mode 100644
index d0489a6..0000000
--- a/cmd/webserver/layouts/index.css
+++ /dev/null
@@ -1 +0,0 @@
-@import "./base/base.css";
diff --git a/cmd/webserver/main.css b/cmd/webserver/main.css
deleted file mode 100644
index 393099d..0000000
--- a/cmd/webserver/main.css
+++ /dev/null
@@ -1,8 +0,0 @@
-@layer core, layouts, pages, components;
-
-@import "./public/reset.css" layer(core);
-@import "./public/variables.css" layer(core);
-
-@import "./layouts/index.css" layer(layouts);
-@import "./pages/index.css" layer(pages);
-@import "./components/index.css" layer(components);
diff --git a/cmd/webserver/main.go b/cmd/webserver/main.go
deleted file mode 100644
index 17e59d2..0000000
--- a/cmd/webserver/main.go
+++ /dev/null
@@ -1,104 +0,0 @@
-package main
-
-import (
- "log"
- "net/http"
- "os"
- "path/filepath"
- "strings"
-
- "personal-website/cmd/webserver/cache"
-
- // pages
- "personal-website/cmd/webserver/pages/blog"
- "personal-website/cmd/webserver/pages/home"
- "personal-website/cmd/webserver/pages/post"
- "personal-website/cmd/webserver/pages/projects"
-)
-
-var logRequests = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- log.Printf("%s %s\n", req.Method, req.URL)
- http.DefaultServeMux.ServeHTTP(w, req)
-})
-
-func main() {
- log.Println("Starting server...")
-
- cache.InitCache()
-
- http.HandleFunc("GET /healthy", func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusOK)
- })
-
- http.HandleFunc("GET /robots.txt", staticHandler(http.Dir("static/seo")))
-
- // pages
-
- ascii := createAscii()
-
- pageHome := &home.Props{Pages: []string{"home", "blog", "projects"}, Ascii: ascii}
- http.Handle("GET /", pageHome)
- http.Handle("GET /home", pageHome)
- http.Handle("GET /home/content", pageHome)
-
- pageBlog := &blog.Props{Pages: []string{"home", "blog", "projects"}, Ascii: ascii}
- http.Handle("GET /blog", pageBlog)
- http.Handle("GET /blog/content", pageBlog)
-
- pagePost := &post.Props{Pages: []string{"home", "blog", "projects"}, Ascii: ascii}
- http.Handle("GET /post/{slug}", pagePost)
- http.Handle("GET /post/{slug}/content", pagePost)
-
- pageProjects := &projects.Props{Pages: []string{"home", "blog", "projects"}, Ascii: ascii}
- http.Handle("GET /projects", pageProjects)
- http.Handle("GET /projects/content", pageProjects)
-
- // static
-
- http.Handle("GET /public/", http.StripPrefix("/public/", staticHandler(http.Dir("cmd/webserver/public"))))
- http.Handle("GET /main.css", staticHandler(http.Dir("cmd/webserver/")))
- http.Handle("GET /layouts/", http.StripPrefix("/layouts/", staticHandler(http.Dir("cmd/webserver/layouts"))))
- http.Handle("GET /pages/", http.StripPrefix("/pages/", staticHandler(http.Dir("cmd/webserver/pages"))))
- http.Handle("GET /components/", http.StripPrefix("/components/", staticHandler(http.Dir("cmd/webserver/components"))))
-
- log.Fatal(http.ListenAndServe(":8080", logRequests))
-}
-
-func createAscii() [][]string {
- fileBuffer, err := os.ReadFile("cmd/webserver/public/images/ascii.txt")
- if err != nil {
- log.Printf("main: reading ascii (%v)", err)
- }
-
- lines := strings.Split(string(fileBuffer), "\n")
-
- asciiChars := make([][]string, len(lines))
-
- for i, line := range lines {
- chars := strings.Split(line, "")
- asciiChars[i] = chars
- }
-
- return asciiChars
-}
-
-func staticHandler(root http.FileSystem) http.HandlerFunc {
- fileServer := http.FileServer(root)
-
- return func(w http.ResponseWriter, r *http.Request) {
- ext := strings.ToLower(filepath.Ext(r.URL.Path))
-
- switch ext {
- case ".css":
- w.Header().Set("Cache-Control", "public, max-age=1800, immutable")
- case ".js":
- w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
- case ".jpg", ".jpeg", ".png", ".gif", ".ico":
- w.Header().Set("Cache-Control", "public, max-age=2592000, immutable")
- default:
- w.Header().Set("Cache-Control", "max-age=0")
- }
-
- fileServer.ServeHTTP(w, r)
- }
-}
diff --git a/cmd/webserver/pages/blog/blog.css b/cmd/webserver/pages/blog/blog.css
deleted file mode 100644
index e69de29..0000000
diff --git a/cmd/webserver/pages/blog/blog.go b/cmd/webserver/pages/blog/blog.go
deleted file mode 100755
index 59e7eed..0000000
--- a/cmd/webserver/pages/blog/blog.go
+++ /dev/null
@@ -1,86 +0,0 @@
-package blog
-
-import (
- "html/template"
- "log"
- "net/http"
- "strings"
- "time"
-
- "personal-website/cmd/webserver/layouts/base"
-
- spacer "personal-website/cmd/webserver/components/spacer"
-
- "personal-website/cmd/webserver/cache"
- "personal-website/internal"
-)
-
-type Props struct {
- Ascii [][]string
- PageCurrent string
- Pages []string
- Posts []internal.Post
-}
-
-func (p *Props) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- name := "blog"
-
- // Page props
- p.PageCurrent = name
-
- posts, err := cache.Cache.GetPosts()
- if err != nil {
- log.Printf(name+": failed to get posts (%v)", err)
- }
- p.Posts = posts
-
- // Page Template
- t, err := template.New("").Funcs(template.FuncMap{
- "formatDate": func(date time.Time) string {
- return date.Format("20060102")
- },
- }).ParseFiles("cmd/webserver/pages/" + name + "/" + name + ".tmpl")
- if err != nil {
- log.Printf(name+": failed to parse tmpl file (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- // Layout
- if err = (base.
- Props{
- Ascii: p.Ascii,
- PageCurrent: p.PageCurrent,
- Pages: p.Pages,
- }.Layout(t)); err != nil {
- log.Printf(name+": failed to render layout (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- // Components
- if err = (spacer.Props{}).Component(t); err != nil {
- log.Printf(name+": failed to render spacer (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- // Render
- if strings.HasSuffix(r.URL.Path, "/content") {
- if err = t.ExecuteTemplate(w, "content", p); err != nil {
- log.Printf(name+": failed to render content (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- if err = t.ExecuteTemplate(w, "oob", p); err != nil {
- log.Printf(name+": failed to render oob (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- } else if err = t.ExecuteTemplate(w, "page", p); err != nil {
- log.Printf(name+": failed to render page (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-}
diff --git a/cmd/webserver/pages/blog/blog.tmpl b/cmd/webserver/pages/blog/blog.tmpl
deleted file mode 100644
index e8b28bb..0000000
--- a/cmd/webserver/pages/blog/blog.tmpl
+++ /dev/null
@@ -1,26 +0,0 @@
-{{ define "page" }}
- {{ template "base-layout" . }}
-{{ end }}
-
-{{ define "content" }}
- Ethan Thoma \ Blog
- Blog
-
- {{ range $post := .Posts }}
- -
-
- {{ template "spacer" . }}
-
{{ .Title }}
-
- {{ end }}
-
-{{ end }}
-
-{{ define "oob" }}
- {{ template "header-oob" . }}
-{{ end }}
diff --git a/cmd/webserver/pages/home/home.css b/cmd/webserver/pages/home/home.css
deleted file mode 100644
index e69de29..0000000
diff --git a/cmd/webserver/pages/home/home.go b/cmd/webserver/pages/home/home.go
deleted file mode 100755
index 52b5494..0000000
--- a/cmd/webserver/pages/home/home.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package home
-
-import (
- "html/template"
- "log"
- "net/http"
- "strings"
- "time"
-
- "personal-website/cmd/webserver/layouts/base"
-)
-
-type Props struct {
- Ascii [][]string
- PageCurrent string
- Pages []string
-}
-
-func (p *Props) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- name := "home"
-
- // Page props
- p.PageCurrent = name
-
- // Page Template
- t, err := template.New("").Funcs(template.FuncMap{
- "formatDate": func(date time.Time) string {
- return date.Format("20060102")
- },
- }).ParseFiles("cmd/webserver/pages/" + name + "/" + name + ".tmpl")
- if err != nil {
- log.Printf(name+": failed to parse tmpl file (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- // Layout
- if err = (base.Props{
- Ascii: p.Ascii,
- PageCurrent: p.PageCurrent,
- Pages: p.Pages,
- }.Layout(t)); err != nil {
- log.Printf(name+": failed to render layout (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- // Render
- if strings.HasSuffix(r.URL.Path, "/content") {
- if err = t.ExecuteTemplate(w, "content", p); err != nil {
- log.Printf(name+": failed to render content (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- if err = t.ExecuteTemplate(w, "oob", p); err != nil {
- log.Printf(name+": failed to render oob (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- } else if err = t.ExecuteTemplate(w, "page", p); err != nil {
- log.Printf(name+": failed to render page (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-}
diff --git a/cmd/webserver/pages/home/home.tmpl b/cmd/webserver/pages/home/home.tmpl
deleted file mode 100644
index 79c6195..0000000
--- a/cmd/webserver/pages/home/home.tmpl
+++ /dev/null
@@ -1,26 +0,0 @@
-{{ define "page" }}
- {{ template "base-layout" . }}
-{{ end }}
-
-{{ define "content" }}
- Ethan Thoma
-
-
- Ethan Thoma
-
- ML graduate student @ STASER Lab UBC
-
- Focused on NLP and RL research
-
- Contact:
-
-
-{{ end }}
-
-{{ define "oob" }}
- {{ template "header-oob" . }}
-{{ end }}
diff --git a/cmd/webserver/pages/index.css b/cmd/webserver/pages/index.css
deleted file mode 100644
index 0e83b8a..0000000
--- a/cmd/webserver/pages/index.css
+++ /dev/null
@@ -1,5 +0,0 @@
-@import "./blog/blog.css";
-@import "./home/home.css";
-@import "./post/post.css";
-@import "./projects/projects.css";
-@import "./wasm/wasm.css";
diff --git a/cmd/webserver/pages/post/post.css b/cmd/webserver/pages/post/post.css
deleted file mode 100644
index e69de29..0000000
diff --git a/cmd/webserver/pages/post/post.go b/cmd/webserver/pages/post/post.go
deleted file mode 100755
index 08aa4b1..0000000
--- a/cmd/webserver/pages/post/post.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package post
-
-import (
- "bytes"
- "html/template"
- "log"
- "net/http"
- "strings"
- "time"
-
- "github.com/yuin/goldmark"
- highlighting "github.com/yuin/goldmark-highlighting/v2"
-
- "personal-website/cmd/webserver/cache"
- "personal-website/cmd/webserver/layouts/base"
- "personal-website/internal"
-)
-
-type Props struct {
- Ascii [][]string
- PageCurrent string
- Pages []string
- Post internal.Post
-}
-
-func (p *Props) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- name := "post"
-
- // Page props
- p.PageCurrent = "blog"
-
- slug := r.PathValue("slug")
- post, err := getPost(slug)
- if err != nil {
- http.Error(w, "Internal Server Error", http.StatusInternalServerError)
- }
- p.Post = post
-
- // Page Template
- t, err := template.New("").Funcs(template.FuncMap{
- "formatDate": func(date time.Time) string {
- return date.Format("20060102")
- },
- }).ParseFiles("cmd/webserver/pages/" + name + "/" + name + ".tmpl")
- if err != nil {
- log.Printf(name+": failed to parse tmpl file (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- // Layout
- if err = (base.Props{
- Ascii: p.Ascii,
- PageCurrent: p.PageCurrent,
- Pages: p.Pages,
- }.Layout(t)); err != nil {
- log.Printf(name+": failed to render layout (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- // Render
- w.Header().Set("Cache-Control", "public, max-age=86400, immutable")
-
- if strings.HasSuffix(r.URL.Path, "/content") {
- if err = t.ExecuteTemplate(w, "content", p); err != nil {
- log.Printf(name+": failed to render content (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- if err = t.ExecuteTemplate(w, "oob", p); err != nil {
- log.Printf(name+": failed to render oob (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- } else if err = t.ExecuteTemplate(w, "page", p); err != nil {
- log.Printf(name+": failed to render page (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-}
-
-func getPost(slug string) (internal.Post, error) {
- post, err := cache.Cache.GetPost(slug)
- if err != nil {
- log.Printf("Error getting post %s: %v", slug, err)
- return post, err
- }
-
- mdRenderer := goldmark.New(
- goldmark.WithExtensions(
- highlighting.NewHighlighting(
- highlighting.WithStyle("rose-pine"),
- ),
- ),
- )
-
- var buf bytes.Buffer
- err = mdRenderer.Convert([]byte(post.Content), &buf)
- if err != nil {
- log.Printf("Error parsing post %s to markdown: %v", slug, err)
- return post, err
- }
-
- post.HTML = template.HTML(buf.String())
-
- return post, nil
-}
diff --git a/cmd/webserver/pages/post/post.tmpl b/cmd/webserver/pages/post/post.tmpl
deleted file mode 100644
index 8854fd2..0000000
--- a/cmd/webserver/pages/post/post.tmpl
+++ /dev/null
@@ -1,15 +0,0 @@
-{{ define "page" }}
- {{ template "base-layout" . }}
-{{ end }}
-
-{{ define "content" }}
- Ethan Thoma \ {{ .Post.Title }}
-
-
- {{ .Post.HTML }}
-
-{{ end }}
-
-{{ define "oob" }}
- {{ template "header-oob" . }}
-{{ end }}
diff --git a/cmd/webserver/pages/projects/projects.css b/cmd/webserver/pages/projects/projects.css
deleted file mode 100644
index e69de29..0000000
diff --git a/cmd/webserver/pages/projects/projects.go b/cmd/webserver/pages/projects/projects.go
deleted file mode 100755
index 5124131..0000000
--- a/cmd/webserver/pages/projects/projects.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package projects
-
-import (
- "html/template"
- "log"
- "net/http"
- "strings"
- "time"
-
- "personal-website/cmd/webserver/layouts/base"
-
- spacer "personal-website/cmd/webserver/components/spacer"
-)
-
-type Props struct {
- Ascii [][]string
- PageCurrent string
- Pages []string
-}
-
-func (p *Props) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- name := "projects"
-
- // Page props
- p.PageCurrent = name
-
- // Page Template
- t, err := template.New("").Funcs(template.FuncMap{
- "formatDate": func(date time.Time) string {
- return date.Format("20060102")
- },
- }).ParseFiles("cmd/webserver/pages/" + name + "/" + name + ".tmpl")
- if err != nil {
- log.Printf(name+": failed to parse tmpl file (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- // Layout
- if err = (base.Props{
- Ascii: p.Ascii,
- PageCurrent: p.PageCurrent,
- Pages: p.Pages,
- }.Layout(t)); err != nil {
- log.Printf(name+": failed to render layout (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- // Components
- if err = (spacer.Props{}).Component(t); err != nil {
- log.Printf(name+": failed to render spacer (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- // Render
- if strings.HasSuffix(r.URL.Path, "/content") {
- if err = t.ExecuteTemplate(w, "content", p); err != nil {
- log.Printf(name+": failed to render content (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- if err = t.ExecuteTemplate(w, "oob", p); err != nil {
- log.Printf(name+": failed to render oob (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- } else if err = t.ExecuteTemplate(w, "page", p); err != nil {
- log.Printf(name+": failed to render page (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-}
diff --git a/cmd/webserver/pages/projects/projects.tmpl b/cmd/webserver/pages/projects/projects.tmpl
deleted file mode 100644
index f0771e8..0000000
--- a/cmd/webserver/pages/projects/projects.tmpl
+++ /dev/null
@@ -1,35 +0,0 @@
-{{ define "page" }}
- {{ template "base-layout" . }}
-{{ end }}
-
-{{ define "content" }}
- Ethan Thoma \ Projects
-
-
- Projects
-
-{{ end }}
-
-{{ define "oob" }}
- {{ template "header-oob" . }}
-{{ end }}
diff --git a/cmd/webserver/pages/wasm/wasm.css b/cmd/webserver/pages/wasm/wasm.css
deleted file mode 100644
index e69de29..0000000
diff --git a/cmd/webserver/pages/wasm/wasm.go b/cmd/webserver/pages/wasm/wasm.go
deleted file mode 100755
index 65c310b..0000000
--- a/cmd/webserver/pages/wasm/wasm.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package wasm
-
-import (
- "html/template"
- "log"
- "net/http"
- "strings"
- "time"
-
- "personal-website/cmd/webserver/layouts/base"
-)
-
-type Props struct {
- Ascii [][]string
- PageCurrent string
- Pages []string
-}
-
-func (p *Props) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- name := "wasm"
-
- // Page props
- p.PageCurrent = name
-
- // Page Template
- t, err := template.New("").Funcs(template.FuncMap{
- "formatDate": func(date time.Time) string {
- return date.Format("20060102")
- },
- }).ParseFiles("cmd/webserver/pages/" + name + "/" + name + ".tmpl")
- if err != nil {
- log.Printf(name+": failed to parse tmpl file (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- // Layout
- if err = (base.Props{
- Ascii: p.Ascii,
- PageCurrent: p.PageCurrent,
- Pages: p.Pages,
- }.Layout(t)); err != nil {
- log.Printf(name+": failed to render layout (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- // Render
- if strings.HasSuffix(r.URL.Path, "/content") {
- if err = t.ExecuteTemplate(w, "content", p); err != nil {
- log.Printf(name+": failed to render content (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- if err = t.ExecuteTemplate(w, "oob", p); err != nil {
- log.Printf(name+": failed to render oob (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- } else if err = t.ExecuteTemplate(w, "page", p); err != nil {
- log.Printf(name+": failed to render page (%v)", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-}
diff --git a/cmd/webserver/pages/wasm/wasm.tmpl b/cmd/webserver/pages/wasm/wasm.tmpl
deleted file mode 100644
index 09a780a..0000000
--- a/cmd/webserver/pages/wasm/wasm.tmpl
+++ /dev/null
@@ -1,17 +0,0 @@
-{{ define "page" }}
- {{ template "base-layout" . }}
-{{ end }}
-
-{{ define "content" }}
- Ethan Thoma \ Wasm
-
-
-
-
-{{ end }}
-
-{{ define "oob" }}
- {{ template "header-oob" . }}
-{{ end }}
diff --git a/cmd/webserver/public/images/ascii.txt b/cmd/webserver/public/images/ascii.txt
deleted file mode 100644
index a4cd987..0000000
--- a/cmd/webserver/public/images/ascii.txt
+++ /dev/null
@@ -1,31 +0,0 @@
- π∞≠≠≈≠≠=≠≈∞∞∞∞∞ππ
- π∞∞ π∞∞π √∞ √≠∞
- ≈≈ √≠ π∞π √πππ∞∞
- ∞≈ √∞ ∞π ∞π πππ=
- ∞π π∞≈√√√√√√ √ π =π
- π∞ π≠ππ√√ √π π≠
- ≈≠≠√√ √≈π ππ ∞√
- √=≈=≠==≠≠≠∞≈∞∞π π∞∞π √∞
- π≠∞π≈≠≈≈√∞≠=≠≠≠≠∞√ ∞√√∞
- ∞∞ ππ π π∞≈≈≠π π≠
- ≈≠∞√ =∞ π=≠√∞∞ ≠=
- ∞√ππ π÷√ ∞≈∞≈∞∞≈≠≈=
- √ π∞√ π≠π∞÷≠≠π
- ∞π √π π≠÷≈π
- π∞∞π √∞π
- √≈√ πππ√∞∞∞π
- π≠√ π∞
- ∞≠ π∞∞π∞
- π≠ ≠ ∞ ∞π
- ≈×π∞×=π ∞≠≠√
- ∞∞≠≈ ∞÷≠∞∞∞√×∞ π∞
- ≠π ≈≈≈√∞∞≈π ≈√ ∞×≈
- π≠√ π≠π =√ √∞ π≠∞ππ∞
- ≈π≈≈π ≈π√≈ ∞≈π≠√ π≈≈√
- √≈∞π ∞√ ≠∞ ππ≈≈∞π π√
- ≠≠≠≠∞√√≈≈√π π≠≠=÷÷=
- π=≠≠∞≈√ π≈ √≠ √≠π
-√ππ∞≈√π√∞∞ ∞=≠≠=÷∞≠≈π
- π ∞∞ππ
- π∞π√∞
- ππππ
diff --git a/cmd/webserver/public/reset.css b/cmd/webserver/public/reset.css
deleted file mode 100644
index b1a0ecd..0000000
--- a/cmd/webserver/public/reset.css
+++ /dev/null
@@ -1,120 +0,0 @@
-/* Remove default margin */
-* {
- margin: 0;
- padding: 0;
- border: 0;
- font: inherit;
- font-size: 100%;
- vertical-align: baseline;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- text-size-adjust: none;
-}
-
-/* Box sizing rules */
-*,
-*::before,
-*::after {
- box-sizing: border-box;
-}
-
-/* Remove default margin */
-body,
-h1,
-h2,
-h3,
-h4,
-p,
-figure,
-blockquote,
-dl,
-dd {
- margin-block-end: 0;
-}
-
-/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
-ul[role='list'],
-ol[role='list'] {
- list-style: none;
-}
-
-/* Set core root defaults */
-html:focus-within {
- scroll-behavior: smooth;
-}
-
-html {
- height: 100%;
-}
-
-/* Set core body defaults */
-body {
- min-height: 100%;
- width: 100vw;
- max-width: 100%;
- box-sizing: border-box;
- position: relative;
- text-rendering: optimizeSpeed;
- line-height: 1.5;
-}
-
-/* A elements that don't have a class get default styles */
-a:not([class]) {
- text-decoration-skip-ink: auto;
-}
-
-/* Make images easier to work with */
-img,
-picture,
-svg {
- max-width: 100%;
- display: block;
-}
-
-footer,
-header,
-nav,
-section,
-main {
- display: block;
-}
-
-blockquote,
-q {
- quotes: none;
-}
-
-blockquote:before,
-blockquote:after,
-q:before,
-q:after {
- content: '';
- content: none;
-}
-
-table {
- border-collapse: collapse;
- border-spacing: 0;
-}
-
-input {
- -webkit-appearance: none;
- appearance: none;
- border-radius: 0;
-}
-
-/* Remove all animations, transitions and smooth scroll for people that prefer not to see them */
-@media (prefers-reduced-motion: reduce) {
- html:focus-within {
- scroll-behavior: auto;
- }
-
- *,
- *::before,
- *::after {
- animation-duration: 0.01ms !important;
- animation-iteration-count: 1 !important;
- transition-duration: 0.01ms !important;
- scroll-behavior: auto !important;
- }
-}
diff --git a/cmd/webserver/public/variables.css b/cmd/webserver/public/variables.css
deleted file mode 100644
index a4bcfe1..0000000
--- a/cmd/webserver/public/variables.css
+++ /dev/null
@@ -1,49 +0,0 @@
-:root {
- /* font size */
- --fs-100: clamp(0.7813rem, 0.7748rem + 0.0325vw, 0.8rem);
- --fs-200: clamp(0.9375rem, 0.9159rem + 0.1085vw, 1rem);
- --fs-300: clamp(1.125rem, 1.0819rem + 0.2169vw, 1.25rem);
- --fs-400: clamp(1.35rem, 1.2767rem + 0.3688vw, 1.5625rem);
- --fs-500: clamp(1.62rem, 1.5051rem + 0.5781vw, 1.9531rem);
- --fs-600: clamp(1.9438rem, 1.7722rem + 0.8633vw, 2.4413rem);
- --fs-700: clamp(2.3325rem, 2.0844rem + 1.2484vw, 3.0519rem);
- --fs-800: clamp(2.7994rem, 2.4491rem + 1.7625vw, 3.815rem);
-
- /* @link https://utopia.fyi/space/calculator?c=320,18,1.2,1280,20,1.25,5,2,&s=0.75|0.5|0.25,1.5|2|3|4|6,s-l&g=s,l,xl,12 */
-
- /* space */
- --space-3xs: clamp(0.3125rem, 0.3125rem + 0vi, 0.3125rem);
- --space-2xs: clamp(0.5625rem, 0.5417rem + 0.1042vi, 0.625rem);
- --space-xs: clamp(0.875rem, 0.8542rem + 0.1042vi, 0.9375rem);
- --space-s: clamp(1.125rem, 1.0833rem + 0.2083vi, 1.25rem);
- --space-m: clamp(1.6875rem, 1.625rem + 0.3125vi, 1.875rem);
- --space-l: clamp(2.25rem, 2.1667rem + 0.4167vi, 2.5rem);
- --space-xl: clamp(3.375rem, 3.25rem + 0.625vi, 3.75rem);
- --space-2xl: clamp(4.5rem, 4.3333rem + 0.8333vi, 5rem);
- --space-3xl: clamp(6.75rem, 6.5rem + 1.25vi, 7.5rem);
-
- /* One-up pairs */
- --space-3xs-2xs: clamp(0.3125rem, 0.2083rem + 0.5208vi, 0.625rem);
- --space-2xs-xs: clamp(0.5625rem, 0.4375rem + 0.625vi, 0.9375rem);
- --space-xs-s: clamp(0.875rem, 0.75rem + 0.625vi, 1.25rem);
- --space-s-m: clamp(1.125rem, 0.875rem + 1.25vi, 1.875rem);
- --space-m-l: clamp(1.6875rem, 1.4167rem + 1.3542vi, 2.5rem);
- --space-l-xl: clamp(2.25rem, 1.75rem + 2.5vi, 3.75rem);
- --space-xl-2xl: clamp(3.375rem, 2.8333rem + 2.7083vi, 5rem);
- --space-2xl-3xl: clamp(4.5rem, 3.5rem + 5vi, 7.5rem);
-
- /* Two-up pairs */
- --space-3xs-xs: clamp(0.3125rem, 0.1042rem + 1.0417vi, 0.9375rem);
- --space-2xs-s: clamp(0.5625rem, 0.3333rem + 1.1458vi, 1.25rem);
- --space-xs-m: clamp(0.875rem, 0.5417rem + 1.6667vi, 1.875rem);
- --space-s-l: clamp(1.125rem, 0.6667rem + 2.2917vi, 2.5rem);
- --space-m-xl: clamp(1.6875rem, 1rem + 3.4375vi, 3.75rem);
- --space-l-2xl: clamp(2.25rem, 1.3333rem + 4.5833vi, 5rem);
- --space-xl-3xl: clamp(3.375rem, 2rem + 6.875vi, 7.5rem);
-
- /* Colors */
- --clr-base: #faf4ed;
- --clr-text: #575279;
- --clr-link: #907aa9;
- --clr-high: #cecacd;
-}
diff --git a/flake.lock b/flake.lock
index 718eee5..e3014f4 100644
--- a/flake.lock
+++ b/flake.lock
@@ -5,11 +5,11 @@
"systems": "systems"
},
"locked": {
- "lastModified": 1710146030,
- "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
+ "lastModified": 1726560853,
+ "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
- "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
+ "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
@@ -18,6 +18,60 @@
"type": "github"
}
},
+ "flake-utils_2": {
+ "inputs": {
+ "systems": "systems_2"
+ },
+ "locked": {
+ "lastModified": 1694529238,
+ "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "flake-utils_3": {
+ "locked": {
+ "lastModified": 1667395993,
+ "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "gitignore": {
+ "inputs": {
+ "nixpkgs": [
+ "templ",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1709087332,
+ "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
+ "owner": "hercules-ci",
+ "repo": "gitignore.nix",
+ "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
+ "type": "github"
+ },
+ "original": {
+ "owner": "hercules-ci",
+ "repo": "gitignore.nix",
+ "type": "github"
+ }
+ },
"gomod2nix": {
"inputs": {
"flake-utils": [
@@ -28,11 +82,33 @@
]
},
"locked": {
- "lastModified": 1725515722,
- "narHash": "sha256-+gljgHaflZhQXtr3WjJrGn8NXv7MruVPAORSufuCFnw=",
+ "lastModified": 1728509152,
+ "narHash": "sha256-tQo1rg3TlwgyI8eHnLvZSlQx9d/o2Rb4oF16TfaTOw0=",
+ "owner": "nix-community",
+ "repo": "gomod2nix",
+ "rev": "d5547e530464c562324f171006fc8f639aa01c9f",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-community",
+ "repo": "gomod2nix",
+ "type": "github"
+ }
+ },
+ "gomod2nix_2": {
+ "inputs": {
+ "flake-utils": "flake-utils_2",
+ "nixpkgs": [
+ "templ",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1722589758,
+ "narHash": "sha256-sbbA8b6Q2vB/t/r1znHawoXLysCyD4L/6n6/RykiSnA=",
"owner": "nix-community",
"repo": "gomod2nix",
- "rev": "1c6fd4e862bf2f249c9114ad625c64c6c29a8a08",
+ "rev": "4e08ca09253ef996bd4c03afa383b23e35fe28a1",
"type": "github"
},
"original": {
@@ -43,11 +119,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1725432240,
- "narHash": "sha256-+yj+xgsfZaErbfYM3T+QvEE2hU7UuE+Jf0fJCJ8uPS0=",
+ "lastModified": 1728888510,
+ "narHash": "sha256-nsNdSldaAyu6PE3YUA+YQLqUDJh+gRbBooMMekZJwvI=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "ad416d066ca1222956472ab7d0555a6946746a80",
+ "rev": "a3c0b3b21515f74fd2665903d4ce6bc4dc81c77c",
"type": "github"
},
"original": {
@@ -57,11 +133,28 @@
"type": "github"
}
},
+ "nixpkgs_2": {
+ "locked": {
+ "lastModified": 1724322575,
+ "narHash": "sha256-kRYwAdYsaICNb2WYcWtBFG6caSuT0v/vTAyR8ap0IR0=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "2a02822b466ffb9f1c02d07c5dd6b96d08b56c6b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "release-24.05",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
"root": {
"inputs": {
"flake-utils": "flake-utils",
"gomod2nix": "gomod2nix",
- "nixpkgs": "nixpkgs"
+ "nixpkgs": "nixpkgs",
+ "templ": "templ"
}
},
"systems": {
@@ -78,6 +171,64 @@
"repo": "default",
"type": "github"
}
+ },
+ "systems_2": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
+ },
+ "templ": {
+ "inputs": {
+ "gitignore": "gitignore",
+ "gomod2nix": "gomod2nix_2",
+ "nixpkgs": "nixpkgs_2",
+ "xc": "xc"
+ },
+ "locked": {
+ "lastModified": 1728899295,
+ "narHash": "sha256-hV+vZEZW/EgfkrVke4+rBiaisgIApTKpYFM0MbmfM7Q=",
+ "owner": "a-h",
+ "repo": "templ",
+ "rev": "bb5e41b1cc6d4edb5cb2e173650320bb28fd74e0",
+ "type": "github"
+ },
+ "original": {
+ "owner": "a-h",
+ "repo": "templ",
+ "type": "github"
+ }
+ },
+ "xc": {
+ "inputs": {
+ "flake-utils": "flake-utils_3",
+ "nixpkgs": [
+ "templ",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1724404748,
+ "narHash": "sha256-p6rXzNiDm2uBvO1MLzC5pJp/0zRNzj/snBzZI0ce62s=",
+ "owner": "joerdav",
+ "repo": "xc",
+ "rev": "960ff9f109d47a19122cfb015721a76e3a0f23a2",
+ "type": "github"
+ },
+ "original": {
+ "owner": "joerdav",
+ "repo": "xc",
+ "type": "github"
+ }
}
},
"root": "root",
diff --git a/flake.nix b/flake.nix
index ea6bf0c..d513c41 100644
--- a/flake.nix
+++ b/flake.nix
@@ -11,6 +11,7 @@
flake-utils.follows = "flake-utils";
};
};
+ templ.url = "github:a-h/templ";
};
outputs =
@@ -19,45 +20,66 @@
nixpkgs,
flake-utils,
gomod2nix,
+ templ,
}:
(flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
+ gopkgs = gomod2nix.legacyPackages.${system};
+ templpkgs = templ.packages.${system}.templ;
callPackage = pkgs.darwin.apple_sdk_11_0.callPackage or pkgs.callPackage;
-
- odin = callPackage ./nix/odin.nix {
- MacOSX-SDK = pkgs.darwin.apple_sdk;
- inherit (pkgs.darwin) Security;
- };
in
rec {
- packages.default = callPackage ./nix/webserver.nix {
- pname = "webserver";
- version = "0.1";
- inherit pkgs odin;
- inherit (gomod2nix.legacyPackages.${system}) buildGoApplication;
+ packages.default = callPackage ./services/webserver {
+ inherit (pkgs) makeWrapper tailwindcss;
+ inherit (gopkgs) buildGoApplication;
+ inherit templpkgs;
};
- packages.uploader = callPackage ./nix/uploader.nix {
- pname = "uploader";
- version = "0.1";
- inherit (gomod2nix.legacyPackages.${system}) buildGoApplication;
- };
+ packages.uploader = callPackage ./cmd/uploader { inherit (gopkgs) buildGoApplication; };
- packages.container = callPackage ./nix/container.nix {
- derivation = packages.default;
+ packages.blob = callPackage ./services/blob { inherit (gopkgs) buildGoApplication; };
- inherit pkgs;
+ packages.container = pkgs.dockerTools.buildImage {
+ name = packages.default.pname;
+ tag = packages.default.version;
+ created = "now";
+ copyToRoot = pkgs.buildEnv {
+ name = "image-root";
+ paths = [ packages.default ];
+ pathsToLink = [
+ "/bin"
+ "/public"
+ ];
+ };
+ config = {
+ Cmd = [ "${packages.default}/bin/${packages.default.pname}" ];
+ Env = [ "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" ];
+ ExposedPorts = {
+ "8080/tcp" = { };
+ };
+ };
};
- devShells.default = callPackage ./nix/shell.nix {
- uploader = packages.uploader;
+ devShells.default =
+ let
+ goEnv = gopkgs.mkGoEnv { pwd = ./.; };
+ in
+ pkgs.mkShell {
+ packages = [
+ goEnv
+ gopkgs.gomod2nix
+ pkgs.air
+ pkgs.turso-cli
+ pkgs.gopls
+ pkgs.tailwindcss
+ # uploader
+ templpkgs
+ ];
+ };
- inherit odin;
- inherit (gomod2nix.legacyPackages.${system}) mkGoEnv gomod2nix;
- };
}
));
}
diff --git a/go.mod b/go.mod
index 9e01443..14a0533 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
)
require (
+ github.com/a-h/templ v0.2.778 // indirect
github.com/alecthomas/chroma/v2 v2.9.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/coder/websocket v1.8.12 // indirect
diff --git a/go.sum b/go.sum
index bf8e132..d896ca6 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,5 @@
+github.com/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM=
+github.com/a-h/templ v0.2.778/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
diff --git a/gomod2nix.toml b/gomod2nix.toml
index f25b857..35642de 100644
--- a/gomod2nix.toml
+++ b/gomod2nix.toml
@@ -1,6 +1,9 @@
schema = 3
[mod]
+ [mod."github.com/a-h/templ"]
+ version = "v0.2.778"
+ hash = "sha256-54iSeNJU4dMMkxHEh0KyRu//vADMnt93fjmXzqc2xPE="
[mod."github.com/alecthomas/chroma/v2"]
version = "v2.9.1"
hash = "sha256-rqRjAGKEBtCzoNe3zS9x+J3CM2EwzQAQli5/DWEGtJo="
diff --git a/nix/client.nix b/nix/client.nix
deleted file mode 100644
index 8e44252..0000000
--- a/nix/client.nix
+++ /dev/null
@@ -1,25 +0,0 @@
-{ pkgs, odin }:
-pkgs.stdenv.mkDerivation rec {
- pname = "client";
- version = "0.1";
- src = ../client;
-
- nativeBuildInputs = [ odin ];
-
- buildPhase = ''
- mkdir -p $out/wasm
- odin build $src -show-timings -out:$out/wasm/${pname}.wasm -no-bounds-check -o:size -target:js_wasm32
-
- mkdir -p $out/js
- cp ${odin.src}/vendor/wgpu/wgpu.js $out/js
- cp ${odin.src}/vendor/wasm/js/runtime.js $out/js
-
- touch $out/bin
- '';
-
- doCheck = true;
-
- checkPhase = ''
- odin test $src
- '';
-}
diff --git a/nix/container.nix b/nix/container.nix
deleted file mode 100644
index 461d8f1..0000000
--- a/nix/container.nix
+++ /dev/null
@@ -1,21 +0,0 @@
-{ pkgs, derivation }:
-pkgs.dockerTools.buildImage {
- name = derivation.pname;
- tag = derivation.version;
- created = "now";
- copyToRoot = pkgs.buildEnv {
- name = "image-root";
- paths = [ derivation ];
- pathsToLink = [
- "/bin"
- "/cmd/${derivation.pname}"
- ];
- };
- config = {
- Cmd = [ "${derivation}/bin/${derivation.pname}" ];
- Env = [ "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" ];
- ExposedPorts = {
- "8080/tcp" = { };
- };
- };
-}
diff --git a/nix/odin.nix b/nix/odin.nix
deleted file mode 100644
index 9b6dc19..0000000
--- a/nix/odin.nix
+++ /dev/null
@@ -1,94 +0,0 @@
-{
- lib,
- fetchFromGitHub,
- llvmPackages_17,
- makeBinaryWrapper,
- libiconv,
- MacOSX-SDK,
- Security,
- which,
-}:
-
-let
- llvmPackages = llvmPackages_17;
- inherit (llvmPackages) stdenv;
-in
-stdenv.mkDerivation rec {
- pname = "odin";
- version = "dev-2024-09";
-
- src = fetchFromGitHub {
- owner = "odin-lang";
- repo = "Odin";
- rev = version;
- hash = "sha256-rbKaGj4jwR+SySt+XJ7K9rtpQsL60IKJ55/1uNkVE1U=";
- };
-
- nativeBuildInputs = [
- makeBinaryWrapper
- which
- ];
-
- buildInputs = lib.optionals stdenv.isDarwin [
- libiconv
- Security
- ];
-
- LLVM_CONFIG = "${llvmPackages.llvm.dev}/bin/llvm-config";
-
- postPatch =
- lib.optionalString stdenv.isDarwin ''
- substituteInPlace src/linker.cpp \
- --replace-fail '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk' ${MacOSX-SDK}
- ''
- + ''
- substituteInPlace build_odin.sh \
- --replace-fail '-framework System' '-lSystem'
- patchShebangs build_odin.sh
- '';
-
- dontConfigure = true;
-
- buildFlags = [ "release" ];
-
- installPhase = ''
- runHook preInstall
-
- mkdir -p $out/bin
- cp odin $out/bin/odin
-
- mkdir -p $out/share
- cp -r base $out/share/base
- cp -r core $out/share/core
- cp -r vendor $out/share/vendor
-
- wrapProgram $out/bin/odin \
- --prefix PATH : ${
- lib.makeBinPath (
- with llvmPackages;
- [
- bintools
- llvm
- clang
- lld
- ]
- )
- } \
- --set-default ODIN_ROOT $out/share
-
- runHook postInstall
- '';
-
- meta = with lib; {
- description = "A fast, concise, readable, pragmatic and open sourced programming language";
- mainProgram = "odin";
- homepage = "https://odin-lang.org/";
- license = licenses.bsd3;
- maintainers = with maintainers; [
- luc65r
- astavie
- znaniye
- ];
- platforms = platforms.x86_64 ++ [ "aarch64-darwin" ];
- };
-}
diff --git a/nix/ols.nix b/nix/ols.nix
deleted file mode 100644
index db7ff0e..0000000
--- a/nix/ols.nix
+++ /dev/null
@@ -1,18 +0,0 @@
-{ pkgs, odin }:
-
-let
- ols = pkgs.ols.overrideAttrs (
- finalAttrs: previousAttrs: {
- buildInputs = [ odin ];
- env.ODIN_ROOT = "${odin}/share";
- }
- );
-in
-pkgs.mkShell {
- packages = [
- odin
- ols
- ];
-
- env.ODIN_ROOT = "${odin}/share";
-}
diff --git a/nix/shell.nix b/nix/shell.nix
deleted file mode 100644
index dfcd250..0000000
--- a/nix/shell.nix
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- pkgs,
- mkGoEnv,
- gomod2nix,
- uploader,
- odin,
-}:
-
-let
- ols = pkgs.ols.overrideAttrs (
- finalAttrs: previousAttrs: {
- buildInputs = [ odin ];
- env.ODIN_ROOT = "${odin}/share";
- }
- );
-
- goEnv = mkGoEnv { pwd = ../.; };
-in
-pkgs.mkShell {
- packages = [
- goEnv
- gomod2nix
- pkgs.air
- pkgs.turso-cli
- uploader
-
- odin
- ols
- ];
-
- env.ODIN_ROOT = "${odin}/share";
-}
diff --git a/nix/uploader.nix b/nix/uploader.nix
deleted file mode 100644
index f8767f6..0000000
--- a/nix/uploader.nix
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- pname,
- version,
- buildGoApplication,
-}:
-
-buildGoApplication {
- inherit pname version;
- src = ../.;
- pwd = ../.;
- modules = ../gomod2nix.toml;
- subPackages = [ "cmd/${pname}" ];
-}
diff --git a/nix/webserver.nix b/nix/webserver.nix
deleted file mode 100644
index e0ce6f0..0000000
--- a/nix/webserver.nix
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- pname,
- version,
- pkgs,
- fetchurl,
- buildGoApplication,
- odin,
-}:
-let
- htmxVersion = "2.0.2";
- htmx = fetchurl {
- url = "https://github.com/bigskysoftware/htmx/releases/download/v${htmxVersion}/htmx.min.js";
- hash = "sha256-4XRtl1nsDUPFwoRFIzOjELtf1yheusSy3Jv0TXK1qIc=";
- };
-
- client = pkgs.callPackage ./client.nix { inherit odin; };
-in
-buildGoApplication {
- inherit pname version;
- src = ../.;
- pwd = ../.;
- modules = ../gomod2nix.toml;
- subPackages = [ "cmd/${pname}" ];
- nativeBuildInputs = [ pkgs.lightningcss ];
- postInstall = ''
- static=$out/cmd/webserver/public
-
- mkdir -p $static
-
- rsync -a ./cmd/webserver/public $out/cmd/webserver --exclude styles --exclude js --exclude wasm --exclude='*.css'
-
- mkdir -p $static/js
- cp ${htmx} $static/js/htmx.min.js
- cp -r ${client.out}/js/* $static/js
-
- mkdir -p $static/wasm
- cp -r ${client.out}/wasm/* $static/wasm
-
- lightningcss --minify --bundle ./cmd/webserver/main.css -t "> .5% or last 2 versions" -o $out/cmd/webserver/main.css
-
- find ./cmd/${pname} -name "*.tmpl" -exec sh -c '
- for file do
- dest="$out/cmd/${pname}/''${file#./cmd/${pname}/}"
- echo $(dirname "$dest")
- mkdir -p "$(dirname "$dest")"
- cp "$file" "$dest"
- done
- ' sh {} +
- '';
-}
diff --git a/services/blob/auth.go b/services/blob/auth.go
new file mode 100644
index 0000000..63b6868
--- /dev/null
+++ b/services/blob/auth.go
@@ -0,0 +1,32 @@
+package main
+
+import (
+ "log"
+ "net/http"
+ "strings"
+)
+
+func BearerAuth(handler http.HandlerFunc, validToken string) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ auth := r.Header.Get("Authorization")
+ if auth == "" {
+ log.Printf("auth is %s", auth)
+ http.Error(w, "unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ token := strings.TrimPrefix(auth, "Bearer ")
+ if token == auth {
+ http.Error(w, "invalid authorization format", http.StatusUnauthorized)
+ return
+ }
+
+ if token != validToken {
+ log.Printf("auth is %s, not %s", auth, validToken)
+ http.Error(w, "unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ handler.ServeHTTP(w, r)
+ }
+}
diff --git a/services/blob/default.nix b/services/blob/default.nix
new file mode 100644
index 0000000..b9ac9da
--- /dev/null
+++ b/services/blob/default.nix
@@ -0,0 +1,9 @@
+{ buildGoApplication }:
+buildGoApplication rec {
+ pname = "blob";
+ version = "0.1.0";
+ src = ../../.;
+ pwd = ./.;
+ modules = ../../gomod2nix.toml;
+ subPackages = [ "services/${pname}" ];
+}
diff --git a/services/blob/handlers.go b/services/blob/handlers.go
new file mode 100644
index 0000000..f191b9c
--- /dev/null
+++ b/services/blob/handlers.go
@@ -0,0 +1,114 @@
+package main
+
+import (
+ "encoding/json"
+ "io"
+ "log"
+ "mime"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+const (
+ uploadLimit = 10 << 20
+)
+
+func handlerDelete(w http.ResponseWriter, r *http.Request) {
+ filename := strings.TrimPrefix(r.URL.Path, "/files/")
+ if filename == "" {
+ http.Error(w, "filename not specified", http.StatusBadRequest)
+ return
+ }
+
+ if err := FileDelete(filename); err != nil {
+ if os.IsNotExist(err) {
+ http.Error(w, "file not found", http.StatusNotFound)
+ } else {
+ http.Error(w, "failed to delete file", http.StatusInternalServerError)
+ log.Printf("blob-service: error deleting file (%v)", err)
+ }
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("file deleted successfully"))
+}
+
+func handlerDownload(w http.ResponseWriter, r *http.Request) {
+ filename := strings.TrimPrefix(r.URL.Path, "/files/")
+ if filename == "" {
+ http.Error(w, "filename not specified", http.StatusBadRequest)
+ return
+ }
+
+ file, err := FileGet(filename)
+ if err != nil {
+ if os.IsNotExist(err) {
+ http.Error(w, "file not found", http.StatusNotFound)
+ } else {
+ http.Error(w, "failed to retrieve file", http.StatusInternalServerError)
+ log.Printf("blob-service: error retrieving file (%v)", err)
+ }
+ return
+ }
+ defer file.Close()
+
+ w.Header().Set("Content-Disposition", "attachment; filename="+filename)
+
+ contentType := mime.TypeByExtension(filepath.Ext(filename))
+ if contentType == "" {
+ w.Header().Set("Content-Type", "application/octet-stream")
+ }
+ w.Header().Set("Content-Type", contentType)
+
+ if _, err = io.Copy(w, file); err != nil {
+ http.Error(w, "failed to send file", http.StatusInternalServerError)
+ log.Printf("blob-service: error sending file (%v)", err)
+ }
+}
+
+func handlerUpload(w http.ResponseWriter, r *http.Request) {
+ r.Body = http.MaxBytesReader(w, r.Body, uploadLimit)
+
+ filename := strings.TrimPrefix(r.URL.Path, "/files/")
+ if filename == "" {
+ http.Error(w, "filename not specified", http.StatusBadRequest)
+ return
+ }
+
+ err := r.ParseMultipartForm(uploadLimit)
+ if err != nil {
+ http.Error(w, "failed to parse form data", http.StatusBadRequest)
+ return
+ }
+
+ file, _, err := r.FormFile("file")
+ if err != nil {
+ http.Error(w, "failed to get file from request", http.StatusBadRequest)
+ return
+ }
+ defer file.Close()
+
+ if err = FileSave(filename, file); err != nil {
+ http.Error(w, "failed to save file", http.StatusInternalServerError)
+ log.Printf("blob-service: error saving file (%v)", err)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("file uploaded successfully"))
+}
+
+func handlerList(w http.ResponseWriter, r *http.Request) {
+ files, err := FilesList()
+ if err != nil {
+ http.Error(w, "failed to list files", http.StatusInternalServerError)
+ log.Printf("blob-service: error listing files (%v)", err)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(files)
+}
diff --git a/services/blob/main.go b/services/blob/main.go
new file mode 100644
index 0000000..be37e53
--- /dev/null
+++ b/services/blob/main.go
@@ -0,0 +1,33 @@
+package main
+
+import (
+ "log"
+ "net/http"
+ "os"
+)
+
+const (
+ Port = ":8080"
+)
+
+var (
+ Token = os.Getenv("BLOB_TOKEN")
+)
+
+func main() {
+ if Token == "" {
+ log.Fatalf("blob-service: no token set, set BLOB_TOKEN environment var")
+ }
+
+ http.HandleFunc("DELETE /files/", BearerAuth(handlerDelete, Token))
+ http.HandleFunc("GET /files/", rateLimit(handlerDownload, 100))
+ http.HandleFunc("PUT /files/", BearerAuth(handlerUpload, Token))
+
+ http.HandleFunc("GET /files", rateLimit(handlerList, 10))
+
+ log.Printf("blob-service: server started on port %s", Port)
+ err := http.ListenAndServe(Port, nil)
+ if err != nil {
+ log.Fatalf("blob-service: server failed (%v)", err)
+ }
+}
diff --git a/services/blob/ratelimiter.go b/services/blob/ratelimiter.go
new file mode 100644
index 0000000..626c0bf
--- /dev/null
+++ b/services/blob/ratelimiter.go
@@ -0,0 +1,43 @@
+package main
+
+import (
+ "net"
+ "net/http"
+ "sync"
+ "time"
+)
+
+const duration = time.Minute
+
+var (
+ clients = make(map[string]int)
+ clientsMu = sync.Mutex{}
+)
+
+func rateLimit(next http.HandlerFunc, maxCount int) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ ip, _, _ := net.SplitHostPort(r.RemoteAddr)
+ count := clients[ip]
+
+ if count > maxCount {
+ http.Error(w, "too many requests...", http.StatusTooManyRequests)
+ return
+ }
+
+ clientsMu.Lock()
+ clients[ip]++
+ clientsMu.Unlock()
+
+ next.ServeHTTP(w, r)
+
+ go func() {
+ time.Sleep(duration)
+ clientsMu.Lock()
+ clients[ip]--
+ if clients[ip] <= 0 {
+ delete(clients, ip)
+ }
+ clientsMu.Unlock()
+ }()
+ }
+}
diff --git a/services/blob/storage.go b/services/blob/storage.go
new file mode 100644
index 0000000..016dd35
--- /dev/null
+++ b/services/blob/storage.go
@@ -0,0 +1,59 @@
+package main
+
+import (
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+const storageDir = "data"
+
+func init() {
+ if _, err := os.Stat(storageDir); os.IsNotExist(err) {
+ os.Mkdir(storageDir, 0755)
+ }
+}
+
+func FileDelete(filename string) error {
+ filePath := filepath.Join(storageDir, filename)
+ return os.Remove(filePath)
+}
+
+func FileGet(filename string) (*os.File, error) {
+ filePath := filepath.Join(storageDir, filename)
+ return os.Open(filePath)
+}
+
+func FileSave(filename string, data io.Reader) error {
+ filePath := filepath.Join(storageDir, filename)
+ file, err := os.Create(filePath)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ _, err = io.Copy(file, data)
+ return err
+}
+
+func FilesList() ([]string, error) {
+ filenames := make([]string, 0)
+ if err := filepath.Walk(storageDir,
+ func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if !info.IsDir() {
+ filenames = append(filenames, strings.TrimPrefix(path, storageDir+"/"))
+ }
+
+ return nil
+ },
+ ); err != nil {
+ return nil, err
+ }
+
+ return filenames, nil
+}
diff --git a/cmd/webserver/cache/cache.go b/services/webserver/cache/cache.go
similarity index 100%
rename from cmd/webserver/cache/cache.go
rename to services/webserver/cache/cache.go
diff --git a/services/webserver/components/footer.templ b/services/webserver/components/footer.templ
new file mode 100644
index 0000000..1cdc218
--- /dev/null
+++ b/services/webserver/components/footer.templ
@@ -0,0 +1,9 @@
+package components
+
+type Footer struct{}
+
+templ (p Footer) View() {
+
+}
diff --git a/services/webserver/components/header.templ b/services/webserver/components/header.templ
new file mode 100644
index 0000000..61c5004
--- /dev/null
+++ b/services/webserver/components/header.templ
@@ -0,0 +1,13 @@
+package components
+
+type Header struct {
+ PageCurrent string
+ Pages []string
+}
+
+templ (p Header) View() {
+
+}
diff --git a/services/webserver/components/nav.templ b/services/webserver/components/nav.templ
new file mode 100755
index 0000000..96c95c8
--- /dev/null
+++ b/services/webserver/components/nav.templ
@@ -0,0 +1,27 @@
+package components
+
+type Nav struct {
+ PageCurrent string
+ Pages []string
+}
+
+templ (p Nav) View() {
+
+}
diff --git a/services/webserver/default.nix b/services/webserver/default.nix
new file mode 100644
index 0000000..6001816
--- /dev/null
+++ b/services/webserver/default.nix
@@ -0,0 +1,46 @@
+{
+ fetchurl,
+ makeWrapper,
+ buildGoApplication,
+ tailwindcss,
+ templpkgs,
+}:
+let
+ htmxVersion = "2.0.2";
+ htmx = fetchurl {
+ url = "https://github.com/bigskysoftware/htmx/releases/download/v${htmxVersion}/htmx.min.js";
+ hash = "sha256-4XRtl1nsDUPFwoRFIzOjELtf1yheusSy3Jv0TXK1qIc=";
+ };
+in
+buildGoApplication rec {
+ pname = "webserver";
+ version = "0.1";
+ src = ../../.;
+ pwd = ./.;
+ modules = ../../gomod2nix.toml;
+ subPackages = [ "services/${pname}" ];
+ nativeBuildInputs = [
+ makeWrapper
+ tailwindcss
+ ];
+ preBuild = ''
+ ${templpkgs}/bin/templ generate .
+ '';
+ postInstall = ''
+ public=./services/${pname}/public
+ static=$out/public
+
+ mkdir -p $static
+
+ tailwindcss -c $public/tailwind.config.js -i $public/main.css -o $static/main.css --minify
+
+ rsync -a $public $out --exclude js --exclude='*.css'
+
+ mkdir -p $static/js
+ cp ${htmx} $static/js/htmx.min.js
+
+ mv $out/bin/${pname} $out/bin/.${pname}-unwrapped
+ makeWrapper $out/bin/.${pname}-unwrapped $out/bin/${pname} \
+ --chdir $out
+ '';
+}
diff --git a/services/webserver/layouts/base.templ b/services/webserver/layouts/base.templ
new file mode 100644
index 0000000..244f468
--- /dev/null
+++ b/services/webserver/layouts/base.templ
@@ -0,0 +1,37 @@
+package layouts
+
+import "personal-website/services/webserver/components"
+
+type Base struct {
+ Pages []string
+ PageCurrent string
+ Title string
+}
+
+templ (p Base) View() {
+
+
+
+
+
+
+ { p.Title }
+
+
+
+
+
+
+
+
+ @components.Header{
+ PageCurrent: p.PageCurrent,
+ Pages: p.Pages,
+ }.View()
+
+ { children... }
+
+ @components.Footer{}.View()
+
+
+}
diff --git a/services/webserver/main.go b/services/webserver/main.go
new file mode 100644
index 0000000..d77f22e
--- /dev/null
+++ b/services/webserver/main.go
@@ -0,0 +1,124 @@
+package main
+
+import (
+ "bytes"
+ "html/template"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/a-h/templ"
+ "github.com/yuin/goldmark"
+ highlighting "github.com/yuin/goldmark-highlighting/v2"
+
+ "personal-website/internal"
+ "personal-website/services/webserver/cache"
+ "personal-website/services/webserver/pages"
+)
+
+var (
+ port = os.Getenv("WEBSERVER_PORT")
+)
+
+var logRequests = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ log.Printf("%s %s\n", req.Method, req.URL)
+ http.DefaultServeMux.ServeHTTP(w, req)
+})
+
+func main() {
+ log.Println("Starting server...")
+
+ cache.InitCache()
+
+ http.HandleFunc("GET /healthy", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ })
+
+ navList := []string{"home", "blog", "projects"}
+
+ pageHome := pages.Home{Pages: navList}
+ http.Handle("GET /", templ.Handler(pageHome.View()))
+ http.Handle("GET /home", templ.Handler(pageHome.View()))
+
+ pageBlog := pages.Blog{Pages: navList}
+ http.HandleFunc("GET /blog", func(w http.ResponseWriter, r *http.Request) {
+ posts, err := cache.Cache.GetPosts()
+ if err != nil {
+ log.Printf("failed to fetch posts from cache (%v)", err)
+ }
+
+ pageBlog.View(posts).Render(r.Context(), w)
+ })
+
+ pagePost := pages.Post{Pages: navList}
+ http.HandleFunc("GET /post/{slug}", func(w http.ResponseWriter, r *http.Request) {
+ slug := r.PathValue("slug")
+
+ post, err := slugToHTML(slug)
+ if err != nil {
+ log.Printf("failed to get post %s from cache (%v)", slug, err)
+ w.WriteHeader(http.StatusInternalServerError)
+ }
+
+ pagePost.View(post).Render(r.Context(), w)
+ })
+
+ pageProjects := pages.Projects{Pages: navList}
+ http.Handle("GET /projects", templ.Handler(pageProjects.View()))
+
+ // static
+ http.Handle("GET /public/", http.StripPrefix("/public/", staticHandler(http.Dir("public"))))
+ http.Handle("GET /robots.txt", staticHandler(http.Dir("static/seo")))
+
+ log.Fatal(http.ListenAndServe(port, logRequests))
+}
+
+func slugToHTML(slug string) (internal.Post, error) {
+ post, err := cache.Cache.GetPost(slug)
+ if err != nil {
+ log.Printf("Error getting post %s: %v", slug, err)
+ return post, err
+ }
+
+ mdRenderer := goldmark.New(
+ goldmark.WithExtensions(
+ highlighting.NewHighlighting(
+ highlighting.WithStyle("rose-pine"),
+ ),
+ ),
+ )
+
+ var buf bytes.Buffer
+ err = mdRenderer.Convert([]byte(post.Content), &buf)
+ if err != nil {
+ log.Printf("Error parsing post %s to markdown: %v", slug, err)
+ return post, err
+ }
+
+ post.HTML = template.HTML(buf.String())
+
+ return post, nil
+}
+
+func staticHandler(root http.FileSystem) http.HandlerFunc {
+ fileServer := http.FileServer(root)
+
+ return func(w http.ResponseWriter, r *http.Request) {
+ ext := strings.ToLower(filepath.Ext(r.URL.Path))
+
+ switch ext {
+ case ".css":
+ w.Header().Set("Cache-Control", "public, max-age=1800, immutable")
+ case ".js":
+ w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
+ case ".jpg", ".jpeg", ".png", ".gif", ".ico":
+ w.Header().Set("Cache-Control", "public, max-age=2592000, immutable")
+ default:
+ w.Header().Set("Cache-Control", "max-age=0")
+ }
+
+ fileServer.ServeHTTP(w, r)
+ }
+}
diff --git a/services/webserver/pages/blog.templ b/services/webserver/pages/blog.templ
new file mode 100644
index 0000000..30ee89d
--- /dev/null
+++ b/services/webserver/pages/blog.templ
@@ -0,0 +1,38 @@
+package pages
+
+import (
+ "personal-website/internal"
+ "personal-website/services/webserver/layouts"
+)
+
+type Blog struct {
+ Pages []string
+}
+
+templ (p Blog) View(posts []internal.Post) {
+ @layouts.Base{
+ Pages: p.Pages,
+ PageCurrent: "blog",
+ Title: "Ethan Thoma \\ Blog",
+ }.View() {
+
+
+
+ for _, post := range posts {
+ -
+
+ -
+
{ post.Title }
+
+ }
+
+
+ }
+}
diff --git a/services/webserver/pages/home.templ b/services/webserver/pages/home.templ
new file mode 100644
index 0000000..0b91fa1
--- /dev/null
+++ b/services/webserver/pages/home.templ
@@ -0,0 +1,30 @@
+package pages
+
+import "personal-website/services/webserver/layouts"
+
+type Home struct {
+ Pages []string
+}
+
+templ (p Home) View() {
+ @layouts.Base{
+ Pages: p.Pages,
+ PageCurrent: "home",
+ Title: "Ethan Thoma",
+ }.View() {
+
+
+
+ ML graduate student @ STASER Lab UBC
+
+ Focused on NLP and RL research
+
+ Contact:
+
+
+ }
+}
diff --git a/services/webserver/pages/post.templ b/services/webserver/pages/post.templ
new file mode 100644
index 0000000..4bb1e75
--- /dev/null
+++ b/services/webserver/pages/post.templ
@@ -0,0 +1,22 @@
+package pages
+
+import (
+ "personal-website/internal"
+ "personal-website/services/webserver/layouts"
+)
+
+type Post struct {
+ Pages []string
+}
+
+templ (p Post) View(post internal.Post) {
+ @layouts.Base{
+ Pages: p.Pages,
+ PageCurrent: "blog",
+ Title: "Ethan Thoma \\ " + post.Title,
+ }.View() {
+
+ @templ.Raw(post.HTML)
+
+ }
+}
diff --git a/services/webserver/pages/projects.templ b/services/webserver/pages/projects.templ
new file mode 100644
index 0000000..64a50cc
--- /dev/null
+++ b/services/webserver/pages/projects.templ
@@ -0,0 +1,45 @@
+package pages
+
+import "personal-website/services/webserver/layouts"
+
+type Projects struct {
+ Pages []string
+}
+
+templ (p Projects) View() {
+ @layouts.Base{
+ Pages: p.Pages,
+ PageCurrent: "projects",
+ Title: "Ethan Thoma \\ Projects",
+ }.View() {
+
+
+
+
+ }
+}
diff --git a/cmd/webserver/public/favicon/android-chrome-192x192.png b/services/webserver/public/favicon/android-chrome-192x192.png
similarity index 100%
rename from cmd/webserver/public/favicon/android-chrome-192x192.png
rename to services/webserver/public/favicon/android-chrome-192x192.png
diff --git a/cmd/webserver/public/favicon/android-chrome-512x512.png b/services/webserver/public/favicon/android-chrome-512x512.png
similarity index 100%
rename from cmd/webserver/public/favicon/android-chrome-512x512.png
rename to services/webserver/public/favicon/android-chrome-512x512.png
diff --git a/cmd/webserver/public/favicon/apple-touch-icon.png b/services/webserver/public/favicon/apple-touch-icon.png
similarity index 100%
rename from cmd/webserver/public/favicon/apple-touch-icon.png
rename to services/webserver/public/favicon/apple-touch-icon.png
diff --git a/cmd/webserver/public/favicon/favicon-16x16.png b/services/webserver/public/favicon/favicon-16x16.png
similarity index 100%
rename from cmd/webserver/public/favicon/favicon-16x16.png
rename to services/webserver/public/favicon/favicon-16x16.png
diff --git a/cmd/webserver/public/favicon/favicon-32x32.png b/services/webserver/public/favicon/favicon-32x32.png
similarity index 100%
rename from cmd/webserver/public/favicon/favicon-32x32.png
rename to services/webserver/public/favicon/favicon-32x32.png
diff --git a/cmd/webserver/public/favicon/favicon.ico b/services/webserver/public/favicon/favicon.ico
similarity index 100%
rename from cmd/webserver/public/favicon/favicon.ico
rename to services/webserver/public/favicon/favicon.ico
diff --git a/cmd/webserver/public/favicon/site.webmanifest b/services/webserver/public/favicon/site.webmanifest
similarity index 100%
rename from cmd/webserver/public/favicon/site.webmanifest
rename to services/webserver/public/favicon/site.webmanifest
diff --git a/services/webserver/public/main.css b/services/webserver/public/main.css
new file mode 100644
index 0000000..1f881dc
--- /dev/null
+++ b/services/webserver/public/main.css
@@ -0,0 +1,69 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer components {
+ .content {
+ &>*+* {
+ @apply mt-2xs-xs;
+ }
+
+ & h1 {
+ @apply text-2xl;
+ @apply mb-m;
+ @apply font-extrabold;
+ @apply leading-none;
+ @apply tracking-tight;
+ }
+
+ & h2 {
+ @apply text-lg;
+ @apply mt-l-xl;
+ @apply font-extrabold;
+ @apply leading-none;
+ @apply tracking-tight;
+ }
+
+ & h3 {
+ @apply text-m;
+ @apply mt-l-xl;
+ @apply font-extrabold;
+ @apply leading-none;
+ @apply tracking-tight;
+ }
+
+ & pre {
+ @apply bg-high;
+ @apply text-sm;
+ @apply overflow-x-auto;
+ @apply p-xs;
+
+ &>code {
+ @apply break-words;
+ }
+ }
+
+ & a {
+ @apply underline;
+ }
+
+ & ul,
+ & ol {
+ @apply list-inside;
+
+ &>li {
+ @apply ml-m;
+ }
+
+ &>*+li {
+ @apply mt-3xs;
+ }
+ }
+
+ & blockquote {
+ @apply border-l-4;
+ @apply border-link;
+ @apply pl-3xs;
+ }
+ }
+}
diff --git a/cmd/webserver/public/seo/robots.txt b/services/webserver/public/seo/robots.txt
similarity index 100%
rename from cmd/webserver/public/seo/robots.txt
rename to services/webserver/public/seo/robots.txt
diff --git a/cmd/webserver/public/seo/sitemap.xml b/services/webserver/public/seo/sitemap.xml
similarity index 100%
rename from cmd/webserver/public/seo/sitemap.xml
rename to services/webserver/public/seo/sitemap.xml
diff --git a/services/webserver/public/tailwind.config.js b/services/webserver/public/tailwind.config.js
new file mode 100644
index 0000000..0e4e6dd
--- /dev/null
+++ b/services/webserver/public/tailwind.config.js
@@ -0,0 +1,83 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ['./**/*.{html,templ,go}'],
+ theme: {
+ colors: {
+ 'base': '#fafaf8',
+ 'content': '#141413',
+ 'link': '#907aa9',
+ 'high': '#cecacd',
+ },
+
+ fontSize: {
+ // @link https://utopia.fyi/type/calculator?c=320,16,1.2,1240,22,1.25,5,2,&s=0.75|0.5|0.25,1.5|2|3|4|6|8|11|15|19,3xs-xs|2xs-s|xs-m|s-l|m-xl|l-2xl|xl-3xl|2xl-4xl|3xl-5xl|4xl-6xl|5xl-6xl|6xl-7xl&g=s,l,xl,12
+
+ 'xs': 'clamp(0.6944rem, 0.6299rem + 0.3227vw, 0.88rem)',
+ 'sm': 'clamp(0.9375rem, 0.9159rem + 0.1085vw, 1rem)',
+ 'm': 'clamp(1rem, 0.8696rem + 0.6522vw, 1.375rem)',
+ 'lg': 'clamp(1.35rem, 1.2767rem + 0.3688vw, 1.5625rem)',
+ 'xl': 'clamp(1.62rem, 1.5051rem + 0.5781vw, 1.9531rem)',
+ '2xl': 'clamp(1.9438rem, 1.7722rem + 0.8633vw, 2.4413rem)',
+ '3xl': 'clamp(2.3325rem, 2.0844rem + 1.2484vw, 3.0519rem)',
+ '4xl': 'clamp(2.7994rem, 2.4491rem + 1.7625vw, 3.815rem)',
+ },
+
+ spacing: {
+ // @link https://utopia.fyi/space/calculator?c=320,16,1.2,1240,22,1.25,5,2,&s=0.75|0.5|0.25,1.5|2|3|4|6|8|11|15|19,3xs-xs|2xs-s|xs-m|s-l|m-xl|l-2xl|xl-3xl|2xl-4xl|3xl-5xl|4xl-6xl|5xl-6xl|6xl-7xl|s-2xl&g=s,l,xl,12
+
+ '3xs': 'clamp(0.25rem, 0.2065rem + 0.2174vw, 0.375rem)',
+ '2xs': 'clamp(0.5rem, 0.4348rem + 0.3261vw, 0.6875rem)',
+ 'xs': 'clamp(0.75rem, 0.6413rem + 0.5435vw, 1.0625rem)',
+ 's': 'clamp(1rem, 0.8696rem + 0.6522vw, 1.375rem)',
+ 'm': 'clamp(1.5rem, 1.3043rem + 0.9783vw, 2.0625rem)',
+ 'l': 'clamp(2rem, 1.7391rem + 1.3043vw, 2.75rem)',
+ 'xl': 'clamp(3rem, 2.6087rem + 1.9565vw, 4.125rem)',
+ '2xl': 'clamp(4rem, 3.4783rem + 2.6087vw, 5.5rem)',
+ '3xl': 'clamp(6rem, 5.2174rem + 3.913vw, 8.25rem)',
+ '4xl': 'clamp(8rem, 6.9565rem + 5.2174vw, 11rem)',
+ '5xl': 'clamp(11rem, 9.5652rem + 7.1739vw, 15.125rem)',
+ '6xl': 'clamp(15rem, 13.0435rem + 9.7826vw, 20.625rem)',
+ '7xl': 'clamp(19rem, 16.5217rem + 12.3913vw, 26.125rem)',
+
+ // One-up pairs
+ '3xs-2xs': 'clamp(0.25rem, 0.0978rem + 0.7609vw, 0.6875rem)',
+ '2xs-xs': 'clamp(0.5rem, 0.3043rem + 0.9783vw, 1.0625rem)',
+ 'xs-s': 'clamp(0.75rem, 0.5326rem + 1.087vw, 1.375rem)',
+ 's-m': 'clamp(1rem, 0.6304rem + 1.8478vw, 2.0625rem)',
+ 'm-l': 'clamp(1.5rem, 1.0652rem + 2.1739vw, 2.75rem)',
+ 'l-xl': 'clamp(2rem, 1.2609rem + 3.6957vw, 4.125rem)',
+ 'xl-2xl': 'clamp(3rem, 2.1304rem + 4.3478vw, 5.5rem)',
+ '2xl-3xl': 'clamp(4rem, 2.5217rem + 7.3913vw, 8.25rem)',
+ '3xl-4xl': 'clamp(6rem, 4.2609rem + 8.6957vw, 11rem)',
+ '4xl-5xl': 'clamp(8rem, 5.5217rem + 12.3913vw, 15.125rem)',
+ '5xl-6xl': 'clamp(11rem, 7.6522rem + 16.7391vw, 20.625rem)',
+ '6xl-7xl': 'clamp(15rem, 11.1304rem + 19.3478vw, 26.125rem)',
+
+ // Two-up pairs
+ '3xs-xs': 'clamp(0.25rem, -0.0326rem + 1.413vw, 1.0625rem)',
+ '2xs-s': 'clamp(0.5rem, 0.1957rem + 1.5217vw, 1.375rem)',
+ 'xs-m': 'clamp(0.75rem, 0.2935rem + 2.2826vw, 2.0625rem)',
+ 's-l': 'clamp(1rem, 0.3913rem + 3.0435vw, 2.75rem)',
+ 'm-xl': 'clamp(1.5rem, 0.587rem + 4.5652vw, 4.125rem)',
+ 'l-2xl': 'clamp(2rem, 0.7826rem + 6.087vw, 5.5rem)',
+ 'xl-3xl': 'clamp(3rem, 1.1739rem + 9.1304vw, 8.25rem)',
+ '2xl-4xl': 'clamp(4rem, 1.5652rem + 12.1739vw, 11rem)',
+ '3xl-5xl': 'clamp(6rem, 2.8261rem + 15.8696vw, 15.125rem)',
+ '4xl-6xl': 'clamp(8rem, 3.6087rem + 21.9565vw, 20.625rem)',
+ '5xl-6xl': 'clamp(11rem, 7.6522rem + 16.7391vw, 20.625rem)',
+ '6xl-7xl': 'clamp(15rem, 11.1304rem + 19.3478vw, 26.125rem)',
+
+ // Custom
+ 's-2xl': 'clamp(1rem, -0.5652rem + 7.8261vw, 5.5rem)',
+ },
+
+ extend: {
+ maxWidth: {
+ 'content': 'min(clamp(320px, 75%, 640px), 100svw)',
+ },
+ },
+ },
+ corePlugins: {
+ preflight: true,
+ },
+}