Defining the base documents
Document are wrapping your routes and are streamed eagerly.
They are housing critical assets, page metadata, hydration helpers, asset entry points…
Example
📄 /src/document.ts
We're using the server-only "html" from Lit SSR here, not the regular Lit package.
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 { createMetadatafunction createMetadata(config: MetadataConfig, warn?: boolean): ServerRenderedTemplate[]
Ouput all useful tags to put in document head.
} from '@gracile/metadata';
const SITE_TITLEconst SITE_TITLE: "My Website"
= 'My Website';
You can insert inline, critical scripts or styles snippets like here:
const criticalAssetsconst criticalAssets: ServerRenderedTemplate
= 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.
`
<script>
{
const msg = 'Hello';
console.log(msg);
}
</script>
`;
export const documentconst document: (props: {
url: URL;
title?: string;
}) => ServerRenderedTemplate
= (propsprops: {
url: URL;
title?: string;
}
: { urlurl: URL
: URL; titletitle?: string | undefined
?: string }) => 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>
Critical assets.
${criticalAssetsconst criticalAssets: ServerRenderedTemplate
}
Global stylesheets or ES modules for this document.
<link rel="stylesheet" href="/src/styles/global.scss" />
Use the full real path (relative to the project root)
<script type="module" src="/src/document.client.ts"></script>
${createMetadatafunction createMetadata(config: MetadataConfig, warn?: boolean): ServerRenderedTemplate[]
Ouput all useful tags to put in document head.
({
siteTitleMetadataConfig.siteTitle?: string | undefined
: SITE_TITLEconst SITE_TITLE: "My Website"
,
pageTitleMetadataConfig.pageTitle?: string | undefined
: `${SITE_TITLEconst SITE_TITLE: "My Website"
} | ${propsprops: {
url: URL;
title?: string;
}
.titletitle?: string | undefined
|| 'Home'}`,
faviconUrlMetadataConfig.faviconUrl?: string | undefined
: '/public/favicon.svg',
pageDescriptionMetadataConfig.pageDescription?: string | undefined
: 'A cool website',
})}
</head>
<body>
Don't forget to add this. Otherwise your route's page template won't be inserted!
<route-template-outlet></route-template-outlet>
</body>
</html>
`;
📄 /src/routes/my-page.ts
import { defineRoutefunction defineRoute<GetHandlerData extends R.HandlerDataHtml = undefined, PostHandlerData extends R.HandlerDataHtml = undefined, CatchAllHandlerData extends R.HandlerDataHtml = undefined, StaticPathOptions extends R.StaticPathOptionsGeneric | undefined = undefined, RouteContext extends R.RouteContextGeneric = {
...;
}>(options: {
handler?: StaticPathOptions extends object ? never : R.Handler<CatchAllHandlerData> | {
GET?: R.Handler<GetHandlerData>;
POST?: R.Handler<PostHandlerData>;
QUERY?: R.Handler<Response>;
PUT?: R.Handler<Response>;
PATCH?: R.Handler<Response>;
DELETE?: R.Handler<Response>;
HEAD?: R.Handler<Response>;
OPTIONS?: R.Handler<Response>;
} | undefined;
staticPaths?: () => R.MaybePromise<StaticPathOptions[]> | undefined;
prerender?: boolean | undefined;
document?: R.DocumentTemplate<RouteContext> | undefined;
template?: R.BodyTemplate<RouteContext> | undefined;
}): (RouteModule: typeof R.RouteModule) => R.RouteModule
Defines a file-based route for Gracile to consume.
} from '@gracile/gracile/route';
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';
Alternatively, you can use the server-only "html" tag if you don't need hydration.
// import { html } from '@gracile/gracile/server-html';
import { documentconst document: (props: {
url: URL;
title?: string;
}) => ServerRenderedTemplate
} from '../document.js';
export default defineRoutedefineRoute<undefined, undefined, undefined, undefined, {
url: URL;
props: undefined;
params: Parameters;
}>(options: {
handler?: Handler<undefined> | {
GET?: Handler<undefined> | undefined;
... 6 more ...;
OPTIONS?: Handler<...> | undefined;
} | undefined;
staticPaths?: (() => MaybePromise<...> | undefined) | undefined;
prerender?: boolean | undefined;
document?: DocumentTemplate<...> | undefined;
template?: BodyTemplate<...> | undefined;
}): (RouteModule: typeof RouteModule) => RouteModule
Defines a file-based route for Gracile to consume.
({
documentdocument?: DocumentTemplate<{
url: URL;
props: undefined;
params: Parameters;
}> | undefined
A function that returns a server only template.
Route context is provided at runtime during the build.
: (contextcontext: {
url: URL;
props: undefined;
params: Parameters;
}
) => documentfunction document(props: {
url: URL;
title?: string;
}): ServerRenderedTemplate
({ ...contextcontext: {
url: URL;
props: undefined;
params: Parameters;
}
, titletitle?: string | undefined
: 'My Page' }),
templatetemplate?: BodyTemplate<{
url: URL;
props: undefined;
params: Parameters;
}> | undefined
A function that returns a server only or a Lit client hydratable template.
Route context is provided at runtime during the build.
: ({ 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>
`,
});
Important
You CAN nest server OR regular templates in server templates.
You CANNOT nest server templates in regular templates.
Hydration
If you need client-side interactivity with Lit Custom Elements, don’t forget to import the hydration helper in the document script entry point:
📄 /src/document.client.ts
It's important to hydrate eagerly, before defining any Lit Element in the global registry.
import '@gracile/gracile/hydration';
Then import your elements, here globally, or per page (e.g. in /src/routes/my-route.client.ts).
import './my-ubiquitous-lit-element.ts';
// ...
Page outlet
<route-template-outlet></route-template-outlet>
is a special tag, a placeholder that will be replaced with your page rendering.
It’s one of the very few “unholy” things Gracile is doing.
However it is not meant as just an esthetic thing, compared to a more traditional comment marker (e.g., <!--SSR_OUTLET-->
).
Defined as a Custom Element, it might serve as a smart fallback or an ultimate error boundary in the future.
E.g., if the rendering fail, it could be used to show your own error overlay. It
can be an interesting pattern to explore.
<slot></slot>
could have been used to, but it’s a bit of a stretch of what a real slot is, and can clash with legitimate slots.
Declarative shadow DOM support
It’s pretty good!
But if you have to support older browsers, you can use a ponyfill.
Notes
Gracile is purposedly separating the HTML base document with the rest of the
page rendering.
It is because, under the hood, it will render it beforehand and stream it to the
user, resulting in a faster response time.
Usually there isn’t too much work at this level, and without those premices, the
browser isn’t kickstarting its work before receiving the real meat of the
markup, which can take longer to process streamed chunks.
Note that you’ll benefit from looser error boundaries, too. When a sub-template fails to render, you still get the valid markup, instead of a giant crash.
Also, base document holds critical assets and Vite’s magic sauce for dev. Hence an additional reason to process it alone, in the pre-pass.
Finally, we are using @lit-labs/ssr
own html
tagged template literal, which
is different from the lit
’s one.
In short: it can handle all the “special” HTML tags (<script>
, <title>
,…)
while being faster to process, due to its non-interactive nature.
It’s also 1:1 HTML!
Lit “regular” templates can also be used without the “DSL-y” bits (@event
,
.property
…) if you want to go your way, like no JS, vanilla JS, Alpine.js, etc.
You’ll find that the “Lit HTML” abstraction, overall, is tiny, and opt-in for
the “special” stuff, like @event
, property .binding
, so you might just use it whenever you can.
It’s better to separate the document from your route; because each of the Lit and Lit SSR, server-only html
tags will clash otherwise.
It’s possible to rename it through import though, like server.html
,
but you will probably lose formatting, syntax highlighting, TypeScript Lit Plugin hover info, etc.
Keep in mind you can use conditional logic inside your base document, so you can alter metadata, assets loading etc. to cater to each route.
Most projects won’t need to define multiple documents, but if you find yourself having too many divergences, or for other reasons like security, you have the flexibility to do that.
Gracile isn’t aware of anything other than you route files and their sibling assets, you can locate and name documents, components, etc. with your conventions.