diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 13547c1..7f5b4bb 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -23,6 +23,10 @@ jobs: steps: - uses: actions/checkout@v4 + - id: vars + run: | + echo "sha_short=$(git rev-parse --short "$GITHUB_SHA")" >> "$GITHUB_OUTPUT" + - uses: actions/setup-node@v4 with: check-latest: true @@ -31,17 +35,40 @@ jobs: cache-dependency-path: | package-lock.json - - run: npx tailwindcss -i ./src/input.css -o ./src/output.css + - uses: actions/setup-go@v5 + with: + check-latest: true + cache-dependency-path: go.sum + go-version-file: go.mod + cache: true + + - run: make build-deps + + - run: go mod download + + - run: go mod verify + + - run: make build + + - uses: actions/upload-artifact@v4 + if: github.ref != 'refs/heads/main' + with: + if-no-files-found: error + name: 'public-${{ steps.vars.outputs.sha_short }}' + path: ./public + retention-days: 1 - name: Upload artifact + if: github.ref == 'refs/heads/main' uses: actions/upload-pages-artifact@v3 with: - path: ./src + path: ./public # Deploy job deploy: # Add a dependency to the build job needs: build + if: github.ref == 'refs/heads/main' # Grant GITHUB_TOKEN the permissions required to make a Pages deployment permissions: diff --git a/.gitignore b/.gitignore index d65bbed..7b8ef8b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules -output.css \ No newline at end of file +/public + +*_templ.go \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..09962d6 --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +GOOS=linux +GOARCH=amd64 +CGO_ENABLED=0 + +install: test-deps build-deps ## install from the current working tree + @npm i + @go install -v . + +clean: ## remove build artifacts from the working tree + @rm -rf public + +test-deps: ## install test dependencies + @go install github.com/bokwoon95/wgo@latest + @go get -v ./... + @go mod tidy + +build-deps: ## install build dependencies + @go install github.com/a-h/templ/cmd/templ@latest + +deps: build-deps test-deps ## install build and test dependencies + +assets: clean + @cp -a assets/. ./public/ + +generate: ## Go and Templ generate + @npx tailwindcss -i style.css -o ./public/style.css + @templ generate + +run: ## run and watch + @wgo -file=.go -file=.templ -file=.css -file=.md -xfile=_templ.go make generate :: go run main.go --dev + +build: assets generate ## build public static + @go run main.go + +update: ## update dependencies + @go get -u + @go mod tidy + @make install + @go mod tidy + +help: ## Show this help. + @grep -F -h "##" $(MAKEFILE_LIST) | grep -F -v grep | sed -e 's/\\$$//' | sed -e 's/:.\+##/\n\t/' diff --git a/assets/clouds_1.webp b/assets/clouds_1.webp new file mode 100644 index 0000000..b67ece0 Binary files /dev/null and b/assets/clouds_1.webp differ diff --git a/assets/clouds_2.webp b/assets/clouds_2.webp new file mode 100644 index 0000000..d54139a Binary files /dev/null and b/assets/clouds_2.webp differ diff --git a/assets/clouds_3.png b/assets/clouds_3.png new file mode 100644 index 0000000..49beca2 Binary files /dev/null and b/assets/clouds_3.png differ diff --git a/assets/favicon.png b/assets/favicon.png new file mode 100644 index 0000000..67709be Binary files /dev/null and b/assets/favicon.png differ diff --git a/src/ogimg.png b/assets/ogimg.png similarity index 100% rename from src/ogimg.png rename to assets/ogimg.png diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..888697d --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/pnmcosta/csta.dev + +go 1.22 + +require ( + github.com/a-h/templ v0.2.648 + github.com/gosimple/slug v1.14.0 + github.com/yuin/goldmark v1.7.1 + github.com/yuin/goldmark-meta v1.1.0 +) + +require ( + github.com/gosimple/unidecode v1.0.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..486346d --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/a-h/templ v0.2.648 h1:A1ggHGIE7AONOHrFaDTM8SrqgqHL6fWgWCijQ21Zy9I= +github.com/a-h/templ v0.2.648/go.mod h1:SA7mtYwVEajbIXFRh3vKdYm/4FYyLQAtPH1+KxzGPA8= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gosimple/slug v1.14.0 h1:RtTL/71mJNDfpUbCOmnf/XFkzKRtD6wL6Uy+3akm4Es= +github.com/gosimple/slug v1.14.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= +github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= +github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= +github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/posts/posts.go b/internal/posts/posts.go new file mode 100644 index 0000000..294601d --- /dev/null +++ b/internal/posts/posts.go @@ -0,0 +1,121 @@ +package posts + +import ( + "bytes" + "io/fs" + "log" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/yuin/goldmark" + meta "github.com/yuin/goldmark-meta" + "github.com/yuin/goldmark/parser" +) + +type Post struct { + Title string + Summary string + Tags []string + Date time.Time + Content []byte +} + +type walker struct { + posts []Post +} + +func ParsePosts() []Post { + w := walker{} + filepath.WalkDir("./posts", w.walk) + // stort by latest + sort.Slice(w.posts, func(i, j int) bool { return w.posts[i].Date.After(w.posts[j].Date) }) + return w.posts +} + +func (w *walker) walk(s string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if !d.IsDir() { + if !strings.HasSuffix(s, ".md") { + return nil + } + + content, err := os.ReadFile(s) + if err != nil { + log.Printf("%s: could not read\n", s) + return err + } + + markdown := goldmark.New( + goldmark.WithExtensions( + meta.Meta, + ), + ) + + var buf bytes.Buffer + context := parser.NewContext() + if err := markdown.Convert(content, &buf, parser.WithContext(context)); err != nil { + log.Printf("%s: invalid markdown\n", s) + return err + } + + parsed := buf.Bytes() + if len(parsed) == 0 { + log.Printf("%s: empty\n", s) + return nil + } + + post := Post{ + Content: parsed, + } + + metaData := meta.Get(context) + if value, ok := metaData["Title"].(string); ok && len(value) > 0 { + post.Title = value + } else { + log.Printf("%s: title required\n", s) + return nil + } + + if value, ok := metaData["Summary"].(string); ok && len(value) > 0 { + post.Summary = value + } + + if value, ok := metaData["Tags"].([]interface{}); ok && len(value) > 0 { + var strValue []string = make([]string, len(value)) + for i, d := range value { + if v, ok := d.(string); ok { + strValue[i] = v + } + } + post.Tags = strValue + } + + if value, ok := metaData["Date"].(string); ok && len(value) > 0 { + if dt, err := time.Parse("2006/01/02", value); err == nil { + post.Date = dt + } + } + + if post.Date.IsZero() { + fi, err := os.Stat(s) + if err != nil { + log.Printf("%s: err stating: %s\n", s, err) + } + post.Date = fi.ModTime().UTC() + } + + if post.Date.IsZero() { + post.Date = time.Now().UTC() + } + + log.Printf("%s: post parsed\n", s) + w.posts = append(w.posts, post) + } + return nil +} diff --git a/internal/templates/index/view.templ b/internal/templates/index/view.templ new file mode 100644 index 0000000..49fe841 --- /dev/null +++ b/internal/templates/index/view.templ @@ -0,0 +1,50 @@ +package index + +import "github.com/pnmcosta/csta.dev/internal/posts" +import "github.com/pnmcosta/csta.dev/internal/templates/layout" +import "path" +import "github.com/gosimple/slug" + +templ View(posts []posts.Post) { + @layout.Base() { +
+ Olá, I'm Pedro Maia Costa a product + software + engineer based in Braga, Portugal. +
++ Currently working on/with SocialClique developing + Shopify Apps. +
++ Having previously worked at/with Juni, The SimGrid, + + JPMorgan + Chase & Co. + and All human. +
+ if len(posts) > 0 { +I've also recently started sharing my experience and know-how on these blog articles:
++ for i, post := range posts { + if i >0 { + ; + } + { post.Title } ({ post.Date.Format("2006/01/02") }) + } +
+ } ++ Drop me a comment to pedro@csta.dev +
+All the best
+ } +} diff --git a/internal/templates/layout/base.templ b/internal/templates/layout/base.templ new file mode 100644 index 0000000..583e79b --- /dev/null +++ b/internal/templates/layout/base.templ @@ -0,0 +1,66 @@ +package layout + +templ Base() { + + + + + + +{ post.Summary }
+ //{ strings.Join(post.Tags, ", ") }
+} diff --git a/main.go b/main.go new file mode 100644 index 0000000..4176963 --- /dev/null +++ b/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "context" + "flag" + "log" + "net/http" + "os" + "path" + + "github.com/gosimple/slug" + "github.com/pnmcosta/csta.dev/internal/posts" + "github.com/pnmcosta/csta.dev/internal/templates/index" + templates "github.com/pnmcosta/csta.dev/internal/templates/post" +) + +var devFlag = flag.Bool("dev", false, "if true public folder will be served") + +func main() { + flag.Parse() + + posts := posts.ParsePosts() + + // Output path. + rootPath := "public" + if err := os.Mkdir(rootPath, 0755); err != nil { + if !os.IsExist(err) { + log.Fatalf("failed to create output directory: %v", err) + } + } + + // Create an index page. + name := path.Join(rootPath, "index.html") + f, err := os.Create(name) + if err != nil { + log.Fatalf("failed to create output file: %v", err) + } + + // Write it out. + err = index.View(posts).Render(context.Background(), f) + if err != nil { + log.Fatalf("failed to write index page: %v", err) + } + + // Create a page for each post. + for _, post := range posts { + // Create the output directory. + dir := path.Join(rootPath, post.Date.Format("2006/01/02"), slug.Make(post.Title)) + if err := os.MkdirAll(dir, 0755); err != nil && err != os.ErrExist { + log.Fatalf("failed to create dir %q: %v", dir, err) + } + + // Create the output file. + name := path.Join(dir, "index.html") + f, err := os.Create(name) + if err != nil { + log.Fatalf("failed to create output file: %v", err) + } + + // Create an unsafe component containing raw HTML. + content := templates.Unsafe(string(post.Content)) + + // Use templ to render the template containing the raw HTML. + err = templates.View(post, content).Render(context.Background(), f) + if err != nil { + log.Fatalf("failed to write output file: %v", err) + } + } + + if !*devFlag { + return + } + + http.Handle("/", http.FileServer(http.Dir("./public"))) + log.Print("Listening on 127.0.0.1:3000...") + err = http.ListenAndServe("127.0.0.1:3000", nil) + if err != nil { + log.Fatal(err) + } +} diff --git a/package.json b/package.json index f14fba9..132b50d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "scripts": { - "dev": "npx tailwindcss -i ./src/input.css -o ./src/output.css --watch" + "dev": "npx tailwindcss -i style.css -o ./public/style.css --watch" }, "devDependencies": { "tailwindcss": "^3.4.3" diff --git a/src/favicon.png b/src/favicon.png deleted file mode 100644 index c6aea26..0000000 Binary files a/src/favicon.png and /dev/null differ diff --git a/src/index.html b/src/index.html deleted file mode 100644 index 333e14b..0000000 --- a/src/index.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - -Olá, I'm Pedro Maia Costa a product - software - engineer based in Braga, Portugal.
- -Currently working on/with SocialClique developing - Shopify Apps.
- -Having previously worked at/with Juni, The SimGrid, JPMorgan - Chase & Co. and All human.
- -Drop me a line to pedro@csta.dev -
- -All the best
-