commit 2686a045fddb9c06f0cf5344322987275028a149 Author: Gustavo "Guz" L. de Mello Date: Mon Jun 24 11:50:06 2024 -0300 chore: initial setup diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..7fd05db --- /dev/null +++ b/.envrc @@ -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 diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..6ad6a8a --- /dev/null +++ b/.github/workflows/checks.yml @@ -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 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..c8f7e9e --- /dev/null +++ b/.github/workflows/deploy.yml @@ -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 diff --git a/.github/workflows/deploy_pr_preview.yml b/.github/workflows/deploy_pr_preview.yml new file mode 100644 index 0000000..cd01fed --- /dev/null +++ b/.github/workflows/deploy_pr_preview.yml @@ -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 diff --git a/.github/workflows/deploy_preview.yml b/.github/workflows/deploy_preview.yml new file mode 100644 index 0000000..84e0592 --- /dev/null +++ b/.github/workflows/deploy_preview.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..785dc37 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules +Session.vim +.direnv +.vercel +*_templ.go +*_templ.txt +dist +tmp +bin +static/uno.css diff --git a/api/ai.go b/api/ai.go new file mode 100644 index 0000000..97c1235 --- /dev/null +++ b/api/ai.go @@ -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") +} diff --git a/api/robots.go b/api/robots.go new file mode 100644 index 0000000..82cfa7e --- /dev/null +++ b/api/robots.go @@ -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") +} diff --git a/cmd/build/main.go b/cmd/build/main.go new file mode 100644 index 0000000..a67fdb6 --- /dev/null +++ b/cmd/build/main.go @@ -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) + } +} diff --git a/cmd/vercel/main.go b/cmd/vercel/main.go new file mode 100644 index 0000000..ef87034 --- /dev/null +++ b/cmd/vercel/main.go @@ -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) + } +} diff --git a/config/routes.go b/config/routes.go new file mode 100644 index 0000000..b75b763 --- /dev/null +++ b/config/routes.go @@ -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) +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..2fac8de --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d3aead0 --- /dev/null +++ b/flake.nix @@ -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 + ]; + }; + }); + }; +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b9dfe1e --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module extrovert + +go 1.22.3 + +require github.com/a-h/templ v0.2.707 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1ad2589 --- /dev/null +++ b/go.sum @@ -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= diff --git a/internals/helpers.go b/internals/helpers.go new file mode 100644 index 0000000..bf944d9 --- /dev/null +++ b/internals/helpers.go @@ -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 + } +} diff --git a/internals/middleware.go b/internals/middleware.go new file mode 100644 index 0000000..3e60e11 --- /dev/null +++ b/internals/middleware.go @@ -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} +} diff --git a/internals/static_writer.go b/internals/static_writer.go new file mode 100644 index 0000000..976a1a7 --- /dev/null +++ b/internals/static_writer.go @@ -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 +} diff --git a/layouts/page.templ b/layouts/page.templ new file mode 100644 index 0000000..6598ed3 --- /dev/null +++ b/layouts/page.templ @@ -0,0 +1,20 @@ +package layouts + +templ Page(title string) { + + + + + { title } + + + { children... } + + +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..c34154a --- /dev/null +++ b/main.go @@ -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) + } +} diff --git a/makefile b/makefile new file mode 100644 index 0000000..d2f79ba --- /dev/null +++ b/makefile @@ -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 diff --git a/pages/index.templ b/pages/index.templ new file mode 100644 index 0000000..03b31f5 --- /dev/null +++ b/pages/index.templ @@ -0,0 +1,11 @@ +package pages + +import ( + "extrovert/layouts" +) + +templ Index() { + @layouts.Page("013") { +

Hello, world

+ } +} diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..a22c396 --- /dev/null +++ b/vercel.json @@ -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 + } + ] +}