From d423b5401b4f3de2501c6092ba43ffcccb3a61e7 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:02:12 -0600 Subject: [PATCH 1/5] chore: add pull request workflow --- .github/workflows/deploy.yml | 4 +-- .github/workflows/pull_request.yml | 44 +++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bbd7a24..e46e518 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -18,7 +18,7 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Login to Docker Hub @@ -40,7 +40,7 @@ jobs: needs: build steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Run deploy script diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index af860d4..8ba0c0c 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1 +1,43 @@ -# TODO: implement pull request workflow +name: Pull Request +on: + pull_request: + branches: + - main +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: '1.25' + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: latest + build: + name: Build server + runs-on: ubuntu-24.04-arm + steps: + - name: Checkout code + uses: actions/checkout@v6 + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: '1.25' + - name: Download dependencies + run: go mod download + - name: Build application + run: CGO_ENABLED=0 GOOS=linux go build -o server ./cmd/web/main.go + docker: + name: Build Docker image + runs-on: ubuntu-24.04-arm + steps: + - name: Checkout code + uses: actions/checkout@v6 + - name: Build Docker image + run: docker build -t links.stevanfreeborn.com . + From a34467a67d1adb7e9e3b59e8ac606e518a1802ac Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:34:15 -0600 Subject: [PATCH 2/5] feat: define links in separate json file --- cmd/web/main.go | 1 + internal/assets/images/favicon.ico | Bin 0 -> 15086 bytes internal/assets/json/links.json | 22 ++++++++++ internal/assets/main.go | 4 ++ internal/assets/templates/index.gohtml | 30 ++++---------- internal/handlers/main.go | 53 +++++++++++++++++++++---- 6 files changed, 81 insertions(+), 29 deletions(-) create mode 100644 internal/assets/images/favicon.ico create mode 100644 internal/assets/json/links.json diff --git a/cmd/web/main.go b/cmd/web/main.go index 7dcae99..bff4884 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -20,6 +20,7 @@ func main() { mux.HandleFunc("/", handlers.Index) mux.HandleFunc("/css/", handlers.CSS) mux.HandleFunc("/fonts/", handlers.Fonts) + mux.HandleFunc("/images/", handlers.Images) loggingMux := middleware.Logging(mux) diff --git a/internal/assets/images/favicon.ico b/internal/assets/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e1cc98aa38ba76f9142036aad57504921b06eb5d GIT binary patch literal 15086 zcmeI3U2IfE6vwAlEGke0^uef$QG`S!7!^O0CE>xK@k!JN6T_PdzDNS#ekm) z5fa{vHt`G5L?h5920{cQwn$QhZYdN?OIbi*P1at2=W-7@JKo*iWtV_*H<_F>=i`6Q z%$=D#ckW&18eF5BJJ&&{yH92~H_tgYeR@=%?A!rllcA!2>1yZR!eACQxVXg>k*B}3 z2htu$dm!zBv*Bx|JQwrT-l{a%5hWxrgnb&2uhw_CXL_MeZkK^54?e*Y_CuakTGx zxjvNNjon#vjW+**fr0zbFF$Wn>v^Q*WL)%S}!?J{FgYgY8q~_sZVA zdnc+5vSpMrF*u4hK--^G`+8DvkJ`A@HzdVpzE8%t$NG=Ke=FBJDgIb?J9q9J=f@iv z8EGENPxQmX!;@%h-uEA?j!$>J<~_0Q^5UIIlDpX8x7+j8v9cL?8)Xe;Pn=D?+Q7V= z?ct9k$seERJimaPw)OP%O!EDiU-w`;5@TQUbpUg!kL$;etCMTa`cdyvJNShZSph?hAx)y5WH?#Z5Q$()yK=jnsN!NCa@r(7HG`^DNN<)d-? zs~?y8G3LV*izBgpJN5`SS{wAoJ)1|YUib*-h-W8j@JGIFlDgbexURA<)^$9j$cE|j z8!QI0A!|o{qt&A;!9l;!>Xk3o23@z*E-8)X-@mSdjj5AX9>;{?K}4ZnDJvQ{6JSPFsu{Iy3nieH2180&ZVzUZ3NFOK5B$odFg zoLy2G@vfqef3}#ygFC4ASa?~t4pZd0^8EOK@ACY5lf?CWob z{%0))n4GlN{-X`DC)i=J6Y|IHe5sU4^c-Q^P1vBW&(@5Y&l2V>dGz>eZl(O`>kSs0 zddWSzl5aIS=JexW%bYFgD^r&-uuL(#ySpb)Uy4uJXGe4|7=OiGt{4}C$}>4JmvBwq zrA;4FR?y$~+gLL0$nAv5Z6EcgvAs^7`=F1alV=^xIi%Q}#J6tGm^PCSM&py^JS$4S8Aq7!wbWb31K0 zWpY15{VVDVF3Y6Tq|zQpdm!zBvKQ`Sa1vgiC=b4W*BQ%H*uEx4ve ziBrhD&6y}Pn|E1gKAUqHp)EN)g?7B&6w=N&8=-~ls%sjAW?CDa(B{_Gl`YN{n_5?O z2yI-|naesi+R)jV&(IFn(OD3>a`CF7(2m<%M|pb@LLLM;myKvkNb?dOc~SC$@~fT725CBFJ|+E5i%v*?X%AG<1MJmjvS&YkZYXYr2ralxj=7PVNFY^`0yA0TGQw*exr_T9I| zEf=3S_MCUK*x5LK9#&TlQ|0*X)g0v9NadEQUZ}{U^QmyWst>x7_l8Mx;2e3r;*-lM z8&`SNL)S&|<#}J{%YL=P|AKM=%bpFFysr~%6YD(_>u;5-OsJm&V={d2VQqXK8#xEp z_6R*@ZH%s{a%AtTTxCj;+cos8$}3TsJBlZib_}UK^#iJtvlfw~U#Bv)vlQKk>hZ~1 zT{*tJz1`0tJSQf%af(IUTf|@MLgVl}tl#2n;$;4{Al{(ZA#b3BXYRz@qVdu1io+3^ z+LX(&oL@^zi$5O1cT?c(x0qw$THA85%E_g@w5@0{e4ckoCGUkT7QXWSOvm<E+}* z-CtpF6mP9!5a(0I*9xr*r9Q>_*mtYeu2!Dduh}@%?fIkT`dzJY%K68AOJMVX-%-xD zo^sm#u@_g?pPBWHkz%Di@PFw6*4Efxk>i#ECo6eMZlEdXD>MZC1sC+^zijC{F+SUO zVnQZ=Vs&%=)iuq9xog^rlh?G3UfaAqaGCAF%1m33dy#{h4p2HWpk!l%Ae3_o{$duM zRa`0O6yEw&)wlKPOV^EKt^1vNxTk_Hr@UNgT;l5MrmeP0OovZ>?JjhJ0&L(r^AZ6r$%>@!JS&mdL(Bu*pH*q3_e{B>pz$SiHPL z6(22Q+5eaH=gvfB@5^_Ml+LVk>p5p#&(~VJ_#*blubs<1%sDTm*os}-Ia~Gn2TDk$ AJ^%m! literal 0 HcmV?d00001 diff --git a/internal/assets/json/links.json b/internal/assets/json/links.json new file mode 100644 index 0000000..5b17358 --- /dev/null +++ b/internal/assets/json/links.json @@ -0,0 +1,22 @@ +[ + { + "href": "https://youtube.stevanfreeborn.com", + "icon": "fa-brands fa-square-youtube fa-lg", + "text": "YouTube" + }, + { + "href": "https://github.stevanfreeborn.com", + "icon": "fa-brands fa-square-github fa-lg", + "text": "GitHub" + }, + { + "href": "https://blog.stevanfreeborn.com", + "icon": "fa-solid fa-square-rss fa-lg", + "text": "Blog" + }, + { + "href": "https://linkedin.stevanfreeborn.com", + "icon": "fa-brands fa-square-linkedin fa-lg", + "text": "LinkedIn" + } +] diff --git a/internal/assets/main.go b/internal/assets/main.go index ce695cb..6867e41 100644 --- a/internal/assets/main.go +++ b/internal/assets/main.go @@ -12,4 +12,8 @@ var ( CSS embed.FS //go:embed "fonts/*" Fonts embed.FS + //go:embed "json/*" + JSON embed.FS + //go:embed "images/*" + Images embed.FS ) diff --git a/internal/assets/templates/index.gohtml b/internal/assets/templates/index.gohtml index e12222b..21a211a 100644 --- a/internal/assets/templates/index.gohtml +++ b/internal/assets/templates/index.gohtml @@ -9,6 +9,7 @@ integrity="sha512-2SwdPD6INVrV/lHTZbO2nodKhrnDdJK9/kg2XD1r9uGqPo1cUbujc+IYdlYdEErWNu69gVcYgdxlmVmzTWnetw==" crossorigin="anonymous" referrerpolicy="no-referrer" /> + @@ -18,35 +19,20 @@ alt="A man with short brown hair, a grey hoodie, and a white t-shirt smiles at the camera. He has a white earbud in his left ear. The background is a blurred office setting with warm lighting visible through glass partitions." />

Stevan Freeborn

-

I'm a {{ .Age }}-year old dad of 2 who enjoys drinking coffee, lifting weights, and solving problems with code.

+

I'm a {{ .Age }}-year old dad of 2 who enjoys drinking coffee, lifting weights, and solving problems with code. +

diff --git a/internal/handlers/main.go b/internal/handlers/main.go index d55c241..6fd7e0a 100644 --- a/internal/handlers/main.go +++ b/internal/handlers/main.go @@ -2,6 +2,8 @@ package handlers import ( + "encoding/json" + "io/fs" "math" "net/http" "text/template" @@ -10,21 +12,24 @@ import ( "github.com/StevanFreeborn/links.stevanfreeborn.com/internal/assets" ) +const LINKS_JSON_PATH = "json/links.json" const DAYS_IN_YEAR = 365 const HOURS_IN_DAY = 24 var birthday time.Time = time.Date(1993, time.April, 21, 0, 0, 0, 0, time.UTC) +type Link struct { + Href string `json:"href"` + Icon string `json:"icon"` + Text string `json:"text"` +} + type IndexViewModel struct { - Age float64 + Age float64 + Links []Link } func Index(w http.ResponseWriter, r *http.Request) { - // TODO: Links coming in external JSON data - // this could be static file in the repo - // or it could be fetched - // { link: "link", text: "text", "icon": "icon" } - t, err := template.ParseFS(assets.Templates, "templates/index.gohtml") if err != nil { @@ -32,10 +37,25 @@ func Index(w http.ResponseWriter, r *http.Request) { return } + linksFile, err := assets.JSON.Open(LINKS_JSON_PATH) + + if err != nil { + http.Error(w, "Unable to load links", http.StatusInternalServerError) + return + } + + links, err := readLinksFromJSON(linksFile) + + if err != nil { + http.Error(w, "Unable to parse links", http.StatusInternalServerError) + return + } + age := time.Since(birthday).Hours() / HOURS_IN_DAY / DAYS_IN_YEAR viewModel := IndexViewModel{ - Age: math.Floor(age), + Age: math.Floor(age), + Links: links, } t.Execute(w, viewModel) @@ -48,3 +68,22 @@ func CSS(w http.ResponseWriter, r *http.Request) { func Fonts(w http.ResponseWriter, r *http.Request) { http.ServeFileFS(w, r, assets.Fonts, r.URL.Path) } + +func Images(w http.ResponseWriter, r *http.Request) { + http.ServeFileFS(w, r, assets.Images, r.URL.Path) +} + +func readLinksFromJSON(file fs.File) ([]Link, error) { + defer file.Close() + var links []Link + + decoder := json.NewDecoder(file) + + err := decoder.Decode(&links) + + if err != nil { + return nil, err + } + + return links, nil +} From 5be69ab79c775bd2845a399b61d3dda9a099cf56 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:42:09 -0600 Subject: [PATCH 3/5] chore: update workflow --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8ba0c0c..61e62bc 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -17,7 +17,7 @@ jobs: - name: Run golangci-lint uses: golangci/golangci-lint-action@v6 with: - version: latest + version: v2.4 build: name: Build server runs-on: ubuntu-24.04-arm From 325a7a33d665ae97756e42fe521c0e261bfd2fc7 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:44:12 -0600 Subject: [PATCH 4/5] chore: update workflow --- .github/workflows/pull_request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 61e62bc..9b1d153 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -15,9 +15,9 @@ jobs: with: go-version: '1.25' - name: Run golangci-lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v9 with: - version: v2.4 + version: latest build: name: Build server runs-on: ubuntu-24.04-arm From 04ea5f8ef7738be7a0bf4953ca7057c7232231ea Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:52:22 -0600 Subject: [PATCH 5/5] fix: address unchecked errors --- internal/handlers/main.go | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/internal/handlers/main.go b/internal/handlers/main.go index 6fd7e0a..5273311 100644 --- a/internal/handlers/main.go +++ b/internal/handlers/main.go @@ -30,23 +30,23 @@ type IndexViewModel struct { } func Index(w http.ResponseWriter, r *http.Request) { - t, err := template.ParseFS(assets.Templates, "templates/index.gohtml") + t, parseTemplateError := template.ParseFS(assets.Templates, "templates/index.gohtml") - if err != nil { + if parseTemplateError != nil { http.Error(w, "Unable to load template", http.StatusInternalServerError) return } - linksFile, err := assets.JSON.Open(LINKS_JSON_PATH) + linksFile, openLinksFileError := assets.JSON.Open(LINKS_JSON_PATH) - if err != nil { + if openLinksFileError != nil { http.Error(w, "Unable to load links", http.StatusInternalServerError) return } - links, err := readLinksFromJSON(linksFile) + links, readLinksFileError := readLinksFromJSON(linksFile) - if err != nil { + if readLinksFileError != nil { http.Error(w, "Unable to parse links", http.StatusInternalServerError) return } @@ -58,7 +58,12 @@ func Index(w http.ResponseWriter, r *http.Request) { Links: links, } - t.Execute(w, viewModel) + executeError := t.Execute(w, viewModel) + + if executeError != nil { + http.Error(w, "Unable to render template", http.StatusInternalServerError) + return + } } func CSS(w http.ResponseWriter, r *http.Request) { @@ -74,7 +79,16 @@ func Images(w http.ResponseWriter, r *http.Request) { } func readLinksFromJSON(file fs.File) ([]Link, error) { - defer file.Close() + var closeErr error = nil + + defer func() { + closeErr = file.Close() + }() + + if closeErr != nil { + return nil, closeErr + } + var links []Link decoder := json.NewDecoder(file)