Working with assets
Thanks to its solid Vite foundations, you have plenty of ways to work with any kind of static or optimized assets, from fonts to WASM.
Scripts and styles via HTML tags
There are multiple cases for using HTML tags for importing client-side JS or CSS:
1. Globally, at the document
level
When you are associating multiple routes with the same enclosing document, you might want to inject shared assets for them.
<link rel="stylesheet" href="/src/document.scss" />
<script type="module" src="/src/document.client.ts"></script>
You can always add more link
or script
, but it’s probably cleaner to add CSS @import
or JS import
in single entry points, for you own sources or those from vendors.
Important
While it is strongly recommended to use .js
even for TS, and relative paths,
not absolute,
these rules won’t apply in HTML when using Vite / Rollup based
systems for bundle entrypoints.
You have to provide the full real path relative to the project root, like
/src/components/my-element.ts
.
Same for non-vanilla CSS.
Alternatively, you can use this notation, with URL
and import.meta.url
:
new URLvar URL: new (url: string | URL, base?: string | URL) => URL
The URL interface represents an object providing static methods used for creating object URLs.
URL
class is a global reference for import { URL } from 'url'
https://nodejs.org/api/url.html#the-whatwg-url-api
('./my-asset.css', import.meta.urlImportMeta.url: string
The absolute file:
URL of the module.
).pathnameURL.pathname: string
;
E.g.
📄 /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: 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.
`
<head>
...
<link
rel="stylesheet"
href=${new URLvar URL: new (url: string | URL, base?: string | URL) => URL
The URL interface represents an object providing static methods used for creating object URLs.
URL
class is a global reference for import { URL } from 'url'
https://nodejs.org/api/url.html#the-whatwg-url-api
('./document.css', import.meta.urlImportMeta.url: string
The absolute file:
URL of the module.
).pathnameURL.pathname: string
}
/>
<script
type="module"
src=${new URLvar URL: new (url: string | URL, base?: string | URL) => URL
The URL interface represents an object providing static methods used for creating object URLs.
URL
class is a global reference for import { URL } from 'url'
https://nodejs.org/api/url.html#the-whatwg-url-api
('./document.client.ts', import.meta.urlImportMeta.url: string
The absolute file:
URL of the module.
).pathnameURL.pathname: string
}
></script>
...
</head>
`;
It’s arguably more verbose but it is also a more canonical way to achieve this endeavour.
Dealing with relative paths can be preferable, too.
For both dev. and build, Vite will resolve those paths for you properly.
This works, too, for explicit entry point grouping:
<script type="module">
import '/src/document.client.ts';
import '/src/my-lib.ts';
</script>
Note that Vite will deduplicate/bundle per-route assets properly for build. E.g.
<script type="module" src="/src/alpha.ts"></script>
<script type="module" src="/src/beta.ts"></script>
<script type="module" src="..."></script>
It becomes one after build
<script type="module" src="/assets/page-bundle-123h4sh.js"></script>
2. Per-route assets injection
📄 /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: () => 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.
`
<!doctype html>
<html lang="en">
<head>
...
Route sibling page assets will be appended here, right before the closing HEAD
</head>
...
</html>
`;
Given:
- project/
- src/
- routes/
- my-page.ts
- my-page.css (or
.{scss,less,…}
) - my-page.client.ts
- routes/
- …
- src/
Result in:
<link rel="stylesheet" href="/src/routes/my-page.css" />
<script type="module" src="/src/routes/my-page.client.ts"></script>
This mechanism isn’t here just for convenience, but to make to streamline the way the server handler will be generated (but it’s also useful for static mode).
Putting all route assets at the document level allows for deterministic optimizations by the bundler at build time, which wouldn’t be possible (at least easily) at run time.
3. Anywhere in a server template
You can also put script
(module or inline) and link
anywhere in your HTML
partials or server-rendered Lit Elements, but it’s not a recommended way.
First of all, it makes static analysis harder, for things like modules preloader. Also, it’s generally recommended to put assets in <head>
for
performance reasons, not in semi-random places in the body.
This rule is not set in stone, though. You might have legitimate reasons for doing otherwise.
For static build, Vite will handle this just fine and apply optimizations on the
whole page at once, it will even put modules (async) in the <head>
for you, with preloaders.
However for server output, since your page output is not deterministic, those
will not be hoisted in the head.
You’ll have to make sure to use the “URL
+ import.meta.url
” syntax, not /src/foo/bar.ts
;
That way, the correct path will be dynamically injected in place when building the Gracile server handler for production, hence, matching the dev. behavior.
The public/
folder for static assets
As you know, Gracile is built on top of Vite, so
naturally, it will inherit Vite’s capabilities like serving assets as-is,
from the public
directory.
Putting an asset in it, will make it available from the base URL, e.g., /public/favicon.svg
→ /favicon.svg
.
More details on the Vite docs.
Exotic file formats
For anything outside the realm of HTML/CSS/JS, you have a large collection of Vite plugins to cater to your needs.
Vite provides ?raw
or ?inline
flags, web workers loading mechanisms etc.
Gracile support this, naturally, but also provides a handful of plugins specifically
tailored to html
template literals.