feat: configurable landing page for static sites#2733
Conversation
🦋 Changeset detectedLatest commit: 22cde07 The changes in this PR will be included in the next version bump. This PR includes changesets to release 19 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
📝 WalkthroughWalkthroughAdds an optional project-level landingPage config (redirect | include | exclude), schema and JSON Schema entries, runtime validation and warnings, a view-filtering utility with tests, plugin/virtual-module type propagation, route redirect/filter integration, and duplicated docs content inserted twice. Changes
Sequence Diagram(s)sequenceDiagram
participant Browser
participant Loader
participant Projects
participant Vite
participant UI
Browser->>Loader: GET "/"
Loader->>Projects: read projects (includes landingPage)
Projects-->>Loader: projects[] (with landingPage)
alt first project landingPage.redirect === true
Loader->>Browser: redirect to /view/index (mask root)
else
Loader->>Vite: compute views/diagrams
Vite-->>Loader: views list
Loader->>UI: render single-index with views
UI->>UI: filterLandingPageViews(views, landingPage)
UI-->>Browser: rendered filtered grid
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
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)
📝 Coding Plan for PR comments
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
packages/likec4/src/vite/vite-build.ts (1)
81-93: Extract the landing-page matcher into shared code.This
#tag/view.idmatching logic is now duplicated in both build paths and separately inpackages/likec4/app/src/filterLandingPageViews.ts. Pulling it into one shared helper will keep the warning behavior aligned with the actual runtime filter.Also applies to: 103-115
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/likec4/src/vite/vite-build.ts` around lines 81 - 93, Extract the duplicated "#tag"/"view.id" matching logic into a single shared helper (e.g., matchLandingPage or landingPageMatcher) and replace the inline logic in vite-build.ts (the landingPage check that uses patterns.some(p => p.startsWith('#') ? v.tags?.some(t => t === p.slice(1)) : v.id === p)) and the code in filterLandingPageViews.ts to call that helper; ensure the helper accepts (diagrams, patterns) or (view, patterns) and implements the exact semantics: pattern starting with '#' matches view.tags entries (compare to p.slice(1)), otherwise matches view.id, and keep the existing hasMatch/warning behavior by importing and invoking the new helper from both places.packages/likec4/app/src/filterLandingPageViews.ts (1)
4-29: Accept the minimal view shape instead of fullLayoutedView.This helper only uses
idandtags, but the current signature forces callers/tests to manufacture fullLayoutedViews and is already leaking anas anyinto the new spec. A small reusable input type here keeps the API honest and removes that unsafe cast downstream.♻️ Suggested refactor
import type { LikeC4ProjectConfig } from '@likec4/config' import type { LayoutedView } from '@likec4/core/types' +export type LandingPageView = Pick<LayoutedView, 'id' | 'tags'> + export function filterLandingPageViews( - views: ReadonlyArray<LayoutedView>, + views: ReadonlyArray<LandingPageView>, filter: LikeC4ProjectConfig['landingPage'], -): ReadonlyArray<LayoutedView> { +): ReadonlyArray<LandingPageView> { if (!filter) return views if ('include' in filter) { return views.filter(view => matchesAny(view, filter.include)) } @@ return views } -function matchesAny(view: LayoutedView, patterns: string[]): boolean { +function matchesAny(view: LandingPageView, patterns: ReadonlyArray<string>): boolean { return patterns.some(pattern => {As per coding guidelines, "Agent types and interfaces should be defined separately and reused across the codebase" and "TypeScript-first repo; use explicit types Avoid using any, casts with as".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/likec4/app/src/filterLandingPageViews.ts` around lines 4 - 29, Change filterLandingPageViews and matchesAny to accept a small reusable input type (e.g., an interface with id: string and tags?: string[]) instead of LayoutedView so callers/tests no longer need to fabricate full LayoutedView or use `as any`; define this MinimalView type near other shared types and update the function signatures (filterLandingPageViews(views: ReadonlyArray<MinimalView>, ...) and matchesAny(view: MinimalView, patterns: string[])) and any call sites/tests to pass the minimal shape; ensure logic inside matchesAny still uses view.id and view.tags the same way and export the new type for reuse across the codebase.packages/likec4/app/src/filterLandingPageViews.spec.ts (1)
18-45: Add a redirect regression test.
landingPage.redirectis part of the new public contract, and this helper currently treats it as a no-op only through the final fall-through. An explicit spec here would keep that behavior from regressing if the branching changes later.➕ Suggested test
describe('filterLandingPageViews', () => { const views = [ mockView('landscape', ['public']), mockView('context', ['public']), mockView('internal', ['draft']), mockView('detail', []), ] it('returns all views when no filter', () => { expect(filterLandingPageViews(views, undefined)).toEqual(views) }) + + it('returns all views when redirect is configured', () => { + expect(filterLandingPageViews(views, { redirect: true })).toEqual(views) + }) it('includes by view ID', () => {As per coding guidelines, "Aim to cover new features with relevant tests; keep test names descriptive."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/likec4/app/src/filterLandingPageViews.spec.ts` around lines 18 - 45, Add a spec to filterLandingPageViews.spec.ts that verifies landingPage.redirect is treated as a no-op by filterLandingPageViews: create a test (e.g., "treats landingPage.redirect as no-op") that passes a view object with a landingPage.redirect property to filterLandingPageViews (both with undefined filter and with include/exclude filters) and assert the view is preserved in results; reference the filterLandingPageViews helper and the landingPage.redirect field so the test prevents regression if branching changes later.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/config/src/schema.ts`:
- Around line 108-122: The landingPage schema currently allows selectors equal
to "#" which leads to an empty tag in filterLandingPageViews.ts; update the
element schema used in the include and exclude arrays inside the landingPage
union (the z.string().nonempty() instances) to reject the single-character "#"
(e.g. replace with z.string().nonempty().refine(s => s !== '#', { message:
'selector cannot be "#"' })) so "#tag" remains allowed but a lone "#" is
invalid; make this change for both include and exclude array element schemas in
the landingPage config.
In `@packages/vite-plugin/src/modules.d.ts`:
- Around line 3-10: The LandingPageConfig union type is mismatched with
runtime/schema: update the variant currently defined as { redirectTo: string }
to match the runtime shape { redirect: true } so consumers of the
likec4:projects module get correct types; edit the LandingPageConfig declaration
(and any related Project.landingPage usage) to replace redirectTo with a {
redirect: true } variant and ensure the Project type still allows landingPage?:
LandingPageConfig.
---
Nitpick comments:
In `@packages/likec4/app/src/filterLandingPageViews.spec.ts`:
- Around line 18-45: Add a spec to filterLandingPageViews.spec.ts that verifies
landingPage.redirect is treated as a no-op by filterLandingPageViews: create a
test (e.g., "treats landingPage.redirect as no-op") that passes a view object
with a landingPage.redirect property to filterLandingPageViews (both with
undefined filter and with include/exclude filters) and assert the view is
preserved in results; reference the filterLandingPageViews helper and the
landingPage.redirect field so the test prevents regression if branching changes
later.
In `@packages/likec4/app/src/filterLandingPageViews.ts`:
- Around line 4-29: Change filterLandingPageViews and matchesAny to accept a
small reusable input type (e.g., an interface with id: string and tags?:
string[]) instead of LayoutedView so callers/tests no longer need to fabricate
full LayoutedView or use `as any`; define this MinimalView type near other
shared types and update the function signatures (filterLandingPageViews(views:
ReadonlyArray<MinimalView>, ...) and matchesAny(view: MinimalView, patterns:
string[])) and any call sites/tests to pass the minimal shape; ensure logic
inside matchesAny still uses view.id and view.tags the same way and export the
new type for reuse across the codebase.
In `@packages/likec4/src/vite/vite-build.ts`:
- Around line 81-93: Extract the duplicated "#tag"/"view.id" matching logic into
a single shared helper (e.g., matchLandingPage or landingPageMatcher) and
replace the inline logic in vite-build.ts (the landingPage check that uses
patterns.some(p => p.startsWith('#') ? v.tags?.some(t => t === p.slice(1)) :
v.id === p)) and the code in filterLandingPageViews.ts to call that helper;
ensure the helper accepts (diagrams, patterns) or (view, patterns) and
implements the exact semantics: pattern starting with '#' matches view.tags
entries (compare to p.slice(1)), otherwise matches view.id, and keep the
existing hasMatch/warning behavior by importing and invoking the new helper from
both places.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 263f008d-5619-4d0b-ab18-73282992f5af
📒 Files selected for processing (13)
apps/docs/src/content/docs/dsl/Config/index.mdxpackages/config/src/schema.spec.tspackages/config/src/schema.tspackages/likec4/app/src/filterLandingPageViews.spec.tspackages/likec4/app/src/filterLandingPageViews.tspackages/likec4/app/src/routes/_single/single-index.tsxpackages/likec4/app/src/routes/index.tsxpackages/likec4/src/vite/vite-build.tspackages/vite-plugin/src/modules.d.tspackages/vite-plugin/src/plugin.tspackages/vite-plugin/src/virtuals/_shared.tspackages/vite-plugin/src/virtuals/projects.tsschemas/likec4-config.schema.json
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/likec4/app/src/routes/index.tsx (1)
2-2: Inconsistent data source:projectsimport vscontext.projects.Line 6 uses
context.projectsfrom the route context, but line 16 uses the directly importedprojectsfrom'likec4:projects'. While they should contain the same data, mixing data sources can lead to subtle inconsistencies and makes the code harder to reason about.Proposed fix: use context consistently
-import { projects } from 'likec4:projects'- if (projects[0]?.landingPage && 'redirect' in projects[0].landingPage) { + if (context.projects[0]?.landingPage && 'redirect' in context.projects[0].landingPage) {Also applies to: 16-16
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/likec4/app/src/routes/index.tsx` at line 2, The file mixes two data sources— the top-level import "projects" and the route context's "context.projects"—causing potential inconsistencies; remove the direct import of projects and update all usages (e.g., the occurrence at line 16) to use context.projects instead so the component relies solely on the route context (ensure you delete the import "import { projects } from 'likec4:projects'" and replace any remaining references to projects with context.projects).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/likec4/app/src/routes/index.tsx`:
- Around line 16-25: The redirect currently hardcodes viewId 'index' in the
projects[0]?.landingPage block and can break if that view doesn't exist; update
the logic around projects[0]?.landingPage and the throw redirect call to
validate and resolve a real target: check for an explicit redirect target (e.g.,
landingPage.redirect as string), if present use it; otherwise search the
project's views (e.g., projects[0].views) and resolve the first available viewId
and fall back to a safe default (like the single-index grid route) before
calling throw redirect; ensure the redirect params.viewId uses the resolved id
so we never redirect to a non-existent 'index'.
---
Nitpick comments:
In `@packages/likec4/app/src/routes/index.tsx`:
- Line 2: The file mixes two data sources— the top-level import "projects" and
the route context's "context.projects"—causing potential inconsistencies; remove
the direct import of projects and update all usages (e.g., the occurrence at
line 16) to use context.projects instead so the component relies solely on the
route context (ensure you delete the import "import { projects } from
'likec4:projects'" and replace any remaining references to projects with
context.projects).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2e6431b6-48e9-4b87-a49a-8f302b3d652a
📒 Files selected for processing (2)
.changeset/landing-page-config.mdpackages/likec4/app/src/routes/index.tsx
| if (projects[0]?.landingPage && 'redirect' in projects[0].landingPage) { | ||
| throw redirect({ | ||
| to: '/view/$viewId/', | ||
| params: { viewId: 'index' }, | ||
| mask: { | ||
| to: '/', | ||
| unmaskOnReload: true, | ||
| }, | ||
| }) | ||
| } |
There was a problem hiding this comment.
Hardcoded 'index' viewId may break if no such view exists.
When landingPage: { redirect: true } is set, the code redirects to a view with viewId: 'index'. However, there's no validation that a view named 'index' actually exists in the project. If the user enables this option without having an 'index' view, the redirect leads to a broken page.
Consider one of:
- Add build-time validation (similar to include/exclude pattern validation in
vite-build.ts) that warns whenredirect: trueis set but no 'index' view exists. - Accept an optional target viewId in the redirect config (e.g.,
{ redirect: 'my-landing-view' }). - Fall back to the first available view or the single-index grid if 'index' doesn't exist.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/likec4/app/src/routes/index.tsx` around lines 16 - 25, The redirect
currently hardcodes viewId 'index' in the projects[0]?.landingPage block and can
break if that view doesn't exist; update the logic around
projects[0]?.landingPage and the throw redirect call to validate and resolve a
real target: check for an explicit redirect target (e.g., landingPage.redirect
as string), if present use it; otherwise search the project's views (e.g.,
projects[0].views) and resolve the first available viewId and fall back to a
safe default (like the single-index grid route) before calling throw redirect;
ensure the redirect params.viewId uses the resolved id so we never redirect to a
non-existent 'index'.
There was a problem hiding this comment.
This finding is not a real issue; in my opinion no fix isneeded.
Here's why:
- 'index' is a convention in LikeC4, not an arbitrary hardcode. The root view group treats 'index' as its default view (see packages/core/src/model/LikeC4Model.ts:214-216). The same viewId: 'index' pattern is used in project.$projectId/index.tsx:12.
- The redirect config is z.literal(true) — it's a boolean flag, not a string target. There's no "explicit redirect target" to check for; the schema doesn't support one.
- The redirect option is an explicit user opt-in. Users set landingPage: { redirect: true } when they want to skip the grid and go to their index view. If they don't have an index view, they wouldn't configure this.
- The view route already handles missing views — if viewId: 'index' doesn't resolve, the route itself will show an error/404, which is the correct behavior rather than silently falling back.
The suggestion to add view resolution logic, fallbacks, and validation here would be over-engineering for a scenario that doesn't arise in practice.
There was a problem hiding this comment.
@purple52, thank you for the detailed explanation — all four points are well-reasoned and I agree this is not an actionable issue. The 'index' convention, the intentional z.literal(true) schema design, the opt-in nature of the config, and the existing route-level error handling together make the current implementation correct as-is.
ʕ •ᴥ• ʔ 🐇
✏️ Learnings added
Learnt from: purple52
Repo: likec4/likec4 PR: 2733
File: packages/likec4/app/src/routes/index.tsx:16-25
Timestamp: 2026-03-12T14:42:25.024Z
Learning: In LikeC4 (`likec4/likec4`), `'index'` is a conventional default viewId for the root view group, not an arbitrary string. The `landingPage: { redirect: true }` config in `packages/likec4/app/src/routes/index.tsx` intentionally redirects to `viewId: 'index'` because that is the well-known default view. The same `viewId: 'index'` pattern is used in `packages/likec4/app/src/routes/project.$projectId/index.tsx`.
Learnt from: purple52
Repo: likec4/likec4 PR: 2733
File: packages/likec4/app/src/routes/index.tsx:16-25
Timestamp: 2026-03-12T14:42:25.024Z
Learning: In LikeC4 (`likec4/likec4`), the `landingPage.redirect` field in `packages/config/src/schema.ts` is typed as `z.literal(true)` — a boolean flag only. It does not accept a string redirect target, by design. Users who configure `{ redirect: true }` are explicitly opting in and are expected to have an `index` view; the view route itself handles the missing-view case with an error/404.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: ckeller42
Repo: likec4/likec4 PR: 2665
File: packages/likec4/app/src/pages/ViewReact.tsx:191-195
Timestamp: 2026-02-22T11:28:55.122Z
Learning: In `packages/likec4/app/src/pages/ViewReact.tsx`, the `FocusElementFromUrl` component intentionally does not re-process parameter changes post-initialization (unlike `OpenRelationshipBrowserFromUrl`). It focuses once on mount/initialization and only clears state when the parameter becomes falsy. This single-fire behavior is a deliberate design choice.
Learnt from: ckeller42
Repo: likec4/likec4 PR: 2665
File: packages/diagram/src/search/SearchContext.tsx:17-17
Timestamp: 2026-02-22T11:28:44.363Z
Learning: In packages/diagram/src/search/SearchContext.tsx, the currentViewId property in SearchContextValue interface is intentionally typed as `string | null` rather than `ViewId | null`, per explicit design decision.
Closes #2091
Summary
Adds a
landingPageconfiguration option to the project config, allowing users to control what is shown when visiting the root of a generated static site:redirect: true— redirect straight to the index viewinclude— show only matching views in the grid (by view ID or#tag)exclude— hide matching views from the grid (by view ID or#tag)Example configs:
Changes
packages/config— AddlandingPagefield toLikeC4ProjectJsonConfigSchemaas az.unionofredirect: true,include, andexcludevariants. Schema validation tests included.packages/vite-plugin— WirelandingPagethrough thelikec4:projectsvirtual module and HMR change detection.packages/likec4/app— Implementredirectin the/route'sbeforeLoad(redirects to index view). AddfilterLandingPageViewsutility forinclude/excludefiltering in the single-project index grid. 6 filter tests.packages/likec4/src/vite— Build-time validation: warn ifinclude/excludepatterns match no views (single and multi-project).schemas/— Regenerated JSON schema.apps/docs— DocumentlandingPageconfiguration with examples.Checklist
mainbefore creating this PR.Summary by CodeRabbit
New Features
Bug Fixes / Observability
Documentation
Tests