chore: initial setup

This commit is contained in:
Gustavo "Guz" L. de Mello
2024-06-24 11:50:06 -03:00
commit 2686a045fd
23 changed files with 732 additions and 0 deletions

4
.envrc Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,11 @@
package pages
import (
"extrovert/layouts"
)
templ Index() {
@layouts.Page("013") {
<h1>Hello, world</h1>
}
}

16
vercel.json Normal file
View 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
}
]
}