@real-router/svelte 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -363,18 +363,30 @@ Enable screen reader announcements for route changes:
363
363
 
364
364
  When enabled, a visually hidden `aria-live` region announces each navigation. Focus moves to the first `<h1>` on the new page. See [Accessibility guide](https://github.com/greydragon888/real-router/wiki/Accessibility) for details.
365
365
 
366
+ ## Scroll Restoration
367
+
368
+ Opt-in preservation of scroll position across navigations:
369
+
370
+ ```svelte
371
+ <RouterProvider {router} scrollRestoration={{ mode: "restore" }}>
372
+ <!-- Your app -->
373
+ </RouterProvider>
374
+ ```
375
+
376
+ Restores scroll on back/forward, scrolls to top (or `#hash`) on push. Three modes: `"restore"` (default), `"top"`, `"manual"`. Custom containers via `scrollContainer: () => HTMLElement | null`. Lifecycle tied to the provider — created on mount, destroyed on unmount. See [Scroll Restoration guide](https://github.com/greydragon888/real-router/wiki/Scroll-Restoration) for details.
377
+
366
378
  ## Documentation
367
379
 
368
380
  Full documentation: [Wiki](https://github.com/greydragon888/real-router/wiki)
369
381
 
370
- - [RouterProvider](https://github.com/greydragon888/real-router/wiki/RouterProvider) · [RouteView](https://github.com/greydragon888/real-router/wiki/RouteView) · [RouterErrorBoundary](https://github.com/greydragon888/real-router/wiki/RouterErrorBoundary) · [Link](https://github.com/greydragon888/real-router/wiki/Link)
382
+ - [RouterProvider](https://github.com/greydragon888/real-router/wiki/RouterProvider) · [RouteView](https://github.com/greydragon888/real-router/wiki/RouteView) · [RouterErrorBoundary](https://github.com/greydragon888/real-router/wiki/RouterErrorBoundary) · [Link](https://github.com/greydragon888/real-router/wiki/Link) · [Scroll Restoration](https://github.com/greydragon888/real-router/wiki/Scroll-Restoration)
371
383
  - [useRouter](https://github.com/greydragon888/real-router/wiki/useRouter) · [useRoute](https://github.com/greydragon888/real-router/wiki/useRoute) · [useRouteNode](https://github.com/greydragon888/real-router/wiki/useRouteNode) · [useNavigator](https://github.com/greydragon888/real-router/wiki/useNavigator) · [useRouteUtils](https://github.com/greydragon888/real-router/wiki/useRouteUtils) · [useRouterTransition](https://github.com/greydragon888/real-router/wiki/useRouterTransition)
372
384
 
373
385
  ## Examples
374
386
 
375
- 16 runnable examples — each is a standalone Vite app. Run: `cd examples/svelte/basic && pnpm dev`
387
+ 16 runnable examples — each is a standalone Vite app. Run: `cd examples/web/svelte/basic && pnpm dev`
376
388
 
377
- [basic](../../examples/svelte/basic) · [nested-routes](../../examples/svelte/nested-routes) · [auth-guards](../../examples/svelte/auth-guards) · [data-loading](../../examples/svelte/data-loading) · [lazy-loading](../../examples/svelte/lazy-loading) · [async-guards](../../examples/svelte/async-guards) · [hash-routing](../../examples/svelte/hash-routing) · [persistent-params](../../examples/svelte/persistent-params) · [error-handling](../../examples/svelte/error-handling) · [dynamic-routes](../../examples/svelte/dynamic-routes) · [link-action](../../examples/svelte/link-action) · [lazy-loading-svelte](../../examples/svelte/lazy-loading-svelte) · [snippets-routing](../../examples/svelte/snippets-routing) · [reactive-source](../../examples/svelte/reactive-source) · [search-schema](../../examples/svelte/search-schema) · [combined](../../examples/svelte/combined)
389
+ [basic](../../examples/web/svelte/basic) · [nested-routes](../../examples/web/svelte/nested-routes) · [auth-guards](../../examples/web/svelte/auth-guards) · [data-loading](../../examples/web/svelte/data-loading) · [lazy-loading](../../examples/web/svelte/lazy-loading) · [async-guards](../../examples/web/svelte/async-guards) · [hash-routing](../../examples/web/svelte/hash-routing) · [persistent-params](../../examples/web/svelte/persistent-params) · [error-handling](../../examples/web/svelte/error-handling) · [dynamic-routes](../../examples/web/svelte/dynamic-routes) · [link-action](../../examples/web/svelte/link-action) · [lazy-loading-svelte](../../examples/web/svelte/lazy-loading-svelte) · [snippets-routing](../../examples/web/svelte/snippets-routing) · [reactive-source](../../examples/web/svelte/reactive-source) · [search-schema](../../examples/web/svelte/search-schema) · [combined](../../examples/web/svelte/combined)
378
390
 
379
391
  ## Related Packages
380
392
 
@@ -1,13 +1,17 @@
1
1
  <script lang="ts">
2
2
  import { getNavigator } from "@real-router/core";
3
3
  import { createRouteSource } from "@real-router/sources";
4
- import { createRouteAnnouncer } from "./dom-utils/index.js";
5
- import { setContext } from "svelte";
4
+ import {
5
+ createRouteAnnouncer,
6
+ createScrollRestoration,
7
+ } from "./dom-utils";
8
+ import { setContext, untrack } from "svelte";
6
9
 
7
10
  import { createReactiveSource } from "./createReactiveSource.svelte";
8
11
  import { createRouteContext } from "./createRouteContext.svelte";
9
12
  import { NAVIGATOR_KEY, ROUTE_KEY, ROUTER_KEY } from "./context";
10
13
 
14
+ import type { ScrollRestorationOptions } from "./dom-utils";
11
15
  import type { Router } from "@real-router/core";
12
16
  import type { Snippet } from "svelte";
13
17
 
@@ -15,8 +19,13 @@
15
19
  router,
16
20
  children,
17
21
  announceNavigation,
18
- }: { router: Router; children: Snippet; announceNavigation?: boolean } =
19
- $props();
22
+ scrollRestoration,
23
+ }: {
24
+ router: Router;
25
+ children: Snippet;
26
+ announceNavigation?: boolean;
27
+ scrollRestoration?: ScrollRestorationOptions;
28
+ } = $props();
20
29
 
21
30
  $effect(() => {
22
31
  if (!announceNavigation) return;
@@ -24,6 +33,27 @@
24
33
  return () => announcer.destroy();
25
34
  });
26
35
 
36
+ // $derived memoizes by === so inline `{ mode: "restore" }` doesn't thrash:
37
+ // each parent re-render produces a new object ref, but .mode stays "restore"
38
+ // → $derived returns the same primitive → $effect doesn't re-run.
39
+ const srEnabled = $derived(scrollRestoration !== undefined);
40
+ const srMode = $derived(scrollRestoration?.mode);
41
+ const srAnchor = $derived(scrollRestoration?.anchorScrolling);
42
+
43
+ $effect(() => {
44
+ if (!srEnabled) return;
45
+ // scrollContainer is a function ref that naturally changes each render.
46
+ // Read it via `untrack` so this $effect does NOT depend on the parent
47
+ // `scrollRestoration` signal. Without this, a new inline options object
48
+ // would re-run the effect regardless of the primitive $derived memos.
49
+ const sr = createScrollRestoration(router, {
50
+ mode: srMode,
51
+ anchorScrolling: srAnchor,
52
+ scrollContainer: untrack(() => scrollRestoration?.scrollContainer),
53
+ });
54
+ return () => sr.destroy();
55
+ });
56
+
27
57
  const navigator = getNavigator(router);
28
58
  const source = createRouteSource(router);
29
59
  const reactive = createReactiveSource(source);
@@ -1,9 +1,11 @@
1
+ import type { ScrollRestorationOptions } from "./dom-utils";
1
2
  import type { Router } from "@real-router/core";
2
3
  import type { Snippet } from "svelte";
3
4
  type $$ComponentProps = {
4
5
  router: Router;
5
6
  children: Snippet;
6
7
  announceNavigation?: boolean;
8
+ scrollRestoration?: ScrollRestorationOptions;
7
9
  };
8
10
  declare const RouterProvider: import("svelte").Component<$$ComponentProps, {}, "">;
9
11
  type RouterProvider = ReturnType<typeof RouterProvider>;
@@ -1,6 +1,6 @@
1
1
  import { ROUTER_KEY, getContextOrThrow } from "../context";
2
2
  import { EMPTY_OPTIONS, EMPTY_PARAMS, NOOP } from "../constants";
3
- import { shouldNavigate, applyLinkA11y } from "../dom-utils/index.js";
3
+ import { shouldNavigate, applyLinkA11y } from "../dom-utils";
4
4
  /**
5
5
  * Factory function that captures router context during component initialization.
6
6
  * Must be called during component init (not inside event handlers or effects).
@@ -6,7 +6,7 @@
6
6
  shouldNavigate,
7
7
  buildHref,
8
8
  buildActiveClassName,
9
- } from "../dom-utils/index.js";
9
+ } from "../dom-utils";
10
10
 
11
11
  import type { NavigationOptions, Params } from "@real-router/core";
12
12
  import type { Snippet } from "svelte";
@@ -1,6 +1,10 @@
1
1
  <script lang="ts" module>
2
2
  import { startsWithSegment } from "@real-router/route-utils";
3
3
 
4
+ // Snippet names reserved by RouteView for non-segment slots. Iteration in
5
+ // `getActiveSegment` skips these so they don't accidentally match a route.
6
+ const RESERVED_SLOT_NAMES = new Set(["self", "notFound"]);
7
+
4
8
  export function getActiveSegment(
5
9
  routeName: string,
6
10
  node: string,
@@ -9,7 +13,7 @@
9
13
  const prefix = node ? `${node}.` : "";
10
14
 
11
15
  for (const segment in snippets) {
12
- if (segment === "notFound") continue;
16
+ if (RESERVED_SLOT_NAMES.has(segment)) continue;
13
17
  if (startsWithSegment(routeName, prefix + segment)) {
14
18
  return segment;
15
19
  }
@@ -28,10 +32,12 @@
28
32
 
29
33
  let {
30
34
  nodeName,
35
+ self,
31
36
  notFound,
32
37
  ...segmentSnippets
33
38
  }: {
34
39
  nodeName: string;
40
+ self?: Snippet;
35
41
  notFound?: Snippet;
36
42
  [key: string]: Snippet | string | undefined;
37
43
  } = $props();
@@ -45,6 +51,8 @@
45
51
  {#if segment}
46
52
  {@const snippet = segmentSnippets[segment] as Snippet}
47
53
  {@render snippet()}
54
+ {:else if self && route.name === nodeName}
55
+ {@render self()}
48
56
  {:else if route.name === UNKNOWN_ROUTE && notFound}
49
57
  {@render notFound()}
50
58
  {/if}
@@ -2,6 +2,7 @@ export declare function getActiveSegment(routeName: string, node: string, snippe
2
2
  import type { Snippet } from "svelte";
3
3
  type $$ComponentProps = {
4
4
  nodeName: string;
5
+ self?: Snippet;
5
6
  notFound?: Snippet;
6
7
  [key: string]: Snippet | string | undefined;
7
8
  };
@@ -1,3 +1,5 @@
1
1
  export { createRouteAnnouncer } from "./route-announcer.js";
2
+ export { createScrollRestoration } from "./scroll-restore.js";
2
3
  export { shouldNavigate, buildHref, buildActiveClassName, shallowEqual, applyLinkA11y, } from "./link-utils.js";
3
4
  export type { RouteAnnouncerOptions } from "./route-announcer.js";
5
+ export type { ScrollRestorationOptions, ScrollRestorationMode, } from "./scroll-restore.js";
@@ -1,2 +1,3 @@
1
1
  export { createRouteAnnouncer } from "./route-announcer.js";
2
+ export { createScrollRestoration } from "./scroll-restore.js";
2
3
  export { shouldNavigate, buildHref, buildActiveClassName, shallowEqual, applyLinkA11y, } from "./link-utils.js";
@@ -0,0 +1,10 @@
1
+ import type { Router } from "@real-router/core";
2
+ export type ScrollRestorationMode = "restore" | "top" | "manual";
3
+ export interface ScrollRestorationOptions {
4
+ mode?: ScrollRestorationMode | undefined;
5
+ anchorScrolling?: boolean | undefined;
6
+ scrollContainer?: (() => HTMLElement | null) | undefined;
7
+ }
8
+ export declare function createScrollRestoration(router: Router, options?: ScrollRestorationOptions): {
9
+ destroy: () => void;
10
+ };
@@ -0,0 +1,157 @@
1
+ const STORAGE_KEY = "real-router:scroll";
2
+ const NOOP_INSTANCE = Object.freeze({
3
+ destroy: () => {
4
+ /* no-op */
5
+ },
6
+ });
7
+ export function createScrollRestoration(router, options) {
8
+ if (typeof globalThis.window === "undefined") {
9
+ return NOOP_INSTANCE;
10
+ }
11
+ const mode = options?.mode ?? "restore";
12
+ // mode "manual" = utility does nothing. Don't flip history.scrollRestoration,
13
+ // don't subscribe, don't register pagehide — leave the browser's native
14
+ // auto-restore intact for the app to override if it wants to.
15
+ if (mode === "manual") {
16
+ return NOOP_INSTANCE;
17
+ }
18
+ const anchorEnabled = options?.anchorScrolling ?? true;
19
+ const getContainer = options?.scrollContainer;
20
+ const prevScrollRestoration = history.scrollRestoration;
21
+ try {
22
+ history.scrollRestoration = "manual";
23
+ }
24
+ catch {
25
+ // Ignore — some embedded contexts may reject the assignment.
26
+ }
27
+ // Resolve the container lazily on every event so containers mounted AFTER
28
+ // the provider still get correct scroll handling. Falls back to window when
29
+ // the getter is absent or returns null (pre-mount).
30
+ const readPos = () => {
31
+ const element = getContainer?.();
32
+ return element ? element.scrollTop : globalThis.scrollY;
33
+ };
34
+ const writePos = (top) => {
35
+ const element = getContainer?.();
36
+ if (element) {
37
+ element.scrollTop = top;
38
+ }
39
+ else {
40
+ globalThis.scrollTo(0, top);
41
+ }
42
+ };
43
+ const scrollToHashOrTop = () => {
44
+ const hash = globalThis.location.hash;
45
+ if (anchorEnabled && hash.length > 1) {
46
+ // location.hash is percent-encoded; ids in the DOM are the raw string.
47
+ // Decode for the match. Fall back to the raw slice if the hash contains
48
+ // a malformed escape sequence (decodeURIComponent throws on those).
49
+ let id;
50
+ try {
51
+ id = decodeURIComponent(hash.slice(1));
52
+ }
53
+ catch {
54
+ id = hash.slice(1);
55
+ }
56
+ // eslint-disable-next-line unicorn/prefer-query-selector -- ids may contain CSS-unsafe chars
57
+ const element = document.getElementById(id);
58
+ if (element) {
59
+ element.scrollIntoView();
60
+ return;
61
+ }
62
+ }
63
+ writePos(0);
64
+ };
65
+ let destroyed = false;
66
+ const unsubscribe = router.subscribe(({ route, previousRoute }) => {
67
+ const nav = route.context
68
+ .navigation;
69
+ // Browsers dispatch reload as the initial navigation after refresh, so
70
+ // previousRoute is undefined and capture is naturally skipped. The
71
+ // pre-refresh position was already persisted via pagehide.
72
+ if (previousRoute) {
73
+ putPos(keyOf(previousRoute), readPos());
74
+ }
75
+ // Single rAF so DOM is committed before we read anchors / write scroll.
76
+ // Guard against destroy() racing with the callback.
77
+ requestAnimationFrame(() => {
78
+ if (destroyed) {
79
+ return;
80
+ }
81
+ if (mode === "top" || !nav) {
82
+ scrollToHashOrTop();
83
+ return;
84
+ }
85
+ if (nav.navigationType === "replace") {
86
+ return;
87
+ }
88
+ if (nav.direction === "back" ||
89
+ nav.navigationType === "traverse" ||
90
+ nav.navigationType === "reload") {
91
+ writePos(loadStore()[keyOf(route)] ?? 0);
92
+ return;
93
+ }
94
+ scrollToHashOrTop();
95
+ });
96
+ });
97
+ const onPageHide = () => {
98
+ const current = router.getState();
99
+ if (current) {
100
+ putPos(keyOf(current), readPos());
101
+ }
102
+ };
103
+ globalThis.addEventListener("pagehide", onPageHide);
104
+ return {
105
+ destroy: () => {
106
+ if (destroyed) {
107
+ return;
108
+ }
109
+ destroyed = true;
110
+ unsubscribe();
111
+ globalThis.removeEventListener("pagehide", onPageHide);
112
+ try {
113
+ history.scrollRestoration = prevScrollRestoration;
114
+ }
115
+ catch {
116
+ // Ignore.
117
+ }
118
+ },
119
+ };
120
+ }
121
+ function keyOf(state) {
122
+ return `${state.name}:${canonicalJson(state.params)}`;
123
+ }
124
+ function loadStore() {
125
+ try {
126
+ const raw = sessionStorage.getItem(STORAGE_KEY);
127
+ return raw ? JSON.parse(raw) : {};
128
+ }
129
+ catch {
130
+ return {};
131
+ }
132
+ }
133
+ function putPos(key, pos) {
134
+ try {
135
+ const store = loadStore();
136
+ store[key] = pos;
137
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(store));
138
+ }
139
+ catch {
140
+ // Ignore quota / security errors.
141
+ }
142
+ }
143
+ function canonicalJson(value) {
144
+ return JSON.stringify(value, canonicalReplacer);
145
+ }
146
+ function canonicalReplacer(_key, val) {
147
+ if (val !== null && typeof val === "object" && !Array.isArray(val)) {
148
+ const sorted = {};
149
+ // eslint-disable-next-line unicorn/no-array-sort -- ng-packagr uses pre-ES2023 lib; toSorted unavailable
150
+ const keys = Object.keys(val).sort((left, right) => left.localeCompare(right));
151
+ for (const key of keys) {
152
+ sorted[key] = val[key];
153
+ }
154
+ return sorted;
155
+ }
156
+ return val;
157
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/svelte",
3
- "version": "0.4.2",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Svelte 5 integration for Real-Router",
6
6
  "svelte": "./dist/index.js",
@@ -44,7 +44,7 @@
44
44
  "license": "MIT",
45
45
  "sideEffects": false,
46
46
  "dependencies": {
47
- "@real-router/core": "^0.50.0",
47
+ "@real-router/core": "^0.50.1",
48
48
  "@real-router/route-utils": "^0.2.1",
49
49
  "@real-router/sources": "^0.7.2"
50
50
  },
@@ -58,7 +58,7 @@
58
58
  "svelte": "5.54.0",
59
59
  "svelte-check": "4.4.5",
60
60
  "svelte-eslint-parser": "1.6.0",
61
- "@real-router/browser-plugin": "^0.14.0"
61
+ "@real-router/browser-plugin": "^0.15.1"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "svelte": ">=5.7.0"
@@ -1,13 +1,17 @@
1
1
  <script lang="ts">
2
2
  import { getNavigator } from "@real-router/core";
3
3
  import { createRouteSource } from "@real-router/sources";
4
- import { createRouteAnnouncer } from "./dom-utils/index.js";
5
- import { setContext } from "svelte";
4
+ import {
5
+ createRouteAnnouncer,
6
+ createScrollRestoration,
7
+ } from "./dom-utils";
8
+ import { setContext, untrack } from "svelte";
6
9
 
7
10
  import { createReactiveSource } from "./createReactiveSource.svelte";
8
11
  import { createRouteContext } from "./createRouteContext.svelte";
9
12
  import { NAVIGATOR_KEY, ROUTE_KEY, ROUTER_KEY } from "./context";
10
13
 
14
+ import type { ScrollRestorationOptions } from "./dom-utils";
11
15
  import type { Router } from "@real-router/core";
12
16
  import type { Snippet } from "svelte";
13
17
 
@@ -15,8 +19,13 @@
15
19
  router,
16
20
  children,
17
21
  announceNavigation,
18
- }: { router: Router; children: Snippet; announceNavigation?: boolean } =
19
- $props();
22
+ scrollRestoration,
23
+ }: {
24
+ router: Router;
25
+ children: Snippet;
26
+ announceNavigation?: boolean;
27
+ scrollRestoration?: ScrollRestorationOptions;
28
+ } = $props();
20
29
 
21
30
  $effect(() => {
22
31
  if (!announceNavigation) return;
@@ -24,6 +33,27 @@
24
33
  return () => announcer.destroy();
25
34
  });
26
35
 
36
+ // $derived memoizes by === so inline `{ mode: "restore" }` doesn't thrash:
37
+ // each parent re-render produces a new object ref, but .mode stays "restore"
38
+ // → $derived returns the same primitive → $effect doesn't re-run.
39
+ const srEnabled = $derived(scrollRestoration !== undefined);
40
+ const srMode = $derived(scrollRestoration?.mode);
41
+ const srAnchor = $derived(scrollRestoration?.anchorScrolling);
42
+
43
+ $effect(() => {
44
+ if (!srEnabled) return;
45
+ // scrollContainer is a function ref that naturally changes each render.
46
+ // Read it via `untrack` so this $effect does NOT depend on the parent
47
+ // `scrollRestoration` signal. Without this, a new inline options object
48
+ // would re-run the effect regardless of the primitive $derived memos.
49
+ const sr = createScrollRestoration(router, {
50
+ mode: srMode,
51
+ anchorScrolling: srAnchor,
52
+ scrollContainer: untrack(() => scrollRestoration?.scrollContainer),
53
+ });
54
+ return () => sr.destroy();
55
+ });
56
+
27
57
  const navigator = getNavigator(router);
28
58
  const source = createRouteSource(router);
29
59
  const reactive = createReactiveSource(source);
@@ -2,7 +2,7 @@ import type { ActionReturn } from "svelte/action";
2
2
  import type { Router, Params, NavigationOptions } from "@real-router/core";
3
3
  import { ROUTER_KEY, getContextOrThrow } from "../context";
4
4
  import { EMPTY_OPTIONS, EMPTY_PARAMS, NOOP } from "../constants";
5
- import { shouldNavigate, applyLinkA11y } from "../dom-utils/index.js";
5
+ import { shouldNavigate, applyLinkA11y } from "../dom-utils";
6
6
 
7
7
  export interface LinkActionParams {
8
8
  name: string;
@@ -6,7 +6,7 @@
6
6
  shouldNavigate,
7
7
  buildHref,
8
8
  buildActiveClassName,
9
- } from "../dom-utils/index.js";
9
+ } from "../dom-utils";
10
10
 
11
11
  import type { NavigationOptions, Params } from "@real-router/core";
12
12
  import type { Snippet } from "svelte";
@@ -1,6 +1,10 @@
1
1
  <script lang="ts" module>
2
2
  import { startsWithSegment } from "@real-router/route-utils";
3
3
 
4
+ // Snippet names reserved by RouteView for non-segment slots. Iteration in
5
+ // `getActiveSegment` skips these so they don't accidentally match a route.
6
+ const RESERVED_SLOT_NAMES = new Set(["self", "notFound"]);
7
+
4
8
  export function getActiveSegment(
5
9
  routeName: string,
6
10
  node: string,
@@ -9,7 +13,7 @@
9
13
  const prefix = node ? `${node}.` : "";
10
14
 
11
15
  for (const segment in snippets) {
12
- if (segment === "notFound") continue;
16
+ if (RESERVED_SLOT_NAMES.has(segment)) continue;
13
17
  if (startsWithSegment(routeName, prefix + segment)) {
14
18
  return segment;
15
19
  }
@@ -28,10 +32,12 @@
28
32
 
29
33
  let {
30
34
  nodeName,
35
+ self,
31
36
  notFound,
32
37
  ...segmentSnippets
33
38
  }: {
34
39
  nodeName: string;
40
+ self?: Snippet;
35
41
  notFound?: Snippet;
36
42
  [key: string]: Snippet | string | undefined;
37
43
  } = $props();
@@ -45,6 +51,8 @@
45
51
  {#if segment}
46
52
  {@const snippet = segmentSnippets[segment] as Snippet}
47
53
  {@render snippet()}
54
+ {:else if self && route.name === nodeName}
55
+ {@render self()}
48
56
  {:else if route.name === UNKNOWN_ROUTE && notFound}
49
57
  {@render notFound()}
50
58
  {/if}