Skip to content

Conversation

@gocanto
Copy link
Collaborator

@gocanto gocanto commented Sep 19, 2025

Go live

  • Add env var: ENV_SPA_DIR.
  • Add env var: ENV_APP_URL.
  • Remove middelewares changes.

Summary by CodeRabbit

  • New Features

    • CLI SEO generator to produce static pages (OG/Twitter, JSON‑LD, web manifest) and related CLI commands.
    • New CLI options: “Generate SEO” and “Print Timestamp.”
    • Accounts CLI: “Show Account” replaces “Read Account” with improved key/signature output.
  • Configuration

    • New env vars: ENV_APP_URL, ENV_SPA_DIR.
    • Log date format updated to “2006-01-02.”
    • Example .env files cleaned of default/example values.
  • Build/Chores

    • New Make targets: run-cli-docker, run-metal, build-fresh; removed run-cli-local.
    • docker-compose now sources .env via env_file.
    • .gitignore updated to ignore generated SEO files.

@coderabbitai
Copy link

coderabbitai bot commented Sep 19, 2025

Warning

Rate limit exceeded

@gocanto has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 18 minutes and 31 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between c424c96 and d84f432.

📒 Files selected for processing (5)
  • .gitignore (1 hunks)
  • docker-compose.yml (1 hunks)
  • metal/kernel/kernel_test.go (5 hunks)
  • metal/router/fixture.go (1 hunks)
  • metal/router/router.go (4 hunks)

Walkthrough

Adds an SEO generation subsystem, fixture-driven static routing and WebsiteRoutes, introduces SeoEnvironment and App.URL, renames Ping→PingEnvironment, makes middleware environment-aware with local bypasses, updates router/kernel wiring and CLI, adjusts Makefile/docker/.env examples, and updates tests and repositories to match new types.

Changes

Cohort / File(s) Summary
Environment models & examples
metal/env/app.go, metal/env/env.go, metal/env/ping.go, metal/env/seo.go, metal/kernel/factory.go, .env.example, .env.gh.example, .env.prod.example
Add SeoEnvironment and App.URL; rename PingPingEnvironment; add validate:"required" tags; add ENV_APP_URL and ENV_SPA_DIR to examples; remove many default/example prod/gh entries; wire SEO env in factory.
SEO generation subsystem
metal/cli/seo/*, metal/cli/seo/stub.html, metal/cli/seo/*.go, metal/cli/seo/defaults.go
New seo package: Page/TemplateData types, JsonID (JSON‑LD), Manifest builder, Generator (NewGenerator, Generate, Build, Export, Page.Load), defaults, stub template, support helpers, client and sections renderer.
Router & fixtures
metal/router/static.go, metal/router/fixture.go, metal/router/router.go, metal/router/*.go tests
Add WebsiteRoutes, StaticRouteResource, Fixture; change router to use fixture-driven static route composition; expose Validator publicly and adjust Router fields and static route registration; tests updated to new package/type names.
Kernel / App wiring
metal/kernel/app.go, metal/kernel/helpers.go, metal/kernel/factory.go, metal/kernel/kernel_test.go
Switch App to use external router.Router, add App.NewRouter(), adjust boot flow to use router-produced modem, update imports and tests.
Middleware
pkg/middleware/token_middleware.go, pkg/middleware/public_middleware.go, pkg/middleware/mwguards/valid_timestamp_guard.go, pkg/middleware/pipeline.go
Token middleware gains *env.Environment and local bypass; PublicMiddleware short-circuits when non-production; timestamp guard uses exported TimestampTooOld/TooNew constructors; Pipeline annotated with TODO.
Keep-alive handlers & tests
handler/keep_alive.go, handler/keep_alive_db.go, handler/keep_alive_test.go, handler/keep_alive_db_test.go
Keep-alive handlers and tests updated to use *env.PingEnvironment; constructors and fields adjusted.
CLI (accounts, main, menu)
metal/cli/accounts/handler.go, metal/cli/accounts/handler_test.go, metal/cli/main.go, metal/cli/panel/menu.go
Rename ReadAccountShowAccount, centralize print helper, add generateSEO() and printTimestamp(), update menu options and tests.
Makefile / compose / db tooling
Makefile, metal/makefile/app.mk, metal/makefile/build.mk, metal/makefile/db.mk, docker-compose.yml
Add run-cli-docker, run-metal, build-fresh; remove run-cli-local; db:seed uses --env-file ./.env; docker-compose api-runner uses non-digest image tag and env_file, removes host .env bind.
Repo hygiene
.gitignore
Ignore storage/seo/pages/* while preserving .gitkeep placeholders.
CLI SEO consumer & renderer
metal/cli/seo/client.go, metal/cli/seo/sections.go, metal/cli/seo/categories.go
Add Client to fetch fixture-backed JSON via httptest and unmarshal payloads; Sections renderer produces HTML snippets; categories SEO generator added using repository.
Repository additions
database/repository/categories.go
Add Categories.Get() method to fetch non-deleted categories.
Tests & small updates
metal/router/*_test.go, metal/cli/accounts/handler_test.go, others
Update package declarations, struct field names, and test usages to match renamed types/fields and public Validator.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User (CLI)
  participant CLI as metal/cli
  participant G as seo.Generator
  participant C as seo.Client
  participant R as router.WebsiteRoutes
  participant H as StaticRouteResource
  participant FS as Filesystem

  U->>CLI: Select "Generate SEO"
  CLI->>G: NewGenerator(db, env, validator)
  G->>R: NewWebsiteRoutes(env)
  G->>C: NewClient(R)
  G->>G: Page.Load() (embed stub.html)
  CLI->>G: GenerateHome()
  G->>C: GetProfile/GetTalks/GetProjects
  C->>H: invoke fixture handler (httptest)
  H-->>C: JSON payload
  C-->>G: unmarshaled data
  G->>G: Build TemplateData (JSON-LD, Manifest)
  G->>FS: Write index.html to OutputDir
  FS-->>CLI: success
  CLI-->>U: Success
Loading
sequenceDiagram
  autonumber
  participant Client as C
  participant Server as S
  participant Router as R
  participant PM as PublicMiddleware
  participant TM as TokenMiddleware
  participant Handler as H

  C->>S: HTTP Request
  S->>R: route match
  R->>PM: Handle(w,r)
  alt non-production
    PM-->>R: next(w,r) (short-circuit)
  else production
    PM->>PM: checks (deps, rate, timestamp)
    PM-->>R: next(w,r)
  end
  R->>TM: Handle(w,r) with env
  alt env.App.IsLocal()
    TM-->>R: next(w,r) (bypass)
  else
    TM->>TM: validate token/signature
    TM-->>R: next or error
  end
  R->>Handler: Serve
  Handler-->>Client: Response
  note right of R: Validator is now public for signature route
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

Suggested labels

codex

Poem

I hop through code with whiskers bright,
New pages sprout where fixtures light.
SEO seeds tucked in tidy rows,
Tokens nap while local wind blows.
I stamp my paw — the build now glows. 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title “Add SEO” accurately summarizes the main objective of this pull request, which is to introduce SEO functionality including new environment variables, templates, and CLI generator commands. It is concise and directly reflects the scope of the changeset. It gives a clear signal to reviewers about the primary change without extraneous detail.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gocanto gocanto marked this pull request as ready for review September 23, 2025 04:36
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
pkg/middleware/public_middleware.go (1)

54-61: Enforce IP allowlist in PublicMiddleware.Handle (prod-only).

MakePublicMiddleware sets allowedIP but Handle doesn't validate the client IP; TestPublicMiddleware_IPWhitelist (pkg/middleware/public_middleware_test.go) expects 401 for non-allowed IPs. Insert a production-only check after reading headers (after reqID := ... in pkg/middleware/public_middleware.go) using the existing helper:

if p.isProduction && p.allowedIP != "" {
clientIP := portal.ParseClientIP(r)
if clientIP != p.allowedIP {
return mwguards.UnauthenticatedError("Unauthorized IP", fmt.Sprintf("client ip %s not allowed", clientIP))
}
}

metal/cli/accounts/handler.go (2)

35-53: Avoid double decoding and stale error usage in ShowAccount.

Decode once, check error, then print. print shouldn’t re-decode.

 func (h Handler) ShowAccount(accountName string) error {
     item := h.Tokens.FindBy(accountName)
@@
-    token, err := h.TokenHandler.DecodeTokensFor(
+    token, err := h.TokenHandler.DecodeTokensFor(
         item.AccountName,
         item.SecretKey,
         item.PublicKey,
     )
-
-    if h.print(token, item) != nil {
-        return fmt.Errorf("could not decode the given account [%s] keys: %v", item.AccountName, err)
-    }
+    if err != nil {
+        return fmt.Errorf("could not decode the given account [%s] keys: %v", item.AccountName, err)
+    }
+    if perr := h.print(token, item); perr != nil {
+        return perr
+    }

55-64: print re-decodes and shadows token; use provided token and only decode if nil.

This avoids duplicate work and shadowing.

-func (h Handler) print(token *auth.Token, item *database.APIKey) error {
-    token, err := h.TokenHandler.DecodeTokensFor(
-        item.AccountName,
-        item.SecretKey,
-        item.PublicKey,
-    )
-
-    if err != nil {
-        return fmt.Errorf("could not decode the given account [%s] keys: %v", item.AccountName, err)
-    }
+func (h Handler) print(token *auth.Token, item *database.APIKey) error {
+    var err error
+    if token == nil {
+        token, err = h.TokenHandler.DecodeTokensFor(
+            item.AccountName,
+            item.SecretKey,
+            item.PublicKey,
+        )
+        if err != nil {
+            return fmt.Errorf("could not decode the given account [%s] keys: %v", item.AccountName, err)
+        }
+    }
pkg/middleware/token_middleware.go (1)

37-51: Constructor signature changed — update all MakeTokenMiddleware call sites to include env as first arg

Signature is now: MakeTokenMiddleware(e *env.Environment, tokenHandler *auth.TokenHandler, apiKeys *repository.ApiKeys).

Fix callers still using the 2-arg form:

  • pkg/middleware/token_middleware_additional_test.go: lines 104, 119, 144, 172, 214 (currently MakeTokenMiddleware(th, repo)).
  • pkg/middleware/token_middleware_test.go: lines 28, 41, 55, 74, 223, 304, 340, 372 (mix of MakeTokenMiddleware(nil, nil) and MakeTokenMiddleware(th, repo)).

metal/router/router.go already passes r.Env (no change).

Change calls to MakeTokenMiddleware(, tokenHandler, apiKeys) — use the real env (e.g., r.Env) where available, or nil in unit tests if appropriate — then run tests and pipeline.

🧹 Nitpick comments (50)
pkg/middleware/mwguards/valid_timestamp_guard.go (2)

51-53: Good switch to centralized errors; include structured context for better logs.

Pass request context to the helper so logs are actionable; also align the log message with the condition.

-    return TimestampTooOldError("Request timestamp is too old or invalid", "Request timestamp invalid")
+    return TimestampTooOldError(
+        "Request timestamp is too old or invalid",
+        "Request timestamp expired",
+        map[string]any{
+            "epoch":          epoch,
+            "now":            now,
+            "skew_secs":      skewSecs,
+            "min":            minValue,
+            "max":            maxValue,
+            "disallowFuture": disallowFuture,
+        },
+    )

Please confirm 401 is the intended status per your auth spec for this case.


55-57: Mirror the above: add context and a precise log message for “future” failures.

-    return TimestampTooNewError("Request timestamp is too recent or invalid", "Request timestamp invalid")
+    return TimestampTooNewError(
+        "Request timestamp is too recent or invalid",
+        "Request timestamp too far in future",
+        map[string]any{
+            "epoch":          epoch,
+            "now":            now,
+            "skew_secs":      skewSecs,
+            "min":            minValue,
+            "max":            maxValue,
+            "disallowFuture": disallowFuture,
+        },
+    )

Also sanity‑check that the added slog.Error emissions from these helpers won’t create noisy logs on client mistakes in high‑traffic paths.

pkg/middleware/public_middleware.go (1)

46-48: Don’t short‑circuit the middleware in non‑prod; remove this bypass.

This early return disables dependency checks, replay/timestamp validation, and rate limiting in all non‑prod envs. Given the PR checklist (“Remove middleware changes”), this should not ship.

Apply this diff to drop the bypass:

-        if !p.isProduction {
-            return next(w, r) //@todo remove!
-        }
+        // Middleware remains active in all environments. Any env-specific relaxations are handled explicitly below.
.gitignore (1)

8-11: Broaden ignore to directory (catch files without extensions and nested paths).

Use a directory-wide ignore so generated assets like files without dots (e.g., "index") or nested folders are ignored too.

-# -- [SEO]: static files
-storage/seo/pages/*.*
+# -- [SEO]: static files
+# Ignore everything under pages/, but keep .gitkeep
+storage/seo/pages/**
 !storage/seo/.gitkeep
 !storage/seo/pages/.gitkeep
metal/env/ping.go (2)

18-21: Trim both input and stored creds before comparison.

Prevents false negatives when callers pass whitespace-padded values.

 func (p PingEnvironment) HasInvalidCreds(username, password string) bool {
-	return username != strings.TrimSpace(p.Username) ||
-		password != strings.TrimSpace(p.Password)
+	u := strings.TrimSpace(username)
+	pw := strings.TrimSpace(password)
+	return u != strings.TrimSpace(p.Username) ||
+		pw != strings.TrimSpace(p.Password)
 }

5-8: Add a brief doc comment for the exported type.

Improves public API clarity and passes linters that require comments on exported identifiers.

-type PingEnvironment struct {
+// PingEnvironment holds basic auth credentials for ping endpoints.
+type PingEnvironment struct {
 	Username string `validate:"required,min=16"`
 	Password string `validate:"required,min=16"`
 }
metal/makefile/build.mk (1)

9-13: Prefer $(MAKE) for recursive invocations.

Propagates flags (e.g., -j, --warn-undefined-variables) and is make‑portable.

 build-fresh:
-	make fresh && \
-	make db:fresh && \
-	make db:migrate && \
-	make db:seed
+	$(MAKE) fresh && \
+	$(MAKE) db:fresh && \
+	$(MAKE) db:migrate && \
+	$(MAKE) db:seed
metal/makefile/app.mk (2)

56-58: Use $(MAKE) to chain targets.

More robust than calling the binary directly.

 run-cli-docker:
-	make run-cli DB_SECRET_USERNAME=./database/infra/secrets/pg_username DB_SECRET_PASSWORD=./database/infra/secrets/pg_password DB_SECRET_DBNAME=./database/infra/secrets/pg_dbname
+	$(MAKE) run-cli DB_SECRET_USERNAME=./database/infra/secrets/pg_username DB_SECRET_PASSWORD=./database/infra/secrets/pg_password DB_SECRET_DBNAME=./database/infra/secrets/pg_dbname

62-63: Consider mirroring env guards from run-cli.

run-metal runs outside Docker; if it also requires DB secrets, reuse the guard or load a local env file.

Makefile (1)

100-100: Fix minor typo in help text.

Double “the”.

-	@printf "  $(BOLD)$(GREEN)supv:api:logs-err$(NC): Show the the API service supervisor error logs.\n\n"
+	@printf "  $(BOLD)$(GREEN)supv:api:logs-err$(NC): Show the API service supervisor error logs.\n\n"

Note: There’s a similar “the the” in supv:api:logs above; consider fixing in a follow‑up.

metal/cli/seo/support.go (1)

19-27: Harden Fetch: set Accept header, check status code, and wrap handler error.

Improves correctness and debuggability.

 func Fetch[T any](response *T, handler func() router.StaticRouteResource) error {
-	req := httptest.NewRequest("GET", "http://localhost:8080/proxy", nil)
+	req := httptest.NewRequest("GET", "http://localhost:8080/proxy", nil)
+	req.Header.Set("Accept", "application/json")
 	rr := httptest.NewRecorder()
 
 	maker := handler()
 
-	if err := maker.Handle(rr, req); err != nil {
-		return err
+	if apiErr := maker.Handle(rr, req); apiErr != nil {
+		return fmt.Errorf("handler error: %v", apiErr)
 	}
+
+	if rr.Code < 200 || rr.Code >= 300 {
+		return fmt.Errorf("unexpected status code: %d", rr.Code)
+	}

Please confirm router.ApiError implements the error interface; if not, we should wrap it as shown.

metal/cli/seo/data.go (3)

30-38: Use numeric types for OG image dimensions with validation.

Avoids accidental non‑numeric values and enables range checks.

 type TagOgData struct {
 	Type        string `validate:"required,oneof=website"`
 	Image       string `validate:"required,url"`
 	ImageAlt    string `validate:"required,min=10"`
-	ImageWidth  string `validate:"required"`
-	ImageHeight string `validate:"required"`
+	ImageWidth  int    `validate:"required,gt=0"`
+	ImageHeight int    `validate:"required,gt=0"`
 	SiteName    string `validate:"required,min=5"`
 	Locale      string `validate:"required,min=5"`
 }

19-25: Avoid html/template.JS unless the content is guaranteed safe.

If these fields can ever include untrusted data, prefer passing structured data and JSON‑encoding in the template to prevent XSS.

Example direction:

  • Make JsonLD and Manifest concrete structs.
  • Add helper funcs in the template to JSON‑marshal them safely (or pre‑marshal to string without using htmltemplate.JS).

5-10: Confirm validator tag for directory paths.

Is dirpath supported by your validator? If not, consider a custom rule or reuse an existing tag (e.g., path existence check).

I can add a custom validator or switch to a supported tag once confirmed.

handler/keep_alive_db_test.go (2)

18-18: Use credentials that satisfy PingEnvironment validation tags.

PingEnvironment enforces min=16 for Username/Password; "user"/"pass" may start failing if validation is invoked in constructor/wiring. Use longer test creds to future‑proof.

Apply:

- e := env.PingEnvironment{Username: "user", Password: "pass"}
+ e := env.PingEnvironment{Username: "user-aaaaaaaaaaaa", Password: "pass-aaaaaaaaaaaa"}

17-17: Don’t discard potential error from MakeTestDB.

If MakeTestDB returns an error, failing fast improves signal.

- db, _ := handlertests.MakeTestDB(t)
+ db, err := handlertests.MakeTestDB(t)
+ if err != nil {
+   t.Fatalf("MakeTestDB: %v", err)
+ }
metal/env/seo.go (1)

3-5: Tighten validation for SpaDir (directory existence).

“required” ensures presence only. Consider verifying that SpaDir points to an existing directory or add a custom validator, since generators will read/write here.

Example options:

  • Add a custom validator tag (e.g., “dir”) in portal.Validator and use: validate:"required,dir".
  • Or, validate SpaDir in env.Environment.Validate() by checking os.Stat and Mode().IsDir().
metal/env/app.go (1)

28-30: Lang() default is fine; consider future override hook.

Returning a constant works now. If i18n lands, plan for an env/config override (e.g., AppEnvironment.Language with sane default).

handler/keep_alive_test.go (1)

16-16: Align test creds with PingEnvironment min length.

Same rationale as the DB test—use ≥16‑char strings to avoid future validation coupling issues.

- e := env.PingEnvironment{Username: "user", Password: "pass"}
+ e := env.PingEnvironment{Username: "user-aaaaaaaaaaaa", Password: "pass-aaaaaaaaaaaa"}
pkg/middleware/pipeline.go (1)

13-13: Document deprecation and plan removal of TokenHandler.

Inline “@todo Remove!” is easy to miss. Mark as Deprecated in a doc comment, reference an issue, and define removal timeline to avoid accidental API breakage.

-type Pipeline struct {
+// Pipeline builds request middleware chains.
+type Pipeline struct {
   Env              *env.Environment
   ApiKeys          *repository.ApiKeys
-  TokenHandler     *auth.TokenHandler //@todo Remove!
+  // Deprecated: TokenHandler will be removed after vX.Y (see issue #NNN).
+  TokenHandler     *auth.TokenHandler
   PublicMiddleware PublicMiddleware
 }
.env.example (2)

7-7: Provide a sensible default/example for ENV_APP_URL.

Helps first‑time setup.

-ENV_APP_URL=
+ENV_APP_URL="http://localhost:8080"

37-38: Add guidance/example for ENV_SPA_DIR.

Clarify expected path semantics (absolute vs relative) and typical location.

-# --- SEO: SPA application directory
-ENV_SPA_DIR=
+# --- SEO: SPA application directory (directory must exist; absolute or project‑relative)
+ENV_SPA_DIR="./storage/spa"
metal/kernel/helpers.go (1)

11-13: Accept a pointer to Router to avoid copying and escape‑addressing a value param.

Current signature copies Router, then takes its address—this allocates and decouples later caller mutations. Accepting *router.Router is simpler and avoids unintended copies.

-func (a *App) SetRouter(router router.Router) {
-  a.router = &router
-}
+func (a *App) SetRouter(r *router.Router) {
+  a.router = r
+}
metal/router/router_keep_alive_test.go (1)

14-17: Test creds violate min-length validation; use ≥16 chars.
PingEnvironment enforces min=16; align test data accordingly.

-		Env:      &env.Environment{Ping: env.PingEnvironment{Username: "user", Password: "pass"}},
+		Env:      &env.Environment{Ping: env.PingEnvironment{Username: "user-credential-1234", Password: "pass-credential-1234"}},
metal/router/router_keep_alive_db_test.go (1)

16-17: Test creds violate min-length validation; use ≥16 chars.
Align with PingEnvironment tags to avoid drifting from production constraints.

-		Env:      &env.Environment{Ping: env.PingEnvironment{Username: "user", Password: "pass"}},
+		Env:      &env.Environment{Ping: env.PingEnvironment{Username: "user-credential-1234", Password: "pass-credential-1234"}},
metal/cli/seo/stub.html (2)

8-10: Fix meta usage: theme-color vs color-scheme.
color-scheme expects “light dark”; theme color should hold the hex. Replace and drop nonstandard theme-scheme.

-	<meta name="color-scheme" content="{{.ThemeColor}}">
-	<meta name="theme-scheme" content="{{.ThemeColor}}">
+	<meta name="theme-color" content="{{.ThemeColor}}">
+	<meta name="color-scheme" content="light dark">

14-15: Trim stray space in canonical href.
Minor template cleanliness.

-	<link rel="canonical" href="{{.Canonical }}">
+	<link rel="canonical" href="{{.Canonical}}">
docker-compose.yml (1)

85-85: Pin builder image to a digest for supply‑chain safety.
Tag-only images drift; prefer digest pinning.

-          image: golang:1.25.1-alpine
+          image: golang:1.25.1-alpine@sha256:<PINNED_DIGEST>
handler/keep_alive.go (1)

15-20: *Signature update to env.PingEnvironment — LGTM.
Optional: metal/env/ping.go:18–20 — HasInvalidCreds uses != for string comparison; replace with crypto/subtle.ConstantTimeCompare on trimmed []byte to avoid timing leaks.

handler/keep_alive_db.go (1)

16-22: *Signature update to env.PingEnvironment — LGTM.

Small ask: avoid hanging on DB ping — update Connection.Ping (database/connection.go) to use context.WithTimeout and call conn.PingContext(ctx) with a short timeout so the handler cannot block.

metal/kernel/kernel_test.go (1)

160-170: Avoid package shadowing — rename local router variable.

Rename the local var to avoid shadowing the imported router package; update the SetRouter call.

File: metal/kernel/kernel_test.go (lines 160-170)

-	router := router.Router{
+	rt := router.Router{
 		Env: env,
 		Mux: http.NewServeMux(),
 		Pipeline: middleware.Pipeline{
 			Env:              env,
 			ApiKeys:          &repository.ApiKeys{DB: &database.Connection{}},
 			TokenHandler:     handler,
 			PublicMiddleware: middleware.MakePublicMiddleware("", false),
 		},
 		Db: &database.Connection{},
 	}
-	app := &App{}
-	app.SetRouter(router)
+	app := &App{}
+	app.SetRouter(rt)

Confirmed: Pipeline.Env is still set in multiple files (metal/kernel/app.go, metal/kernel/kernel_test.go, metal/router/router_keep_alive_test.go, metal/router/router_keep_alive_db_test.go) — keep Pipeline.Env.

metal/cli/main.go (1)

165-194: LGTM: timestamp utility is fine.

Small nit: if consistency with UTC is desired across outputs, use now.UTC(). Not blocking.

metal/makefile/db.mk (1)

59-61: compose run --env-file portability.

docker compose --env-file with run isn’t uniformly available across environments and is redundant if the service already has env_file in docker-compose.yml. Prefer relying on the service’s env_file or pass -e flags only.

Apply this diff to simplify:

-db:seed:
-	docker compose --env-file ./.env run --rm $(DB_MIGRATE_DOCKER_ENV_FLAGS) $(DB_API_RUNNER_SERVICE) \
- 		 go run ./database/seeder/main.go
+db:seed:
+	docker compose run --rm $(DB_MIGRATE_DOCKER_ENV_FLAGS) $(DB_API_RUNNER_SERVICE) \
+		go run ./database/seeder/main.go

If you require variable substitution from .env for compose itself, use:

  • docker compose --env-file ./.env up (not run), or
  • service-level env_file: ./.env in docker-compose.yml.
metal/kernel/factory.go (2)

116-118: Minor: error message wording.

“invalid [logs Credentials] model” reads odd. Consider “invalid [LOGS] model”.

- panic(errorSuffix + "invalid [logs Credentials] model: " + validate.GetErrorsAsJson())
+ panic(errorSuffix + "invalid [LOGS] model: " + validate.GetErrorsAsJson())

136-151: Minor: naming consistency.

Variable blog holds the env; envModel or cfg would be clearer.

- blog := &env.Environment{
+ model := &env.Environment{
...
- if _, err := validate.Rejects(blog); err != nil {
+ if _, err := validate.Rejects(model); err != nil {
...
- return blog
+ return model
metal/cli/seo/jsonld.go (1)

57-60: Schema property: use foundingDate instead of foundedYear.

Schema.org expects foundingDate (ISO date string) for Organization; foundedYear isn’t a standard property.

-            "foundedYear": j.FoundedYear,
+            "foundingDate": j.FoundedYear,
metal/router/web.go (1)

10-13: Path constants lack leading slash.

For consistency with router paths (e.g., "/profile"), prefer “/about”, “/resume”, “/projects”.

-const AboutPage = "about"
-const ResumePage = "resume"
-const ProjectsPage = "projects"
+const AboutPage = "/about"
+const ResumePage = "/resume"
+const ProjectsPage = "/projects"
metal/kernel/app.go (1)

86-99: Avoid copying the router by value in Boot.

Copying the struct is unnecessary and can surprise if Router gains internal state. Use the pointer directly.

-    modem := *a.router
-
-    modem.KeepAlive()
-    modem.KeepAliveDB()
-    modem.Profile()
+    modem := a.router
+
+    modem.KeepAlive()
+    modem.KeepAliveDB()
+    modem.Profile()
     modem.Experience()
     modem.Projects()
     modem.Social()
     modem.Talks()
     modem.Education()
     modem.Recommendations()
     modem.Posts()
     modem.Categories()
     modem.Signature()
metal/cli/accounts/handler.go (1)

68-74: Secrets printed in plaintext.

Consider gating plaintext secret output behind a confirmation prompt or environment check to reduce accidental exposure, especially for ShowAccount.

metal/cli/seo/web.go (1)

26-28: Content/semantics nits: description typo and invalid theme color.

  • Duplicate word “of” in Description.
  • ThemeColor must be a valid CSS color; “light dark” is not.

Apply:

-const Description = "Gustavo is a full-stack Software Engineer leader with over two decades of of experience..."
+const Description = "Gustavo is a full-stack Software Engineering leader with over two decades of experience..."

-const ThemeColor = "light dark"
+const ThemeColor = "#ffffff"
metal/cli/seo/manifest.go (2)

51-56: Use relative start_url for better PWA behavior.

Absolute URLs work but relative “/” is recommended and matches scope.

-		StartURL:    tmpl.SiteURL,
+		StartURL:    WebHomeUrl,

21-23: Unused field: Manifest.Now is never read.

If not used elsewhere, remove to simplify.

metal/cli/seo/generator.go (4)

154-177: Provide required BgColor, valid ThemeColor, and richer icons.

  • TemplateData.BgColor is required (tags) but not set.
  • ThemeColor should be a valid color.
  • Add at least 192x192 and 512x512 PNG icons for manifest quality.

Apply:

 	data := TemplateData{
 		OGTagOg:        og,
-		Robots:         Robots,
+		Robots:         Robots,
 		Twitter:        twitter,
-		ThemeColor:     ThemeColor,
+		ThemeColor:     ThemeColor, // e.g., "#ffffff"
 		Lang:           g.Tmpl.Lang,
 		Description:    Description,
 		Canonical:      g.Tmpl.SiteURL,
 		AppleTouchIcon: g.Tmpl.LogoURL,
 		Title:          g.Tmpl.SiteName,
 		JsonLD:         NewJsonID(g.Tmpl).Render(),
-		Categories:     []string{"one", "two"}, //@todo Fetch this!
+		Categories:     []string{"one", "two"}, //@todo Fetch this!
+		BgColor:        "#ffffff",
 		HrefLang: []HrefLangData{
 			{Lang: g.Tmpl.Lang, Href: g.Tmpl.SiteURL},
 		},
 		Favicons: []FaviconData{
 			{
-				Rel:   "icon",
-				Sizes: "48x48",
-				Type:  "image/ico",
-				Href:  g.Tmpl.SiteURL + "/favicon.ico",
+				Rel:   "icon",
+				Sizes: "192x192",
+				Type:  "image/png",
+				Href:  g.Tmpl.SiteURL + "/icons/icon-192.png",
+			},
+			{
+				Rel:   "icon",
+				Sizes: "512x512",
+				Type:  "image/png",
+				Href:  g.Tmpl.SiteURL + "/icons/icon-512.png",
 			},
 		},
 	}

137-184: Generate() builds data but discards it.

Consider returning the assembled TemplateData (and/or writing output) to make this method useful to callers.


45-59: Minor: naming consistency for URL fields.

Constants use Url while Template uses LogoURL/WebRepoURL/APIRepoURL. Consider consistent “URL”.


66-73: Router field on Generator is unused here.

If not used elsewhere, remove to keep Generator focused.

metal/router/fixture.go (2)

29-56: Mutable receiver state makes sequencing order-sensitive.

resolveFor mutates the same Fixture instance; it works but is easy to misuse and not concurrency-safe. Prefer pure helpers that return paths without side effects.

Example:

-func (f *Fixture) resolveFor(slug string) *Fixture {
-	f.fullPath = f.getFileFor(slug)
-	f.file = slug
-	return f
-}
+func (f *Fixture) resolveFor(slug string) *Fixture {
+	f.fullPath = f.getFileFor(slug)
+	f.file = slug
+	return f
+}
+
+// Consider adding:
+func (f Fixture) FileFor(slug string) (route, fullPath string) {
+	return slug, f.getFileFor(slug)
+}

Then adapt callers to use FileFor.


38-40: Parity: consider GetSocialFile().

Other fixtures expose a File() helper; Social does not.

+func (f *Fixture) GetSocialFile() string {
+	return f.resolveFor(FixtureSocial).fullPath
+}
metal/router/router.go (2)

51-60: HTTP verb for index should be GET.

Listing posts on POST is unexpected and likely a mistake.

Apply:

-	r.Mux.HandleFunc("POST /posts", index)
+	r.Mux.HandleFunc("GET /posts", index)

176-197: Nil safety: ensure Router.Mux is initialized before route registration.

If r.Mux can be nil here, handlers will panic on use.

Consider constructing Router with a non-nil ServeMux or guarding with a panic-friendly check early in setup.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b0211a and cd61e50.

📒 Files selected for processing (42)
  • .env.example (2 hunks)
  • .env.gh.example (0 hunks)
  • .env.prod.example (0 hunks)
  • .gitignore (1 hunks)
  • Makefile (2 hunks)
  • docker-compose.yml (1 hunks)
  • handler/keep_alive.go (1 hunks)
  • handler/keep_alive_db.go (1 hunks)
  • handler/keep_alive_db_test.go (1 hunks)
  • handler/keep_alive_test.go (1 hunks)
  • metal/cli/accounts/handler.go (4 hunks)
  • metal/cli/main.go (3 hunks)
  • metal/cli/panel/menu.go (1 hunks)
  • metal/cli/seo/data.go (1 hunks)
  • metal/cli/seo/generator.go (1 hunks)
  • metal/cli/seo/jsonld.go (1 hunks)
  • metal/cli/seo/manifest.go (1 hunks)
  • metal/cli/seo/stub.html (1 hunks)
  • metal/cli/seo/support.go (1 hunks)
  • metal/cli/seo/web.go (1 hunks)
  • metal/env/app.go (2 hunks)
  • metal/env/env.go (1 hunks)
  • metal/env/ping.go (1 hunks)
  • metal/env/seo.go (1 hunks)
  • metal/kernel/app.go (2 hunks)
  • metal/kernel/factory.go (4 hunks)
  • metal/kernel/helpers.go (1 hunks)
  • metal/kernel/kernel_test.go (3 hunks)
  • metal/kernel/router.go (0 hunks)
  • metal/makefile/app.mk (2 hunks)
  • metal/makefile/build.mk (1 hunks)
  • metal/makefile/db.mk (2 hunks)
  • metal/router/fixture.go (1 hunks)
  • metal/router/router.go (1 hunks)
  • metal/router/router_keep_alive_db_test.go (2 hunks)
  • metal/router/router_keep_alive_test.go (2 hunks)
  • metal/router/router_signature_test.go (2 hunks)
  • metal/router/web.go (1 hunks)
  • pkg/middleware/mwguards/valid_timestamp_guard.go (1 hunks)
  • pkg/middleware/pipeline.go (1 hunks)
  • pkg/middleware/public_middleware.go (1 hunks)
  • pkg/middleware/token_middleware.go (3 hunks)
💤 Files with no reviewable changes (3)
  • .env.gh.example
  • metal/kernel/router.go
  • .env.prod.example
🧰 Additional context used
🧬 Code graph analysis (28)
handler/keep_alive_db.go (3)
metal/env/ping.go (1)
  • PingEnvironment (5-8)
database/connection.go (1)
  • Connection (12-17)
metal/kernel/router.go (1)
  • r (93-101)
pkg/middleware/pipeline.go (1)
pkg/auth/handler.go (1)
  • TokenHandler (10-14)
metal/kernel/helpers.go (3)
metal/kernel/app.go (1)
  • App (17-24)
metal/router/router.go (1)
  • Router (16-25)
metal/kernel/router.go (4)
  • r (127-129)
  • Env (25-31)
  • r (111-113)
  • r (33-40)
metal/env/seo.go (3)
metal/kernel/router.go (1)
  • Env (25-31)
metal/env/logs.go (1)
  • Level (3-7)
metal/env/network.go (1)
  • HttpHost (3-8)
metal/kernel/kernel_test.go (3)
metal/router/router.go (1)
  • Router (16-25)
metal/kernel/router.go (4)
  • r (127-129)
  • r (111-113)
  • Env (25-31)
  • r (119-121)
metal/kernel/router_signature_test.go (1)
  • TestSignatureRoute_PublicMiddleware (15-44)
handler/keep_alive.go (2)
metal/env/ping.go (1)
  • PingEnvironment (5-8)
metal/kernel/router.go (1)
  • r (93-101)
metal/router/router_keep_alive_db_test.go (4)
metal/env/env.go (1)
  • Environment (9-17)
metal/env/ping.go (1)
  • PingEnvironment (5-8)
metal/kernel/router_keep_alive_test.go (1)
  • TestKeepAliveRoute (12-39)
metal/kernel/router.go (3)
  • Env (25-31)
  • r (93-101)
  • r (83-91)
pkg/middleware/public_middleware.go (1)
pkg/middleware/public_middleware_test.go (1)
  • TestPublicMiddleware_IPWhitelist (111-152)
handler/keep_alive_test.go (3)
metal/env/ping.go (1)
  • PingEnvironment (5-8)
metal/kernel/router_keep_alive_test.go (1)
  • TestKeepAliveRoute (12-39)
metal/kernel/router_keep_alive_db_test.go (1)
  • TestKeepAliveDBRoute (13-42)
metal/router/router_keep_alive_test.go (4)
metal/env/env.go (1)
  • Environment (9-17)
metal/env/ping.go (1)
  • PingEnvironment (5-8)
metal/kernel/router_keep_alive_db_test.go (1)
  • TestKeepAliveDBRoute (13-42)
metal/kernel/router.go (2)
  • Env (25-31)
  • r (93-101)
pkg/middleware/mwguards/valid_timestamp_guard.go (2)
pkg/middleware/mwguards/mw_response_messages.go (2)
  • TimestampTooOldError (101-112)
  • TimestampTooNewError (114-125)
pkg/middleware/mwguards/valid_timestamp_guard_test.go (2)
  • TestValidate_TooOldTimestamp (40-49)
  • TestValidate_FutureWithinSkew_Behavior (51-68)
metal/router/router_signature_test.go (2)
pkg/portal/validator.go (2)
  • Validator (13-16)
  • GetDefaultValidator (23-33)
metal/kernel/router.go (4)
  • r (127-129)
  • r (76-81)
  • Env (25-31)
  • r (33-40)
metal/cli/seo/manifest.go (3)
metal/cli/seo/web.go (12)
  • Description (26-26)
  • ThemeColor (28-28)
  • WebHomeUrl (12-12)
  • WebHomeName (11-11)
  • WebPostUrl (15-15)
  • WebPostName (14-14)
  • WebProjectsUrl (21-21)
  • WebProjectsName (20-20)
  • WebAboutUrl (18-18)
  • WebAboutName (17-17)
  • WebResumeUrl (24-24)
  • WebResumeName (23-23)
metal/cli/seo/generator.go (1)
  • Template (22-34)
metal/cli/seo/data.go (1)
  • TemplateData (12-28)
metal/cli/seo/generator.go (17)
metal/cli/seo/web.go (8)
  • AboutPhotoUrl (9-9)
  • LogoUrl (8-8)
  • RepoWebUrl (6-6)
  • RepoApiUrl (5-5)
  • GocantoUrl (4-4)
  • Robots (27-27)
  • ThemeColor (28-28)
  • Description (26-26)
metal/env/env.go (1)
  • Environment (9-17)
pkg/portal/validator.go (1)
  • Validator (13-16)
database/connection.go (1)
  • Connection (12-17)
metal/router/web.go (3)
  • WebsiteRoutes (31-38)
  • NewWebsiteRoutes (40-49)
  • StaticRouteResource (15-17)
metal/router/router.go (1)
  • Router (16-25)
metal/router/fixture.go (4)
  • FixtureProfile (9-9)
  • Fixture (15-20)
  • FixtureTalks (7-7)
  • FixtureProjects (10-10)
handler/profile.go (1)
  • MakeProfileHandler (16-20)
handler/talks.go (1)
  • MakeTalksHandler (16-20)
handler/projects.go (1)
  • MakeProjectsHandler (16-20)
handler/payload/talks.go (1)
  • TalksResponse (3-6)
handler/payload/profile.go (1)
  • ProfileResponse (3-6)
handler/payload/projects.go (1)
  • ProjectsResponse (3-6)
metal/cli/seo/support.go (1)
  • Fetch (19-34)
metal/cli/seo/data.go (5)
  • TagOgData (30-38)
  • TwitterData (40-44)
  • TemplateData (12-28)
  • HrefLangData (46-49)
  • FaviconData (51-56)
metal/cli/seo/jsonld.go (1)
  • NewJsonID (27-41)
metal/cli/seo/manifest.go (2)
  • NewManifest (40-93)
  • Manifest (10-23)
metal/env/app.go (2)
metal/kernel/router.go (1)
  • Env (25-31)
metal/env/env_test.go (1)
  • TestAppEnvironmentChecks (43-67)
handler/keep_alive_db_test.go (1)
metal/env/ping.go (1)
  • PingEnvironment (5-8)
metal/cli/seo/data.go (2)
metal/cli/seo/web.go (3)
  • Description (26-26)
  • Robots (27-27)
  • ThemeColor (28-28)
metal/cli/seo/manifest.go (1)
  • Manifest (10-23)
pkg/middleware/token_middleware.go (5)
metal/env/env.go (1)
  • Environment (9-17)
metal/kernel/app.go (1)
  • App (17-24)
pkg/middleware/token_middleware_additional_test.go (2)
  • TestTokenMiddlewareGuardDependencies (88-100)
  • TestTokenMiddleware_PublicTokenMismatch (102-115)
pkg/middleware/token_middleware_test.go (3)
  • TestTokenMiddleware_DB_Integration (199-277)
  • TestTokenMiddleware_DB_Integration_HappyPath (280-335)
  • TestTokenMiddlewareHandleInvalid (40-52)
metal/kernel/router.go (1)
  • Env (25-31)
metal/cli/accounts/handler.go (7)
database/attrs.go (1)
  • APIKeyAttr (7-11)
metal/cli/accounts/factory.go (1)
  • Handler (12-17)
pkg/auth/schema.go (1)
  • Token (11-17)
database/model.go (1)
  • APIKey (27-39)
pkg/auth/encryption.go (1)
  • CreateSignatureFrom (80-85)
metal/cli/accounts/handler_test.go (2)
  • TestCreateReadSignature (21-35)
  • TestReadAccountNotFound (45-51)
pkg/auth/handler.go (1)
  • SetupNewAccount (49-72)
metal/router/web.go (4)
pkg/http/schema.go (1)
  • ApiError (11-15)
metal/router/fixture.go (2)
  • Fixture (15-20)
  • NewFixture (22-27)
metal/env/env.go (1)
  • Environment (9-17)
metal/kernel/router.go (8)
  • addStaticRoute (19-23)
  • r (111-113)
  • r (103-105)
  • r (119-121)
  • r (115-117)
  • r (107-109)
  • r (56-65)
  • r (123-125)
metal/cli/main.go (3)
pkg/cli/message.go (2)
  • Errorln (9-11)
  • Error (5-7)
metal/cli/seo/generator.go (1)
  • NewGenerator (45-74)
pkg/portal/validator.go (1)
  • GetDefaultValidator (23-33)
metal/env/env.go (9)
metal/kernel/app.go (1)
  • App (17-24)
metal/env/app.go (1)
  • AppEnvironment (9-14)
metal/env/db.go (1)
  • DBEnvironment (5-14)
metal/env/logs.go (1)
  • LogsEnvironment (3-7)
metal/env/network.go (1)
  • NetEnvironment (3-8)
pkg/portal/sentry.go (1)
  • Sentry (8-12)
metal/env/sentry.go (1)
  • SentryEnvironment (3-6)
metal/env/ping.go (1)
  • PingEnvironment (5-8)
metal/env/seo.go (1)
  • SeoEnvironment (3-5)
metal/router/router.go (10)
metal/env/env.go (1)
  • Environment (9-17)
pkg/middleware/pipeline.go (1)
  • Pipeline (10-15)
database/connection.go (1)
  • Connection (12-17)
pkg/portal/validator.go (1)
  • Validator (13-16)
metal/router/web.go (2)
  • WebsiteRoutes (31-38)
  • StaticRouteResource (15-17)
pkg/http/schema.go (1)
  • ApiHandler (25-25)
pkg/http/handler.go (1)
  • MakeApiHandler (9-28)
pkg/middleware/token_middleware.go (1)
  • MakeTokenMiddleware (37-51)
metal/router/fixture.go (1)
  • Fixture (15-20)
metal/kernel/router.go (9)
  • r (56-65)
  • Env (25-31)
  • r (33-40)
  • r (111-113)
  • r (115-117)
  • r (107-109)
  • r (42-54)
  • r (119-121)
  • r (67-74)
metal/kernel/factory.go (6)
metal/env/env.go (2)
  • GetEnvVar (23-25)
  • Environment (9-17)
metal/env/logs.go (1)
  • LogsEnvironment (3-7)
metal/env/network.go (1)
  • NetEnvironment (3-8)
metal/env/sentry.go (1)
  • SentryEnvironment (3-6)
metal/env/ping.go (1)
  • PingEnvironment (5-8)
metal/env/seo.go (1)
  • SeoEnvironment (3-5)
metal/cli/seo/jsonld.go (2)
metal/cli/seo/web.go (1)
  • FoundedYear (3-3)
metal/cli/seo/generator.go (1)
  • Template (22-34)
metal/cli/seo/support.go (1)
metal/router/web.go (1)
  • StaticRouteResource (15-17)
metal/router/fixture.go (4)
handler/talks_test.go (1)
  • TestTalksHandler (5-12)
handler/file_handler_test.go (2)
  • make (18-24)
  • runFileHandlerTest (26-87)
metal/kernel/router.go (4)
  • r (119-121)
  • r (111-113)
  • r (123-125)
  • addStaticRoute (19-23)
handler/projects_test.go (1)
  • TestProjectsHandler (5-12)
metal/kernel/app.go (7)
metal/router/router.go (1)
  • Router (16-25)
pkg/auth/handler.go (2)
  • MakeTokensHandler (16-26)
  • TokenHandler (10-14)
pkg/middleware/pipeline.go (1)
  • Pipeline (10-15)
database/repository/api_keys.go (1)
  • ApiKeys (16-18)
pkg/middleware/public_middleware.go (2)
  • PublicMiddleware (20-29)
  • MakePublicMiddleware (31-42)
metal/router/web.go (2)
  • WebsiteRoutes (31-38)
  • NewWebsiteRoutes (40-49)
metal/kernel/router.go (3)
  • r (127-129)
  • Env (25-31)
  • r (33-40)
🔇 Additional comments (17)
metal/cli/panel/menu.go (1)

92-94: LGTM; confirm wiring for options 4 and 5.

Ensure "4) Generate SEO." triggers the SEO pipeline and "5) Print Timestamp." is handled in cli/main.go, plus update tests/help if any.

metal/env/app.go (1)

7-14: URL field addition looks good; add a quick validation test.

The validate:"required,url" tag is appropriate. Add a unit test asserting invalid/missing ENV_APP_URL fails validation to avoid regressions.

metal/router/router_signature_test.go (2)

21-22: Validator field switch to exported looks correct.

Using Validator: portal.GetDefaultValidator() matches the new Router API.


34-38: Guard against time‑skew flakiness in public middleware.

If the middleware enforces a tight timestamp window, tests may flicker. Consider injecting a fixed clock or widening the allowed skew for tests.

metal/kernel/kernel_test.go (2)

12-12: Import refactor to new router pkg — LGTM.


123-123: Type migration to router.Router — LGTM. Add ENV_APP_URL & ENV_SPA_DIR to test fixtures.

metal/kernel/factory.go reads ENV_APP_URL (line 65) and ENV_SPA_DIR (line 105); .env.example defines them (.env.example:7, .env.example:38). No fixtures/fixtures entry setting these was found — add ENV_APP_URL and ENV_SPA_DIR to your test fixtures so App.URL and Seo.SpaDir validation is exercised.

docker-compose.yml (2)

83-86: env_file for api-runner — LGTM.


83-95: Verify .env contains ENV_APP_URL and ENV_SPA_DIR

Automated check produced no output; confirm ENV_APP_URL and ENV_SPA_DIR exist in .env to unblock SEO/env validation.

metal/cli/seo/stub.html (1)

40-41: JsonLD is correctly typed as htmltemplate.JS — no change required.
TemplateData.JsonLD is htmltemplate.JS in metal/cli/seo/data.go (line 19), so the JSON-LD will not be HTML-escaped.

metal/env/env.go (1)

10-16: Environment schema tightening — LGTM; add test fixtures for ENV_APP_URL and ENV_SPA_DIR

Factory reads ENV_APP_URL (metal/kernel/factory.go:65) and ENV_SPA_DIR (metal/kernel/factory.go:105); extend test fixtures/test setup (or CI test env) to set ENV_APP_URL and ENV_SPA_DIR so validations pass.

metal/cli/main.go (1)

147-164: LGTM: generateSEO flow is cohesive.

Constructor wiring matches NewGenerator signature and GenerateHome is invoked with errors surfaced.

metal/kernel/factory.go (1)

104-107: New required var: ENV_SPA_DIR — add to env files and ensure path exists at runtime.
.env.example:38 currently has ENV_SPA_DIR= (empty). Add/populate ENV_SPA_DIR in .env and your production env files and verify the SPA directory is present/mounted at runtime.

metal/makefile/db.mk (1)

18-18: Confirm removal of Ping creds from migration flags.

db:seed uses DB_MIGRATE_DOCKER_ENV_FLAGS but those flags no longer pass ENV_PING_USERNAME/ENV_PING_PASSWORD; the code reads these vars (metal/kernel/factory.go) and docker-compose.yml/.env.example reference them — ensure ./.env or the host environment provides ENV_PING_USERNAME and ENV_PING_PASSWORD when running make db:seed, or re-add -e ENV_PING_USERNAME -e ENV_PING_PASSWORD to DB_MIGRATE_DOCKER_ENV_FLAGS in metal/makefile/db.mk.

metal/kernel/app.go (1)

59-67: Incorrect — no change required: Router already instantiates token middleware with env.

Router.PipelineFor calls middleware.MakeTokenMiddleware(r.Env, r.Pipeline.TokenHandler, r.Pipeline.ApiKeys) and App.NewRouter already sets Pipeline.Env; leave as-is.

Likely an incorrect or invalid review comment.

metal/cli/seo/manifest.go (1)

58-89: Shortcuts rely on constants corrected in web.go.

Once URLs there are fixed, these become valid manifest shortcuts. No code changes here beyond adopting those constants.

metal/router/router.go (2)

98-175: Static fixture composition looks correct and matches test expectations.

Endpoints should resolve to /profile, /experience, /projects, /social, /talks, /education, /recommendations.


36-49: Token middleware receives r.Env — confirm no public auth bypass remains

Token middleware is wired with r.Env in PipelineFor; sandbox ripgrep skipped files so full verification failed. Run locally and paste results:

# search for bypass/allow-public flags
rg -n -S -i -uu --hidden --glob '!vendor/**' 'bypass|skip.*auth|noauth|no-auth|ALLOW_.*PUBLIC|PUBLIC_BYPASS|non-?prod|AllowUnauthenticated|SkipAuth|allow_unauthenticated'

# search for token middleware / pipeline usage
rg -n -S -i -uu --hidden --glob '!vendor/**' -C3 'MakeTokenMiddleware|TokenMiddleware|MakeApiHandler|func .*PipelineFor|Pipeline\.TokenHandler|Pipeline\.ApiKeys|Pipeline\.Chain'

# search for env vars referenced
rg -n -S -i -uu --hidden --glob '!vendor/**' 'ENV_SPA_DIR|ENV_APP_URL|ENV_ALLOW|ALLOW_PUBLIC'

@gocanto gocanto marked this pull request as draft September 23, 2025 05:38
@gocanto gocanto marked this pull request as ready for review September 23, 2025 09:11
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
metal/cli/accounts/handler_test.go (1)

115-133: Inconsistency: main.go still calls ReadAccount instead of ShowAccount.

The test has been updated to use ShowAccount, but according to the code snippet from metal/cli/main.go, the showApiAccount function still calls handler.ReadAccount(account) on line 128.

This mismatch will cause a compilation error. The main.go file needs to be updated to use the renamed method:

-	if err = handler.ReadAccount(account); err != nil {
+	if err = handler.ShowAccount(account); err != nil {
🧹 Nitpick comments (18)
.env.example (3)

5-7: Satisfy dotenv-linter: reorder and unquote date format.

Addresses UnorderedKey and QuoteCharacter warnings.

Apply this diff:

-ENV_APP_LOGS_DIR="./storage/logs/logs_%s.log"
-ENV_APP_LOGS_DATE_FORMAT="2006-01-02"
+ENV_APP_LOGS_DATE_FORMAT=2006-01-02
+ENV_APP_LOGS_DIR="./storage/logs/logs_%s.log"

7-7: Add example and guidance for ENV_APP_URL (avoid trailing slash).

Improves DX and reduces ambiguity for SEO link generation.

Apply this diff:

-ENV_APP_URL=
+# Base URL for absolute links (no trailing slash), e.g. http://localhost:8080
+ENV_APP_URL=http://localhost:8080

Please verify the code trims/validates trailing slashes when reading this var to avoid double slashes in generated URLs.


37-38: Document expected path for ENV_SPA_DIR and provide example.

Clarifies how SEO locates SPA assets.

Apply this diff:

-# --- SEO: SPA application directory
-ENV_SPA_DIR=
+# --- SEO: SPA application directory
+# Absolute or repo-relative path to built SPA assets used for SEO (e.g., ./spa/dist)
+ENV_SPA_DIR=./spa/dist

Confirm the runtime expects this directory to exist and handles missing/invalid paths with a clear error.

metal/cli/seo/jsonld.go (1)

38-39: Consider making the Now function configurable for testing.

The Now field is initialized with a function that always returns time.Now().UTC(). This makes testing time-dependent behavior difficult.

Consider accepting the time function as a parameter or providing a default that can be overridden:

-func NewJsonID(tmpl Page) *JsonID {
+func NewJsonID(tmpl Page, opts ...func(*JsonID)) *JsonID {
 	return &JsonID{
 		Lang:        tmpl.Lang,
 		SiteURL:     tmpl.SiteURL,
 		LogoURL:     tmpl.LogoURL,
 		OrgName:     tmpl.SiteName,
 		WebName:     tmpl.SiteName,
 		APIName:     tmpl.SiteName,
 		SameAs:      tmpl.SameAsURL,
 		APIRepoURL:  tmpl.APIRepoURL,
 		WebRepoURL:  tmpl.WebRepoURL,
 		FoundedYear: fmt.Sprintf("%d", FoundedYear),
 		Now:         func() time.Time { return time.Now().UTC() },
+	}
+	
+	for _, opt := range opts {
+		opt(j)
 	}
+	
+	return j
 }
metal/cli/seo/generator.go (4)

65-95: Consider extracting resource factory functions.

The resource initialization logic could be extracted into a separate method for better organization and testability.

Consider extracting the resource map creation:

+func (g *Generator) createResourceHandlers() map[string]func() router.StaticRouteResource {
+	web := g.WebsiteRoutes
+	resource := make(map[string]func() router.StaticRouteResource)
+	
+	resource[router.FixtureProfile] = func() router.StaticRouteResource {
+		return handler.MakeProfileHandler(web.Fixture.GetProfileFile())
+	}
+	
+	resource[router.FixtureTalks] = func() router.StaticRouteResource {
+		return handler.MakeTalksHandler(web.Fixture.GetTalksFile())
+	}
+	
+	resource[router.FixtureProjects] = func() router.StaticRouteResource {
+		return handler.MakeProjectsHandler(web.Fixture.GetProjectsFile())
+	}
+	
+	return resource
+}

 func (g *Generator) GenerateHome() error {
 	var err error
-	web := g.WebsiteRoutes
-	resource := make(map[string]func() router.StaticRouteResource)
-	
-	resource[router.FixtureProfile] = func() router.StaticRouteResource {
-		return handler.MakeProfileHandler(web.Fixture.GetProfileFile())
-	}
-	
-	resource[router.FixtureTalks] = func() router.StaticRouteResource {
-		return handler.MakeTalksHandler(web.Fixture.GetTalksFile())
-	}
-	
-	resource[router.FixtureProjects] = func() router.StaticRouteResource {
-		return handler.MakeProjectsHandler(web.Fixture.GetProjectsFile())
-	}
+	resource := g.createResourceHandlers()

127-127: Consider using structured logging instead of fmt.Println.

Using fmt.Println for status messages is not ideal for production code. Consider using a structured logger that respects log levels.

Consider using a logger from your environment or a logging package:

-	fmt.Println("Home: Done.")
+	// Consider using a logger from g.Env or a logging package
+	// log.Info("SEO generation completed", "page", "home")

177-191: Consider consolidating validation error handling.

The validation error messages are repetitive and could be consolidated for better maintainability.

Consider creating a helper method:

+func (g *Generator) validateWithContext(data interface{}, context string) error {
+	if _, err := g.Validator.Rejects(data); err != nil {
+		return fmt.Errorf("invalid %s: %s", context, g.Validator.GetErrorsAsJson())
+	}
+	return nil
+}

 // Then use it like:
-	if _, err := g.Validator.Rejects(og); err != nil {
-		return TemplateData{}, fmt.Errorf("invalid og data: %s", g.Validator.GetErrorsAsJson())
-	}
+	if err := g.validateWithContext(og, "og data"); err != nil {
+		return TemplateData{}, err
+	}

161-161: Fetch categories from DB (replace hardcoded list)

Replace the hardcoded Categories: []string{"one","two"} in metal/cli/seo/generator.go:161 with a DB-backed fetch using repository.Categories{DB: g.DB}.GetAll(...) (see database/repository/categories.go) and map the returned database.Category items to a []string (e.g., use Slug or Name) to populate TemplateData.Categories. I can implement this change or open an issue to track it.

metal/cli/accounts/handler.go (2)

28-30: Wrap returned error with %w

Use %w to preserve the original error.

-	if err = h.print(token, item); err != nil {
-		return fmt.Errorf("could not decode the given account [%s] keys: %v", item.AccountName, err)
-	}
+	if err = h.print(token, item); err != nil {
+		return fmt.Errorf("could not decode the given account [%s] keys: %w", item.AccountName, err)
+	}

55-77: print: simplify API and avoid double work; beware of printing secrets

  • The function re-decodes tokens; the token param is redundant. Drop it and accept only item; adjust callers.
  • You’re printing plaintext Secret Key. If this output can end up in logs/CI, consider gating behind an explicit flag.

Proposed signature change:

-func (h Handler) print(token *auth.Token, item *database.APIKey) error {
+func (h Handler) print(item *database.APIKey) error {
-	token, err := h.TokenHandler.DecodeTokensFor(
+	token, err := h.TokenHandler.DecodeTokensFor(
 		item.AccountName,
 		item.SecretKey,
 		item.PublicKey,
 	)

Then update call sites accordingly:

- if err = h.print(token, item); err != nil { ... }
+ if err = h.print(item); err != nil { ... }
metal/cli/seo/defaults.go (2)

5-10: Prefer Go initialism style for URL/API names

Use URL/API capitalization: e.g., GocantoURL, RepoAPIURL, RepoWebURL, LogoURL, AboutPhotoURL.

This is a breaking rename; ensure all references are updated across seo package before applying.


25-31: Consider environment-driven Robots/ThemeColor for non‑prod

Robots “index,follow” and theme color could vary by env (e.g., noindex in staging).

Are ENV_APP_URL/ENV_SPA_DIR (per PR checklist) intended to influence these defaults? If yes, wire them through SeoEnvironment and use here instead of hard-coded constants.

metal/cli/seo/data.go (4)

15-16: Use BCP 47 language tags (“en-GB”), not “en_GB”

Validator will enforce the current (non-standard) form. Prefer “en-GB”.

-	Lang          string             `validate:"required,oneof=en_GB"`
+	Lang          string             `validate:"required,oneof=en-GB"`
-	StubPath      string             `validate:"required,oneof=stub.html"`
+	StubPath      string             `validate:"required,oneof=stub.html"`

19-35: Align TemplateData.Lang with BCP 47 and Page.Lang

Keep TemplateData.Lang consistent with Page.Lang (“en-GB”).

-	Lang           string         `validate:"required,oneof=en_GB"`
+	Lang           string         `validate:"required,oneof=en-GB"`

53-56: Expand hreflang validation to support “en-GB”

Current oneof=en is too restrictive for regional variants.

-	Lang string `validate:"required,oneof=en"`
+	Lang string `validate:"required,oneof=en en-GB"`

58-63: Rel type too restrictive for favicons

Allow common rel values like apple-touch-icon.

-	Rel   string `validate:"required,oneof=icon"`
+	Rel   string `validate:"required,oneof=icon apple-touch-icon"`
metal/cli/seo/manifest.go (1)

50-88: Confirm StartURL/Scope semantics

StartURL is absolute (tmpl.SiteURL) while Scope is “/”. Many UAs expect StartURL relative to scope. Consider StartURL = WebHomeUrl, and keep absolute URLs for Canonical elsewhere.

-	StartURL:    tmpl.SiteURL,
+	StartURL:    WebHomeUrl,
metal/router/router.go (1)

180-186: Use PublicPipelineFor for static resources

Serve static fixtures without token auth.

 func addStaticRoute[H StaticRouteResource](r *Router, route, fixture string, maker func(string) H) {
 	abstract := maker(fixture)
-	resolver := r.PipelineFor(abstract.Handle)
+	resolver := r.PublicPipelineFor(abstract.Handle)

 	route = strings.TrimLeft(route, "/")
 	r.Mux.HandleFunc("GET /"+route, resolver)
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd61e50 and 0452d7f.

📒 Files selected for processing (14)
  • .env.example (2 hunks)
  • metal/cli/accounts/handler.go (4 hunks)
  • metal/cli/accounts/handler_test.go (2 hunks)
  • metal/cli/seo/data.go (1 hunks)
  • metal/cli/seo/defaults.go (1 hunks)
  • metal/cli/seo/generator.go (1 hunks)
  • metal/cli/seo/jsonld.go (1 hunks)
  • metal/cli/seo/manifest.go (1 hunks)
  • metal/cli/seo/stub.html (1 hunks)
  • metal/cli/seo/support.go (1 hunks)
  • metal/env/app.go (2 hunks)
  • metal/router/router.go (5 hunks)
  • metal/router/static.go (1 hunks)
  • pkg/middleware/token_middleware.go (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • pkg/middleware/token_middleware.go
  • metal/env/app.go
  • metal/cli/seo/stub.html
🧰 Additional context used
🧬 Code graph analysis (9)
metal/cli/seo/jsonld.go (2)
metal/cli/seo/defaults.go (1)
  • FoundedYear (27-27)
metal/cli/seo/data.go (1)
  • Page (5-17)
metal/cli/accounts/handler.go (7)
database/attrs.go (1)
  • APIKeyAttr (7-11)
metal/cli/accounts/factory.go (1)
  • Handler (12-17)
pkg/auth/schema.go (1)
  • Token (11-17)
database/model.go (1)
  • APIKey (27-39)
pkg/auth/encryption.go (1)
  • CreateSignatureFrom (80-85)
metal/cli/main.go (3)
  • showApiAccount (116-134)
  • generateApiAccountsHTTPSignature (136-154)
  • createNewApiAccount (96-114)
pkg/auth/handler.go (1)
  • SetupNewAccount (49-72)
metal/cli/seo/support.go (1)
metal/router/static.go (1)
  • StaticRouteResource (10-12)
metal/cli/accounts/handler_test.go (1)
metal/cli/main.go (1)
  • showApiAccount (116-134)
metal/cli/seo/manifest.go (2)
metal/cli/seo/defaults.go (10)
  • Description (31-31)
  • ThemeColor (29-29)
  • WebHomeUrl (13-13)
  • WebHomeName (14-14)
  • WebProjectsUrl (23-23)
  • WebProjectsName (22-22)
  • WebAboutUrl (17-17)
  • WebAboutName (16-16)
  • WebResumeUrl (20-20)
  • WebResumeName (19-19)
metal/cli/seo/data.go (2)
  • Page (5-17)
  • TemplateData (19-35)
metal/router/static.go (4)
pkg/http/schema.go (1)
  • ApiError (11-15)
metal/router/fixture.go (2)
  • Fixture (15-20)
  • NewFixture (22-27)
metal/env/env.go (1)
  • Environment (9-17)
metal/kernel/router.go (9)
  • addStaticRoute (19-23)
  • r (103-105)
  • r (119-121)
  • r (107-109)
  • r (115-117)
  • r (111-113)
  • Handle (15-17)
  • r (127-129)
  • r (123-125)
metal/cli/seo/data.go (2)
metal/cli/seo/defaults.go (5)
  • AboutPhotoUrl (9-9)
  • StubPath (28-28)
  • Description (31-31)
  • Robots (30-30)
  • ThemeColor (29-29)
metal/cli/seo/manifest.go (1)
  • Manifest (10-23)
metal/cli/seo/generator.go (5)
metal/cli/seo/data.go (6)
  • Page (5-17)
  • TemplateData (19-35)
  • TagOgData (37-45)
  • TwitterData (47-51)
  • HrefLangData (53-56)
  • FaviconData (58-63)
pkg/portal/validator.go (1)
  • Validator (13-16)
metal/router/static.go (3)
  • WebsiteRoutes (14-20)
  • NewWebsiteRoutes (22-30)
  • StaticRouteResource (10-12)
metal/cli/seo/defaults.go (9)
  • LogoUrl (8-8)
  • StubPath (28-28)
  • RepoWebUrl (7-7)
  • RepoApiUrl (6-6)
  • AboutPhotoUrl (9-9)
  • GocantoUrl (5-5)
  • Robots (30-30)
  • ThemeColor (29-29)
  • Description (31-31)
metal/cli/seo/support.go (2)
  • Fetch (12-27)
  • ManifestDataURL (29-34)
metal/router/router.go (13)
metal/router/static.go (2)
  • WebsiteRoutes (14-20)
  • StaticRouteResource (10-12)
metal/env/env.go (1)
  • Environment (9-17)
pkg/portal/validator.go (1)
  • Validator (13-16)
pkg/middleware/pipeline.go (1)
  • Pipeline (10-15)
handler/signatures.go (1)
  • MakeSignaturesHandler (24-29)
handler/profile.go (1)
  • MakeProfileHandler (16-20)
metal/router/fixture.go (1)
  • Fixture (15-20)
handler/experience.go (1)
  • MakeExperienceHandler (16-20)
handler/projects.go (1)
  • MakeProjectsHandler (16-20)
handler/social.go (1)
  • MakeSocialHandler (16-20)
handler/talks.go (1)
  • MakeTalksHandler (16-20)
handler/education.go (1)
  • MakeEducationHandler (16-20)
handler/recommendations.go (1)
  • MakeRecommendationsHandler (16-20)
🪛 dotenv-linter (3.3.0)
.env.example

[warning] 6-6: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 6-6: [UnorderedKey] The ENV_APP_LOGS_DATE_FORMAT key should go before the ENV_APP_LOGS_DIR key

(UnorderedKey)

🔇 Additional comments (12)
metal/cli/seo/support.go (1)

22-24: Bug: json.Unmarshal uses &response (double pointer).

metal/router/static.go (1)

1-31: LGTM! Clean interface and struct design.

The StaticRouteResource interface and WebsiteRoutes struct are well-designed with clear responsibilities. The constructor properly initializes all fields from the environment.

metal/cli/seo/jsonld.go (1)

137-141: Security: remove enc.SetEscapeHTML(false)

metal/cli/seo/generator.go (2)

97-106: LGTM! Data struct is correctly populated after fetches.

The data struct is properly constructed after the Fetch calls with the populated responses. This ensures the template receives the loaded data correctly.


118-125: LGTM! OutputDir is correctly treated as a directory.

The code properly creates the OutputDir directory and writes index.html within it, addressing the previous concern about treating the directory path correctly.

metal/cli/accounts/handler.go (3)

18-22: Persisting APIKey using repository looks good

Storing encrypted keys via APIKeyAttr is correct.


48-50: Capture and return h.print error; avoid stale err and redundant decode

You’re returning a possibly stale err and decoding twice. Remove the pre-decode and return the print error directly.

-	token, err := h.TokenHandler.DecodeTokensFor(
-		item.AccountName,
-		item.SecretKey,
-		item.PublicKey,
-	)
-
-	if h.print(token, item) != nil {
-		return fmt.Errorf("could not decode the given account [%s] keys: %v", item.AccountName, err)
-	}
+	if perr := h.print(nil, item); perr != nil {
+		return perr
+	}

Also applies to: 42-47


35-35: Method renamed to ShowAccount — verify & update call sites

I ran a repo search for "ReadAccount(" and found no matches; confirm there are no remaining references (including receiver calls, method expressions, generated or vendor files).

metal/cli/seo/manifest.go (1)

41-48: Good guard against out-of-range favicon access

Using the first favicon with a safe fallback fixes the panic risk. LGTM.

metal/router/router.go (3)

17-22: Ensure non-nil Router deps at construction

WebsiteRoutes, Env, Validator, Mux, Pipeline, Db must be set; otherwise handlers will panic at runtime.

Would you like a simple constructor that validates these and returns an error if any are nil?


35-39: Token middleware now env-aware: LGTM

Passing r.Env into MakeTokenMiddleware aligns with the env-aware bypasses.


70-74: Signatures handler uses public Validator: LGTM

This matches the Router field change and handler.MakeSignaturesHandler signature.

@gocanto gocanto marked this pull request as draft September 24, 2025 04:31
@gocanto gocanto marked this pull request as ready for review September 24, 2025 08:51
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (9)
metal/cli/seo/data.go (3)

21-21: Relax overly strict min lengths to avoid false negatives.

Typical site names (e.g., “Oullin”) will fail min=10, causing unnecessary build failures for Title/ImageAlt.

-	Title          string          `validate:"required,min=10"`
+	Title          string          `validate:"required,min=3"`
...
-	ImageAlt    string `validate:"required,min=10"`
+	ImageAlt    string `validate:"required,min=5"`
...
-	ImageAlt string `validate:"required,min=10"`
+	ImageAlt string `validate:"required,min=5"`

Also applies to: 41-41, 51-51


9-9: Validate each SameAsURL element, not just the slice.

Ensure every entry is a valid URI.

-	SameAsURL     []string           `validate:"required"`
+	SameAsURL     []string           `validate:"required,dive,uri"`

14-14: Name consistency: use URL acronym casing.

Consider renaming AboutPhotoUrlAboutPhotoURL for consistency with other fields (LogoURL, SiteURL).

Note: This is API‑breaking; if you want, I can generate a follow‑up PR with the refactor and call‑site updates across the repo.

metal/cli/main.go (1)

64-69: Preflight-check required env vars before SEO generation.

Fail fast if ENV_APP_URL or ENV_SPA_DIR is unset; current flow will error later via validation with a less actionable message.

Apply this diff inside generateSEO():

 func generateSEO() error {
-	gen, err := seo.NewGenerator(
+	// Preflight checks for required env values
+	if environment.App.URL == "" {
+		return fmt.Errorf("ENV_APP_URL is not set")
+	}
+	if environment.Seo.SpaDir == "" {
+		return fmt.Errorf("ENV_SPA_DIR is not set")
+	}
+
+	gen, err := seo.NewGenerator(
 		dbConn,
 		environment,
 		portal.GetDefaultValidator(),
 	)

Also ensure your .env(.example) has sane defaults for both values.

metal/cli/seo/client.go (4)

67-75: Avoid shadowing the imported handler package in fetch.

Parameter name handler shadows the imported package name; rename for clarity.

-func fetch[T any](response *T, handler func() router.StaticRouteResource) error {
+func fetch[T any](response *T, factory func() router.StaticRouteResource) error {
 	req := httptest.NewRequest("GET", "http://localhost:8080/proxy", nil)
 	rr := httptest.NewRecorder()

-	maker := handler()
+	maker := factory()

33-34: Unify error message (remove stray "home:" prefix).

Keep error prefixes consistent across methods.

-		return nil, fmt.Errorf("home: error fetching talks: %w", err)
+		return nil, fmt.Errorf("error fetching talks: %w", err)

13-22: Drop unused WebsiteRoutes from Client to reduce coupling.

Field isn’t used; keep only what you need.

 type Client struct {
-	WebsiteRoutes *router.WebsiteRoutes
 	Fixture       router.Fixture
 }

 func NewClient(routes *router.WebsiteRoutes) *Client {
 	return &Client{
-		WebsiteRoutes: routes,
 		Fixture:       routes.Fixture,
 	}
 }

73-79: Check HTTP status code before unmarshalling.

If a handler writes a non‑2xx status without returning an error, Unmarshal may mislead. Guard on status code.

 	if err := maker.Handle(rr, req); err != nil {
 		return err
 	}

+	if rr.Code < 200 || rr.Code > 299 {
+		return fmt.Errorf("unexpected status code %d: %s", rr.Code, rr.Body.String())
+	}
+
 	if err := json.Unmarshal(rr.Body.Bytes(), response); err != nil {
 		return err
 	}
metal/cli/seo/generator.go (1)

195-203: Consider standard favicon type.

Many UAs expect image/x-icon for .ico. Optional tweak.

-				Type:  "image/ico",
+				Type:  "image/x-icon",
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0452d7f and c152b8a.

📒 Files selected for processing (6)
  • metal/cli/main.go (3 hunks)
  • metal/cli/seo/client.go (1 hunks)
  • metal/cli/seo/data.go (1 hunks)
  • metal/cli/seo/generator.go (1 hunks)
  • metal/cli/seo/stub.html (1 hunks)
  • metal/cli/seo/support.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • metal/cli/seo/stub.html
  • metal/cli/seo/support.go
🧰 Additional context used
🧬 Code graph analysis (4)
metal/cli/seo/client.go (8)
metal/router/static.go (2)
  • WebsiteRoutes (14-20)
  • StaticRouteResource (10-12)
metal/router/fixture.go (1)
  • Fixture (15-20)
handler/payload/talks.go (1)
  • TalksResponse (3-6)
handler/talks.go (1)
  • MakeTalksHandler (16-20)
handler/payload/profile.go (1)
  • ProfileResponse (3-6)
handler/profile.go (1)
  • MakeProfileHandler (16-20)
handler/payload/projects.go (1)
  • ProjectsResponse (3-6)
handler/projects.go (1)
  • MakeProjectsHandler (16-20)
metal/cli/main.go (3)
pkg/cli/message.go (2)
  • Errorln (9-11)
  • Error (5-7)
metal/cli/seo/generator.go (1)
  • NewGenerator (32-67)
pkg/portal/validator.go (1)
  • GetDefaultValidator (23-33)
metal/cli/seo/data.go (2)
metal/cli/seo/defaults.go (5)
  • AboutPhotoUrl (9-9)
  • StubPath (28-28)
  • Description (31-31)
  • Robots (30-30)
  • ThemeColor (29-29)
metal/cli/seo/manifest.go (1)
  • Manifest (10-23)
metal/cli/seo/generator.go (14)
metal/cli/seo/data.go (6)
  • Page (5-17)
  • TemplateData (19-36)
  • TagOgData (38-46)
  • TwitterData (48-52)
  • HrefLangData (54-57)
  • FaviconData (59-64)
metal/cli/seo/client.go (2)
  • Client (13-16)
  • NewClient (18-23)
metal/env/env.go (1)
  • Environment (9-17)
pkg/portal/validator.go (1)
  • Validator (13-16)
database/connection.go (1)
  • Connection (12-17)
metal/router/static.go (2)
  • WebsiteRoutes (14-20)
  • NewWebsiteRoutes (22-30)
metal/cli/seo/defaults.go (9)
  • LogoUrl (8-8)
  • StubPath (28-28)
  • RepoWebUrl (7-7)
  • RepoApiUrl (6-6)
  • AboutPhotoUrl (9-9)
  • GocantoUrl (5-5)
  • Robots (30-30)
  • ThemeColor (29-29)
  • Description (31-31)
handler/payload/talks.go (1)
  • TalksResponse (3-6)
handler/payload/profile.go (1)
  • ProfileResponse (3-6)
handler/payload/projects.go (1)
  • ProjectsResponse (3-6)
pkg/cli/message.go (1)
  • Successln (17-19)
metal/cli/seo/jsonld.go (1)
  • NewJsonID (27-41)
metal/cli/seo/manifest.go (2)
  • Manifest (10-23)
  • NewManifest (40-91)
metal/cli/seo/support.go (1)
  • ManifestDataURL (8-13)
🔇 Additional comments (4)
metal/cli/main.go (2)

140-140: Rename ripple is correct.

ShowAccount call site is updated and aligns with the handler refactor.


165-194: LGTM on the timestamp utility.

Self-contained and harmless; good as a CLI helper.

metal/cli/seo/generator.go (2)

150-158: Correct handling of OutputDir (write index.html inside).

Good fix: you now create the directory and write to <OutputDir>/index.html, addressing the earlier issue.


208-218: Validation sequence looks good (duplication removed).

Only one validation for twitter remains; the duplicate check seen earlier is gone.

@gocanto gocanto marked this pull request as draft September 25, 2025 02:25
@gocanto gocanto marked this pull request as ready for review September 25, 2025 03:21
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
metal/cli/seo/generator.go (1)

111-116: Consider logging the export target for easier troubleshooting.

After Export succeeds, log the absolute output path (or URL) so operators immediately know where the generated file was written. That small addition pays for itself when multiple pages are emitted.

metal/cli/seo/data.go (1)

6-16: Add dive to slice validations.

All slice fields (SameAsURL, HrefLang, Favicons, Categories, Body) currently only check that the slice itself isn’t empty. Add dive so each element is validated too; otherwise malformed URLs or empty strings slip through unseen.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c152b8a and 1fee021.

📒 Files selected for processing (3)
  • metal/cli/seo/data.go (1 hunks)
  • metal/cli/seo/generator.go (1 hunks)
  • metal/cli/seo/sections.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
metal/cli/seo/data.go (4)
metal/cli/seo/defaults.go (5)
  • AboutPhotoUrl (9-9)
  • StubPath (28-28)
  • Description (31-31)
  • Robots (30-30)
  • ThemeColor (29-29)
metal/cli/seo/manifest.go (1)
  • Manifest (10-23)
database/repository/categories.go (1)
  • Categories (12-14)
pkg/markdown/schema.go (2)
  • Title (11-19)
  • FrontMatter (21-26)
metal/cli/seo/generator.go (14)
metal/cli/seo/data.go (6)
  • Page (5-17)
  • TemplateData (19-36)
  • TagOgData (38-46)
  • TwitterData (48-52)
  • HrefLangData (54-57)
  • FaviconData (59-64)
metal/cli/seo/client.go (2)
  • Client (13-16)
  • NewClient (18-23)
metal/env/env.go (1)
  • Environment (9-17)
pkg/portal/validator.go (1)
  • Validator (13-16)
metal/router/static.go (2)
  • WebsiteRoutes (14-20)
  • NewWebsiteRoutes (22-30)
metal/cli/seo/defaults.go (9)
  • LogoUrl (8-8)
  • StubPath (28-28)
  • RepoWebUrl (7-7)
  • RepoApiUrl (6-6)
  • AboutPhotoUrl (9-9)
  • GocantoUrl (5-5)
  • Robots (30-30)
  • ThemeColor (29-29)
  • Description (31-31)
handler/payload/talks.go (1)
  • TalksResponse (3-6)
handler/payload/profile.go (1)
  • ProfileResponse (3-6)
handler/payload/projects.go (1)
  • ProjectsResponse (3-6)
metal/cli/seo/sections.go (1)
  • NewSections (12-14)
pkg/cli/message.go (1)
  • Successln (17-19)
metal/cli/seo/jsonld.go (1)
  • NewJsonID (27-41)
metal/cli/seo/manifest.go (2)
  • Manifest (10-23)
  • NewManifest (40-91)
metal/cli/seo/support.go (1)
  • ManifestDataURL (8-13)
metal/cli/seo/sections.go (3)
handler/payload/profile.go (1)
  • ProfileResponse (3-6)
handler/payload/talks.go (1)
  • TalksResponse (3-6)
handler/payload/projects.go (1)
  • ProjectsResponse (3-6)

@gocanto
Copy link
Collaborator Author

gocanto commented Sep 25, 2025

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Sep 25, 2025

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (11)
metal/makefile/build.mk (1)

10-13: Use $(MAKE) for recursive invocations.

Hard-coding make drops jobserver flags and MAKEFLAGS propagation; splitting the commands also removes the brittle && chaining.

-build-fresh:
-	make fresh && \
-	make db:fresh && \
-	make db:migrate && \
-	make db:seed
+build-fresh:
+	$(MAKE) fresh
+	$(MAKE) db:fresh
+	$(MAKE) db:migrate
+	$(MAKE) db:seed
pkg/middleware/pipeline.go (1)

13-13: Use Go’s deprecation convention for TokenHandler field

Adopt a “Deprecated:” doc comment to surface in docs/linters.

Apply:

-	TokenHandler     *auth.TokenHandler //@todo Remove!
+	// Deprecated: remove after migrating token flow off Pipeline. Use token middleware wiring via env.
+	TokenHandler     *auth.TokenHandler
metal/makefile/db.mk (1)

59-61: Nice: compose now pulls from .env; consider making the path configurable

Optional: promote ./.env to a variable (e.g., ENV_FILE ?= ./.env) used across make targets to avoid drift.

+ENV_FILE ?= ./.env
 ...
-	docker compose --env-file ./.env run --rm $(DB_MIGRATE_DOCKER_ENV_FLAGS) $(DB_API_RUNNER_SERVICE) \
+	docker compose --env-file $(ENV_FILE) run --rm $(DB_MIGRATE_DOCKER_ENV_FLAGS) $(DB_API_RUNNER_SERVICE) \
  		 go run ./database/seeder/main.go
metal/kernel/helpers.go (1)

11-12: Consider accepting a pointer to avoid an extra copy.

SetRouter ultimately stores a *router.Router, so taking a pointer parameter would skip the intermediate copy and signal the expected ownership a bit more clearly.

metal/kernel/app.go (3)

35-39: Avoid copying Router; keep and use the pointer

You copy the router value into App and later copy it again in Boot. Use the pointer throughout to avoid unintended state divergence and reduce allocations.

Apply:

- if modem, err := app.NewRouter(); err != nil {
+ if modem, err := app.NewRouter(); err != nil {
    return nil, err
- } else {
-   app.SetRouter(*modem)
- }
+ } else {
+   app.router = modem
+ }

81-100: Call router methods on the pointer; don’t copy before use

Copying the Router struct before invoking methods is unnecessary and can be risky if it contains mutable state.

Apply:

 func (a *App) Boot() {
   if a == nil || a.router == nil {
     panic("kernel error > boot: Invalid setup")
   }
-
-  modem := *a.router
-
-  modem.KeepAlive()
-  modem.KeepAliveDB()
-  modem.Profile()
-  modem.Experience()
-  modem.Projects()
-  modem.Social()
-  modem.Talks()
-  modem.Education()
-  modem.Recommendations()
-  modem.Posts()
-  modem.Categories()
-  modem.Signature()
+  a.router.KeepAlive()
+  a.router.KeepAliveDB()
+  a.router.Profile()
+  a.router.Experience()
+  a.router.Projects()
+  a.router.Social()
+  a.router.Talks()
+  a.router.Education()
+  a.router.Recommendations()
+  a.router.Posts()
+  a.router.Categories()
+  a.router.Signature()
 }

44-76: *Change SetRouter signature to accept router.Router
SetRouter is still used in kernel_test.go (lines 125, 174) and in app.go (line 38); for API stability, update it to

func (a *App) SetRouter(r *router.Router) {
    a.router = r
}
metal/env/env.go (1)

27-42: Remove redundant fallback branch in GetSecretOrEnv

Current code returns the env var for any read error; the last return duplicates the NotExist branch. Simplify without changing behavior.

Apply:

 func GetSecretOrEnv(secretName string, envVarName string) string {
   secretPath := filepath.Join(SecretsDir, secretName)

   // Try to read the secret file first.
   content, err := os.ReadFile(secretPath)
   if err == nil {
     return strings.TrimSpace(string(content))
   }

-  // If the file does not exist, fall back to the environment variable.
-  if os.IsNotExist(err) {
-    return GetEnvVar(envVarName) // Use your existing function here
-  }
-
-  return GetEnvVar(envVarName)
+  // Fall back to the environment variable for any error.
+  return GetEnvVar(envVarName)
 }
metal/cli/seo/defaults.go (1)

5-10: Static asset defaults OK; consider future configurability

Constants are fine for now. Later, allow overrides via env or config to avoid rebuilding for changes.

metal/cli/seo/generator.go (2)

141-200: Ensure SiteName meets min-length validation or adjust schema; externalize categories

  • TemplateData.Title, TagOgData.ImageAlt and TwitterData.ImageAlt use g.Page.SiteName and require min=10; short names (<10 chars) will fail validation. Either lower the min constraint or generate a longer Title/ImageAlt.
  • data.Categories is hardcoded ([]string{"one","two"}); replace the TODO by fetching categories from configuration or an API.

31-66: Approve: Page validation covers SiteURL (required,uri) and OutputDir (required)
val.Rejects(page) enforces non-empty & valid URI for ENV_APP_URL and non-empty ENV_SPA_DIR; directory writability must be ensured at deploy or via an explicit runtime check.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b0211a and 1fee021.

📒 Files selected for processing (44)
  • .env.example (2 hunks)
  • .env.gh.example (0 hunks)
  • .env.prod.example (0 hunks)
  • .gitignore (1 hunks)
  • Makefile (2 hunks)
  • docker-compose.yml (1 hunks)
  • handler/keep_alive.go (1 hunks)
  • handler/keep_alive_db.go (1 hunks)
  • handler/keep_alive_db_test.go (1 hunks)
  • handler/keep_alive_test.go (1 hunks)
  • metal/cli/accounts/handler.go (4 hunks)
  • metal/cli/accounts/handler_test.go (2 hunks)
  • metal/cli/main.go (3 hunks)
  • metal/cli/panel/menu.go (1 hunks)
  • metal/cli/seo/client.go (1 hunks)
  • metal/cli/seo/data.go (1 hunks)
  • metal/cli/seo/defaults.go (1 hunks)
  • metal/cli/seo/generator.go (1 hunks)
  • metal/cli/seo/jsonld.go (1 hunks)
  • metal/cli/seo/manifest.go (1 hunks)
  • metal/cli/seo/sections.go (1 hunks)
  • metal/cli/seo/stub.html (1 hunks)
  • metal/cli/seo/support.go (1 hunks)
  • metal/env/app.go (2 hunks)
  • metal/env/env.go (1 hunks)
  • metal/env/ping.go (1 hunks)
  • metal/env/seo.go (1 hunks)
  • metal/kernel/app.go (2 hunks)
  • metal/kernel/factory.go (4 hunks)
  • metal/kernel/helpers.go (1 hunks)
  • metal/kernel/kernel_test.go (3 hunks)
  • metal/makefile/app.mk (2 hunks)
  • metal/makefile/build.mk (1 hunks)
  • metal/makefile/db.mk (2 hunks)
  • metal/router/fixture.go (1 hunks)
  • metal/router/router.go (5 hunks)
  • metal/router/router_keep_alive_db_test.go (2 hunks)
  • metal/router/router_keep_alive_test.go (2 hunks)
  • metal/router/router_signature_test.go (2 hunks)
  • metal/router/static.go (1 hunks)
  • pkg/middleware/mwguards/valid_timestamp_guard.go (1 hunks)
  • pkg/middleware/pipeline.go (1 hunks)
  • pkg/middleware/public_middleware.go (1 hunks)
  • pkg/middleware/token_middleware.go (3 hunks)
💤 Files with no reviewable changes (2)
  • .env.gh.example
  • .env.prod.example
🧰 Additional context used
🪛 dotenv-linter (3.3.0)
.env.example

[warning] 6-6: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 6-6: [UnorderedKey] The ENV_APP_LOGS_DATE_FORMAT key should go before the ENV_APP_LOGS_DIR key

(UnorderedKey)

🔇 Additional comments (39)
pkg/middleware/mwguards/valid_timestamp_guard.go (1)

51-57: Double-check new error constructors return 401.

The prior implementation explicitly set Status: http.StatusUnauthorized when the timestamp was out of range. Please confirm TimestampTooOldError / TimestampTooNewError preserve that status; if they default to another code, callers will regress. If they need the unauthorized status, update the constructors or override it here.

metal/cli/accounts/handler_test.go (1)

28-30: Update the failure message to match ShowAccount.

The method was renamed, but the failure string still says “read”.

-	if err := h.ShowAccount("tester"); err != nil {
-		t.Fatalf("read: %v", err)
+	if err := h.ShowAccount("tester"); err != nil {
+		t.Fatalf("show: %v", err)
metal/cli/seo/data.go (2)

21-21: Loosen the title/image-alt min-length to accept the default config.

The generator still feeds TemplateData.Title and TagOgData.ImageAlt with Page.SiteName (“Oullin”), so min=10 keeps rejecting the stock configuration. Please relax or drop the 10-character floor so validation passes out of the box.

-	Title          string          `validate:"required,min=10"`
+	Title          string          `validate:"required,min=5"`
@@
-	ImageAlt    string `validate:"required,min=10"`
+	ImageAlt    string `validate:"required,min=5"`

Also applies to: 41-41


55-55: Allow both base and regional hreflang codes.

Generator.Build() still emits both en and en_GB links, so restricting the validator to en_GB makes the build fail whenever the base language entry is present. Please accept the base code (and common hyphen variant) in the tag.

-	Lang string `validate:"required,oneof=en_GB"`
+	Lang string `validate:"required,oneof=en en_GB en-GB"`
metal/cli/seo/jsonld.go (1)

137-138: Restore json.Encoder’s HTML escaping on JSON-LD output.

Disabling SetEscapeHTML while returning template.JS reopens the </script> injection vector if any value comes from configuration. Let encoding/json escape HTML characters.

-	enc := json.NewEncoder(&buf)
-	enc.SetEscapeHTML(false)
+	enc := json.NewEncoder(&buf)
metal/kernel/kernel_test.go (2)

12-12: Import switch to router package: OK


123-124: Ensure tests set public middleware headers when bypass is disabled
PublicMiddleware in production (isProduction=true) rejects requests missing portal.RequestIDHeader and portal.TimestampHeader; tests exercising these routes must call

req.Header.Set(portal.RequestIDHeader, "<id>")
req.Header.Set(portal.TimestampHeader, "<timestamp>")
metal/cli/panel/menu.go (1)

92-94: Option handling for choices 4 and 5 is correctly mapped
case 4 calls generateSEO() and case 5 calls printTimestamp(), matching the menu text.

metal/router/router_keep_alive_db_test.go (1)

16-16: No env.Ping references found; please manually verify that all old Ping usages are removed.

metal/router/router_keep_alive_test.go (1)

14-14: PingEnvironment rename fully migrated
No remaining env.Ping references found.

metal/makefile/db.mk (1)

18-18: No seeder/migrations reference ENV_PING_USERNAME or ENV_PING_PASSWORD – verified by a repo-wide search; safe to remove these vars from db.mk.

handler/keep_alive_test.go (1)

16-18: Fixture reflects renamed environment type.

Using env.PingEnvironment here keeps the test aligned with the handler’s dependency after the rename.

metal/cli/seo/support.go (1)

8-13: Helper cleanly packages manifest data.

The base64 wrapping plus template.URL casting gives the template a safe, self-contained manifest URL.

.env.example (2)

6-8: App log format and URL placeholders look good.

Thanks for documenting the updated date format and the new ENV_APP_URL requirement directly in the example file.


37-38: SPA directory placeholder captured.

Adding ENV_SPA_DIR here makes the new SEO configuration discoverable for ops.

metal/env/ping.go (1)

5-21: Rename propagates cleanly.

The PingEnvironment type and its helpers mirror the old behavior while making the intent explicit.

Makefile (1)

57-60: CLI targets nicely expanded.

The new run-cli-docker and run-metal options make local workflows clearer in the help output.

handler/keep_alive.go (1)

15-20: Handler signature matches environment rename.

The struct field and constructor now consistently depend on *env.PingEnvironment, which keeps the handler wiring straightforward.

metal/router/router_signature_test.go (1)

1-22: Test update tracks router API changes.

Switching to the router package and the exported Validator field keeps this test up-to-date with the public surface.

metal/makefile/app.mk (1)

56-63: Local CLI shortcut looks good

Line 56 wraps the secret paths so we don’t have to export them manually; thanks for the ergonomic target.

metal/cli/seo/stub.html (1)

1-53: Meta coverage is solid

Lines 1-53 cover the essential meta tags (canonical, hreflang, OG/Twitter) the generator needs; LGTM.

pkg/middleware/token_middleware.go (1)

55-57: Thanks for guarding local mode

Line 55’s t.env != nil check removes the panic risk while preserving the local bypass—good catch.

handler/keep_alive_db.go (1)

16-22: PingEnvironment wiring matches the renames

Lines 16-22 align the DB keep-alive handler with the new env.PingEnvironment type; looks consistent.

metal/router/static.go (1)

22-29: WebsiteRoutes initializer reads clearly

Lines 22-29 cleanly plumb the environment through to the fixture bundle; LGTM.

metal/cli/seo/client.go (1)

67-82: Generic fetch helper is tidy

Lines 67-82 keep the fixture fetch logic DRY across payload types—looks good.

metal/kernel/factory.go (1)

63-144: Environment wiring covers the new SEO fields nicely

Lines 63-144 consistently populate and validate the new SEO block; all good.

metal/cli/main.go (1)

147-191: Still missing ENV_SPA_DIR/ENV_APP_URL validation before SEO generation

Lines 147-163 still call into the generator without verifying that ENV_SPA_DIR exists/is writable or that ENV_APP_URL is populated. In empty .env setups (including .env.example), environment.Seo.SpaDir is "", so gen.Generate() fails later with a cryptic filesystem error instead of a clear message. The previous review already requested these checks; we still need to guard and prepare the output directory up front.

Please validate the env vars and create the directory (or fail fast) before instantiating the generator. Suggested fix:

diff --git a/metal/cli/main.go b/metal/cli/main.go
@@
-import (
-	"fmt"
-	"time"
+import (
+	"fmt"
+	"os"
+	"strings"
+	"time"
@@
 func generateSEO() error {
+	if environment == nil {
+		return fmt.Errorf("environment not initialized")
+	}
+
+	spaDir := strings.TrimSpace(environment.Seo.SpaDir)
+	if spaDir == "" {
+		return fmt.Errorf("ENV_SPA_DIR is required")
+	}
+	if err := os.MkdirAll(spaDir, 0o755); err != nil {
+		return fmt.Errorf("unable to prepare SPA dir %q: %w", spaDir, err)
+	}
+
+	if strings.TrimSpace(environment.App.URL) == "" {
+		return fmt.Errorf("ENV_APP_URL is required")
+	}
+
 	gen, err := seo.NewGenerator(
 		dbConn,
 		environment,
 		portal.GetDefaultValidator(),
 	)
metal/cli/seo/sections.go (1)

16-23: Fix the string ↔︎ template.HTML concatenation bug.

Sections.Profile currently tries to add a raw string to template.HTML, which doesn’t compile. Build the markup as a plain string (with the escaped fields) and only then cast to template.HTML.

-func (s *Sections) Profile(profile *payload.ProfileResponse) template.HTML {
-	return "<h1>Profile</h1>" +
-		template.HTML("<p"+
-			template.HTMLEscapeString(profile.Data.Name)+", "+
-			template.HTMLEscapeString(profile.Data.Profession)+
-			"</p>",
-		)
-}
+func (s *Sections) Profile(profile *payload.ProfileResponse) template.HTML {
+	var builder strings.Builder
+	builder.WriteString("<h1>Profile</h1>")
+	builder.WriteString("<p>")
+	builder.WriteString(template.HTMLEscapeString(profile.Data.Name))
+	builder.WriteString(", ")
+	builder.WriteString(template.HTMLEscapeString(profile.Data.Profession))
+	builder.WriteString("</p>")
+
+	return template.HTML(builder.String())
+}
metal/cli/accounts/handler.go (1)

48-50: Propagate the actual print error in ShowAccount.

h.print may fail for reasons unrelated to the earlier decode, but the code drops that error and reuses the stale err (which can be nil). Capture h.print’s return value so we surface the real failure.

-	if h.print(token, item) != nil {
-		return fmt.Errorf("could not decode the given account [%s] keys: %v", item.AccountName, err)
-	}
+	if perr := h.print(token, item); perr != nil {
+		return fmt.Errorf("could not decode the given account [%s] keys: %v", item.AccountName, perr)
+	}
metal/router/router.go (1)

181-185: Serve fixture endpoints via the public pipeline
These profile/experience/projects/etc. fixtures are meant to be unauthenticated, but wiring them through PipelineFor still attaches the token middleware and keeps 401’ing public requests—the same issue flagged earlier. Switch this to the public pipeline so the SPA (and crawlers/SEO) can reach the JSON without auth.

-	resolver := r.PipelineFor(abstract.Handle)
+	resolver := r.PublicPipelineFor(abstract.Handle)
metal/env/seo.go (1)

3-5: LGTM: minimal env type with required field

SeoEnvironment is simple and correct; required tag is appropriate.

handler/keep_alive_db_test.go (1)

18-19: LGTM: updated to PingEnvironment

The test correctly uses env.PingEnvironment and constructs the handler with a pointer.

metal/env/env.go (1)

9-17: Stronger validation posture looks good

Marking all sub-environments as required (including Seo and PingEnvironment) will surface misconfiguration early.

metal/cli/seo/generator.go (3)

96-103: Section assembly is safe and readable

Switching to sections.* returning template.HTML keeps type consistency and avoids the previous []template.HTML append issue.


120-139: Correctly treat OutputDir as a directory and write a file inside it

MkdirAll on OutputDir and writing origin.seo.html resolves the earlier path misuse.


202-221: Template loading via embed.FS with func map is solid

Good error wrapping; ManifestDataURL export is available to templates.

metal/cli/seo/defaults.go (1)

27-32: Meta defaults look reasonable

Theme color, robots, and description are sensible defaults.

metal/cli/seo/manifest.go (2)

40-49: Good: safe icons fallback and no out-of-range indexing

Selecting index 0 with a fallback to LogoURL avoids panics when only one favicon exists.


93-129: Manifest JSON rendering is robust

HTML escaping disabled (correct for JSON), with compaction and sensible fallback paths. Returning template.JS is appropriate for embedding.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
database/repository/categories.go (1)

20-24: Add explicit ordering for deterministic SEO exports

The results currently depend on the database’s default ordering, so the list can shuffle unexpectedly between runs. Given this powers SEO/static generation, we should return a deterministic sequence.

 	err := c.DB.Sql().
 		Model(&database.Category{}).
+		Order("categories.name ASC").
 		Where("categories.deleted_at is null").
 		Find(&categories).Error
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1fee021 and c424c96.

📒 Files selected for processing (5)
  • database/repository/categories.go (1 hunks)
  • metal/cli/seo/categories.go (1 hunks)
  • metal/cli/seo/data.go (1 hunks)
  • metal/cli/seo/generator.go (1 hunks)
  • metal/cli/seo/sections.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • metal/cli/seo/sections.go
  • metal/cli/seo/data.go
🧰 Additional context used
🧬 Code graph analysis (3)
metal/cli/seo/categories.go (3)
database/connection.go (1)
  • Connection (12-17)
database/repository/categories.go (1)
  • Categories (13-15)
database/model.go (1)
  • Category (106-118)
metal/cli/seo/generator.go (12)
metal/cli/seo/data.go (6)
  • Page (5-18)
  • TemplateData (20-37)
  • TagOgData (39-47)
  • TwitterData (49-53)
  • HrefLangData (55-58)
  • FaviconData (60-65)
metal/cli/seo/client.go (2)
  • Client (13-16)
  • NewClient (18-23)
metal/env/env.go (1)
  • Environment (9-17)
pkg/portal/validator.go (1)
  • Validator (13-16)
database/connection.go (1)
  • Connection (12-17)
metal/router/static.go (2)
  • WebsiteRoutes (14-20)
  • NewWebsiteRoutes (22-30)
metal/cli/seo/categories.go (1)
  • NewCategories (16-21)
metal/cli/seo/defaults.go (9)
  • LogoUrl (8-8)
  • StubPath (28-28)
  • RepoWebUrl (7-7)
  • RepoApiUrl (6-6)
  • AboutPhotoUrl (9-9)
  • GocantoUrl (5-5)
  • Robots (30-30)
  • ThemeColor (29-29)
  • Description (31-31)
metal/cli/seo/sections.go (1)
  • NewSections (12-14)
metal/cli/seo/jsonld.go (1)
  • NewJsonID (27-41)
metal/cli/seo/manifest.go (2)
  • Manifest (10-23)
  • NewManifest (40-91)
metal/cli/seo/support.go (1)
  • ManifestDataURL (8-13)
database/repository/categories.go (2)
database/connection.go (1)
  • Connection (12-17)
database/model.go (1)
  • Category (106-118)
🔇 Additional comments (1)
metal/cli/seo/generator.go (1)

168-206: Generator fails validation when App.Name is shorter than 10 chars
Title, og.ImageAlt, and twitter.ImageAlt are all populated with g.Page.SiteName, but TemplateData, TagOgData, and TwitterData each impose min=10. In our default config (config/environments/base.yaml) App.Name is "Oullin" (6 chars), so Build always returns invalid template data and GenerateHome never writes a file. Please either relax the validator constraints or ensure we populate these fields with strings that satisfy the minimum length.

 	og := TagOgData{
 		ImageWidth:  "600",
 		ImageHeight: "400",
 		Type:        "website",
 		Locale:      g.Page.Lang,
-		ImageAlt:    g.Page.SiteName,
+		ImageAlt:    fmt.Sprintf("%s portfolio", g.Page.SiteName),
 		SiteName:    g.Page.SiteName,
 		Image:       g.Page.AboutPhotoUrl,
 	}
@@
 	twitter := TwitterData{
 		Card:     "summary_large_image",
 		Image:    g.Page.AboutPhotoUrl,
-		ImageAlt: g.Page.SiteName,
+		ImageAlt: fmt.Sprintf("%s portfolio", g.Page.SiteName),
 	}
@@
 		AppleTouchIcon: g.Page.LogoURL,
-		Title:          g.Page.SiteName,
+		Title:          fmt.Sprintf("%s | Software Engineering Leadership", g.Page.SiteName),

@gocanto gocanto marked this pull request as draft September 25, 2025 04:24
@gocanto gocanto marked this pull request as ready for review September 25, 2025 04:28
@gocanto gocanto changed the title Add SEO [feature]: Add SEO Foundation Sep 25, 2025
@gocanto gocanto changed the title [feature]: Add SEO Foundation [feature]: Add SEO engine Sep 25, 2025
@gocanto gocanto merged commit 7e727dc into main Sep 25, 2025
1 of 2 checks passed
@gocanto gocanto deleted the feature/add-seo branch September 25, 2025 04:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants