Skip to content

feat: Zero-config SSR support#48

Merged
patrick91 merged 3 commits intomainfrom
feat/zero-config-ssr
Feb 17, 2026
Merged

feat: Zero-config SSR support#48
patrick91 merged 3 commits intomainfrom
feat/zero-config-ssr

Conversation

@patrick91
Copy link
Contributor

@patrick91 patrick91 commented Feb 17, 2026

Summary

  • CrossDocs now owns the full app setup — consumers just pass FastAPI kwargs and access docs.app:

    docs = CrossDocs(title="My Docs", docs_url=None, redoc_url=None)
    app = docs.app  # ready for uvicorn

    Internally calls configure_inertia(), creates FastAPI(lifespan=inertia_lifespan), mounts static files, and includes the docs router. mount() still works for backward compatibility.

  • Fix SSR hydration mismatch — the SSR setup now wraps <App> with <ComponentsProvider> to match the client-side tree

  • Fix SSR crash from duplicate ThemeContextuseTheme() returns safe defaults during SSR instead of throwing, since bundlers (Vite with noExternal: true) create two copies of ThemeContext from the separate ./ and ./ssr package entries

Test plan

  • Run cross-docs Python tests
  • Build SSR bundle in a consumer project (bun run build && bun run build:ssr)
  • Start in production mode — view-source should show pre-rendered HTML
  • Verify client-side hydration works without console errors

Summary by CodeRabbit

  • New Features

    • Zero‑config FastAPI app creation with lazy or immediate initialization, exposing a ready-to-run app with routing and static file serving.
    • Support for providing custom UI component providers via configuration for consistent server/client rendering.
  • Bug Fixes

    • Improved SSR resilience: safe fallback theme behavior to prevent server-side crashes and a provider wrap to fix hydration mismatches.

- Add `**fastapi_kwargs` to `CrossDocs.__init__` and `self.app` property
  that creates a fully configured FastAPI app with Inertia wired up
  (configure_inertia, lifespan, static files, router mounting)
- Wrap SSR setup with `ComponentsProvider` to match client-side tree
  and fix hydration mismatch
- Make `useTheme()` return safe defaults during SSR to handle duplicate
  ThemeContext in bundled SSR builds
@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

Warning

Rate limit exceeded

@patrick91 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 27 minutes and 34 seconds before requesting another review.

⌛ How to resolve this issue?

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

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

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

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

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds SSR-safe defaults to the theme provider, wraps server render with ComponentsProvider, and adds zero-config FastAPI app creation in CrossDocs with Inertia, lifespan, static mounting, and lazy/explicit app exposure.

Changes

Cohort / File(s) Summary
SSR / Theme & Server Render
js/src/components/ThemeProvider.tsx, js/src/ssr.tsx
Return safe defaults from useTheme during SSR when ThemeContext is null. Wrap server-side render with ComponentsProvider inside ThemeProvider to align server/client component trees.
CrossDocs FastAPI app
python/cross_docs/routes.py
Add fastapi_kwargs support to CrossDocs.__init__, introduce _app attribute, _create_app helper, and public app property to create or return a fully configured FastAPI app (Inertia config, optional lifespan, /static mount, and route mounting).
Release notes
RELEASE.md
Documented zero-config SSR support, ComponentsProvider SSR wrap to fix hydration mismatch, and SSR-safe ThemeContext defaults.

Sequence Diagram(s)

sequenceDiagram
    participant CrossDocs
    participant FastAPI
    participant Inertia
    participant StaticFiles

    CrossDocs->>CrossDocs: __init__(**fastapi_kwargs)
    CrossDocs->>CrossDocs: _create_app(**fastapi_kwargs)
    CrossDocs->>FastAPI: Instantiate FastAPI(...)
    CrossDocs->>Inertia: configure_inertia(app)
    rect rgba(100,150,200,0.5)
    Note over Inertia: apply inertia_lifespan if provided
    end
    CrossDocs->>StaticFiles: mount('/static')
    CrossDocs->>FastAPI: include_router(cross_docs_routes)
    FastAPI-->>CrossDocs: configured app
    CrossDocs->>CrossDocs: store in _app and expose via app property
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble code where servers meet the sun,
Guarding themes on SSR till work is done,
I spin a FastAPI den with Inertia near,
Mount static doors and routes appear—
Hooray, the burrow's whole and cozy fun!

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Zero-config SSR support' clearly and concisely summarizes the primary change: adding zero-config server-side rendering support to CrossDocs. It directly aligns with the main objective of allowing consumers to pass FastAPI kwargs and access a fully configured app property.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/zero-config-ssr

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
python/cross_docs/routes.py (3)

50-56: **fastapi_kwargs type hint is Any — consider documenting or narrowing.

Using **fastapi_kwargs: Any is pragmatic for forwarding to FastAPI(...), but it means IDE autocompletion and type checking are lost for the caller. Since Unpack with TypedDict is available in Python 3.12+ (or typing_extensions), this may not be worth changing now, but a brief docstring listing the most common kwargs (title, docs_url, redoc_url, lifespan) would improve DX.

The existing docstring on lines 64-67 partially covers this — just noting for completeness.

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

In `@python/cross_docs/routes.py` around lines 50 - 56, The __init__ parameter
**fastapi_kwargs is currently typed as Any which hides IDE/autocomplete and type
checking; update the __init__ docstring for the class (and the __init__
signature comment) to enumerate the common FastAPI kwargs you expect callers to
pass (e.g., title, docs_url, redoc_url, lifespan) and either narrow the type by
creating a small TypedDict/Unpack alias for those keys (using typing_extensions
for older Pythons) or leave the var-kwargs but explicitly document the supported
keys and types in the docstring for __init__ (referencing the __init__ parameter
name fastapi_kwargs and the DocsConfig type for context).

86-99: The app property silently creates a full FastAPI app — could surprise mount() users.

When a user instantiates CrossDocs() (no kwargs) intending to call docs.mount(existing_app), accidentally accessing docs.app (e.g., in logging or debugging) will silently spin up a separate FastAPI instance with its own Inertia config and static mounts. This is a side-effect-heavy property.

Consider either raising an error if _app is None and no fastapi_kwargs were provided, or adding a flag to distinguish "user wants auto-app" from "user will mount manually."

Proposed sketch
     def __init__(
         self,
         config: DocsConfig | None = None,
         *,
         docs_component: str | None = None,
         home_component: str | None = None,
         **fastapi_kwargs: Any,
     ):
         ...
+        self._auto_app = bool(fastapi_kwargs)
         self._app: FastAPI | None = None
         if fastapi_kwargs:
             self._app = self._create_app(**fastapi_kwargs)

     `@property`
     def app(self) -> FastAPI:
-        if self._app is None:
-            self._app = self._create_app()
-        return self._app
+        if self._app is None:
+            if not self._auto_app:
+                # Still allow lazy creation, but could also raise here
+                self._app = self._create_app()
+        return self._app
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python/cross_docs/routes.py` around lines 86 - 99, The app property currently
auto-creates a FastAPI instance via _create_app when self._app is None which can
surprise callers who intend to mount CrossDocs into an existing app; add an
explicit opt-in flag in the constructor (e.g., auto_create_app: bool = True) and
store it on the instance, then change the app property to: if self._app is None
and not self.auto_create_app raise a clear RuntimeError instructing the user to
call mount(existing_app) (otherwise keep the existing lazy-creation behavior by
calling _create_app); update __init__ to accept the new flag and ensure mount()
still sets self._app so subsequent app accesses work.

101-115: Call configure_inertia at module level, not inside _create_app.

The current design calls configure_inertia() inside the _create_app() instance method. This means the configuration is repeated if multiple CrossDocs instances are created with fastapi_kwargs or if .app is accessed multiple times (though the lazy caching prevents the latter). While configure_inertia() likely handles multiple calls gracefully, this is an architectural inconsistency with the canonical usage pattern.

In website/app.py, configure_inertia() is called once at module level before app creation. Consider moving the call out of _create_app() to keep global configuration separate from instance creation, or document that only one auto-creating instance per process is supported.

The hardcoded paths ("app.tsx", "static/build/ssr/ssr.js", etc.) are intentional for the "zero-config" feature and are fine.

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

In `@python/cross_docs/routes.py` around lines 101 - 115, Move the
configure_inertia call out of the CrossDocs._create_app method and call it once
at module import time instead: remove the configure_inertia(...) invocation from
_create_app, add a single configure_inertia(...) call at module level (above the
CrossDocs class) with the same arguments, and keep the rest of _create_app
intact (retain fastapi_kwargs.setdefault("lifespan", inertia_lifespan), FastAPI
instantiation, app.mount and self.mount usage). Ensure configure_inertia is
imported in the module so the module-level call runs once when the module is
loaded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@python/cross_docs/routes.py`:
- Around line 50-56: The __init__ parameter **fastapi_kwargs is currently typed
as Any which hides IDE/autocomplete and type checking; update the __init__
docstring for the class (and the __init__ signature comment) to enumerate the
common FastAPI kwargs you expect callers to pass (e.g., title, docs_url,
redoc_url, lifespan) and either narrow the type by creating a small
TypedDict/Unpack alias for those keys (using typing_extensions for older
Pythons) or leave the var-kwargs but explicitly document the supported keys and
types in the docstring for __init__ (referencing the __init__ parameter name
fastapi_kwargs and the DocsConfig type for context).
- Around line 86-99: The app property currently auto-creates a FastAPI instance
via _create_app when self._app is None which can surprise callers who intend to
mount CrossDocs into an existing app; add an explicit opt-in flag in the
constructor (e.g., auto_create_app: bool = True) and store it on the instance,
then change the app property to: if self._app is None and not
self.auto_create_app raise a clear RuntimeError instructing the user to call
mount(existing_app) (otherwise keep the existing lazy-creation behavior by
calling _create_app); update __init__ to accept the new flag and ensure mount()
still sets self._app so subsequent app accesses work.
- Around line 101-115: Move the configure_inertia call out of the
CrossDocs._create_app method and call it once at module import time instead:
remove the configure_inertia(...) invocation from _create_app, add a single
configure_inertia(...) call at module level (above the CrossDocs class) with the
same arguments, and keep the rest of _create_app intact (retain
fastapi_kwargs.setdefault("lifespan", inertia_lifespan), FastAPI instantiation,
app.mount and self.mount usage). Ensure configure_inertia is imported in the
module so the module-level call runs once when the module is loaded.

Accessing `docs.app` without passing FastAPI kwargs now raises a
RuntimeError instead of silently creating a full app with side effects.
@patrick91 patrick91 merged commit 6dafab6 into main Feb 17, 2026
3 checks passed
@patrick91 patrick91 deleted the feat/zero-config-ssr branch February 17, 2026 05:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant