Skip to content

feat(landing): Astro marketing site with blog, RSS, i18n, SEO#158

Merged
emilwojtaszek merged 8 commits into
mainfrom
feat/landing-page
Apr 21, 2026
Merged

feat(landing): Astro marketing site with blog, RSS, i18n, SEO#158
emilwojtaszek merged 8 commits into
mainfrom
feat/landing-page

Conversation

@emilwojtaszek

@emilwojtaszek emilwojtaszek commented Apr 20, 2026

Copy link
Copy Markdown
Member

Summary

Adds the KSeF Hub public landing site as a standalone Astro 5 + Tailwind v4 project at landing/, deployed to GitHub Pages independently of the Phoenix app.

  • Polish-default i18n (/ is PL, /en/ is EN), language switcher preserves the current path on toggle
  • Content collection for the blog (src/content/blog/{locale}/{slug}.md) + one seed PL post on the 2026 obligation + per-locale RSS feeds with auto-discovery
  • FAQ accordion with FAQPage schema, plus SoftwareApplication / Organization / BlogPosting JSON-LD across the surface
  • Full SEO meta: hreflang alternates, locale-aware sitemap, OG + Twitter cards, preconnect font loading, apple-touch-icon, theme-color (light/dark)
  • Design tokens ported one-time from assets/css/app.css; two theme-independent tokens added for always-dark code surfaces
  • Quality gates wired into CI: i18n parity check (custom script), astro check, and lychee link check, all required before the Pages artifact uploads
  • Dependabot tracks landing npm deps weekly (grouped: astro, tailwind, dev-tooling) plus GitHub Actions monthly
  • make landing.* targets for discoverability; workflow isolation via paths-ignore: ['landing/**'] on ci.yml

Architecture decisions and trade-offs are documented in docs/adr/0048-public-landing-page.md.

One-time manual step after merge

  • Settings → Pages → Source: GitHub Actions — required once per repo before the first deploy lands. The workflow will error visibly until this is set.

Test plan

  • cd landing && npm ci succeeds on a fresh checkout (Apple Silicon Macs may need npm install --no-save @rolldown/binding-darwin-arm64 — documented in README)
  • make landing.check passes (i18n parity + typecheck)
  • make landing.build produces 7 pages (home PL/EN, blog index PL/EN, 1 PL post, 2 RSS endpoints)
  • npm run dev renders / and /en/ correctly; language switcher flips both and survives on /blog/
  • PR workflow run shows: i18n ✓, check ✓, build ✓, lychee ✓
  • After merge + Pages settings flip: main workflow deploys to https://appunite.github.io/appunite-ksef-ex/, /en/ alternate renders, /blog/ lists the seed post, /blog/rss.xml returns valid RSS 2.0
  • Elixir ci.yml is not triggered by landing-only commits (path-filtered)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Public landing site launched with localized pages (PL/EN), homepage sections (hero, features, ledger, API, pricing, FAQ, closing), language switcher, SEO/meta handling, blog with per-locale posts and RSS, and interactive API docs.
  • Documentation

    • New onboarding docs, landing-specific README/CLAUDE, ADR describing conventions, i18n rules, content schema, and local dev/build flow.
  • Chores

    • Path-filtered CI and GitHub Pages workflow, Dependabot setup, Makefile targets, Node tool pinning, i18n parity check and OpenAPI fetch scripts.

Ships the KSeF Hub public marketing site as a separate Astro 5 +
Tailwind v4 project in landing/, deployed to GitHub Pages by a
path-filtered workflow. Zero runtime coupling with the Phoenix app.

Polish is the default locale (EN at /en/), content collection + one
seed post for the blog, per-locale RSS feeds with auto-discovery,
FAQPage / SoftwareApplication / Organization / BlogPosting JSON-LD,
and a full SEO surface (hreflang alternates, locale-aware sitemap,
OG + Twitter meta, preconnect font loading).

CI gates: i18n parity check, astro check, and lychee link check all
run before the Pages artifact uploads. Dependabot tracks landing npm
deps weekly and GitHub Actions monthly.

See docs/adr/0048-public-landing-page.md for the architecture
decision and trade-offs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a standalone Astro 5 + Tailwind v4 + TypeScript landing site under landing/ (i18n: pl default, en prefix), content collection/blog/RSS/sitemap, many localized components/layouts/styles, build/dev scripts, GitHub Actions for Pages, Dependabot config, Makefile targets, and CI path filters to ignore landing-only changes.

Changes

Cohort / File(s) Summary
CI / Dependabot
.github/workflows/ci.yml, .github/workflows/landing.yml, .github/dependabot.yml
New landing workflow: build, link-check, artifact upload, Pages deploy; main CI now ignores landing/**; Dependabot configured for npm (weekly) and Actions (monthly) with grouping, labels, commit message prefixes, and open PR limits.
Monorepo docs & ADR
README.md, CLAUDE.md, docs/adr/0048-public-landing-page.md, docs/architecture.md
Documentation and ADR added/updated to describe the standalone landing project, conventions, i18n rules, deployment flow, and isolation decisions.
Makefile targets
Makefile
Added .PHONY entries and thin targets: landing.install, landing.dev, landing.build, landing.preview, landing.check.
Landing project config
landing/package.json, landing/astro.config.mjs, landing/tsconfig.json, landing/.tool-versions, landing/.gitignore
New Astro project config, scripts, deps, strict TS config, Node pinned to 24.3.0, and gitignore for build/artifacts.
Build & tooling scripts
landing/scripts/fetch-openapi.mjs, landing/scripts/check-i18n.mjs
Added OpenAPI fetcher with cache-fallback and i18n parity checker that exits non‑zero on mismatch.
i18n system & data
landing/src/i18n/index.ts, landing/src/i18n/en.json, landing/src/i18n/pl.json, landing/src/i18n/README.md
Typed locale utilities (detection, URL helpers, localized asset helpers) and full en/pl dictionaries plus guidance.
Content & collections
landing/src/content.config.ts, landing/src/content/blog/pl/obowiazek-ksef-2026.md
Defined blog collection with Zod schema and added a Polish blog post.
Pages, routing & RSS
landing/src/pages/*, landing/src/pages/en/*, landing/src/pages/blog/*
Added Base layout, homepage pages (pl/en), blog indices, per-post pages with getStaticPaths, and locale-aware RSS endpoints.
Components
landing/src/components/*
Added ~20+ localized UI components (Hero, Features, LedgerPreview, SEO with typed props, Nav, Footer, LangSwitcher, and many primitives).
Layouts & styles
landing/src/layouts/Base.astro, landing/src/styles/global.css, landing/src/styles/tokens.css
Base layout with SEO/i18n integration; comprehensive global styles and design tokens with dark-mode handling and Tailwind component layer.
Config constants & public assets
landing/src/config.ts, landing/public/robots.txt
Exported repo/contact constants and robots.txt; OpenAPI output produced by fetch script at build time.
New content-heavy pages
landing/src/pages/blog/[...slug].astro, landing/src/pages/en/blog/[...slug].astro
Dynamic blog post pages with exported getStaticPaths() and locale-aware rendering (higher logic density).
Core app minor change
lib/ksef_hub_web/api_spec.ex
Updated OpenAPI Info.title from "Invoi API" to "KSeF Hub API".

Sequence Diagram(s)

sequenceDiagram
    participant GH as "GitHub Event"
    participant GHA as "GitHub Actions (landing.yml)"
    participant Runner as "CI Runner"
    participant Node as "Node / npm (pinned)"
    participant Fetch as "fetch-openapi.mjs"
    participant i18nCheck as "check-i18n.mjs"
    participant Build as "Astro build"
    participant LinkCheck as "lychee (link-check)"
    participant Pages as "GitHub Pages"

    GH->>GHA: push / pull_request touching landing/**
    GHA->>Runner: start build job (checkout, setup-node)
    Runner->>Node: npm ci (cache keyed by landing/package-lock.json)
    Runner->>Fetch: run prebuild fetch-openapi.mjs
    Fetch-->>Runner: write landing/public/openapi.json (or warn and reuse cached)
    Runner->>i18nCheck: run test:i18n (exits non-zero on mismatch)
    Runner->>Node: npm run check (typecheck)
    Runner->>Build: npm run build -> produces landing/dist
    Runner->>LinkCheck: run lychee against landing/dist/**/*.html
    Runner->>GHA: upload landing/dist artifact
    alt push to main
        GHA->>Pages: deploy artifact via actions/deploy-pages
        Pages-->>GH: publish complete (pages URL)
    else PR or non-main
        GHA-->>GH: artifact uploaded (no deploy)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped in with Node and Tailwind in my pack,

Fetched the spec, checked locales, then ran the link attack.
Built the pages, uploaded art — Pages waved from afar,
A tiny rabbit cheered: "Landing live! Hip-hop hoorah!" 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a standalone Astro marketing site with blog, RSS, and internationalization (i18n) support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/landing-page

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 19

🧹 Nitpick comments (20)
Makefile (1)

132-143: Minor: models.train target appears after the Landing section.

The models.train target belongs to the "ML Models" section but is now positioned after the "Landing" section, breaking the logical grouping. Consider moving it above the # --- Landing --- comment block for better organization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 132 - 143, The Makefile target models.train is
misplaced after the "# --- Landing ---" section, breaking logical grouping; move
the entire models.train target block so it appears within the "ML Models"
section (i.e., above the "# --- Landing ---" comment), preserving its content
and spacing so the target remains unchanged but is correctly grouped with other
ML Models targets.
landing/src/components/Features.astro (1)

41-41: Consider applying text-wrap: balance to the H2.

Same as other section components—if section-h2 doesn't include text-wrap: balance, consider adding it for consistent heading rendering. As per coding guidelines: "Apply text-wrap: balance to H1/H2 elements."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/components/Features.astro` at line 41, The H2 element using class
"section-h2" (rendering {t.featuresSection.heading}) needs CSS to apply
text-wrap: balance for consistent heading rendering; update the "section-h2"
rule (or add a utility) to include text-wrap: balance (or add an inline
style/class that applies it) so H1/H2 headings follow the project's guideline
for balanced wrapping.
landing/src/components/HeroDiagram.astro (2)

4-6: Hardcoded strings bypass i18n dictionary.

The diagramTitle uses an inline ternary instead of pulling from the i18n JSON files. This violates the coding guideline requiring all user-visible strings to go through src/i18n/{en,pl}.json.

♻️ Suggested refactor

Add to en.json and pl.json:

"hero": {
  "diagramTitle": "Invoice sources flow into one ledger, then out to consumers."
}

Then in the component:

 import { getLocaleFromUrl } from "../i18n";
+import { useTranslations } from "../i18n";
 const locale = getLocaleFromUrl(Astro.url);
-const diagramTitle = locale === "pl"
-  ? "Źródła faktur przepływają do jednego rejestru, a stamtąd do konsumentów."
-  : "Invoice sources flow into one ledger, then out to consumers.";
+const t = useTranslations(locale);
+const diagramTitle = t.hero.diagramTitle;

As per coding guidelines: "all user-visible strings must go through src/i18n/{en,pl}.json via i18n, never hardcoded"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/components/HeroDiagram.astro` around lines 4 - 6, The
diagramTitle variable in the HeroDiagram component is hardcoded with a locale
ternary; update it to use the i18n dictionary instead: add a "hero.diagramTitle"
entry to both src/i18n/en.json and src/i18n/pl.json, then replace the ternary in
HeroDiagram.astro so it reads the string via the existing i18n lookup/helper
(use the same i18n function/context used elsewhere in this component) rather
than the inline locale check, keeping the key name "hero.diagramTitle" to match
the JSON entries.

17-75: SVG text labels are hardcoded English.

The SVG contains multiple hardcoded text strings (e.g., "KSeF", "inbound email", "PDF + OCR", "ledger", "REST API") that won't be translated for Polish users. Consider whether these technical labels should remain English (as technical IDs) or be localized.

If these labels should be localized, extract them to the i18n dictionaries and pass them as props or use useTranslations. If they're intentionally kept as technical/universal terms, add a comment explaining the decision.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/components/HeroDiagram.astro` around lines 17 - 75, The SVG
hardcodes English labels inside the HeroDiagram component (strings like "KSeF",
"inbound email", "PDF + OCR", "foreign / manual", "ledger", "accountant export",
"REST API", "payment transfers", "BI / dashboards"); update HeroDiagram to
either (A) pull these labels from the i18n layer (use useTranslations or accept
them as props) and replace the literal text nodes with the translated values, or
(B) if they are intentionally technical/universal, add a clear code comment at
the top of the HeroDiagram component documenting that these labels are
intentionally left in English and should not be localized. Ensure all
occurrences listed above are handled consistently.
landing/src/components/FAQ.astro (1)

24-24: Add text-wrap: balance to the H2 element.

Per coding guidelines, H1/H2 elements should have text-wrap: balance applied.

♻️ Suggested fix
-      <h2 class="section-h2">{t.faq.heading}</h2>
+      <h2 class="section-h2" style="text-wrap: balance;">{t.faq.heading}</h2>

Alternatively, add text-wrap: balance to the .section-h2 class in your global styles if it should apply to all section headings.

As per coding guidelines: "Apply text-wrap: balance to H1/H2 elements"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/components/FAQ.astro` at line 24, H2 heading lacks the required
text-wrap setting; add text-wrap: balance to the H2 by either updating the
element <h2 class="section-h2">{t.faq.heading}</h2> to include the style or
(preferable) add text-wrap: balance to the .section-h2 rule in your global
stylesheet so all section headings receive the balance behavior.
landing/src/pages/en/api.astro (1)

21-24: Consider adding Subresource Integrity (SRI) hash for CDN script.

Loading external JavaScript from a CDN without an SRI hash means a CDN compromise could inject malicious code into your pages.

🛡️ Suggested improvement
     <script
       src="https://cdn.jsdelivr.net/npm/@scalar/api-reference@1"
+      integrity="sha384-<hash>"
+      crossorigin="anonymous"
       is:inline
     ></script>

Generate the hash using:

curl -s https://cdn.jsdelivr.net/npm/@scalar/api-reference@1 | openssl dgst -sha384 -binary | openssl base64 -A
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/pages/en/api.astro` around lines 21 - 24, The CDN script tag that
loads "https://cdn.jsdelivr.net/npm/@scalar/api-reference@1" should include a
Subresource Integrity (SRI) attribute and crossorigin to protect against CDN
tampering; generate the SHA-384 base64 hash for that exact URL (e.g. using the
provided curl | openssl command), then add integrity="sha384-<computed-hash>"
and crossorigin="anonymous" to the <script
src="https://cdn.jsdelivr.net/npm/@scalar/api-reference@1"> element so the
browser verifies the file before executing it.
landing/scripts/check-i18n.mjs (1)

23-24: Consider deriving locales from the file system.

The locales array is hardcoded, requiring manual updates when adding new locales. This could drift out of sync with src/i18n/index.ts.

♻️ Optional improvement
+import { readdirSync } from "node:fs";
+
 const here = dirname(fileURLToPath(import.meta.url));
 const dictDir = resolve(here, "../src/i18n");

 const base = "en";
-const locales = ["pl"];
+const locales = readdirSync(dictDir)
+  .filter((f) => f.endsWith(".json") && f !== `${base}.json`)
+  .map((f) => f.replace(".json", ""));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/scripts/check-i18n.mjs` around lines 23 - 24, Replace the hardcoded
locales array with dynamic discovery: in the check-i18n script, read the i18n
locale directories at runtime (use fs.readdirSync/Promises to list entries,
filter for directories and skip the base locale and any dot-files), sort the
resulting names and assign them to the locales variable instead of the hardcoded
["pl"]; ensure the base variable remains as-is and add a clear error if no
locales are found.
landing/src/components/Pricing.astro (1)

25-25: Add text-wrap: balance to the H2 element.

Per coding guidelines, H1/H2 elements should have text-wrap: balance applied.

♻️ Suggested fix
-      <h2 class="section-h2 mb-4">{t.pricing.heading}</h2>
+      <h2 class="section-h2 mb-4" style="text-wrap: balance;">{t.pricing.heading}</h2>

As per coding guidelines: "Apply text-wrap: balance to H1/H2 elements"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/components/Pricing.astro` at line 25, The H2 element in
Pricing.astro (<h2 class="section-h2 mb-4">{t.pricing.heading}</h2>) needs the
text-wrap: balance rule applied; update the element to include the balancing
style (either by adding a utility class that applies text-wrap: balance or by
adding the style via the existing section-h2 class) so that all H1/H2 headings
follow the coding guideline for balanced wrapping.
landing/src/pages/api.astro (1)

28-33: Consider using a CSS variable for the nav height.

The 3.5rem value in calc(100vh - 3.5rem) is a magic number coupled to the Nav component's height. If the Nav height changes, this calculation will silently break.

♻️ Suggested improvement

Define a --nav-height token in tokens.css and reference it here:

  main#scalar-root {
-   min-height: calc(100vh - 3.5rem);
+   min-height: calc(100vh - var(--nav-height, 3.5rem));
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/pages/api.astro` around lines 28 - 33, Replace the hard-coded
3.5rem magic number in the main#scalar-root style with a CSS variable so the
layout stays in sync with the Nav component; add a --nav-height token into
tokens.css (or your global token file) and update the rule for main#scalar-root
to use calc(100vh - var(--nav-height)); ensure the Nav component sets/uses the
same --nav-height value so both places reference the single source of truth.
landing/src/components/LangSwitcher.astro (1)

20-26: Consider using aria-current="page" instead of aria-current="true".

For language navigation marking the current page's language, aria-current="page" is more semantically appropriate than aria-current="true". The page value explicitly indicates the element represents the current page within a set of pages.

♻️ Suggested fix
       <span
         class="px-1.5 py-0.5 font-medium text-[var(--foreground)]"
-        aria-current="true"
+        aria-current="page"
       >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/components/LangSwitcher.astro` around lines 20 - 26, In
LangSwitcher.astro update the span rendered when loc === current to use
aria-current="page" instead of aria-current="true" so the current language is
semantically marked as the current page; find the conditional that renders the
span (the block using loc === current and localeLabels[loc]) and replace the
aria-current value accordingly.
landing/src/components/LedgerPreview.astro (2)

63-68: Duplicate hardcoded colors in legend array.

The same guideline violation exists here. These colors should reference the same CSS custom properties as the rows data.

♻️ Suggested fix
 const legend = [
   { label: t.ledger.sources.ksef, color: "var(--brand)" },
-  { label: t.ledger.sources.email, color: "#6b9bff" },
-  { label: t.ledger.sources.pdf, color: "#f59e0b" },
-  { label: t.ledger.sources.api, color: "#a78bfa" },
+  { label: t.ledger.sources.email, color: "var(--source-email)" },
+  { label: t.ledger.sources.pdf, color: "var(--source-pdf)" },
+  { label: t.ledger.sources.api, color: "var(--source-api)" },
 ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/components/LedgerPreview.astro` around lines 63 - 68, The legend
array in LedgerPreview.astro is using hardcoded hex colors; update the legend
constant (legend) so each entry uses the same CSS custom properties as the rows
data (e.g., replace "#6b9bff", "#f59e0b", "#a78bfa" and "var(--brand)" usage
with the corresponding --custom-property names used by the rows rendering) so
colors come from CSS variables instead of literals; locate the legend definition
and swap each color value to the matching CSS custom property name (e.g.,
var(--your-email-color), var(--your-pdf-color), var(--your-api-color),
var(--brand)) to keep styles consistent.

28-29: Hardcoded hex colors violate the design token constraint.

The colors #6b9bff, #f59e0b, and #a78bfa are not defined in tokens.css. As per coding guidelines, you should never invent new colors beyond those defined in src/styles/tokens.css.

Either add these source-indicator colors to tokens.css as semantic tokens (e.g., --source-email, --source-pdf, --source-api) or map them to existing tokens.

♻️ Suggested approach

Add to src/styles/tokens.css:

--source-email: `#6b9bff`;
--source-pdf: `#f59e0b`;
--source-api: `#a78bfa`;

Then update the component:

-    sourceColor: "#6b9bff",
+    sourceColor: "var(--source-email)",

As per coding guidelines: Use CSS custom properties for all colors, type scale, spacing, and radii; never invent new colors beyond those defined in src/styles/tokens.css.

Also applies to: 38-39, 58-59

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/components/LedgerPreview.astro` around lines 28 - 29,
LedgerPreview.astro currently uses hardcoded hex colors for sourceColor and
statusColor (e.g., "#6b9bff", "#f59e0b", "#a78bfa") which violates the design
token rule; update by adding semantic CSS custom properties to
src/styles/tokens.css (for example --source-email, --source-pdf, --source-api
and appropriate --status- tokens) and replace the hardcoded values in
LedgerPreview.astro (references: sourceColor, statusColor) to use those custom
properties (e.g., var(--source-email), var(--status-warning)) or map them to
existing tokens to remove any literal hex values.
landing/CLAUDE.md (1)

24-46: Add language specifier to fenced code block.

The directory structure code block is missing a language specifier, which triggers markdownlint MD040.

♻️ Suggested fix
-```
+```text
 landing/
 ├── public/                     # served verbatim (robots.txt, favicon.svg)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/CLAUDE.md` around lines 24 - 46, The fenced code block in
landing/CLAUDE.md (the directory structure block starting with triple backticks)
lacks a language specifier; update the opening fence to include a language like
"text" (e.g., change ``` to ```text) so markdownlint MD040 is satisfied and the
directory tree renders as plain text.
landing/scripts/fetch-openapi.mjs (1)

28-35: Consider adding a timeout to the fetch call.

The fetch call has no timeout, which could cause the build to hang indefinitely if the upstream server is unresponsive (e.g., accepts connection but never sends a response).

♻️ Suggested improvement
 try {
-  const res = await fetch(SPEC_URL);
+  const res = await fetch(SPEC_URL, { signal: AbortSignal.timeout(30_000) });
   if (!res.ok) {
     throw new Error(`HTTP ${res.status} ${res.statusText}`);
   }

AbortSignal.timeout() is available in Node 18+ and provides a clean 30-second timeout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/scripts/fetch-openapi.mjs` around lines 28 - 35, The fetch call to
SPEC_URL can hang because it has no timeout; update the code around the existing
fetch usage (the fetch call in the try block) to create an AbortSignal with a
30s timeout (e.g., using AbortSignal.timeout(30000)) and pass that signal to
fetch(SPEC_URL, { signal }); also handle AbortError in the catch path so you log
a clear timeout error and exit non‑zero (same pattern used for other fetch
failures), keeping references to SPEC_URL, fetch, writeFile and OUT intact.
landing/src/components/Hero.astro (1)

19-19: Add text-wrap: balance to the H1 element.

The coding guidelines specify that H1/H2 elements should use text-wrap: balance. The paragraph below correctly uses text-wrap: pretty, but the headline is missing this style.

Suggested fix
-        <h1 class="hero-h1 mb-6">{t.hero.titleLine1}<br />{t.hero.titleLine2}</h1>
+        <h1 class="hero-h1 mb-6" style="text-wrap: balance;">{t.hero.titleLine1}<br />{t.hero.titleLine2}</h1>

As per coding guidelines: "Apply text-wrap: balance to H1/H2 elements and text-wrap: pretty to long paragraphs in Astro components".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/components/Hero.astro` at line 19, The H1 element (class
"hero-h1") is missing the required text-wrap rule; update the <h1
class="hero-h1"> element in Hero.astro to apply text-wrap: balance (e.g., add a
style or utility that sets text-wrap: balance) so H1/H2 follow the coding
guideline for balanced wrapping, keeping the paragraph's existing text-wrap:
pretty unchanged.
landing/src/pages/en/blog/index.astro (1)

40-40: Add text-wrap: balance to the H1 element.

Suggested fix
-        <h1 class="section-h2 mb-4">{t.blog.heading}</h1>
+        <h1 class="section-h2 mb-4" style="text-wrap: balance;">{t.blog.heading}</h1>

As per coding guidelines: "Apply text-wrap: balance to H1/H2 elements".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/pages/en/blog/index.astro` at line 40, The H1 element rendering
{t.blog.heading} (h1 with class "section-h2 mb-4") needs the text-wrap: balance
rule applied; update the element to include the class or inline style that
enables text-wrap: balance (or add a utility class like "text-wrap-balance") so
the H1 obeys the coding guideline for balanced wrapping.
landing/README.md (1)

25-53: Consider adding a language identifier to the fenced code block.

The directory structure block could use text or plaintext as a language identifier to satisfy markdown linting rules, though this is purely cosmetic.

Suggested fix
-```
+```text
 src/
 ├── assets/                         # SVGs imported by components
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/README.md` around lines 25 - 53, The fenced directory tree block in
README.md lacks a language tag; update the opening fence ("```") to include a
plain text language identifier (e.g., change to "```text" or "```plaintext") so
the code block becomes fenced with a language for markdown linting and syntax
highlighting; locate the directory tree block starting with "src/" in the README
and modify its opening fence accordingly.
landing/src/pages/blog/[...slug].astro (1)

93-93: Consider using Person type for individual authors in JSON-LD.

The author field uses Organization type, but the default author is "KSeF Hub" which could be either. If individual human authors are expected in the future, Person would be more semantically accurate. This is a minor schema.org consideration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/pages/blog/`[...slug].astro at line 93, The JSON-LD currently
hardcodes the author as an Organization using the object with "@type":
"Organization" and name: post.data.author; change this to use "@type": "Person"
for individual authors (or make it conditional) so human authors are
semantically correct: inspect post.data.author (and any site default like "KSeF
Hub") and output {"@type":"Person", "name": post.data.author} for individual
names, or retain {"@type":"Organization", "name": ...} only when the author is
genuinely an organization.
landing/src/pages/blog/index.astro (1)

40-40: Add text-wrap: balance to the H1 element.

Suggested fix
-        <h1 class="section-h2 mb-4">{t.blog.heading}</h1>
+        <h1 class="section-h2 mb-4" style="text-wrap: balance;">{t.blog.heading}</h1>

As per coding guidelines: "Apply text-wrap: balance to H1/H2 elements".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/pages/blog/index.astro` at line 40, The H1 rendering
{t.blog.heading} lacks the required text-wrap: balance styling; update the <h1
class="section-h2 mb-4"> (or its CSS rule for .section-h2) to include text-wrap:
balance—either add the inline style attribute or add the property to the
.section-h2 rule in your stylesheet so H1/H2 elements follow the coding
guideline.
landing/src/i18n/index.ts (1)

70-73: Move locale switcher labels to dictionaries instead of hardcoding in TS.

PL/EN are rendered to users via LangSwitcher, so they should come from i18n JSON.

As per coding guidelines: "landing/**/*.{ts,tsx,astro}: all user-visible strings must go through src/i18n/{en,pl}.json via i18n, never hardcoded".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/i18n/index.ts` around lines 70 - 73, The localeLabels constant is
hardcoded and must be replaced with strings pulled from the i18n JSONs used by
LangSwitcher; update the implementation that defines localeLabels to read the
translated labels from your translation dictionaries (src/i18n/en.json and
src/i18n/pl.json) instead of literal "PL"/"EN", and adjust LangSwitcher to use
the i18n key (e.g., a new "lang.*" keys) via the existing translation accessor
(your t/i18n loader) so that user-visible labels come from the JSON files rather
than hardcoded TS.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/adr/0048-public-landing-page.md`:
- Around line 1-123: Add ADR-0048 to the architecture index and map the new
landing feature: open docs/architecture.md and add an entry for "0048. Public
landing page as standalone Astro project" linking to
docs/adr/0048-public-landing-page.md and add a feature mapping row for the
landing/ folder (describe it as "landing: Astro 5 + Tailwind v4 static marketing
site, i18n, blog, RSS, sitemap, GitHub Pages deploy"). After updating the file,
run the repository's sync command (/update-architecture) to regenerate any
derived indexes and CI metadata so the ADR and landing feature are visible to
tooling and docs consumers.

In `@landing/CLAUDE.md`:
- Around line 31-32: Update the documentation line in CLAUDE.md that currently
says "Polish (translation pending; currently == en.json)" to reflect that
pl.json now contains actual Polish translations (e.g., change to "Polish
(translated)" or similar), referencing the files en.json and pl.json and
ensuring the comment accurately describes pl.json's current state.

In `@landing/public/robots.txt`:
- Line 4: robots.txt currently hardcodes the sitemap URL ("Sitemap:
https://appunite.github.io/appunite-ksef-ex/sitemap-index.xml"); update it to
derive the base URL from the site config instead of hardcoding: reference the
deployment URL export in landing/src/config.ts (e.g., SITE_URL or PUBLIC_URL)
and either (A) replace the static line with a build-time step that generates
robots.txt using that config value (add a script in the build pipeline that
writes "Sitemap: ${SITE_URL}/sitemap-index.xml"), or (B) at minimum add a
comment above the Sitemap line pointing to landing/src/config.ts and document
the manual update requirement in your deployment ADR; modify the build scripts
or CI job that runs site build to emit the generated robots.txt so it reflects
custom domains.

In `@landing/src/components/ApiSection.astro`:
- Line 13: The API section paragraph in ApiSection.astro (the <p
class="...">{t.api.body}</p>) needs the "text-wrap: pretty" applied; update that
<p> in ApiSection.astro to include the pretty wrap (either add style="text-wrap:
pretty" or a utility/class like "text-wrap-pretty" and ensure the CSS utility is
defined) so the long paragraph uses pretty wrapping per the guideline.
- Around line 25-43: The preformatted curl/code sample in the ApiSection
component is hardcoded; move it into the i18n dictionaries and replace the
literal block with a lookup. Add a new key (e.g. api.codeSample or structured
keys like api.sample.curl and api.sample.response) to src/i18n/en.json and
src/i18n/pl.json, ensure the string preserves newlines/escaping, then update
ApiSection.astro to render that value via the i18n helper (e.g. t.api.codeSample
or t.api.sample.curl / t.api.sample.response) instead of embedding the
<pre><code> HTML directly so all visible text comes from i18n.

In `@landing/src/components/ClosingCTA.astro`:
- Line 12: The CTA description paragraph in ClosingCTA.astro (the <p> rendering
{t.closing.body}) needs explicit pretty wrapping; update that <p> (the element
with class "text-lg text-[var(--muted-foreground)] mb-8") to apply text-wrap:
pretty—either add a style attribute style="text-wrap: pretty" or a CSS class
that sets text-wrap: pretty (and update your stylesheet) so the long paragraph
copy uses the pretty wrap rule as per guidelines.

In `@landing/src/components/Nav.astro`:
- Line 16: The brand label "KSeF Hub" in Nav.astro is hardcoded inside the span
with class "text-sm font-bold tracking-tight"; add a key (e.g. "brand.name") to
the i18n JSON dictionaries for supported locales and replace the hardcoded text
with a translation lookup in the Nav.astro component (use the existing i18n
helper/import used across the project) so the span renders the localized string
instead of the literal "KSeF Hub".
- Around line 19-22: The nav links use hash-only hrefs (e.g., the <a> elements
rendering {t.nav.features}, {t.nav.ledger}, {t.nav.api}, {t.nav.selfHost}) which
break when not on the homepage; update each href to prepend the site's home base
(use the existing home variable or prop) so links become `${home}#features`,
`${home}#ledger`, `${home}#api`, and `${home}#self-host` respectively, ensuring
the link construction happens where the <a> tags are generated in Nav.astro and
preserving the displayed text values (t.nav.*).

In `@landing/src/components/OpenSource.astro`:
- Line 20: The long Open Source paragraphs (the <p> that renders {t.os.body} and
the other paragraph in this component) need explicit pretty wrapping; update
both <p> elements to apply text-wrap: pretty (for example add style="text-wrap:
pretty;" or an equivalent utility class like text-[text-wrap:pretty]) so the
long copy uses pretty wrapping while preserving the existing classes
(text-[var(--muted-foreground)] text-lg leading-relaxed mt-5).

In `@landing/src/components/SEO.astro`:
- Around line 37-42: The alternates array (hreflang entries) and xDefault are
built using localizedUrl(locale) which currently returns locale home pages, so
update them to include the current route/path when generating locale
equivalents: call localizedUrl with both the locale and the current page path
(or canonical pathname) so alternates = locales.map(loc => ({ hreflang: loc,
href: `${site}${localizedUrl(loc, currentPath)}`})) and set xDefault =
`${site}${localizedUrl(defaultLocale, currentPath)}`; ensure you obtain
currentPath from the page’s canonical/path variable (e.g.,
Astro.request.url.pathname or the page’s canonical prop) and use the same
approach in the alternates and xDefault construction.
- Around line 56-115: The SEO.astro component contains hardcoded user-visible
strings ("KSeF Hub Blog", "KSeF Hub", "BusinessApplication", etc.); replace
those literals with i18n keys and lookups (e.g., t('seo.siteName'),
t('seo.blogTitle'), t('seo.applicationCategory')) wherever they appear (meta
tags, link titles, og/site_name, offers.applicationCategory, alt texts). Load
the existing i18n helper used across landing (the same import pattern used in
other .astro files) and add the corresponding keys and translations to
src/i18n/en.json and src/i18n/pl.json (and update any defaultLocale usage like
localizedUrl/defaultLocale if needed) so all user-facing strings in SEO.astro
come from the i18n dictionaries instead of being hardcoded.

In `@landing/src/components/TrustStrip.astro`:
- Line 34: In TrustStrip.astro update the div that renders the long paragraph
({item.copy.body}) to apply the paragraph wrapping utility by adding the
text-wrap: pretty class/utility (e.g., add a class like "text-wrap-pretty" or
the framework-specific "text-wrap: pretty") to the div that currently contains
{item.copy.body} so long copy uses pretty wrapping for readability.

In `@landing/src/i18n/index.ts`:
- Around line 10-13: The dicts object uses a type assertion "pl as Dictionary"
which disables structural checks; replace the assertion by applying TypeScript's
`satisfies` operator so the object is checked against Record<Locale, Dictionary>
at compile time. Concretely, remove the `as Dictionary` on `pl` and declare
`const dicts = { en, pl } satisfies Record<Locale, Dictionary>;` (keeping the
existing Locale/Dictionary types and the `pl` and `en` imports) so any shape
mismatch in `pl` is caught by the compiler.

In `@landing/src/pages/blog/`[...slug].astro:
- Around line 8-17: Add a new dynamic page mirroring the Polish route: create
landing/src/pages/en/blog/[...slug].astro with a getStaticPaths implementation
that calls getCollection("blog", ({ data }) => data.locale === "en" &&
!data.draft) and returns posts.map(post => ({ params: { slug:
post.id.split("/").slice(1).join("/") }, props: { post } })); also duplicate the
page rendering logic from the Polish page (e.g., using the same post prop
handling and date formatting used in the Polish file) so individual English
posts under /en/blog/[slug] are rendered with the correct locale filter and
formatted dates.

In `@landing/src/pages/en/blog/index.astro`:
- Around line 55-66: The EN blog index is missing the tags rendering that the PL
version includes; update the English template around the post block (where
slugOf(post.id), localizedUrl(locale, `blog/${slugOf(post.id)}`), and
post.data.title/description are used) to render post.data.tags the same way as
in the PL version: iterate post.data.tags (if present and non-empty) and output
each tag as a pill element with the same classes/structure used in the PL file
so the tags appear beneath the description and match styling and behavior across
locales.

In `@landing/src/styles/global.css`:
- Around line 25-173: Many classes in this stylesheet use hardcoded pixel values
instead of design tokens; update the classes (e.g., .container-page, .hero-h1,
.section-h2, .btn-primary, .btn-ghost, .nav-link, .card-flat, .kbd, .feat-icon,
.tag-pill, .code-block) to use the project's CSS custom property tokens for type
scale, spacing, heights, gaps, and radii (replace font-size,
padding-/padding-inline, height, gap, border-radius, letter-spacing values with
the appropriate --token names from src/styles/tokens.css), and swap any color
literals with defined color tokens; ensure you use the established token names
(spacing, type, radius, color) consistently and remove any new hardcoded px
values so the components inherit the design system.
- Around line 175-179: Replace the hardcoded hex colors in the .code-block token
classes (.code-block .k, .code-block .s, .code-block .n, .code-block .c,
.code-block .p) with CSS custom properties defined in
landing/src/styles/tokens.css; add the corresponding color variables to
tokens.css (e.g., --color-code-k, --color-code-s, --color-code-n,
--color-code-c, --color-code-p) and update the rules in global.css to use
var(--color-...) so all code token colors come from the tokens source of truth.

In `@landing/src/styles/tokens.css`:
- Around line 12-13: Update the Geist-loading comment in tokens.css to
accurately state the actual font-loading strategy: replace the existing note
that says the Geist font `@import` is in global.css with a brief comment that the
Geist font is loaded via a <link> in Base.astro and documented in global.css
(i.e., link-based loading), referencing the Geist font, tokens.css, global.css
and Base.astro so readers know where to find the actual loader.
- Around line 18-21: The font-family declarations for the CSS custom properties
--font-sans and --font-mono contain unquoted font names (e.g., Roboto, Arial,
SFMono-Regular, Menlo, Monaco, Consolas) that trigger stylelint's
value-keyword-case rule; update the values in landing/src/styles/tokens.css so
every non-generic family name is wrapped in quotes (e.g., "Roboto", "Arial",
"SFMono-Regular", "Menlo", "Monaco", "Consolas") while leaving generic families
(sans-serif, monospace) unquoted, ensuring the strings remain in the same order
within --font-sans and --font-mono.

---

Nitpick comments:
In `@landing/CLAUDE.md`:
- Around line 24-46: The fenced code block in landing/CLAUDE.md (the directory
structure block starting with triple backticks) lacks a language specifier;
update the opening fence to include a language like "text" (e.g., change ``` to
```text) so markdownlint MD040 is satisfied and the directory tree renders as
plain text.

In `@landing/README.md`:
- Around line 25-53: The fenced directory tree block in README.md lacks a
language tag; update the opening fence ("```") to include a plain text language
identifier (e.g., change to "```text" or "```plaintext") so the code block
becomes fenced with a language for markdown linting and syntax highlighting;
locate the directory tree block starting with "src/" in the README and modify
its opening fence accordingly.

In `@landing/scripts/check-i18n.mjs`:
- Around line 23-24: Replace the hardcoded locales array with dynamic discovery:
in the check-i18n script, read the i18n locale directories at runtime (use
fs.readdirSync/Promises to list entries, filter for directories and skip the
base locale and any dot-files), sort the resulting names and assign them to the
locales variable instead of the hardcoded ["pl"]; ensure the base variable
remains as-is and add a clear error if no locales are found.

In `@landing/scripts/fetch-openapi.mjs`:
- Around line 28-35: The fetch call to SPEC_URL can hang because it has no
timeout; update the code around the existing fetch usage (the fetch call in the
try block) to create an AbortSignal with a 30s timeout (e.g., using
AbortSignal.timeout(30000)) and pass that signal to fetch(SPEC_URL, { signal });
also handle AbortError in the catch path so you log a clear timeout error and
exit non‑zero (same pattern used for other fetch failures), keeping references
to SPEC_URL, fetch, writeFile and OUT intact.

In `@landing/src/components/FAQ.astro`:
- Line 24: H2 heading lacks the required text-wrap setting; add text-wrap:
balance to the H2 by either updating the element <h2
class="section-h2">{t.faq.heading}</h2> to include the style or (preferable) add
text-wrap: balance to the .section-h2 rule in your global stylesheet so all
section headings receive the balance behavior.

In `@landing/src/components/Features.astro`:
- Line 41: The H2 element using class "section-h2" (rendering
{t.featuresSection.heading}) needs CSS to apply text-wrap: balance for
consistent heading rendering; update the "section-h2" rule (or add a utility) to
include text-wrap: balance (or add an inline style/class that applies it) so
H1/H2 headings follow the project's guideline for balanced wrapping.

In `@landing/src/components/Hero.astro`:
- Line 19: The H1 element (class "hero-h1") is missing the required text-wrap
rule; update the <h1 class="hero-h1"> element in Hero.astro to apply text-wrap:
balance (e.g., add a style or utility that sets text-wrap: balance) so H1/H2
follow the coding guideline for balanced wrapping, keeping the paragraph's
existing text-wrap: pretty unchanged.

In `@landing/src/components/HeroDiagram.astro`:
- Around line 4-6: The diagramTitle variable in the HeroDiagram component is
hardcoded with a locale ternary; update it to use the i18n dictionary instead:
add a "hero.diagramTitle" entry to both src/i18n/en.json and src/i18n/pl.json,
then replace the ternary in HeroDiagram.astro so it reads the string via the
existing i18n lookup/helper (use the same i18n function/context used elsewhere
in this component) rather than the inline locale check, keeping the key name
"hero.diagramTitle" to match the JSON entries.
- Around line 17-75: The SVG hardcodes English labels inside the HeroDiagram
component (strings like "KSeF", "inbound email", "PDF + OCR", "foreign /
manual", "ledger", "accountant export", "REST API", "payment transfers", "BI /
dashboards"); update HeroDiagram to either (A) pull these labels from the i18n
layer (use useTranslations or accept them as props) and replace the literal text
nodes with the translated values, or (B) if they are intentionally
technical/universal, add a clear code comment at the top of the HeroDiagram
component documenting that these labels are intentionally left in English and
should not be localized. Ensure all occurrences listed above are handled
consistently.

In `@landing/src/components/LangSwitcher.astro`:
- Around line 20-26: In LangSwitcher.astro update the span rendered when loc ===
current to use aria-current="page" instead of aria-current="true" so the current
language is semantically marked as the current page; find the conditional that
renders the span (the block using loc === current and localeLabels[loc]) and
replace the aria-current value accordingly.

In `@landing/src/components/LedgerPreview.astro`:
- Around line 63-68: The legend array in LedgerPreview.astro is using hardcoded
hex colors; update the legend constant (legend) so each entry uses the same CSS
custom properties as the rows data (e.g., replace "#6b9bff", "#f59e0b",
"#a78bfa" and "var(--brand)" usage with the corresponding --custom-property
names used by the rows rendering) so colors come from CSS variables instead of
literals; locate the legend definition and swap each color value to the matching
CSS custom property name (e.g., var(--your-email-color), var(--your-pdf-color),
var(--your-api-color), var(--brand)) to keep styles consistent.
- Around line 28-29: LedgerPreview.astro currently uses hardcoded hex colors for
sourceColor and statusColor (e.g., "#6b9bff", "#f59e0b", "#a78bfa") which
violates the design token rule; update by adding semantic CSS custom properties
to src/styles/tokens.css (for example --source-email, --source-pdf, --source-api
and appropriate --status- tokens) and replace the hardcoded values in
LedgerPreview.astro (references: sourceColor, statusColor) to use those custom
properties (e.g., var(--source-email), var(--status-warning)) or map them to
existing tokens to remove any literal hex values.

In `@landing/src/components/Pricing.astro`:
- Line 25: The H2 element in Pricing.astro (<h2 class="section-h2
mb-4">{t.pricing.heading}</h2>) needs the text-wrap: balance rule applied;
update the element to include the balancing style (either by adding a utility
class that applies text-wrap: balance or by adding the style via the existing
section-h2 class) so that all H1/H2 headings follow the coding guideline for
balanced wrapping.

In `@landing/src/i18n/index.ts`:
- Around line 70-73: The localeLabels constant is hardcoded and must be replaced
with strings pulled from the i18n JSONs used by LangSwitcher; update the
implementation that defines localeLabels to read the translated labels from your
translation dictionaries (src/i18n/en.json and src/i18n/pl.json) instead of
literal "PL"/"EN", and adjust LangSwitcher to use the i18n key (e.g., a new
"lang.*" keys) via the existing translation accessor (your t/i18n loader) so
that user-visible labels come from the JSON files rather than hardcoded TS.

In `@landing/src/pages/api.astro`:
- Around line 28-33: Replace the hard-coded 3.5rem magic number in the
main#scalar-root style with a CSS variable so the layout stays in sync with the
Nav component; add a --nav-height token into tokens.css (or your global token
file) and update the rule for main#scalar-root to use calc(100vh -
var(--nav-height)); ensure the Nav component sets/uses the same --nav-height
value so both places reference the single source of truth.

In `@landing/src/pages/blog/`[...slug].astro:
- Line 93: The JSON-LD currently hardcodes the author as an Organization using
the object with "@type": "Organization" and name: post.data.author; change this
to use "@type": "Person" for individual authors (or make it conditional) so
human authors are semantically correct: inspect post.data.author (and any site
default like "KSeF Hub") and output {"@type":"Person", "name": post.data.author}
for individual names, or retain {"@type":"Organization", "name": ...} only when
the author is genuinely an organization.

In `@landing/src/pages/blog/index.astro`:
- Line 40: The H1 rendering {t.blog.heading} lacks the required text-wrap:
balance styling; update the <h1 class="section-h2 mb-4"> (or its CSS rule for
.section-h2) to include text-wrap: balance—either add the inline style attribute
or add the property to the .section-h2 rule in your stylesheet so H1/H2 elements
follow the coding guideline.

In `@landing/src/pages/en/api.astro`:
- Around line 21-24: The CDN script tag that loads
"https://cdn.jsdelivr.net/npm/@scalar/api-reference@1" should include a
Subresource Integrity (SRI) attribute and crossorigin to protect against CDN
tampering; generate the SHA-384 base64 hash for that exact URL (e.g. using the
provided curl | openssl command), then add integrity="sha384-<computed-hash>"
and crossorigin="anonymous" to the <script
src="https://cdn.jsdelivr.net/npm/@scalar/api-reference@1"> element so the
browser verifies the file before executing it.

In `@landing/src/pages/en/blog/index.astro`:
- Line 40: The H1 element rendering {t.blog.heading} (h1 with class "section-h2
mb-4") needs the text-wrap: balance rule applied; update the element to include
the class or inline style that enables text-wrap: balance (or add a utility
class like "text-wrap-balance") so the H1 obeys the coding guideline for
balanced wrapping.

In `@Makefile`:
- Around line 132-143: The Makefile target models.train is misplaced after the
"# --- Landing ---" section, breaking logical grouping; move the entire
models.train target block so it appears within the "ML Models" section (i.e.,
above the "# --- Landing ---" comment), preserving its content and spacing so
the target remains unchanged but is correctly grouped with other ML Models
targets.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b36c2217-12fb-4c33-9347-5d941e77b80a

📥 Commits

Reviewing files that changed from the base of the PR and between d519219 and 383ee88.

⛔ Files ignored due to path filters (3)
  • landing/package-lock.json is excluded by !**/package-lock.json
  • landing/public/favicon.svg is excluded by !**/*.svg
  • landing/src/assets/logo-mark.svg is excluded by !**/*.svg
📒 Files selected for processing (54)
  • .github/dependabot.yml
  • .github/workflows/ci.yml
  • .github/workflows/landing.yml
  • CLAUDE.md
  • Makefile
  • README.md
  • docs/adr/0048-public-landing-page.md
  • landing/.gitignore
  • landing/.tool-versions
  • landing/CLAUDE.md
  • landing/README.md
  • landing/astro.config.mjs
  • landing/package.json
  • landing/public/robots.txt
  • landing/scripts/check-i18n.mjs
  • landing/scripts/fetch-openapi.mjs
  • landing/src/components/ApiSection.astro
  • landing/src/components/ClosingCTA.astro
  • landing/src/components/FAQ.astro
  • landing/src/components/Features.astro
  • landing/src/components/Footer.astro
  • landing/src/components/GitHubIcon.astro
  • landing/src/components/Hero.astro
  • landing/src/components/HeroDiagram.astro
  • landing/src/components/LangSwitcher.astro
  • landing/src/components/LedgerPreview.astro
  • landing/src/components/LogoMark.astro
  • landing/src/components/Nav.astro
  • landing/src/components/OpenSource.astro
  • landing/src/components/Pricing.astro
  • landing/src/components/SEO.astro
  • landing/src/components/TrustStrip.astro
  • landing/src/components/WhyExists.astro
  • landing/src/config.ts
  • landing/src/content.config.ts
  • landing/src/content/blog/pl/obowiazek-ksef-2026.md
  • landing/src/i18n/README.md
  • landing/src/i18n/en.json
  • landing/src/i18n/index.ts
  • landing/src/i18n/pl.json
  • landing/src/layouts/Base.astro
  • landing/src/pages/api.astro
  • landing/src/pages/blog/[...slug].astro
  • landing/src/pages/blog/index.astro
  • landing/src/pages/blog/rss.xml.ts
  • landing/src/pages/en/api.astro
  • landing/src/pages/en/blog/index.astro
  • landing/src/pages/en/blog/rss.xml.ts
  • landing/src/pages/en/index.astro
  • landing/src/pages/index.astro
  • landing/src/styles/global.css
  • landing/src/styles/tokens.css
  • landing/tsconfig.json
  • lib/ksef_hub_web/api_spec.ex

Comment on lines +1 to +123
---
name: Public landing page as standalone Astro project
description: Ship the KSeF Hub public marketing site as a separate Astro 5 + Tailwind v4 project in the same monorepo, deployed to GitHub Pages. Zero runtime coupling with the Phoenix app.
tags: [landing, marketing, astro, seo, i18n, deployment]
author: emil
date: 2026-04-20
status: Accepted
---

# 0048. Public Landing Page as Standalone Astro Project

Date: 2026-04-20

## Status

Accepted

## Context

KSeF Hub needs a public marketing surface — a place for prospects, OSS contributors, and search engines to find the product. Constraints that shaped the approach:

- **Cost-optimized.** KSeF Hub is open-source and will have many self-hosted instances. The landing represents the project itself (centrally hosted by us), not something that ships with the product. Target cost: free.
- **CDN-cacheable.** Landing traffic must not hit the Phoenix app; marketing spikes cannot pressure the product runtime.
- **Different deployment from the admin app.** No shared container, no shared build, no shared runtime. OSS operators run Phoenix; only we run the landing.
- **Part of the dev pipeline.** Same repo, same PR flow — not a separate project to clone.
- **Shared brand foundation, divergent visual language.** Marketing is airier; admin is denser. They agree on colors/type tokens, not layout.
- **Iterable for SEO.** Polish is the primary audience (KSeF is a Polish mandate). Long-form content (blog, docs, changelog) is expected to follow.

Alternatives considered and rejected:

| Option | Why rejected |
|--------|--------------|
| Phoenix controller + HEEx | Couples landing uptime and scale to the product. Every visitor touches the BEAM. Wrong scaling story. |
| Tableau / NimblePublisher (Elixir SSG) | Same stack, but the Elixir SSG ecosystem meaningfully lags Astro on image optimization, sitemap, content collections, i18n routing, and community extensions — all marketing-page staples. |
| Plain HTML + Tailwind CDN (no build) | Simplest possible; no component reuse, no markdown, no content collections, no per-page SEO tooling. Breaks down at 2+ pages. |
| Separate repo | Adds cross-repo PR overhead with no win for a small OSS project. |
| Cloudflare Pages (vs. GitHub Pages) | Better global CDN TTFB, but one more external account. For an OSS project where "hosted on GitHub Pages" also signals the open-source posture, the delta is not worth the extra service. |

## Decision

Ship the landing as a **standalone Astro 5 + Tailwind v4 + TypeScript** project at `landing/` in the monorepo. Deploy to **GitHub Pages** via a path-filtered GitHub Actions workflow. **Zero runtime coupling** with the Phoenix app.

### Stack

- **Astro 5** (`output: 'static'`) — static HTML, zero JS by default, native i18n routing, native content collections.
- **Tailwind v4** via `@tailwindcss/vite`. No `tailwind.config.js`; customization via `@theme` in CSS.
- **TypeScript** (`strict` preset). Mandatory — the dictionary type is derived from `en.json` and surfaces i18n drift at compile time.
- **`@astrojs/sitemap`** — auto-generated, locale-aware sitemap with `<xhtml:link hreflang>` alternates.
- **`@astrojs/rss`** — one RSS feed per locale.
- **Node 20 LTS** pinned via `landing/.tool-versions` (scoped to the folder; does not add Node to the repo-root `.tool-versions` used by Phoenix devs).

### Isolation

- `.github/workflows/landing.yml` — builds on every PR touching `landing/**`, deploys on push to `main`. Concurrency group `pages`.
- `.github/workflows/ci.yml` — adds `paths-ignore: ['landing/**', '.github/workflows/landing.yml']` on `push` and `pull_request` triggers. Landing-only commits do not burn Elixir CI.
- `landing/` has its own `package.json`, `.gitignore`, `.tool-versions`, `README.md`, `CLAUDE.md`. Nothing leaks outward.
- One-time repo setting: **Settings → Pages → Source: GitHub Actions** (documented; self-correcting if forgotten — the workflow errors loudly).

### Design tokens

CSS custom properties in `landing/src/styles/tokens.css` are a **one-time port** of `assets/css/app.css`. No build-time link; tokens are resynced manually when brand changes land in the admin. Two theme-independent tokens are added that the admin does not have: `--code-bg` and `--code-fg` — always dark, matching the marketing convention for terminal-style code surfaces (in both light and dark pages).

### i18n

- Polish is the default locale. English is an alternate.
- `prefixDefaultLocale: false` — `/` is Polish, `/en/` is English. Clean URLs for the primary audience.
- **All user-visible strings live in `src/i18n/{en,pl}.json`.** Components never hardcode copy — they read via `useTranslations(locale)`. Enforced by convention and documented in `landing/CLAUDE.md`.
- Dictionary type is derived from `en.json`; TypeScript flags any drift between locales at compile time.
- Language switcher preserves the current path when flipping locales (e.g. `/blog/x` → `/en/blog/x`), not just the home.

### Content collections (blog)

- `src/content.config.ts` defines a `blog` collection with Zod-validated frontmatter: `locale`, `title`, `description`, `publishedAt`, `updatedAt?`, `draft`, `tags`, `author`.
- Posts live at `src/content/blog/{locale}/{slug}.md`. Locale is declared in both frontmatter and path.
- `draft: true` excludes a post from the index, RSS, sitemap, and static build — but leaves it reachable in `npm run dev` for preview.
- Per-post SEO: `og:type=article`, `article:published_time`, `article:modified_time`, canonical URL with hreflang alternates, plus `BlogPosting` JSON-LD (headline, date, inLanguage, keywords from tags, author, URL).

### RSS

One feed per locale. `/blog/rss.xml` (PL) and `/en/blog/rss.xml` (EN). Each item emits `<category>` from frontmatter tags and the channel declares `<language>pl-PL>` or `<language>en</language>`. Every page emits a locale-correct `<link rel="alternate" type="application/rss+xml">` in `<head>` for auto-discovery.

### SEO structure

Static HTML + structured data across the surface:

- Per-locale `<html lang>`, `og:locale` + `og:locale:alternate`, canonical URL, hreflang alternates including `x-default`.
- JSON-LD per page: `SoftwareApplication` + `Organization` on every page; `FAQPage` on the home page (FAQ accordion); `BlogPosting` on each blog post.
- Fonts loaded via `preconnect` + `<link rel="stylesheet">` (not CSS `@import`, which would block render twice).
- `apple-touch-icon`, `theme-color` (light + dark variants).
- Polish SEO: the key acronym is expanded once in-copy (`Krajowy System e-Faktur (KSeF)`) and the "2026" obligation year is surfaced in the hero narrative — both are high-volume search signals.

### What goes in the landing vs the admin

- Landing: hero, features, ledger preview, pricing, FAQ, blog, open-source story — all marketing copy and schema.
- Admin: Phoenix LiveView, REST API, sync workers, product workflows.
- The landing's code samples (curl, JSON) are static illustrations and do not hit a live endpoint.

## Consequences

**Good:**

- Free hosting with global CDN caching. Zero infra spend.
- Landing traffic cannot affect Phoenix runtime under any load pattern.
- Marketing changes deploy in under two minutes without touching Elixir CI.
- i18n is first-class from day one. Adding a third locale is a ~20-line diff plus a dictionary.
- Blog posts are plain markdown — reviewable via PR, editable by non-developers.
- SEO surface area (meta, JSON-LD, sitemap, RSS, hreflang) is structurally complete. Content is the only remaining ranking lever.

**Trade-offs accepted:**

- **Second toolchain.** npm/Node lives next to Elixir/OTP in the repo. Tooling diversity up; reviewed and accepted because the payoff (static output, SEO primitives, i18n routing, content collections) is meaningfully higher in Astro than any Elixir SSG.
- **Design-token drift risk.** `tokens.css` is a manual port of `assets/css/app.css`. Brand changes in the admin without a landing follow-up will drift the two surfaces. A build-time link was rejected because it would couple the two projects — the thing we are explicitly avoiding. Mitigated by a header comment in `tokens.css` flagging the port.
- **Component duplication.** HEEx components cannot render in Astro. The landing rebuilds its own atom set (button, card, kbd, tag-pill). Accepted because the visual language divergence is a design requirement.
- **npm optional-dependency bug** for local Apple Silicon (`@rolldown/binding-darwin-arm64`). Fresh clones on new Macs may need `npm install --no-save @rolldown/binding-darwin-arm64`. Documented in `landing/README.md`. CI (Ubuntu) is unaffected.
- **Deploy URL is `appunite.github.io/appunite-ksef-ex/`** until a custom domain lands. `astro.config.mjs` carries `base: '/appunite-ksef-ex'`; drop it and add `public/CNAME` when the domain is ready.

## Implementation pointers

- `landing/CLAUDE.md` — mandatory conventions for future work inside the folder (dictionary-only strings, locale routing invariants, what-NOT-to-do list).
- `landing/README.md` — local dev, build, blog post workflow, RSS mechanics.
- `landing/src/i18n/README.md` — dictionary editing + adding a locale.
- `landing/src/content.config.ts` — blog schema source of truth.
- `.github/workflows/landing.yml` — full build + deploy contract.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

if [ -f docs/architecture.md ]; then
  echo "Found docs/architecture.md"
  rg -n "0048|Public Landing Page|landing" docs/architecture.md
else
  echo "docs/architecture.md not found"
fi

Repository: appunite/appunite-ksef-ex

Length of output: 95


Update docs/architecture.md to include ADR-0048 and the landing feature mapping.

This ADR introduces a new feature area. Per your architecture documentation guidelines, run /update-architecture to sync the architecture index with this new ADR and its landing/ feature mapping.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~55-~55: The official name of this software platform is spelled with a capital “H”.
Context: ...s. - .github/workflows/ci.yml— addspaths-ignore: ['landing/**', '.github/workflows/landing.yml']onpush` and ...

(GITHUB)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/adr/0048-public-landing-page.md` around lines 1 - 123, Add ADR-0048 to
the architecture index and map the new landing feature: open
docs/architecture.md and add an entry for "0048. Public landing page as
standalone Astro project" linking to docs/adr/0048-public-landing-page.md and
add a feature mapping row for the landing/ folder (describe it as "landing:
Astro 5 + Tailwind v4 static marketing site, i18n, blog, RSS, sitemap, GitHub
Pages deploy"). After updating the file, run the repository's sync command
(/update-architecture) to regenerate any derived indexes and CI metadata so the
ADR and landing feature are visible to tooling and docs consumers.

Comment thread landing/CLAUDE.md Outdated
Comment thread landing/public/robots.txt
User-agent: *
Allow: /

Sitemap: https://appunite.github.io/appunite-ksef-ex/sitemap-index.xml

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider deployment-time sitemap URL generation.

The hardcoded GitHub Pages URL will require manual updates if the repository moves or a custom domain is configured, as noted in landing/src/config.ts. Since robots.txt is a static file, you might want to:

  1. Add a comment referencing the deployment URL in config.ts
  2. Generate this file at build time from the site config
  3. Document the manual update requirement in the deployment ADR
📝 Option 1: Add a reference comment
 User-agent: *
 Allow: /
 
+# When deployment URL changes, also update repoUrl in src/config.ts
 Sitemap: https://appunite.github.io/appunite-ksef-ex/sitemap-index.xml
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Sitemap: https://appunite.github.io/appunite-ksef-ex/sitemap-index.xml
User-agent: *
Allow: /
# When deployment URL changes, also update repoUrl in src/config.ts
Sitemap: https://appunite.github.io/appunite-ksef-ex/sitemap-index.xml
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/public/robots.txt` at line 4, robots.txt currently hardcodes the
sitemap URL ("Sitemap:
https://appunite.github.io/appunite-ksef-ex/sitemap-index.xml"); update it to
derive the base URL from the site config instead of hardcoding: reference the
deployment URL export in landing/src/config.ts (e.g., SITE_URL or PUBLIC_URL)
and either (A) replace the static line with a build-time step that generates
robots.txt using that config value (add a script in the build pipeline that
writes "Sitemap: ${SITE_URL}/sitemap-index.xml"), or (B) at minimum add a
comment above the Sitemap line pointing to landing/src/config.ts and document
the manual update requirement in your deployment ADR; modify the build scripts
or CI job that runs site build to emit the generated robots.txt so it reflects
custom domains.

Comment thread landing/src/components/ApiSection.astro Outdated
Comment on lines +25 to +43
<pre><code><span class="c">{t.api.codeComment1}</span>
<span class="k">curl</span> https://ksef-hub.local/v1/invoices \
-H <span class="s">"Authorization: Bearer $KSH_TOKEN"</span> \
-G -d <span class="s">"company=5252344078"</span> \
-d <span class="s">"status=approved"</span> \
-d <span class="s">"type=expense"</span> \
-d <span class="s">"date_from=2026-04-01"</span>

<span class="c"># -&gt; </span>
<span class="p">&#123;</span>
<span class="s">"data"</span><span class="p">:</span> <span class="p">[</span>
<span class="p">&#123;</span> <span class="s">"number"</span><span class="p">:</span> <span class="s">"FV/2026/04/118"</span><span class="p">,</span>
<span class="s">"seller"</span><span class="p">:</span> <span class="s">"Orange Polska"</span><span class="p">,</span>
<span class="s">"brutto"</span><span class="p">:</span> <span class="n">1427.00</span><span class="p">,</span>
<span class="s">"category"</span><span class="p">:</span> <span class="s">"telecom"</span><span class="p">,</span>
<span class="s">"confidence"</span><span class="p">:</span> <span class="n">0.94</span> <span class="p">&#125;</span>
<span class="p">]</span><span class="p">,</span>
<span class="s">"page"</span><span class="p">:</span> <span class="p">&#123;</span> <span class="s">"next"</span><span class="p">:</span> <span class="s">"cursor=inv_5f1b"</span> <span class="p">&#125;</span>
<span class="p">&#125;</span>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Move rendered code-sample text fully into i18n dictionaries.

Line 25-43 hardcodes visible text (curl sample, fields, values) directly in the component. That breaks locale parity and will drift between languages. Please source this block from en.json/pl.json (single multiline key or structured keys).

Suggested direction
-      <pre><code><span class="c">{t.api.codeComment1}</span>
-...hardcoded sample...
-</code></pre>
+      <pre><code>{t.api.codeBlock}</code></pre>

As per coding guidelines: "In landing page, all user-visible strings must go through src/i18n/{en,pl}.json via i18n, never hardcoded".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/components/ApiSection.astro` around lines 25 - 43, The
preformatted curl/code sample in the ApiSection component is hardcoded; move it
into the i18n dictionaries and replace the literal block with a lookup. Add a
new key (e.g. api.codeSample or structured keys like api.sample.curl and
api.sample.response) to src/i18n/en.json and src/i18n/pl.json, ensure the string
preserves newlines/escaping, then update ApiSection.astro to render that value
via the i18n helper (e.g. t.api.codeSample or t.api.sample.curl /
t.api.sample.response) instead of embedding the <pre><code> HTML directly so all
visible text comes from i18n.

Comment thread landing/src/pages/en/blog/index.astro
Comment on lines +25 to +173
.container-page {
max-width: 1120px;
margin-inline: auto;
padding-inline: 24px;
}

.eyebrow {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.12em;
font-weight: 600;
color: var(--brand);
}

.hero-h1 {
font-size: clamp(40px, 5.2vw, 68px);
line-height: 1.02;
letter-spacing: -0.03em;
font-weight: 600;
text-wrap: balance;
}

.section-h2 {
font-size: clamp(28px, 3.2vw, 40px);
line-height: 1.1;
letter-spacing: -0.02em;
font-weight: 600;
text-wrap: balance;
}

.btn-primary {
display: inline-flex;
align-items: center;
gap: 6px;
height: 40px;
padding-inline: 18px;
border-radius: 8px;
background: var(--foreground);
color: var(--background);
font-size: 14px;
font-weight: 500;
text-decoration: none;
transition: opacity 0.15s;
}
.btn-primary:hover {
opacity: 0.9;
text-decoration: none;
}

.btn-ghost {
display: inline-flex;
align-items: center;
gap: 6px;
height: 40px;
padding-inline: 14px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
text-decoration: none;
color: var(--foreground);
}
.btn-ghost:hover {
background: var(--accent);
text-decoration: none;
}

.nav-link {
color: var(--muted-foreground);
font-size: 14px;
text-decoration: none;
transition: color 0.15s;
}
.nav-link:hover {
color: var(--foreground);
text-decoration: none;
}

.hero-bg {
background-image: radial-gradient(
circle at 1px 1px,
color-mix(in oklch, var(--foreground) 10%, transparent) 1px,
transparent 0
);
background-size: 22px 22px;
}

.card-flat {
border: 1px solid var(--border);
border-radius: 12px;
background: var(--card);
padding: 28px;
}

.kbd {
font-family: var(--font-mono);
font-size: 11px;
padding-block: 2px;
padding-inline: 6px;
border-radius: 4px;
background: var(--muted);
color: var(--muted-foreground);
border: 1px solid var(--border);
letter-spacing: 0.04em;
}

.feat-icon {
width: 36px;
height: 36px;
border-radius: 8px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--muted);
color: var(--foreground);
}

.tag-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding-block: 4px;
padding-inline: 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 500;
border: 1px solid var(--border);
background: var(--card);
color: var(--muted-foreground);
}
.tag-pill .dot {
width: 6px;
height: 6px;
border-radius: 999px;
background: var(--brand);
}

pre code {
font-family: var(--font-mono);
font-size: 12.5px;
line-height: 1.6;
}

.code-block {
background: color-mix(in oklch, var(--code-bg) 96%, var(--brand));
color: var(--code-fg);
border-radius: 12px;
padding-block: 20px;
padding-inline: 22px;
overflow-x: auto;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Tokenize spacing/radii/type values instead of hardcoded px values.

Large parts of this stylesheet bypass the token system (font-size, padding, border-radius, etc.), which will drift design consistency and break the token contract.

Proposed direction
 .btn-primary {
   display: inline-flex;
   align-items: center;
-  gap: 6px;
+  gap: var(--space-2);
   height: 40px;
-  padding-inline: 18px;
-  border-radius: 8px;
+  padding-inline: var(--space-4);
+  border-radius: var(--radius);
   background: var(--foreground);
   color: var(--background);
-  font-size: 14px;
-  font-weight: 500;
+  font-size: var(--text-sm);
+  font-weight: var(--fw-medium);
 }

As per coding guidelines: "landing/src/{components,styles}/**/*.{astro,css}: Use CSS custom properties for all colors, type scale, spacing, and radii; never invent new colors beyond those defined in src/styles/tokens.css".

Also applies to: 182-257

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/styles/global.css` around lines 25 - 173, Many classes in this
stylesheet use hardcoded pixel values instead of design tokens; update the
classes (e.g., .container-page, .hero-h1, .section-h2, .btn-primary, .btn-ghost,
.nav-link, .card-flat, .kbd, .feat-icon, .tag-pill, .code-block) to use the
project's CSS custom property tokens for type scale, spacing, heights, gaps, and
radii (replace font-size, padding-/padding-inline, height, gap, border-radius,
letter-spacing values with the appropriate --token names from
src/styles/tokens.css), and swap any color literals with defined color tokens;
ensure you use the established token names (spacing, type, radius, color)
consistently and remove any new hardcoded px values so the components inherit
the design system.

Comment on lines +175 to +179
.code-block .k { color: #7dd3fc; }
.code-block .s { color: #fde68a; }
.code-block .n { color: #86efac; }
.code-block .c { color: #94a3b8; }
.code-block .p { color: #c4b5fd; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Replace hardcoded code-token hex colors with CSS variables from tokens.css.

These literals introduce new colors outside the token source of truth.

Proposed fix
-  .code-block .k { color: `#7dd3fc`; }
-  .code-block .s { color: `#fde68a`; }
-  .code-block .n { color: `#86efac`; }
-  .code-block .c { color: `#94a3b8`; }
-  .code-block .p { color: `#c4b5fd`; }
+  .code-block .k { color: var(--code-token-k); }
+  .code-block .s { color: var(--code-token-s); }
+  .code-block .n { color: var(--code-token-n); }
+  .code-block .c { color: var(--code-token-c); }
+  .code-block .p { color: var(--code-token-p); }

Add the corresponding variables in landing/src/styles/tokens.css.

As per coding guidelines: "landing/src/{components,styles}/**/*.{astro,css}: Use CSS custom properties for all colors, type scale, spacing, and radii; never invent new colors beyond those defined in src/styles/tokens.css".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/styles/global.css` around lines 175 - 179, Replace the hardcoded
hex colors in the .code-block token classes (.code-block .k, .code-block .s,
.code-block .n, .code-block .c, .code-block .p) with CSS custom properties
defined in landing/src/styles/tokens.css; add the corresponding color variables
to tokens.css (e.g., --color-code-k, --color-code-s, --color-code-n,
--color-code-c, --color-code-p) and update the rules in global.css to use
var(--color-...) so all code token colors come from the tokens source of truth.

Comment thread landing/src/styles/tokens.css Outdated
Comment thread landing/src/styles/tokens.css Outdated
emilwojtaszek and others added 2 commits April 21, 2026 07:48
Vite 8 (peer of Astro 5.18) uses rolldown, whose native bindings are
installed via npm optionalDependencies. The lockfile generated on macOS
with older npm did not record cross-platform binding entries, so `npm ci`
on Linux CI could not resolve `@rolldown/binding-linux-x64-gnu` and the
build crashed before Astro config loaded.

Bumping .tool-versions to Node 24 pulls in npm 11 (which fixes the
optional-deps lockfile bug from npm/cli#4828) both locally and in CI
(node-version-file tracks .tool-versions). Regenerating the lockfile
with npm 11 now populates all 15 platform bindings as real entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hs, EN post route

Real bugs:
- Nav section links (#features, #ledger, #api, #self-host) now prepend
  the locale home so they work from /blog/* and /api (previously broken
  on non-home pages).
- SEO hreflang alternates + x-default now carry the current path rather
  than pointing every locale to its home. A /blog/foo EN alternate now
  resolves to /en/blog/foo.
- Added landing/src/pages/en/blog/[...slug].astro — the English post
  route template was missing, so any post under content/blog/en/ had no
  page emitted.

Quality / typing:
- i18n/index.ts: `satisfies Record<Locale, Dictionary>` replaces the
  `pl as Dictionary` assertion so structural drift is caught at compile
  time.
- fetch-openapi.mjs: 30s AbortSignal.timeout so upstream hang cannot
  stall CI.
- LangSwitcher: aria-current="page" (semantic fix for aria-current="true").

Design consistency:
- text-wrap: pretty on long paragraphs in ApiSection, ClosingCTA,
  OpenSource, TrustStrip (.prose scope only handled markdown bodies).
- HeroDiagram: moved the hardcoded PL/EN diagramTitle ternary into the
  i18n dictionaries (hero.diagramTitle).
- EN blog index: added tag pills rendering to match PL.

Docs/meta:
- landing/CLAUDE.md: pl.json is now "Polish (translated)" — the
  "translation pending; currently == en.json" note is stale. Code fences
  tagged `text`.
- landing/README.md: directory tree code fence tagged `text`.
- tokens.css: Geist loader comment now points at Base.astro (previous
  note that `@import` lives in global.css was incorrect).
- docs/architecture.md: added ADR-0048 and a `landing/` feature row.
- Makefile: models.train moved back into the ML Models section (it had
  landed below `# --- Landing ---` and split the landing targets).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (1)
landing/src/styles/tokens.css (1)

18-21: ⚠️ Potential issue | 🟠 Major

Resolve font-family lint violations by quoting non-generic families.

This repeats the earlier unresolved issue: unquoted names on Lines 19-21 (Roboto, Arial, SFMono-Regular, Menlo, Monaco, Consolas) trip stylelint value-keyword-case.

🛠️ Proposed fix
   --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI",
-    Roboto, "Helvetica Neue", Arial, sans-serif;
-  --font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
-    Consolas, "Liberation Mono", "Courier New", monospace;
+    "Roboto", "Helvetica Neue", "Arial", sans-serif;
+  --font-mono: "Geist Mono", ui-monospace, "SFMono-Regular", "Menlo", "Monaco",
+    "Consolas", "Liberation Mono", "Courier New", monospace;
#!/bin/bash
set -euo pipefail
python - <<'PY'
import re, pathlib
p = pathlib.Path("landing/src/styles/tokens.css")
txt = p.read_text()
names = ["Roboto", "Arial", "SFMono-Regular", "Menlo", "Monaco", "Consolas"]
for n in names:
    m = re.search(rf'(?<!["\'])\b{re.escape(n)}\b(?!["\'])', txt)
    if m:
        line = txt.count("\n", 0, m.start()) + 1
        print(f"Unquoted {n} at Line {line}")
PY
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/styles/tokens.css` around lines 18 - 21, The font-family CSS
custom properties --font-sans and --font-mono contain unquoted non-generic
family names (e.g., Roboto, Arial, SFMono-Regular, Menlo, Monaco, Consolas) that
trigger stylelint value-keyword-case; update the declarations for --font-sans
and --font-mono to wrap each non-generic family name in quotes (e.g., "Roboto",
"Arial", "SFMono-Regular", "Menlo", "Monaco", "Consolas") while preserving the
existing order and generic fallbacks so the lint rule is satisfied.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@landing/CLAUDE.md`:
- Line 19: Update the documented Node version in CLAUDE.md by replacing the
mismatched version string "nodejs 20.18.1" with the actual pinned toolchain
version "nodejs 24.3.0" (or rephrase to "Node 24 LTS — pinned via .tool-versions
(nodejs 24.3.0)") so the README matches the project's .tool-versions entry and
avoids confusion for contributors.

In `@landing/README.md`:
- Around line 46-48: Update the README structure tree to include the English
post template by adding an entry for src/pages/en/blog/[...slug].astro under the
en/blog section (alongside index.astro and rss.xml.ts) so the tree shows the EN
post route; ensure the name matches the catch-all filename "[...slug].astro" to
avoid implying EN lacks a post template.

In `@landing/src/pages/en/blog/`[...slug].astro:
- Line 36: Replace the hardcoded suffix in the title expression
(title={`${post.data.title} — KSeF Hub`}) with an i18n lookup: add a key (e.g.,
"site.titleSuffix": "KSeF Hub" in src/i18n/en.json and the Polish equivalent in
src/i18n/pl.json), import/use the existing project i18n helper (the same
translator used elsewhere in the landing pages) and change the template to
compose title={`${post.data.title} — ${t('site.titleSuffix')}`} (or the
equivalent call for your translator) so the suffix is sourced from i18n rather
than hardcoded.
- Around line 86-97: The inline JSON-LD injection using set:html with
JSON.stringify in the <script type="application/ld+json" is:inline
set:html={JSON.stringify(...)} /> can be broken if the frontmatter contains "<"
sequences; update the serialization so after calling JSON.stringify(...) you
replace occurrences of "<" (and optionally the sequence "</") in the resulting
string with their escaped equivalents (e.g., "\\u003c" or similar) before
passing it to set:html to prevent prematurely ending the script tag; locate the
JSON.stringify call in the BlogPosting block and perform the safe-escape
transform on its output.

In `@landing/src/styles/tokens.css`:
- Around line 36-39: The typography token file defines extra weights (500 and
700) that violate the landing rule; remove the extra tokens and keep only the
body 400 and display 600 variables (e.g., keep --fw-regular: 400 and
--fw-semibold: 600) and update any usages that reference --fw-medium or
--fw-bold (for example .label-upper which currently uses var(--fw-medium)) to
use the display variable (var(--fw-semibold)) so all landing CSS/ASTRO files use
only the two allowed weights.
- Around line 141-166: The dark-mode prefers-color-scheme fallback is missing
several overrides present in the explicit [data-theme="dark"] set, causing
inconsistent contrast; update the :root:not([data-theme]) block inside `@media`
(prefers-color-scheme: dark) to include the same token values as your
[data-theme="dark"] theme for the missing symbols (e.g., --foreground,
--card-foreground, --popover-foreground, --primary, --primary-foreground,
--secondary-foreground, --muted-foreground, --accent-foreground, --border,
--input, --ring, --brand, --brand-muted and any other tokens defined in
[data-theme="dark"]) so the OS dark preference produces identical colors to the
explicit dark theme.

---

Duplicate comments:
In `@landing/src/styles/tokens.css`:
- Around line 18-21: The font-family CSS custom properties --font-sans and
--font-mono contain unquoted non-generic family names (e.g., Roboto, Arial,
SFMono-Regular, Menlo, Monaco, Consolas) that trigger stylelint
value-keyword-case; update the declarations for --font-sans and --font-mono to
wrap each non-generic family name in quotes (e.g., "Roboto", "Arial",
"SFMono-Regular", "Menlo", "Monaco", "Consolas") while preserving the existing
order and generic fallbacks so the lint rule is satisfied.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 66d77763-3a54-423f-93e7-256cdf669a54

📥 Commits

Reviewing files that changed from the base of the PR and between 383ee88 and 1f1cdd1.

⛔ Files ignored due to path filters (1)
  • landing/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (20)
  • Makefile
  • docs/architecture.md
  • landing/.tool-versions
  • landing/CLAUDE.md
  • landing/README.md
  • landing/scripts/fetch-openapi.mjs
  • landing/src/components/ApiSection.astro
  • landing/src/components/ClosingCTA.astro
  • landing/src/components/HeroDiagram.astro
  • landing/src/components/LangSwitcher.astro
  • landing/src/components/Nav.astro
  • landing/src/components/OpenSource.astro
  • landing/src/components/SEO.astro
  • landing/src/components/TrustStrip.astro
  • landing/src/i18n/en.json
  • landing/src/i18n/index.ts
  • landing/src/i18n/pl.json
  • landing/src/pages/en/blog/[...slug].astro
  • landing/src/pages/en/blog/index.astro
  • landing/src/styles/tokens.css
✅ Files skipped from review due to trivial changes (8)
  • docs/architecture.md
  • landing/scripts/fetch-openapi.mjs
  • landing/src/components/LangSwitcher.astro
  • landing/src/components/ApiSection.astro
  • landing/src/components/ClosingCTA.astro
  • landing/src/i18n/pl.json
  • landing/src/components/OpenSource.astro
  • landing/src/i18n/en.json
🚧 Files skipped from review as they are similar to previous changes (8)
  • landing/.tool-versions
  • landing/src/components/Nav.astro
  • Makefile
  • landing/src/components/HeroDiagram.astro
  • landing/src/components/TrustStrip.astro
  • landing/src/components/SEO.astro
  • landing/src/pages/en/blog/index.astro
  • landing/src/i18n/index.ts

Comment thread landing/CLAUDE.md Outdated
Comment thread landing/README.md
});
---
<Base
title={`${post.data.title} — KSeF Hub`}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Move hardcoded page-title suffix to i18n keys.

Line 36 hardcodes a user-visible string (KSeF Hub) in the title. Please source it from src/i18n/en.json and src/i18n/pl.json for consistency with the landing i18n rule.

Suggested change
-  title={`${post.data.title} — KSeF Hub`}
+  title={`${post.data.title} — ${t.meta.siteName}`}
As per coding guidelines: “i18n-first with every user-visible string in `src/i18n/{en,pl}.json` (never hardcoded).”
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
title={`${post.data.title} — KSeF Hub`}
title={`${post.data.title} — ${t.meta.siteName}`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/pages/en/blog/`[...slug].astro at line 36, Replace the hardcoded
suffix in the title expression (title={`${post.data.title} — KSeF Hub`}) with an
i18n lookup: add a key (e.g., "site.titleSuffix": "KSeF Hub" in src/i18n/en.json
and the Polish equivalent in src/i18n/pl.json), import/use the existing project
i18n helper (the same translator used elsewhere in the landing pages) and change
the template to compose title={`${post.data.title} — ${t('site.titleSuffix')}`}
(or the equivalent call for your translator) so the suffix is sourced from i18n
rather than hardcoded.

Comment thread landing/src/pages/en/blog/[...slug].astro Outdated
Comment thread landing/src/styles/tokens.css Outdated
Comment thread landing/src/styles/tokens.css
emilwojtaszek and others added 3 commits April 21, 2026 08:12
Lychee failures on the build job had two causes:

1. Root-relative paths (/appunite-ksef-ex/...) emitted by Astro's `base:`
   config could not be resolved against local dist/ without help. Fixed
   by creating a self-symlink dist/appunite-ksef-ex -> . and passing
   --root-dir $GITHUB_WORKSPACE/landing/dist. The prefix collapses back
   onto dist contents without requiring a dev server.

2. The SEO.astro hreflang change in the prior commit naively emitted
   alternates for every locale, including /en/blog/foo for PL-only
   posts that have no English translation — a 404 after deploy.
   LangSwitcher had the same bug (rendered EN link from a PL-only
   post).

   SEO, Nav, LangSwitcher, and Base.astro now accept an optional
   `availableLocales` prop. Static pages (home, blog index, api) still
   default to all locales. Blog post pages compute the set from the
   collection: getStaticPaths walks every post once, groups slugs by
   locale, and hands each page only the locales its slug appears in.
   hreflang, og:locale:alternate, and LangSwitcher now line up.

Also excluded two link targets from lychee that are repo-config issues,
not code bugs, and would otherwise block PR runs:

- https://appunite.github.io/appunite-ksef-ex — 404 until the site is
  first deployed from main. Lychee would fail every PR until then.
- https://github.com/appunite/appunite-ksef-ex/discussions — 404
  because the Discussions feature is not enabled on the repo. The
  footer link stays; the user can enable Discussions and remove the
  exclude in the same PR.

Verified locally with lychee 0.23: 4092 OK, 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- README.md: link to the deployed marketing site at
  appunite.github.io/appunite-ksef-ex and flag the source under landing/.
- landing/CLAUDE.md: update the tech-stack line from "Node 20 LTS
  (nodejs 20.18.1)" to the actual pin (nodejs 24.3.0) and record the
  npm ≥ 10.9 requirement that motivates it (rolldown native bindings,
  npm/cli#4828).
- landing/README.md: add [...slug].astro to the EN blog tree and
  rewrite the darwin-arm64 gotcha as a generic npm-version note — the
  bug was never platform-specific, it was an npm-version issue.
- landing/src/styles/tokens.css: mirror [data-theme="dark"] overrides
  into the @media (prefers-color-scheme: dark) block. --brand-strong
  (used by .prose a:hover) was flipping via the explicit data-theme
  path but not via OS preference, so link hover went wrong on auto
  dark. Also added the missing *-foreground and purple tokens to keep
  the two dark paths in sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The build job sets `defaults.run.working-directory: landing`, so the
previous `ln -sfn . landing/dist/appunite-ksef-ex` step resolved to
`landing/landing/dist/appunite-ksef-ex` and failed. Drop the leading
`landing/` — lychee-action (composite) still runs from the repo root
so its --root-dir and input glob remain prefixed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
landing/src/styles/tokens.css (2)

35-39: ⚠️ Potential issue | 🟠 Major

Remove disallowed typography weights and normalize .label-upper.

--fw-medium (500) and --fw-bold (700), plus .label-upper { font-weight: var(--fw-medium); }, violate the landing typography rule (only 400 and 600).

Proposed fix
   --fw-regular: 400;
-  --fw-medium: 500;
   --fw-semibold: 600;
-  --fw-bold: 700;
@@
   .label-upper {
     font-size: var(--text-xs);
-    font-weight: var(--fw-medium);
+    font-weight: var(--fw-semibold);
     text-transform: uppercase;

As per coding guidelines "landing/**/*.css: Use only one display weight (600) and one body weight (400) for typography."

Also applies to: 213-216

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/styles/tokens.css` around lines 35 - 39, The CSS defines
disallowed typography weights --fw-medium (500) and --fw-bold (700) and sets
.label-upper to use --fw-medium, which violates the landing rule of only 400 and
600; remove the --fw-medium and --fw-bold token declarations and update any
usages (including .label-upper) to use the allowed tokens --fw-regular (400) or
--fw-semibold (600) instead (e.g., change .label-upper to font-weight:
var(--fw-semibold) and audit other references to replace var(--fw-medium) /
var(--fw-bold) with var(--fw-regular) or var(--fw-semibold)).

18-21: ⚠️ Potential issue | 🟠 Major

Fix font-family token values to satisfy stylelint.

--font-sans and --font-mono still include non-generic font family keywords in a form that triggers value-keyword-case, which can fail CI checks.

Proposed fix
   --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI",
-    Roboto, "Helvetica Neue", Arial, sans-serif;
+    "Roboto", "Helvetica Neue", "Arial", sans-serif;
   --font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
-    Consolas, "Liberation Mono", "Courier New", monospace;
+    "SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono",
+    "Courier New", monospace;

As per coding guidelines "landing/**: follow landing/CLAUDE.md and keep landing quality gates green."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/styles/tokens.css` around lines 18 - 21, The CSS custom
properties --font-sans and --font-mono include font-family tokens that trigger
stylelint's value-keyword-case rule; update these two tokens so all generic
family keywords (e.g., sans-serif, monospace, serif, ui-sans-serif, system-ui,
-apple-system) are unquoted and lowercase while keeping custom family names
quoted, ensuring the final values for --font-sans and --font-mono use only
proper-case for custom names and lowercase for generic keywords to satisfy the
linter.
🧹 Nitpick comments (3)
landing/src/pages/blog/[...slug].astro (2)

13-30: Consider extracting shared getStaticPaths logic.

The getStaticPaths function and slugsByLocale computation are nearly identical between the PL and EN blog pages (differing only in the locale filter). This duplication could be reduced by extracting a shared helper.

♻️ Example helper in a shared module
// src/lib/blog.ts
export async function getBlogStaticPaths(targetLocale: Locale) {
  const allPosts = await getCollection("blog", ({ data }) => !data.draft);
  const posts = allPosts.filter((p) => p.data.locale === targetLocale);
  const slugsByLocale = new Map<string, Set<Locale>>();
  for (const p of allPosts) {
    const slug = p.id.split("/").slice(1).join("/");
    if (!slugsByLocale.has(slug)) slugsByLocale.set(slug, new Set());
    slugsByLocale.get(slug)!.add(p.data.locale as Locale);
  }
  return posts.map((post) => {
    const slug = post.id.split("/").slice(1).join("/");
    const available = Array.from(slugsByLocale.get(slug) ?? new Set([targetLocale]));
    return {
      params: { slug },
      props: { post, availableLocales: available as readonly Locale[] },
    };
  });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/pages/blog/`[...slug].astro around lines 13 - 30, Extract the
duplicated getStaticPaths logic (including the slugsByLocale Map build and
filtering of getCollection("blog")) into a shared helper (e.g.,
getBlogStaticPaths(targetLocale: Locale) in src/lib/blog.ts) that accepts a
targetLocale and returns the same array of { params, props } objects; then
replace the local getStaticPaths implementation in this file with a call to that
helper (pass "pl" for this page), and update the other locale page to call the
same helper with its locale so both use getBlogStaticPaths instead of
duplicating logic.

50-50: Move hardcoded page-title suffix to i18n keys.

The title suffix — KSeF Hub is hardcoded. This was flagged in the EN version and applies here too. Source it from i18n dictionaries for consistency.

♻️ Suggested fix
-  title={`${post.data.title} — KSeF Hub`}
+  title={`${post.data.title} — ${t.meta.siteName}`}

Then add "meta": { "siteName": "KSeF Hub" } to both src/i18n/pl.json and src/i18n/en.json.

As per coding guidelines: "All user-visible strings must live in i18n JSON dictionaries."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/pages/blog/`[...slug].astro at line 50, Replace the hardcoded
suffix "— KSeF Hub" in the page title expression with a value pulled from your
i18n dictionaries: add "meta": { "siteName": "KSeF Hub" } to both
src/i18n/pl.json and src/i18n/en.json, then change the title construction in
landing/src/pages/blog/[...slug].astro (the line using
title={`${post.data.title} — KSeF Hub`}) to concatenate the post title with the
translated siteName (e.g., use your translation helper like t('meta.siteName')
or the translated props) so the suffix comes from i18n rather than being
hardcoded.
landing/src/components/Nav.astro (1)

35-38: External link missing target and rel attributes.

The GitHub link opens an external URL but lacks target="_blank" and security attributes. Consider adding them for better UX and security.

♻️ Suggested fix
-      <a href={repoUrl} class="nav-link inline-flex items-center gap-1.5">
+      <a href={repoUrl} class="nav-link inline-flex items-center gap-1.5" target="_blank" rel="noopener noreferrer">
         <GitHubIcon size={13} />
         {t.nav.github}
       </a>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@landing/src/components/Nav.astro` around lines 35 - 38, The anchor rendering
the external GitHub link (the <a> using repoUrl with class "nav-link" and
content including <GitHubIcon /> and {t.nav.github}) should open in a new tab
and be secured; add target="_blank" and rel="noopener noreferrer" attributes to
that <a> element to prevent tab-nabbing and preserve UX when linking to external
sites.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@landing/src/styles/tokens.css`:
- Around line 35-39: The CSS defines disallowed typography weights --fw-medium
(500) and --fw-bold (700) and sets .label-upper to use --fw-medium, which
violates the landing rule of only 400 and 600; remove the --fw-medium and
--fw-bold token declarations and update any usages (including .label-upper) to
use the allowed tokens --fw-regular (400) or --fw-semibold (600) instead (e.g.,
change .label-upper to font-weight: var(--fw-semibold) and audit other
references to replace var(--fw-medium) / var(--fw-bold) with var(--fw-regular)
or var(--fw-semibold)).
- Around line 18-21: The CSS custom properties --font-sans and --font-mono
include font-family tokens that trigger stylelint's value-keyword-case rule;
update these two tokens so all generic family keywords (e.g., sans-serif,
monospace, serif, ui-sans-serif, system-ui, -apple-system) are unquoted and
lowercase while keeping custom family names quoted, ensuring the final values
for --font-sans and --font-mono use only proper-case for custom names and
lowercase for generic keywords to satisfy the linter.

---

Nitpick comments:
In `@landing/src/components/Nav.astro`:
- Around line 35-38: The anchor rendering the external GitHub link (the <a>
using repoUrl with class "nav-link" and content including <GitHubIcon /> and
{t.nav.github}) should open in a new tab and be secured; add target="_blank" and
rel="noopener noreferrer" attributes to that <a> element to prevent tab-nabbing
and preserve UX when linking to external sites.

In `@landing/src/pages/blog/`[...slug].astro:
- Around line 13-30: Extract the duplicated getStaticPaths logic (including the
slugsByLocale Map build and filtering of getCollection("blog")) into a shared
helper (e.g., getBlogStaticPaths(targetLocale: Locale) in src/lib/blog.ts) that
accepts a targetLocale and returns the same array of { params, props } objects;
then replace the local getStaticPaths implementation in this file with a call to
that helper (pass "pl" for this page), and update the other locale page to call
the same helper with its locale so both use getBlogStaticPaths instead of
duplicating logic.
- Line 50: Replace the hardcoded suffix "— KSeF Hub" in the page title
expression with a value pulled from your i18n dictionaries: add "meta": {
"siteName": "KSeF Hub" } to both src/i18n/pl.json and src/i18n/en.json, then
change the title construction in landing/src/pages/blog/[...slug].astro (the
line using title={`${post.data.title} — KSeF Hub`}) to concatenate the post
title with the translated siteName (e.g., use your translation helper like
t('meta.siteName') or the translated props) so the suffix comes from i18n rather
than being hardcoded.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e7f3588e-2ff4-410b-8f2d-8557d81c7ea1

📥 Commits

Reviewing files that changed from the base of the PR and between 1f1cdd1 and a8c5539.

📒 Files selected for processing (11)
  • .github/workflows/landing.yml
  • README.md
  • landing/CLAUDE.md
  • landing/README.md
  • landing/src/components/LangSwitcher.astro
  • landing/src/components/Nav.astro
  • landing/src/components/SEO.astro
  • landing/src/layouts/Base.astro
  • landing/src/pages/blog/[...slug].astro
  • landing/src/pages/en/blog/[...slug].astro
  • landing/src/styles/tokens.css
✅ Files skipped from review due to trivial changes (1)
  • .github/workflows/landing.yml
🚧 Files skipped from review as they are similar to previous changes (2)
  • landing/src/layouts/Base.astro
  • landing/src/components/SEO.astro

emilwojtaszek and others added 2 commits April 21, 2026 08:28
actions/upload-pages-artifact follows symlinks when tar'ing dist/. The
self-cycle symlink added for lychee (dist/appunite-ksef-ex -> .) made
tar recurse infinitely — the prior run at 24707230967 got stuck on the
upload step until GitHub's runner timeout cancelled it.

Remove the symlink in a dedicated cleanup step between lychee and
upload-pages-artifact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…uote font names

CLAUDE.md calls for a two-weight landing typography (body 400 / display
600), but tokens.css shipped --fw-medium (500) and --fw-bold (700). The
only in-repo reference to --fw-medium was .label-upper, which no
component uses. Remove the dead tokens and the unused class so the
token file reflects the rule.

Also quoted the non-generic font-family names (Roboto, Arial,
SFMono-Regular, Menlo, Monaco, Consolas) in --font-sans / --font-mono
to satisfy stylelint's value-keyword-case — no behavioural change, the
CSS spec accepts quoted single-word family names identically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@emilwojtaszek emilwojtaszek merged commit a59331c into main Apr 21, 2026
5 checks passed
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.

1 participant