Routing & Navigation
Routing in Carats is data, not a filesystem. Every route is declared in a single facets object that the client and the server share.
Routes
The routes map goes from a route pattern to a page component. The pattern is matched exactly, except for segments that start with a colon, which capture a parameter.
import { defineFacets } from '@carats/render';
import ErrorPage from './pages/_error';
import NotFound from './pages/_not_found';
import Home from './pages/home';
import Market from './pages/market';
import Profile from './pages/profile';
import Trade from './pages/trade';
export default defineFacets({
routes: {
'/': Home,
'/profile': Profile,
'/market': Market,
'/trade/:symbol': Trade
},
suspense: {
error: ErrorPage,
notFound: NotFound
},
inAppRouting: true
});
A page folder name is a convention, not a requirement. The folder keeps related files together, but the route a folder serves is whatever string it is mapped to, so `/market` can live in a folder called `browse` if that reads better.
Dynamic Parameters
A :name segment becomes a prop on the page component, and it is also available as params.name inside a culet for the same route. The name must match on both sides.
import { Burnish } from '@carats/render';
import Layout from '../_layout';
import './trade.sass';
export default Burnish<TradeData>(function (data) {
this.head = <title>Trade {data.symbol}</title>;
return (
<Layout>
<h1>{data.symbol}</h1>
</Layout>
);
});
`/trade/btc` renders this page with `data.symbol` resolved from the culet registered for the same route, and the page renders a different `
` for every symbol. The framework does the matching in `getPageComponent`, which returns the component, the extracted `params` and the matched `route`.</p><blockquote><p>Route patterns are matched by @carats/url with matchRoute. There is no wildcard, no optional segment and no catch-all: an unmatched URL renders the notFound suspense component instead.</p></blockquote><h2>In-App Routing</h2><p>With inAppRouting enabled, the client intercepts clicks on internal anchors and swaps the page without a full page load. It also requires clientRender() in the client entrypoint. Turn it off when a page depends on a full document load, for example a page that must run a third-party script on every navigation.</p><pre><code class="hljs language-typescript"><span class="hljs-comment">// src/client/entrypoint.ts</span><span class="hljs-keyword">import</span> { mount, clientRender } <span class="hljs-keyword">from</span> <span class="hljs-string">'@carats/csr'</span>;<span class="hljs-keyword">import</span> facets <span class="hljs-keyword">from</span> <span class="hljs-string">'./facets.cara'</span>;<span class="hljs-title function_">mount</span>(facets);<span class="hljs-title function_">clientRender</span>();</code></pre><p>Both lines are required when in-app routing is on. `mount` registers the facets in the browser, and `clientRender` starts intercepting navigation.</p><p>Links need no framework component. A plain anchor is intercepted automatically, and a normal link is followed normally if the interception is disabled, which means pages stay crawlable and work without JavaScript.</p><h2>Programmatic Navigation</h2><p>goTo from @carats/csr navigates the same way an intercepted link click does. It is the right way to submit a search box or any other form that should not reload the document.</p><pre><code class="hljs language-tsx"><span class="hljs-keyword">import</span> { goTo } <span class="hljs-keyword">from</span> <span class="hljs-string">'@carats/csr'</span>;<span class="hljs-keyword">import</span> { afterMount } <span class="hljs-keyword">from</span> <span class="hljs-string">'@carats/hooks'</span>;<span class="hljs-keyword">import</span> <span class="hljs-string">'./search-bar.sass'</span>;<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">SearchInput</span>(<span class="hljs-params"></span>) { <span class="hljs-title function_">afterMount</span>(<span class="hljs-function">() =></span> { <span class="hljs-keyword">const</span> form=<span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">'search-bar'</span>); <span class="hljs-keyword">const</span> <span class="hljs-title function_">submit</span>=(<span class="hljs-params"><span class="hljs-attr">e</span>: <span class="hljs-title class_">Event</span></span>) => { e.<span class="hljs-title function_">preventDefault</span>(); <span class="hljs-keyword">const</span> input=<span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">'search-input'</span>) <span class="hljs-keyword">as</span> <span class="hljs-title class_">HTMLInputElement</span>; <span class="hljs-title function_">goTo</span>(<span class="hljs-string">`/search?q=<span class="hljs-subst">${input.value}</span>`</span>); }; form?.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">'submit'</span>, submit); <span class="hljs-keyword">return</span> <span class="hljs-function">() =></span> form?.<span class="hljs-title function_">removeEventListener</span>(<span class="hljs-string">'submit'</span>, submit); }); <span class="hljs-keyword">return</span> ( <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">form</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"search-bar"</span>></span> <span class="hljs-tag"><<span class="hljs-name">input</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"search-input"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"q"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> /></span> <span class="hljs-tag"></<span class="hljs-name">form</span>></span></span> );}</code></pre><p>Build the query string with the standard `URLSearchParams`, which handles the encoding for you. To read the parameters of the current route from a component, ask a culet for them: a culet receives the matched parameters in `req.params`, and the page receives whatever the culet returned.</p><h2>Suspense States</h2><p>The suspense map supplies the components used when a route cannot produce a page: notFound for an unmatched URL and error for a component or culet that threw. Both are optional, and error receives the thrown Error as its only argument.</p><pre><code class="hljs language-tsx"><span class="hljs-comment">// src/client/pages/_not_found/index.tsx</span><span class="hljs-keyword">import</span> <span class="hljs-title class_">Layout</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'../_layout'</span>;<span class="hljs-keyword">import</span> <span class="hljs-string">'./notfound.sass'</span>;<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">NotFound</span>(<span class="hljs-params"></span>) { <span class="hljs-keyword">return</span> ( <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">Layout</span>></span> <span class="hljs-tag"><<span class="hljs-name">section</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"notfound-page container"</span>></span> <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"notfound-code"</span>></span>404<span class="hljs-tag"></<span class="hljs-name">div</span>></span> <span class="hljs-tag"><<span class="hljs-name">h1</span>></span>Page not found<span class="hljs-tag"></<span class="hljs-name">h1</span>></span> <span class="hljs-tag"><<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn btn-primary"</span>></span>Return to Dashboard<span class="hljs-tag"></<span class="hljs-name">a</span>></span> <span class="hljs-tag"></<span class="hljs-name">section</span>></span> <span class="hljs-tag"></<span class="hljs-name">Layout</span>></span></span> );}</code></pre><pre><code class="hljs language-tsx"><span class="hljs-comment">// src/client/pages/_error/index.tsx</span><span class="hljs-keyword">import</span> <span class="hljs-title class_">Layout</span> <span class="hljs-keyword">from</span> <span class="hljs-string">'../_layout'</span>;<span class="hljs-keyword">import</span> <span class="hljs-string">'./error.sass'</span>;<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">ErrorPage</span>(<span class="hljs-params"><span class="hljs-attr">error</span>: <span class="hljs-title class_">Error</span></span>) { <span class="hljs-keyword">return</span> ( <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">Layout</span>></span> <span class="hljs-tag"><<span class="hljs-name">section</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"error-page container"</span>></span> <span class="hljs-tag"><<span class="hljs-name">h1</span>></span>Something went wrong<span class="hljs-tag"></<span class="hljs-name">h1</span>></span> <span class="hljs-tag"><<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"error-message"</span>></span>{error.message}<span class="hljs-tag"></<span class="hljs-name">p</span>></span> <span class="hljs-tag"><<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn btn-primary"</span>></span>Return to Dashboard<span class="hljs-tag"></<span class="hljs-name">a</span>></span> <span class="hljs-tag"></<span class="hljs-name">section</span>></span> <span class="hljs-tag"></<span class="hljs-name">Layout</span>></span></span> );}</code></pre><p>Wrap both in the shared layout. A failing page that renders without the site chrome is much harder to diagnose than one that keeps the header and navigation.</p></div> </article> </main> <footer> <span class="footer-mark">◆ carats · documentation</span></footer> </div> <script src="/static/js/ui.js"></script> <script type="module" src="/static/js/search.js" defer></script> </body></html>