feat(landing): Astro marketing site with blog, RSS, i18n, SEO#158
Conversation
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>
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a standalone Astro 5 + Tailwind v4 + TypeScript landing site under Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 19
🧹 Nitpick comments (20)
Makefile (1)
132-143: Minor:models.traintarget appears after the Landing section.The
models.traintarget 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 applyingtext-wrap: balanceto the H2.Same as other section components—if
section-h2doesn't includetext-wrap: balance, consider adding it for consistent heading rendering. As per coding guidelines: "Applytext-wrap: balanceto 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
diagramTitleuses an inline ternary instead of pulling from the i18n JSON files. This violates the coding guideline requiring all user-visible strings to go throughsrc/i18n/{en,pl}.json.♻️ Suggested refactor
Add to
en.jsonandpl.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}.jsonvia 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: Addtext-wrap: balanceto the H2 element.Per coding guidelines, H1/H2 elements should have
text-wrap: balanceapplied.♻️ 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: balanceto the.section-h2class in your global styles if it should apply to all section headings.As per coding guidelines: "Apply
text-wrap: balanceto 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
localesarray is hardcoded, requiring manual updates when adding new locales. This could drift out of sync withsrc/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: Addtext-wrap: balanceto the H2 element.Per coding guidelines, H1/H2 elements should have
text-wrap: balanceapplied.♻️ 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: balanceto 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.5remvalue incalc(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-heighttoken intokens.cssand 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 usingaria-current="page"instead ofaria-current="true".For language navigation marking the current page's language,
aria-current="page"is more semantically appropriate thanaria-current="true". Thepagevalue 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#a78bfaare not defined intokens.css. As per coding guidelines, you should never invent new colors beyond those defined insrc/styles/tokens.css.Either add these source-indicator colors to
tokens.cssas 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
fetchcall 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: Addtext-wrap: balanceto the H1 element.The coding guidelines specify that H1/H2 elements should use
text-wrap: balance. The paragraph below correctly usestext-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: balanceto H1/H2 elements andtext-wrap: prettyto 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: Addtext-wrap: balanceto 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: balanceto 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
textorplaintextas 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 usingPersontype for individual authors in JSON-LD.The
authorfield usesOrganizationtype, but the default author is "KSeF Hub" which could be either. If individual human authors are expected in the future,Personwould 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: Addtext-wrap: balanceto 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: balanceto 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/ENare rendered to users viaLangSwitcher, so they should come from i18n JSON.As per coding guidelines: "
landing/**/*.{ts,tsx,astro}: all user-visible strings must go throughsrc/i18n/{en,pl}.jsonvia 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
⛔ Files ignored due to path filters (3)
landing/package-lock.jsonis excluded by!**/package-lock.jsonlanding/public/favicon.svgis excluded by!**/*.svglanding/src/assets/logo-mark.svgis excluded by!**/*.svg
📒 Files selected for processing (54)
.github/dependabot.yml.github/workflows/ci.yml.github/workflows/landing.ymlCLAUDE.mdMakefileREADME.mddocs/adr/0048-public-landing-page.mdlanding/.gitignorelanding/.tool-versionslanding/CLAUDE.mdlanding/README.mdlanding/astro.config.mjslanding/package.jsonlanding/public/robots.txtlanding/scripts/check-i18n.mjslanding/scripts/fetch-openapi.mjslanding/src/components/ApiSection.astrolanding/src/components/ClosingCTA.astrolanding/src/components/FAQ.astrolanding/src/components/Features.astrolanding/src/components/Footer.astrolanding/src/components/GitHubIcon.astrolanding/src/components/Hero.astrolanding/src/components/HeroDiagram.astrolanding/src/components/LangSwitcher.astrolanding/src/components/LedgerPreview.astrolanding/src/components/LogoMark.astrolanding/src/components/Nav.astrolanding/src/components/OpenSource.astrolanding/src/components/Pricing.astrolanding/src/components/SEO.astrolanding/src/components/TrustStrip.astrolanding/src/components/WhyExists.astrolanding/src/config.tslanding/src/content.config.tslanding/src/content/blog/pl/obowiazek-ksef-2026.mdlanding/src/i18n/README.mdlanding/src/i18n/en.jsonlanding/src/i18n/index.tslanding/src/i18n/pl.jsonlanding/src/layouts/Base.astrolanding/src/pages/api.astrolanding/src/pages/blog/[...slug].astrolanding/src/pages/blog/index.astrolanding/src/pages/blog/rss.xml.tslanding/src/pages/en/api.astrolanding/src/pages/en/blog/index.astrolanding/src/pages/en/blog/rss.xml.tslanding/src/pages/en/index.astrolanding/src/pages/index.astrolanding/src/styles/global.csslanding/src/styles/tokens.csslanding/tsconfig.jsonlib/ksef_hub_web/api_spec.ex
| --- | ||
| 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. |
There was a problem hiding this comment.
🧩 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"
fiRepository: 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.
| User-agent: * | ||
| Allow: / | ||
|
|
||
| Sitemap: https://appunite.github.io/appunite-ksef-ex/sitemap-index.xml |
There was a problem hiding this comment.
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:
- Add a comment referencing the deployment URL in
config.ts - Generate this file at build time from the site config
- 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.
| 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.
| <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"># -> </span> | ||
| <span class="p">{</span> | ||
| <span class="s">"data"</span><span class="p">:</span> <span class="p">[</span> | ||
| <span class="p">{</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">}</span> | ||
| <span class="p">]</span><span class="p">,</span> | ||
| <span class="s">"page"</span><span class="p">:</span> <span class="p">{</span> <span class="s">"next"</span><span class="p">:</span> <span class="s">"cursor=inv_5f1b"</span> <span class="p">}</span> | ||
| <span class="p">}</span> |
There was a problem hiding this comment.
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.
| .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; |
There was a problem hiding this comment.
🛠️ 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.
| .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; } |
There was a problem hiding this comment.
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.
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>
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (1)
landing/src/styles/tokens.css (1)
18-21:⚠️ Potential issue | 🟠 MajorResolve 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 stylelintvalue-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
⛔ Files ignored due to path filters (1)
landing/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (20)
Makefiledocs/architecture.mdlanding/.tool-versionslanding/CLAUDE.mdlanding/README.mdlanding/scripts/fetch-openapi.mjslanding/src/components/ApiSection.astrolanding/src/components/ClosingCTA.astrolanding/src/components/HeroDiagram.astrolanding/src/components/LangSwitcher.astrolanding/src/components/Nav.astrolanding/src/components/OpenSource.astrolanding/src/components/SEO.astrolanding/src/components/TrustStrip.astrolanding/src/i18n/en.jsonlanding/src/i18n/index.tslanding/src/i18n/pl.jsonlanding/src/pages/en/blog/[...slug].astrolanding/src/pages/en/blog/index.astrolanding/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
| }); | ||
| --- | ||
| <Base | ||
| title={`${post.data.title} — KSeF Hub`} |
There was a problem hiding this comment.
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}`}📝 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.
| 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.
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>
There was a problem hiding this comment.
♻️ Duplicate comments (2)
landing/src/styles/tokens.css (2)
35-39:⚠️ Potential issue | 🟠 MajorRemove 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 | 🟠 MajorFix font-family token values to satisfy stylelint.
--font-sansand--font-monostill include non-generic font family keywords in a form that triggersvalue-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/**: followlanding/CLAUDE.mdand 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 sharedgetStaticPathslogic.The
getStaticPathsfunction andslugsByLocalecomputation 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 Hubis 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 bothsrc/i18n/pl.jsonandsrc/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 missingtargetandrelattributes.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
📒 Files selected for processing (11)
.github/workflows/landing.ymlREADME.mdlanding/CLAUDE.mdlanding/README.mdlanding/src/components/LangSwitcher.astrolanding/src/components/Nav.astrolanding/src/components/SEO.astrolanding/src/layouts/Base.astrolanding/src/pages/blog/[...slug].astrolanding/src/pages/en/blog/[...slug].astrolanding/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
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>
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./is PL,/en/is EN), language switcher preserves the current path on togglesrc/content/blog/{locale}/{slug}.md) + one seed PL post on the 2026 obligation + per-locale RSS feeds with auto-discoveryFAQPageschema, plusSoftwareApplication/Organization/BlogPostingJSON-LD across the surfaceassets/css/app.css; two theme-independent tokens added for always-dark code surfacesastro check, and lychee link check, all required before the Pages artifact uploadsmake landing.*targets for discoverability; workflow isolation viapaths-ignore: ['landing/**']onci.ymlArchitecture decisions and trade-offs are documented in
docs/adr/0048-public-landing-page.md.One-time manual step after merge
Test plan
cd landing && npm cisucceeds on a fresh checkout (Apple Silicon Macs may neednpm install --no-save @rolldown/binding-darwin-arm64— documented in README)make landing.checkpasses (i18n parity + typecheck)make landing.buildproduces 7 pages (home PL/EN, blog index PL/EN, 1 PL post, 2 RSS endpoints)npm run devrenders/and/en/correctly; language switcher flips both and survives on/blog/https://appunite.github.io/appunite-ksef-ex/,/en/alternate renders,/blog/lists the seed post,/blog/rss.xmlreturns valid RSS 2.0ci.ymlis not triggered by landing-only commits (path-filtered)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Chores