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 { html } from '@gracile/gracile/server-html';
import { createMetadata } from '@gracile/metadata';

const SITE_TITLE = 'My Website';


const criticalAssets = html`
  <script>
    {
      const msg = 'Hello';
      console.log(msg);
    }
  </script>
`;

 "url" is the bare minimum provided in the route context by Gracile, but you can still augment  document props.

export const document = (props: { url: URL; title?: string }) => html`
  <!doctype html>
  <html lang="en">
    <head>
       Critical assets. 
      ${criticalAssets}

       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>


      ${createMetadata({
        siteTitle: SITE_TITLE,
        pageTitle: `${SITE_TITLE} | ${props.title || 'Home'}`,
        faviconUrl: '/public/favicon.svg',
        pageDescription: '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 { defineRoute } from '@gracile/gracile/route';
import { html } 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 { document } from '../document.js';

export default defineRoute({
  document: (context) => document({ ...context, title: 'My Page' }),

  template: ({ url }) => html`
    <main class="content">
      <article class="prose">
        <!-- ... -->
        Hello ${url.pathname}
        <!-- ... -->
      </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.