Defining routes
Like every full-stack meta-framework, routes are the central concept of Gracile.
This is where everything got tied together, and besides that, and add-ons, there aren’t many opinions left.
Gracile comes with a dedicated function that will take care of typings as with JS or TS, and nothing more.
Under the hood, it uses the URLPattern
API which uses converted patterns like this:
/src/routes/foo/[param]/foo.ts
→/foo/:param/foo
/src/routes/bar/[...path]
→/bar/:path*
Index
You have two ways to define an index for a folder.
- Using like:
foo/my-folder/index.ts
- With parentheses like:
foo/(anything).ts
,foo/(foo).ts
…
Respectively:
/src/routes/foo/index.ts
→/foo/
/src/routes/foo/(foo).ts
→/foo/
The parentheses pattern is especially useful for quick file switching with your IDE, where a lot of index
es can be confusing, same when debugging an error trace.
Note that when indexes are noted that way, the first one will be chosen (alphabetically).
Ignored files and directories
- Client-side sibling scripts (
<ROUTE>.client.{js,ts}
) - Client-side sibling styles (
<ROUTE>.{css,scss,…}
) - Leading underscore
_*.{js,ts}
,_my-dir/*
- Leading dotfiles/directories (hidden on OS).
defineRoute
parameters
The defineRoute
provides a type-safe API that can be used with JavaScript or TypeScript.
It’s analog to how numerous OSS projects are providing their configuration API (like Vite’s defineConfig
).
document
Provides the base document for the route’s page template.
Given a pre-existing document, you’ll import it like this in your route configuration:
📄 /src/document.ts
import { htmlfunction html(strings: TemplateStringsArray, ...values: unknown[]): ServerRenderedTemplate
A lit-html template that can only be rendered on the server, and cannot be
hydrated.
These templates can be used for rendering full documents, including the
doctype, and rendering into elements that Lit normally cannot, like
<title>
, <textarea>
, <template>
, and non-executing <script>
tags
like <script type="text/json">
. They are also slightly more efficient than
normal Lit templates, because the generated HTML doesn't need to include
markers for updating.
Server-only html
templates can be composed, and combined, and they support
almost all features that normal Lit templates do, with the exception of
features that don't have a pure HTML representation, like event handlers or
property bindings.
Server-only html
templates can only be rendered on the server, they will
throw an Error if created in the browser. However if you render a normal Lit
template inside a server-only template, then it can be hydrated and updated.
Likewise, if you place a custom element inside a server-only template, it can
be hydrated and update like normal.
A server-only template can't be rendered inside a normal Lit template.
} from '@gracile/gracile/server-html';
export const documentconst document: (options: {
url: URL;
}) => ServerRenderedTemplate
= (optionsoptions: {
url: URL;
}
: { urlurl: URL
: URL }) => htmlfunction html(strings: TemplateStringsArray, ...values: unknown[]): ServerRenderedTemplate
A lit-html template that can only be rendered on the server, and cannot be
hydrated.
These templates can be used for rendering full documents, including the
doctype, and rendering into elements that Lit normally cannot, like
<title>
, <textarea>
, <template>
, and non-executing <script>
tags
like <script type="text/json">
. They are also slightly more efficient than
normal Lit templates, because the generated HTML doesn't need to include
markers for updating.
Server-only html
templates can be composed, and combined, and they support
almost all features that normal Lit templates do, with the exception of
features that don't have a pure HTML representation, like event handlers or
property bindings.
Server-only html
templates can only be rendered on the server, they will
throw an Error if created in the browser. However if you render a normal Lit
template inside a server-only template, then it can be hydrated and updated.
Likewise, if you place a custom element inside a server-only template, it can
be hydrated and update like normal.
A server-only template can't be rendered inside a normal Lit template.
`
<html>
<head>
<!-- ... -->
<title>${url.pathname}</title>
</head>
<body>
<route-template-outlet></route-template-outlet>
</body>
</html>
`;
📄 /src/routes/my-page.ts
import { htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>
Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html
tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
} from 'lit';
import { defineRoutefunction defineRoute<GetHandlerData extends R.HandlerDataHtml = undefined, PostHandlerData extends R.HandlerDataHtml = undefined, StaticPathOptions extends R.StaticPathOptionsGeneric | undefined = undefined, RouteContext extends R.RouteContextGeneric = {
url: URL;
props: StaticPathOptions extends {
params: any;
props: any;
} ? StaticPathOptions["props"] : GetHandlerData | PostHandlerData extends undefined ? never : {
GET: GetHandlerData extends Response ? never : GetHandlerData;
POST: PostHandlerData extends Response ? never : PostHandlerData;
};
params: StaticPathOptions extends {
params: any;
} ? StaticPathOptions["params"] : R.Params;
}>(options: {
handler?: StaticPathOptions extends object ? never : R.Handler<Response> | {
GET?: R.Handler<GetHandlerData>;
POST?: R.Handler<PostHandlerData>;
QUERY?: R.Handler;
PUT?: R.Handler;
PATCH?: R.Handler;
DELETE?: R.Handler;
HEAD?: R.Handler;
OPTIONS?: R.Handler;
};
staticPaths?: (() => StaticPathOptions[]) | undefined;
document?: R.DocumentTemplate<RouteContext>;
template?: R.BodyTemplate<RouteContext>;
}): (RouteModule: typeof R.RouteModule) => R.RouteModule
Defines a route.
See in the documentation.
} from '@gracile/gracile/route';
import { documentconst document: (options: {
url: URL;
}) => ServerRenderedTemplate
} from '../document.js';
export default defineRoutedefineRoute<undefined, undefined, undefined, {
url: URL;
props: never;
params: Params;
}>(options: {
handler?: Handler<Response> | {
GET?: Handler<undefined> | undefined;
... 6 more ...;
OPTIONS?: Handler<...> | undefined;
} | undefined;
staticPaths?: (() => undefined[]) | undefined;
document?: DocumentTemplate<...> | undefined;
template?: BodyTemplate<...> | undefined;
}): (RouteModule: typeof RouteModule) => RouteModule
Defines a route.
See in the documentation.
({
documentdocument?: DocumentTemplate<{
url: URL;
props: never;
params: Params;
}> | undefined
: (contextcontext: {
url: URL;
props: never;
params: Params;
}
) => documentfunction document(options: {
url: URL;
}): ServerRenderedTemplate
(contextcontext: {
url: URL;
props: never;
params: Params;
}
),
templatetemplate?: BodyTemplate<{
url: URL;
props: never;
params: Params;
}> | undefined
: () => htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>
Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html
tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
`...`,
});
template
Provides a server-renderable template.
When combined with an enclosing document
, we’ll call it a “Page”.
When used alone, we’ll call it an HTML “Fragment”.
📄 /src/routes/my-page.ts
import { htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>
Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html
tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
} from 'lit';
import { defineRoutefunction defineRoute<GetHandlerData extends R.HandlerDataHtml = undefined, PostHandlerData extends R.HandlerDataHtml = undefined, StaticPathOptions extends R.StaticPathOptionsGeneric | undefined = undefined, RouteContext extends R.RouteContextGeneric = {
url: URL;
props: StaticPathOptions extends {
params: any;
props: any;
} ? StaticPathOptions["props"] : GetHandlerData | PostHandlerData extends undefined ? never : {
GET: GetHandlerData extends Response ? never : GetHandlerData;
POST: PostHandlerData extends Response ? never : PostHandlerData;
};
params: StaticPathOptions extends {
params: any;
} ? StaticPathOptions["params"] : R.Params;
}>(options: {
handler?: StaticPathOptions extends object ? never : R.Handler<Response> | {
GET?: R.Handler<GetHandlerData>;
POST?: R.Handler<PostHandlerData>;
QUERY?: R.Handler;
PUT?: R.Handler;
PATCH?: R.Handler;
DELETE?: R.Handler;
HEAD?: R.Handler;
OPTIONS?: R.Handler;
};
staticPaths?: (() => StaticPathOptions[]) | undefined;
document?: R.DocumentTemplate<RouteContext>;
template?: R.BodyTemplate<RouteContext>;
}): (RouteModule: typeof R.RouteModule) => R.RouteModule
Defines a route.
See in the documentation.
} from '@gracile/gracile/route';
export default defineRoutedefineRoute<undefined, undefined, undefined, {
url: URL;
props: never;
params: Params;
}>(options: {
handler?: Handler<Response> | {
GET?: Handler<undefined> | undefined;
... 6 more ...;
OPTIONS?: Handler<...> | undefined;
} | undefined;
staticPaths?: (() => undefined[]) | undefined;
document?: DocumentTemplate<...> | undefined;
template?: BodyTemplate<...> | undefined;
}): (RouteModule: typeof RouteModule) => RouteModule
Defines a route.
See in the documentation.
({
// ...
templatetemplate?: BodyTemplate<{
url: URL;
props: never;
params: Params;
}> | undefined
: (contextcontext: {
url: URL;
props: never;
params: Params;
}
) => htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>
Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html
tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
`
<main>
<article class="prose">Hello</article>
</main>
`,
});
staticPaths
Used with static mode only.
You can provide props
and params
for populating page data.
template
and document
contexts will be properly typed thanks to the staticPaths
function return signature.
Hover context.props
and context.params
to see!
📄 /src/routes/[...path].ts
import { htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>
Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html
tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
} from 'lit';
import { defineRoutefunction defineRoute<GetHandlerData extends R.HandlerDataHtml = undefined, PostHandlerData extends R.HandlerDataHtml = undefined, StaticPathOptions extends R.StaticPathOptionsGeneric | undefined = undefined, RouteContext extends R.RouteContextGeneric = {
url: URL;
props: StaticPathOptions extends {
params: any;
props: any;
} ? StaticPathOptions["props"] : GetHandlerData | PostHandlerData extends undefined ? never : {
GET: GetHandlerData extends Response ? never : GetHandlerData;
POST: PostHandlerData extends Response ? never : PostHandlerData;
};
params: StaticPathOptions extends {
params: any;
} ? StaticPathOptions["params"] : R.Params;
}>(options: {
handler?: StaticPathOptions extends object ? never : R.Handler<Response> | {
GET?: R.Handler<GetHandlerData>;
POST?: R.Handler<PostHandlerData>;
QUERY?: R.Handler;
PUT?: R.Handler;
PATCH?: R.Handler;
DELETE?: R.Handler;
HEAD?: R.Handler;
OPTIONS?: R.Handler;
};
staticPaths?: (() => StaticPathOptions[]) | undefined;
document?: R.DocumentTemplate<RouteContext>;
template?: R.BodyTemplate<RouteContext>;
}): (RouteModule: typeof R.RouteModule) => R.RouteModule
Defines a route.
See in the documentation.
} from '@gracile/gracile/route';
import { documentconst document: (options: {
url: URL;
}) => ServerRenderedTemplate
} from '../document.js';
export default defineRoutedefineRoute<undefined, undefined, {
readonly params: {
readonly path: "my/first-cat";
};
readonly props: {
readonly cat: "Kino";
};
} | {
readonly params: {
readonly path: "my/second-cat";
};
readonly props: {
...;
};
}, {
...;
}>(options: {
...;
}): (RouteModule: typeof RouteModule) => RouteModule
Defines a route.
See in the documentation.
({
staticPathsstaticPaths?: (() => ({
readonly params: {
readonly path: "my/first-cat";
};
readonly props: {
readonly cat: "Kino";
};
} | {
readonly params: {
readonly path: "my/second-cat";
};
readonly props: {
readonly cat: "Valentine";
};
})[]) | undefined
: () =>
[
{
paramsparams: {
readonly path: "my/first-cat";
}
: { pathpath: "my/first-cat"
: 'my/first-cat' },
propsprops: {
readonly cat: "Kino";
}
: { catcat: "Kino"
: 'Kino' },
},
{
paramsparams: {
readonly path: "my/second-cat";
}
: { pathpath: "my/second-cat"
: 'my/second-cat' },
propsprops: {
readonly cat: "Valentine";
}
: { catcat: "Valentine"
: 'Valentine' },
},
] as consttype const = [{
readonly params: {
readonly path: "my/first-cat";
};
readonly props: {
readonly cat: "Kino";
};
}, {
readonly params: {
readonly path: "my/second-cat";
};
readonly props: {
readonly cat: "Valentine";
};
}]
,
documentdocument?: DocumentTemplate<{
url: URL;
props: {
readonly cat: "Kino";
} | {
readonly cat: "Valentine";
};
params: {
readonly path: "my/first-cat";
} | {
readonly path: "my/second-cat";
};
}> | undefined
: (contextcontext: {
url: URL;
props: {
readonly cat: "Kino";
} | {
readonly cat: "Valentine";
};
params: {
readonly path: "my/first-cat";
} | {
readonly path: "my/second-cat";
};
}
) => documentfunction document(options: {
url: URL;
}): ServerRenderedTemplate
({ ...contextcontext: {
url: URL;
props: {
readonly cat: "Kino";
} | {
readonly cat: "Valentine";
};
params: {
readonly path: "my/first-cat";
} | {
readonly path: "my/second-cat";
};
}
, titletitle: "Kino" | "Valentine"
: contextcontext: {
url: URL;
props: {
readonly cat: "Kino";
} | {
readonly cat: "Valentine";
};
params: {
readonly path: "my/first-cat";
} | {
readonly path: "my/second-cat";
};
}
.propsprops: {
readonly cat: "Kino";
} | {
readonly cat: "Valentine";
}
.catcat: "Kino" | "Valentine"
}),
Hover the tokens to see the typings reflection.
templatetemplate?: BodyTemplate<{
url: URL;
props: {
readonly cat: "Kino";
} | {
readonly cat: "Valentine";
};
params: {
readonly path: "my/first-cat";
} | {
readonly path: "my/second-cat";
};
}> | undefined
: async (contextcontext: {
url: URL;
props: {
readonly cat: "Kino";
} | {
readonly cat: "Valentine";
};
params: {
readonly path: "my/first-cat";
} | {
readonly path: "my/second-cat";
};
}
) => htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>
Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html
tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
`
<h1>${contextcontext: {
url: URL;
props: {
readonly cat: "Kino";
} | {
readonly cat: "Valentine";
};
params: {
readonly path: "my/first-cat";
} | {
readonly path: "my/second-cat";
};
}
.propsprops: {
readonly cat: "Kino";
} | {
readonly cat: "Valentine";
}
.catcat: "Kino" | "Valentine"
}</h1>
<main>${contextcontext: {
url: URL;
props: {
readonly cat: "Kino";
} | {
readonly cat: "Valentine";
};
params: {
readonly path: "my/first-cat";
} | {
readonly path: "my/second-cat";
};
}
.urlurl: URL
.pathnameURL.pathname: string
}</main>
<footer>${contextcontext: {
url: URL;
props: {
readonly cat: "Kino";
} | {
readonly cat: "Valentine";
};
params: {
readonly path: "my/first-cat";
} | {
readonly path: "my/second-cat";
};
}
.paramsparams: {
readonly path: "my/first-cat";
} | {
readonly path: "my/second-cat";
}
.pathpath: "my/first-cat" | "my/second-cat"
}</footer>
`,
});
handler
(experimental)
Used with server mode only.
Like staticPaths
, handler
is a provider for props
and can receive the current — matched route — params
.
There are two behaviors for the handlers:
-
Returning an instance of
Response
will terminate the pipeline, without going through thetemplate
rendering that happens afterward otherwise.
Useful for redirects, pure JSON API routes… -
Returning anything else will provide the typed
props
for thetemplate
to consume.
Minimal example:
📄 /src/routes/index.ts
import { htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>
Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html
tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
} from 'lit';
import { defineRoutefunction defineRoute<GetHandlerData extends R.HandlerDataHtml = undefined, PostHandlerData extends R.HandlerDataHtml = undefined, StaticPathOptions extends R.StaticPathOptionsGeneric | undefined = undefined, RouteContext extends R.RouteContextGeneric = {
url: URL;
props: StaticPathOptions extends {
params: any;
props: any;
} ? StaticPathOptions["props"] : GetHandlerData | PostHandlerData extends undefined ? never : {
GET: GetHandlerData extends Response ? never : GetHandlerData;
POST: PostHandlerData extends Response ? never : PostHandlerData;
};
params: StaticPathOptions extends {
params: any;
} ? StaticPathOptions["params"] : R.Params;
}>(options: {
handler?: StaticPathOptions extends object ? never : R.Handler<Response> | {
GET?: R.Handler<GetHandlerData>;
POST?: R.Handler<PostHandlerData>;
QUERY?: R.Handler;
PUT?: R.Handler;
PATCH?: R.Handler;
DELETE?: R.Handler;
HEAD?: R.Handler;
OPTIONS?: R.Handler;
};
staticPaths?: (() => StaticPathOptions[]) | undefined;
document?: R.DocumentTemplate<RouteContext>;
template?: R.BodyTemplate<RouteContext>;
}): (RouteModule: typeof R.RouteModule) => R.RouteModule
Defines a route.
See in the documentation.
} from '@gracile/gracile/route';
import { documentconst document: () => ServerRenderedTemplate
} from '../document.js';
const achievementsconst achievements: {
name: string;
}[]
= [{ namename: string
: 'initial' }];
export default defineRoutedefineRoute<undefined, Response, undefined, {
url: URL;
props: {
GET: undefined;
POST: never;
};
params: Params;
}>(options: {
handler?: Handler<Response> | {
GET?: Handler<undefined> | undefined;
... 6 more ...;
OPTIONS?: Handler<...> | undefined;
} | undefined;
staticPaths?: (() => undefined[]) | undefined;
document?: DocumentTemplate<...> | undefined;
template?: BodyTemplate<...> | undefined;
}): (RouteModule: typeof RouteModule) => RouteModule
Defines a route.
See in the documentation.
({
handlerhandler?: Handler<Response> | {
GET?: Handler<undefined> | undefined;
POST?: Handler<Response> | undefined;
QUERY?: Handler<never> | undefined;
... 4 more ...;
OPTIONS?: Handler<...> | undefined;
} | undefined
: {
POSTPOST?: Handler<Response> | undefined
: async (contextcontext: {
url: URL;
params: Params;
request: Request;
response: ResponseInit;
}
) => {
const formDataconst formData: FormData
= await contextcontext: {
url: URL;
params: Params;
request: Request;
response: ResponseInit;
}
.requestrequest: Request
.formDataBody.formData(): Promise<FormData>
();
const nameconst name: string | undefined
= formDataconst formData: FormData
.getFormData.get(name: string): FormDataEntryValue | null
('achievement')?.toStringfunction toString(): string
Returns a string representation of a string.
();
if (nameconst name: string | undefined
) achievementsconst achievements: {
name: string;
}[]
.pushArray<{ name: string; }>.push(...items: {
name: string;
}[]): number
Appends new elements to the end of an array, and returns the new length of the array.
({ namename: string
});
return Responsevar Response: {
new (body?: BodyInit | null, init?: ResponseInit): Response;
prototype: Response;
error(): Response;
json(data: any, init?: ResponseInit): Response;
redirect(url: string | URL, status?: number): Response;
}
This Fetch API interface represents the response to a request.
.redirectfunction redirect(url: string | URL, status?: number): Response
(contextcontext: {
url: URL;
params: Params;
request: Request;
response: ResponseInit;
}
.urlurl: URL
, 303);
},
},
documentdocument?: DocumentTemplate<{
url: URL;
props: {
GET: undefined;
POST: never;
};
params: Params;
}> | undefined
: (contextcontext: {
url: URL;
props: {
GET: undefined;
POST: never;
};
params: Params;
}
) => documentfunction document(): ServerRenderedTemplate
(contextcontext: {
url: URL;
props: {
GET: undefined;
POST: never;
};
params: Params;
}
),
templatetemplate?: BodyTemplate<{
url: URL;
props: {
GET: undefined;
POST: never;
};
params: Params;
}> | undefined
: async (contextcontext: {
url: URL;
props: {
GET: undefined;
POST: never;
};
params: Params;
}
) => htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>
Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html
tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
`
<form method="post">
<input type="text" name="achievement" />
<button>Add an "Achievement"</button>
</form>
<ul>
${achievementsconst achievements: {
name: string;
}[]
.mapArray<{ name: string; }>.map<TemplateResult<1>>(callbackfn: (value: {
name: string;
}, index: number, array: {
name: string;
}[]) => TemplateResult<1>, thisArg?: any): TemplateResult<1>[]
Calls a defined callback function on each element of an array, and returns an array that contains the results.
((achievementachievement: {
name: string;
}
) => htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>
Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html
tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
`<li>${achievementachievement: {
name: string;
}
.namename: string
}</li>`)}
</ul>
`,
});
See also the “Forms” recipe for a full, contextualized example.
HTTP methods
Note that, per the HTML specs, only GET
and POST
can be used with an HTML <form>
element.
Other methods like DELETE
, PUT
, etc. can be used, but Gracile won’t pursue the route template rendering with them.
A new method, “QUERY
”, is also inside the radar, and will possibly be implemented in node:http
and other server environments.
Minimal example
📄 /src/routes/my-page.ts
import { htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>
Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html
tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
} from 'lit';
import { defineRoutefunction defineRoute<GetHandlerData extends R.HandlerDataHtml = undefined, PostHandlerData extends R.HandlerDataHtml = undefined, StaticPathOptions extends R.StaticPathOptionsGeneric | undefined = undefined, RouteContext extends R.RouteContextGeneric = {
url: URL;
props: StaticPathOptions extends {
params: any;
props: any;
} ? StaticPathOptions["props"] : GetHandlerData | PostHandlerData extends undefined ? never : {
GET: GetHandlerData extends Response ? never : GetHandlerData;
POST: PostHandlerData extends Response ? never : PostHandlerData;
};
params: StaticPathOptions extends {
params: any;
} ? StaticPathOptions["params"] : R.Params;
}>(options: {
handler?: StaticPathOptions extends object ? never : R.Handler<Response> | {
GET?: R.Handler<GetHandlerData>;
POST?: R.Handler<PostHandlerData>;
QUERY?: R.Handler;
PUT?: R.Handler;
PATCH?: R.Handler;
DELETE?: R.Handler;
HEAD?: R.Handler;
OPTIONS?: R.Handler;
};
staticPaths?: (() => StaticPathOptions[]) | undefined;
document?: R.DocumentTemplate<RouteContext>;
template?: R.BodyTemplate<RouteContext>;
}): (RouteModule: typeof R.RouteModule) => R.RouteModule
Defines a route.
See in the documentation.
} from '@gracile/gracile/route';
import { documentconst document: (options: {
url: URL;
title: string;
}) => ServerRenderedTemplate
} from '../document.js';
export default defineRoutedefineRoute<undefined, undefined, undefined, {
url: URL;
props: never;
params: Params;
}>(options: {
handler?: Handler<Response> | {
GET?: Handler<undefined> | undefined;
... 6 more ...;
OPTIONS?: Handler<...> | undefined;
} | undefined;
staticPaths?: (() => undefined[]) | undefined;
document?: DocumentTemplate<...> | undefined;
template?: BodyTemplate<...> | undefined;
}): (RouteModule: typeof RouteModule) => RouteModule
Defines a route.
See in the documentation.
({
documentdocument?: DocumentTemplate<{
url: URL;
props: never;
params: Params;
}> | undefined
: (contextcontext: {
url: URL;
props: never;
params: Params;
}
) => documentfunction document(options: {
url: URL;
title: string;
}): ServerRenderedTemplate
({ ...contextcontext: {
url: URL;
props: never;
params: Params;
}
, titletitle: string
: 'My Page' }),
templatetemplate?: BodyTemplate<{
url: URL;
props: never;
params: Params;
}> | undefined
: ({ urlurl: URL
}) => htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>
Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html
tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
`
<main class="content">
<article class="prose">
<!-- ... -->
Hello ${urlurl: URL
.pathnameURL.pathname: string
}
<!-- ... -->
</article>
</main>
`,
});
Bare pages (for redirects, etc.)
Sometimes, you don’t want to bring a page template in a route, just a bare HTML document, maybe with some <meta>
; perfect use-case: page redirects.
It’s totally possible to skip the template
altogether and just use a single document
.
Here, we will redirect the user to another URL, while collecting some analytics, all that with a nice and simple transitive screen:
📄 /src/routes/chat.ts
import { defineRoutefunction defineRoute<GetHandlerData extends R.HandlerDataHtml = undefined, PostHandlerData extends R.HandlerDataHtml = undefined, StaticPathOptions extends R.StaticPathOptionsGeneric | undefined = undefined, RouteContext extends R.RouteContextGeneric = {
url: URL;
props: StaticPathOptions extends {
params: any;
props: any;
} ? StaticPathOptions["props"] : GetHandlerData | PostHandlerData extends undefined ? never : {
GET: GetHandlerData extends Response ? never : GetHandlerData;
POST: PostHandlerData extends Response ? never : PostHandlerData;
};
params: StaticPathOptions extends {
params: any;
} ? StaticPathOptions["params"] : R.Params;
}>(options: {
handler?: StaticPathOptions extends object ? never : R.Handler<Response> | {
GET?: R.Handler<GetHandlerData>;
POST?: R.Handler<PostHandlerData>;
QUERY?: R.Handler;
PUT?: R.Handler;
PATCH?: R.Handler;
DELETE?: R.Handler;
HEAD?: R.Handler;
OPTIONS?: R.Handler;
};
staticPaths?: (() => StaticPathOptions[]) | undefined;
document?: R.DocumentTemplate<RouteContext>;
template?: R.BodyTemplate<RouteContext>;
}): (RouteModule: typeof R.RouteModule) => R.RouteModule
Defines a route.
See in the documentation.
} from '@gracile/gracile/route';
import { htmlfunction html(strings: TemplateStringsArray, ...values: unknown[]): ServerRenderedTemplate
A lit-html template that can only be rendered on the server, and cannot be
hydrated.
These templates can be used for rendering full documents, including the
doctype, and rendering into elements that Lit normally cannot, like
<title>
, <textarea>
, <template>
, and non-executing <script>
tags
like <script type="text/json">
. They are also slightly more efficient than
normal Lit templates, because the generated HTML doesn't need to include
markers for updating.
Server-only html
templates can be composed, and combined, and they support
almost all features that normal Lit templates do, with the exception of
features that don't have a pure HTML representation, like event handlers or
property bindings.
Server-only html
templates can only be rendered on the server, they will
throw an Error if created in the browser. However if you render a normal Lit
template inside a server-only template, then it can be hydrated and updated.
Likewise, if you place a custom element inside a server-only template, it can
be hydrated and update like normal.
A server-only template can't be rendered inside a normal Lit template.
} from '@gracile/gracile/server-html';
import discordLogoconst discordLogo: TemplateResult<1>
from '../assets/icons/discord.svg' with { type: 'svg-lit' };
import { DISCORD_INVITE_URLconst DISCORD_INVITE_URL: "https://discord.gg/Q8nTZKZ9H4"
} from '../content/global.js';
import { googleAnalyticsconst googleAnalytics: ServerRenderedTemplate
} from '../document-helpers.js';
const waitTimeconst waitTime: 2
= 2;
export default defineRoutedefineRoute<undefined, undefined, undefined, {
url: URL;
props: never;
params: Params;
}>(options: {
handler?: Handler<Response> | {
GET?: Handler<undefined> | undefined;
... 6 more ...;
OPTIONS?: Handler<...> | undefined;
} | undefined;
staticPaths?: (() => undefined[]) | undefined;
document?: DocumentTemplate<...> | undefined;
template?: BodyTemplate<...> | undefined;
}): (RouteModule: typeof RouteModule) => RouteModule
Defines a route.
See in the documentation.
({
documentdocument?: DocumentTemplate<{
url: URL;
props: never;
params: Params;
}> | undefined
: () => htmlfunction html(strings: TemplateStringsArray, ...values: unknown[]): ServerRenderedTemplate
A lit-html template that can only be rendered on the server, and cannot be
hydrated.
These templates can be used for rendering full documents, including the
doctype, and rendering into elements that Lit normally cannot, like
<title>
, <textarea>
, <template>
, and non-executing <script>
tags
like <script type="text/json">
. They are also slightly more efficient than
normal Lit templates, because the generated HTML doesn't need to include
markers for updating.
Server-only html
templates can be composed, and combined, and they support
almost all features that normal Lit templates do, with the exception of
features that don't have a pure HTML representation, like event handlers or
property bindings.
Server-only html
templates can only be rendered on the server, they will
throw an Error if created in the browser. However if you render a normal Lit
template inside a server-only template, then it can be hydrated and updated.
Likewise, if you place a custom element inside a server-only template, it can
be hydrated and update like normal.
A server-only template can't be rendered inside a normal Lit template.
`
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
${googleAnalyticsconst googleAnalytics: ServerRenderedTemplate
}
<style>
& {
margin: calc(10dvh + 10dvw);
font-family: system-ui;
font-size: 2rem;
text-align: center;
color-scheme: dark light;
}
svg {
height: 3rem;
width: 3rem;
}
</style>
<title>Gracile - Discord Server (redirecting…)</title>
The current page, "https://gracile.js.org/chat/", will be forgotten from history after the redirection.
<meta
http-equiv="refresh"
content=${`${waitTimeconst waitTime: 2
};URL=${DISCORD_INVITE_URLconst DISCORD_INVITE_URL: "https://discord.gg/Q8nTZKZ9H4"
}`}
/>
</head>
<body>
${discordLogoconst discordLogo: TemplateResult<1>
}
<p>Redirecting to the Discord invitation link…</p>
No need for the <route-template-outlet> here!
</body>
</html>
`,
});
What to put in routes?
Data fetching, dispatching, per-route assets to bundle, components, templates, modules, features…
It’s generally better to use routes as entry points and not put too much UI or logic in there, besides what’s strictly needed to bootstrap the page.
Routes are kind of “magic”, in the sense that you’re not calling them yourself in your
code, but the framework will use them predictably.
Thankfully, Gracile isn’t crowding top level module exports, but just the default one.
While it adds a level of indentation, it avoids clashes with your module-scoped functions.
This is a perfectly reasonable use of ESM default exports.
No static analysis or extraction either, meaning your functions are not in silos and won’t behave in unexpected ways due to custom pre-processing.
Client-side routing
For now, Gracile doesn’t provide any CSR mechanism out of the box.
Due to the varying needs among developers, it’s not in the scope, but at one
point, if it adds value (like client/server symbiosis) an add-on could be
created.
Note that the Metadata add-on
provides a viewTransition
option that will make this browser native
feature quickly available to you (it is just a meta tag), but it’s not supported outside Blink-based
browsers. It can be a nice progressive enhancement though, but not quite
the SPA feel you could get with user-land solutions.
Fortunately, there are plenty of options regarding CSR in the Lit ecosystem:
- Lit’s router
- Nano Stores Router with Nano Store Lit
- Navigo
- thepassle’s app-tools router
- Vaadin router
You might want to try DOM-diffing libraries, too.
Miscellaneous
Trailing slashes
For simplicity and predictability, Gracile is only supporting routes that ends with a slash.
This is for pages and server endpoints. Flexibility will be added for the latter.
Note
The explanation below is extracted from the Rocket web framework documentation.
Below is a summary of investigations by Zach Leatherman and Sebastien Lorber
Legend:
- 🆘 HTTP 404 Error
- 💔 Potentially Broken Assets (e.g.,
<img src="image.avif">
) - 🟡 SEO Warning: Multiple endpoints for the same content
- ✅ Correct, canonical or redirects to canonical
- ➡️ Redirects to canonical
about.html |
about/index.html |
|||
---|---|---|---|---|
Host | /about |
/about/ |
/about |
/about/ |
GitHub Pages | ✅ | 🆘 404 |
➡️ /about/ |
✅ |
Netlify | ✅ | ➡️ /about |
➡️ /about/ |
✅ |
Vercel | 🆘 404 |
🆘 404 |
🟡💔 | ✅ |
Cloudflare Pages | ✅ | ➡️ /about |
➡️ /about/ |
✅ |
Render | ✅ | 🟡💔 | 🟡💔 | ✅ |
Azure Static Web Apps | ✅ | 🆘 404 |
🟡💔 | ✅ |
If you wanna know more be sure to checkout Trailing Slashes on URLs: Contentious or Settled?.