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() { + + + + + + + csta.dev + + + + + + + + + + + + +
+

+ + csta.dev + <csta/> + &csta{ } + #csta + ()=>csta + +

+ { children... } +
+
+
+
+
+
+ + + +} diff --git a/internal/templates/post/unsafe.templ b/internal/templates/post/unsafe.templ new file mode 100644 index 0000000..8903806 --- /dev/null +++ b/internal/templates/post/unsafe.templ @@ -0,0 +1,11 @@ +package post + +import "context" +import "io" + +func Unsafe(html string) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) { + _, err = io.WriteString(w, html) + return + }) +} \ No newline at end of file diff --git a/internal/templates/post/view.templ b/internal/templates/post/view.templ new file mode 100644 index 0000000..c0b6cc2 --- /dev/null +++ b/internal/templates/post/view.templ @@ -0,0 +1,14 @@ +package post + +import "github.com/pnmcosta/csta.dev/internal/posts" +import "github.com/pnmcosta/csta.dev/internal/templates/layout" + +templ View(post posts.Post, content templ.Component) { + @layout.Base() { +
+ @content +
+ } + //

{ 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 @@ - - - - - - - - csta.dev - - - - - - - - - - - - - - - - -
-

- csta.dev - <csta/> - &csta{} - #csta - ()=>csta -

-

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

-
- - - - \ No newline at end of file diff --git a/src/input.css b/src/input.css deleted file mode 100644 index 0f18357..0000000 --- a/src/input.css +++ /dev/null @@ -1,41 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - html { - font-size: 24px - } - - h1, - a { - @apply text-white; - } - - a:hover { - @apply text-zinc-400; - } - - h1 { - overflow: hidden; - padding-bottom: 2px; - } - - #title { - margin-left: -6px; - } - - #title span { - transition: all .5s ease-in-out 0s; - opacity: 0; - width: 0; - height: 0; - display: block; - } - - #title span.visible { - opacity: 1; - width: auto; - height: auto; - } -} \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..60ba2fc --- /dev/null +++ b/style.css @@ -0,0 +1,114 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + + html { + height: 100%; + position:relative; + } + + body { + background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzAwMDAwMCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzExMTExMSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); + background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #000000), color-stop(100%, #111111)); + background: -moz-linear-gradient(#000000, #111111); + background: -webkit-linear-gradient(#000000, #111111); + background: linear-gradient(#000000, #111111); + } + + h1, + a { + @apply text-white; + transition: color .2s ease-in-out 0s; + } + + a:hover { + @apply text-zinc-400; + } + + h1 { + overflow: hidden; + padding-bottom: 2px; + } + + #title { + margin-left: -6px; + } + + #title span { + transition: opacity .5s ease-in-out .5s; + opacity: 0; + width: 0; + height: 0; + display: block; + } + + #title span.visible { + opacity: 1; + width: auto; + height: auto; + } + + + /*** https://codepen.io/vavik96/pen/vEdMXM ***/ + @keyframes clouds-loop-1 { + to { + background-position: -1000px 0; + } + } + + .clouds-1 { + background-image: url("/clouds_2.webp"); + animation: clouds-loop-1 1500s infinite linear; + } + + @keyframes clouds-loop-2 { + to { + background-position: -1000px 0; + } + } + + .clouds-2 { + background-image: url("/clouds_1.webp"); + animation: clouds-loop-2 750s infinite linear; + } + + @keyframes clouds-loop-3 { + to { + background-position: -1579px 0; + } + } + + .clouds-3 { + background-image: url("/clouds_3.png"); + animation: clouds-loop-3 900s infinite linear; + } + + .clouds { + filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40); + opacity: 0.4; + pointer-events: none; + position: absolute; + overflow: hidden; + top: 0; + left: 0; + right: 0; + height: 100%; + z-index: -1; + } + + .clouds-1, + .clouds-2, + .clouds-3 { + background-repeat: repeat-x; + background-position: center; + position: absolute; + bottom: -200px; + right: 0; + left: 0; + height: 500px; + transform: scale(1, -1); + } + +} \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index 5b072de..d044e21 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,6 +1,6 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: ["./src/**/*.{html,js}"], + content: ["./internal/templates/**/*.templ"], theme: { extend: {}, },