chore: initial setup
This commit is contained in:
4
.envrc
Normal file
4
.envrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then
|
||||||
|
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4="
|
||||||
|
fi
|
||||||
|
use flake
|
||||||
33
.github/workflows/checks.yml
vendored
Normal file
33
.github/workflows/checks.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
name: Checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- synchronize
|
||||||
|
- reopened
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
if: ${{ github.repository == 'capytalcode/project-extrovert' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Setup go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.22.2'
|
||||||
|
- name: Generate templ
|
||||||
|
run: |
|
||||||
|
make templ
|
||||||
|
- name: Check
|
||||||
|
uses: golangci/golangci-lint-action@v6
|
||||||
|
with:
|
||||||
|
version: v1.58
|
||||||
37
.github/workflows/deploy.yml
vendored
Normal file
37
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: Deploy to Vercel
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy
|
||||||
|
if: ${{ github.repository == 'capytalcode/project-extrovert' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
deployments: write
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [20]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Setup go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.22.2'
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
make build/static
|
||||||
|
- name: Deploy
|
||||||
|
uses: BetaHuhn/deploy-to-vercel-action@v1
|
||||||
|
with:
|
||||||
|
GITHUB_TOKEN: ${{ SECRETS.GITHUB_TOKEN }}
|
||||||
|
VERCEL_TOKEN: ${{ SECRETS.VERCEL_TOKEN }}
|
||||||
|
VERCEL_ORG_ID: ${{ SECRETS.VERCEL_ORG_ID }}
|
||||||
|
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||||
|
PRODUCTION: true
|
||||||
40
.github/workflows/deploy_pr_preview.yml
vendored
Normal file
40
.github/workflows/deploy_pr_preview.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Deploy preview to Vercel
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- synchronize
|
||||||
|
- reopened
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy
|
||||||
|
if: ${{ github.repository == 'capytalcode/project-extrovert' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
deployments: write
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [20]
|
||||||
|
steps:
|
||||||
|
- name: Setup go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.22.2'
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
make build/static
|
||||||
|
- name: Deploy
|
||||||
|
uses: BetaHuhn/deploy-to-vercel-action@v1
|
||||||
|
with:
|
||||||
|
GITHUB_TOKEN: ${{ SECRETS.GITHUB_TOKEN }}
|
||||||
|
VERCEL_TOKEN: ${{ SECRETS.VERCEL_TOKEN }}
|
||||||
|
VERCEL_ORG_ID: ${{ SECRETS.VERCEL_ORG_ID }}
|
||||||
|
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||||
|
PRODUCTION: false
|
||||||
35
.github/workflows/deploy_preview.yml
vendored
Normal file
35
.github/workflows/deploy_preview.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: Deploy preview to Vercel
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy
|
||||||
|
if: ${{ github.repository == 'capytalcode/project-extrovert' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
deployments: write
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [20]
|
||||||
|
steps:
|
||||||
|
- name: Setup go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.22.2'
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
make build/static
|
||||||
|
- name: Deploy
|
||||||
|
uses: BetaHuhn/deploy-to-vercel-action@v1
|
||||||
|
with:
|
||||||
|
GITHUB_TOKEN: ${{ SECRETS.GITHUB_TOKEN }}
|
||||||
|
VERCEL_TOKEN: ${{ SECRETS.VERCEL_TOKEN }}
|
||||||
|
VERCEL_ORG_ID: ${{ SECRETS.VERCEL_ORG_ID }}
|
||||||
|
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||||
|
PRODUCTION: false
|
||||||
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
node_modules
|
||||||
|
Session.vim
|
||||||
|
.direnv
|
||||||
|
.vercel
|
||||||
|
*_templ.go
|
||||||
|
*_templ.txt
|
||||||
|
dist
|
||||||
|
tmp
|
||||||
|
bin
|
||||||
|
static/uno.css
|
||||||
30
api/ai.go
Normal file
30
api/ai.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"extrovert/internals"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AiTxt(w http.ResponseWriter, r *http.Request) {
|
||||||
|
error := internals.HttpErrorHelper(w)
|
||||||
|
|
||||||
|
aiList, err := http.Get("https://raw.githubusercontent.com/ai-robots-txt/ai.robots.txt/main/ai.txt")
|
||||||
|
if error("Error trying to fetch ai block list", err, http.StatusInternalServerError) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := io.ReadAll(aiList.Body)
|
||||||
|
if error("Error trying to read ai block list", err, http.StatusInternalServerError) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = w.Write(bytes)
|
||||||
|
if error("Error trying to write ai block list", err, http.StatusInternalServerError) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Cache-Control", "max-age=604800, stale-while-revalidate=86400, stale-if-error=86400")
|
||||||
|
w.Header().Add("CDN-Cache-Control", "max-age=604800")
|
||||||
|
w.Header().Add("Content-Type", "text/plain")
|
||||||
|
}
|
||||||
27
api/robots.go
Normal file
27
api/robots.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"extrovert/internals"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RobotsTxt(w http.ResponseWriter, r *http.Request) {
|
||||||
|
error := internals.HttpErrorHelper(w)
|
||||||
|
|
||||||
|
aiList, err := http.Get("https://raw.githubusercontent.com/ai-robots-txt/ai.robots.txt/main/robots.txt")
|
||||||
|
if error("Error trying to fetch ai block list", err, http.StatusInternalServerError) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := io.ReadAll(aiList.Body)
|
||||||
|
if error("Error trying to read ai block list", err, http.StatusInternalServerError) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(bytes)
|
||||||
|
|
||||||
|
w.Header().Add("Cache-Control", "max-age=604800, stale-while-revalidate=86400, stale-if-error=86400")
|
||||||
|
w.Header().Add("CDN-Cache-Control", "max-age=604800")
|
||||||
|
w.Header().Add("Content-Type", "text/plain")
|
||||||
|
}
|
||||||
30
cmd/build/main.go
Normal file
30
cmd/build/main.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"extrovert/config"
|
||||||
|
"extrovert/internals"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
dir := flag.String("d", "./dist", "the directory to write the files")
|
||||||
|
staticDir := flag.String("s", "./static", "the directory to copy static files from")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
w := internals.StaticWriter{
|
||||||
|
DistDir: dir,
|
||||||
|
StaticDir: staticDir,
|
||||||
|
Pages: config.ROUTES,
|
||||||
|
Context: context.Background(),
|
||||||
|
Logger: *log.Default(),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := w.WriteAll()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
65
cmd/vercel/main.go
Normal file
65
cmd/vercel/main.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"extrovert/config"
|
||||||
|
"extrovert/internals"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VercelConfig struct {
|
||||||
|
OutputDirectory string `json:"outputDirectory"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var logger = log.Default()
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
configPath := flag.String("c", "./vercel.json", "the path to the vercel.json file")
|
||||||
|
staticDir := flag.String("s", "./static", "the directory to copy static files from")
|
||||||
|
port := flag.Int("p", 8080, "the port to run the server")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
configFile, err := os.ReadFile(*configPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("Unable to read vercel.json file due to:\n%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var c VercelConfig
|
||||||
|
err = json.Unmarshal(configFile, &c)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("Unable to parse vercel.json file due to:\n%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w := internals.StaticWriter{
|
||||||
|
DistDir: &c.OutputDirectory,
|
||||||
|
StaticDir: staticDir,
|
||||||
|
Pages: config.ROUTES,
|
||||||
|
Context: context.Background(),
|
||||||
|
Logger: *log.Default(),
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Print("Writing static files")
|
||||||
|
err = w.WriteAll()
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Print("Starting server")
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
config.APIROUTES(mux)
|
||||||
|
mux.Handle("/", http.FileServer(http.Dir(c.OutputDirectory)))
|
||||||
|
|
||||||
|
logger.Printf("Running server at port: %v", *port)
|
||||||
|
err = http.ListenAndServe(fmt.Sprintf(":%v", *port), mux)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("Server crashed due to:\n%s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
config/routes.go
Normal file
18
config/routes.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"extrovert/api"
|
||||||
|
"extrovert/internals"
|
||||||
|
"extrovert/pages"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ROUTES = []internals.Page{
|
||||||
|
{Path: "index.html", Component: pages.Index()},
|
||||||
|
}
|
||||||
|
|
||||||
|
func APIROUTES(mux *http.ServeMux) {
|
||||||
|
mux.HandleFunc("/robots.txt", api.RobotsTxt)
|
||||||
|
mux.HandleFunc("/ai.txt", api.AiTxt)
|
||||||
|
}
|
||||||
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1719075281,
|
||||||
|
"narHash": "sha256-CyyxvOwFf12I91PBWz43iGT1kjsf5oi6ax7CrvaMyAo=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "a71e967ef3694799d0c418c98332f7ff4cc5f6af",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
30
flake.nix
Normal file
30
flake.nix
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
description = "learning.rs";
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
};
|
||||||
|
outputs = { nixpkgs, ... }:
|
||||||
|
let
|
||||||
|
systems = [
|
||||||
|
"x86_64-linux"
|
||||||
|
"aarch64-linux"
|
||||||
|
"x86_64-darwin"
|
||||||
|
"aarch64-darwin"
|
||||||
|
];
|
||||||
|
forAllSystems = f: nixpkgs.lib.genAttrs systems (system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs { inherit system; };
|
||||||
|
in
|
||||||
|
f system pkgs);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells = forAllSystems (system: pkgs: {
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
go
|
||||||
|
golangci-lint
|
||||||
|
];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module extrovert
|
||||||
|
|
||||||
|
go 1.22.3
|
||||||
|
|
||||||
|
require github.com/a-h/templ v0.2.707
|
||||||
4
go.sum
Normal file
4
go.sum
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U=
|
||||||
|
github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
20
internals/helpers.go
Normal file
20
internals/helpers.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package internals
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HttpErrorHelper(w http.ResponseWriter) func(msg string, err error, status int) bool {
|
||||||
|
return func(msg string, err error, status int) bool {
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(status)
|
||||||
|
_, err = w.Write([]byte(msg + "\n Error: " + err.Error()))
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
_, _ = w.Write([]byte("Error trying to return error code (somehow):\n" + err.Error()))
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
34
internals/middleware.go
Normal file
34
internals/middleware.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package internals
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Middleware struct {
|
||||||
|
handler http.Handler
|
||||||
|
dev bool
|
||||||
|
noCache bool
|
||||||
|
logger *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m.logger.Printf("Handling request. path=%s", r.URL.Path)
|
||||||
|
|
||||||
|
if m.dev {
|
||||||
|
r.URL.Scheme = "http"
|
||||||
|
} else {
|
||||||
|
r.URL.Scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
m.handler.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
if m.noCache {
|
||||||
|
w.Header().Del("Cache-Control")
|
||||||
|
w.Header().Add("Cache-Control", "max-age=0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMiddleware(handler http.Handler, dev bool, noCache bool, logger *log.Logger) *Middleware {
|
||||||
|
return &Middleware{handler, dev, noCache, logger}
|
||||||
|
}
|
||||||
107
internals/static_writer.go
Normal file
107
internals/static_writer.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package internals
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
)
|
||||||
|
|
||||||
|
const PERMISSIONS = 0755
|
||||||
|
|
||||||
|
type Page struct {
|
||||||
|
Path string
|
||||||
|
Component templ.Component
|
||||||
|
}
|
||||||
|
|
||||||
|
type StaticWriter struct {
|
||||||
|
DistDir *string
|
||||||
|
StaticDir *string
|
||||||
|
Pages []Page
|
||||||
|
Context context.Context
|
||||||
|
Logger log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *StaticWriter) WritePage(path string, writer func(ctx context.Context, w io.Writer) error) error {
|
||||||
|
directory := filepath.Dir(path)
|
||||||
|
err := os.MkdirAll(directory, PERMISSIONS)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
err = writer(w.Context, f)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *StaticWriter) WriteAll() error {
|
||||||
|
for _, page := range w.Pages {
|
||||||
|
p := filepath.Join(*w.DistDir, page.Path)
|
||||||
|
w.Logger.Printf("Writing page %s", p)
|
||||||
|
err := w.WritePage(p, page.Component.Render)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := filepath.WalkDir(*w.StaticDir, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if d.IsDir() || path == *w.StaticDir {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s, err := filepath.Abs(*w.StaticDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.CopyStatic(strings.TrimPrefix(f, s))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *StaticWriter) CopyStatic(path string) error {
|
||||||
|
c, err := os.ReadFile(filepath.Join(*w.StaticDir, path))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p := filepath.Join(*w.DistDir, path)
|
||||||
|
err = os.MkdirAll(filepath.Dir(p), PERMISSIONS)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
b, err := f.Write(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.Logger.Printf("Wrote %v bytes in %s", b, p)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
20
layouts/page.templ
Normal file
20
layouts/page.templ
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package layouts
|
||||||
|
|
||||||
|
templ Page(title string) {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en-US" class="scroll-smooth motion-reduce:scroll-auto">
|
||||||
|
<head>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content={ "width=device-width, " +
|
||||||
|
"initial-scale=1.0, " +
|
||||||
|
"maximum-scale=1.0, " +
|
||||||
|
"user-scalable=no" }
|
||||||
|
/>
|
||||||
|
<title>{ title }</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{ children... }
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
75
main.go
Normal file
75
main.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"extrovert/config"
|
||||||
|
"extrovert/internals"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logger = log.Default()
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
staticDir := flag.String("s", "./static", "the directory to copy static files from")
|
||||||
|
port := flag.Int("p", 8080, "the port to run the server")
|
||||||
|
dev := flag.Bool("d", false, "if the server is in development mode")
|
||||||
|
cache := flag.Bool("c", true, "if the static files are cached")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *dev {
|
||||||
|
log.Printf("Running server in DEVELOPMENT MODE")
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
config.APIROUTES(mux)
|
||||||
|
for _, route := range config.ROUTES {
|
||||||
|
path := "/" + strings.TrimSuffix(route.Path, ".html")
|
||||||
|
if path == "/index" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.Printf("Registering page route. page=%s route=%s", route.Path, path)
|
||||||
|
|
||||||
|
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Add("Content-Type", "text/html")
|
||||||
|
|
||||||
|
err := route.Component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("Unable to render route %s due to %s", route.Path, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/" {
|
||||||
|
logger.Printf("Handling file server request. path=%s", r.URL.Path)
|
||||||
|
http.FileServer(http.Dir(*staticDir)).ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "text/html")
|
||||||
|
|
||||||
|
index := slices.IndexFunc(config.ROUTES, func(route internals.Page) bool {
|
||||||
|
return route.Path == "index.html"
|
||||||
|
})
|
||||||
|
indexPage := config.ROUTES[index]
|
||||||
|
|
||||||
|
err := indexPage.Component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Unable to render index page due to %s", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.Printf("Running server at port: %v", *port)
|
||||||
|
|
||||||
|
middleware := internals.NewMiddleware(mux, *dev, !*cache, log.Default())
|
||||||
|
err := http.ListenAndServe(fmt.Sprintf(":%v", *port), middleware)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("Server crashed due to:\n%s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
54
makefile
Normal file
54
makefile
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
PORT?=8080
|
||||||
|
|
||||||
|
build: templ
|
||||||
|
go build -o bin/www
|
||||||
|
|
||||||
|
build/static: templ
|
||||||
|
go run ./cmd/build
|
||||||
|
|
||||||
|
dev/templ:
|
||||||
|
go run github.com/a-h/templ/cmd/templ@v0.2.707 generate --watch \
|
||||||
|
--proxy=http://localhost:$(PORT) \
|
||||||
|
--open-browser=false
|
||||||
|
|
||||||
|
dev/server:
|
||||||
|
go run github.com/air-verse/air@v1.52.2 \
|
||||||
|
--build.cmd "go build -o tmp/bin/main" \
|
||||||
|
--build.bin "tmp/bin/main" \
|
||||||
|
--build.exclude_dir "node_modules" \
|
||||||
|
--build.include_ext "go" \
|
||||||
|
--build.stop_on_error "false" \
|
||||||
|
--misc.clean_on_exit true \
|
||||||
|
-- -p $(PORT) -d
|
||||||
|
|
||||||
|
dev/sync_assets:
|
||||||
|
go run github.com/air-verse/air@v1.52.2 \
|
||||||
|
--build.cmd "templ generate --notify-proxy" \
|
||||||
|
--build.bin "true" \
|
||||||
|
--build.delay "100" \
|
||||||
|
--build.exclude_dir "" \
|
||||||
|
--build.include_dir "static" \
|
||||||
|
--build.include_ext "js,css"
|
||||||
|
|
||||||
|
dev:
|
||||||
|
make -j3 dev/templ dev/server dev/sync_assets
|
||||||
|
|
||||||
|
run: build
|
||||||
|
./bin/www
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
clean:
|
||||||
|
if [[ -d "dist" ]]; then rm -r ./dist; fi
|
||||||
|
if [[ -d "tmp" ]]; then rm -r ./tmp; fi
|
||||||
|
if [[ -d "bin" ]]; then rm -r ./bin; fi
|
||||||
|
rm ./static/uno.css
|
||||||
|
rm $(TEMPL_FILES)
|
||||||
|
|
||||||
|
# For some reason "templ generate" does not detect the files in CI,
|
||||||
|
# so this is a workaround.
|
||||||
|
TEMPL_FILES=$(patsubst %.templ, %_templ.go, $(wildcard **/*.templ))
|
||||||
|
templ: $(TEMPL_FILES)
|
||||||
|
@echo Generating templ files
|
||||||
|
%_templ.go: %.templ
|
||||||
|
go run github.com/a-h/templ/cmd/templ@v0.2.707 generate -f $^ > /dev/null
|
||||||
11
pages/index.templ
Normal file
11
pages/index.templ
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package pages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"extrovert/layouts"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ Index() {
|
||||||
|
@layouts.Page("013") {
|
||||||
|
<h1>Hello, world</h1>
|
||||||
|
}
|
||||||
|
}
|
||||||
16
vercel.json
Normal file
16
vercel.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||||
|
"outputDirectory": "dist",
|
||||||
|
"redirects": [
|
||||||
|
{
|
||||||
|
"source": "/robots.txt",
|
||||||
|
"destination": "/api/robots",
|
||||||
|
"statusCode": 301
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/ai.txt",
|
||||||
|
"destination": "/api/ai",
|
||||||
|
"statusCode": 301
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user