Skip to main content
Version: 2026-10-13 (archived)

Assets

api.assets is your tool's own files: the stylesheets, templates, data and images in the folder you uploaded, fetched for you the first time you ask for one. Your tool never fetches its own stylesheets or templates. Every method but url() is async, so await it.

async connectedCallback() {
// styles.css is already applied to the shadow root; index.html is a template
this.shadowRoot.replaceChildren(await this.api.assets.getTemplate('index.html'))
this.shadowRoot.querySelector('img.map').src = this.api.assets.url('map.png')
const words = await this.api.assets.getJSON('words.json')
}

Every file is in one of four groups. modules, styles and templates are decided by manifest.json: app.styles and app.templates, or every .css and .html (or .htm) in the folder, in path order, when those keys are absent, and every other .js is a module. other is everything else: images, fonts, data files. The lists are on api.assets by those names (api.assets.styles, …).

await api.assets.getTemplate(name, id?)DocumentFragment

A fresh copy of a template's content, ready to put in the shadow root: the whole file, or with id, one <template id="…"> block inside it. Every call is a new copy, so a template can be stamped out many times — one per card, one per row. The file is fetched once; a missing block throws, naming the file.

await api.assets.getStyle(name)CSSStyleSheet

A stylesheet, built once and the same object on every call. If your shadow root is open (this.attachShadow({ mode: 'open' })), every stylesheet is already adopted into it when connectedCallback runs — do nothing. With a closed root, adopt them yourself: for (const name of this.api.assets.styles) root.adoptedStyleSheets.push(await this.api.assets.getStyle(name)). This works in every browser Voshi supports, older Safari included.

await api.assets.getJSON(name)object

A data file of yours, parsed. The file is fetched once and parsed on every call, so the object is yours to change.

await api.assets.getFile(name, group = 'other')string

Any of your files as text, exactly as uploaded, fetched on every call. group is the list the name must be in: 'other' for a data or text file, 'templates' for a template's raw text, and so on.

api.assets.url(name)string

The URL of any of your tool's own files — an image, a font, a data file — for anything the browser fetches itself, such as an <img> or a canvas image. Synchronous. new URL('map.png', import.meta.url) names the same file, and is fine too.

A name that is not one of the tool's files throws, naming the file.

Organizing templates​

Keep one file per view — index.html for the main screen, views/setup.html for an instructor's setup screen — and put a view's small repeated pieces inside that file as <template id="…"> blocks:

<!-- index.html -->
<h2>Measurements</h2>
<ul id="log"></ul>

<template id="row">
<li><span class="when"></span> <span class="value"></span></li>
</template>
async connectedCallback() {
this.shadowRoot.replaceChildren(await this.api.assets.getTemplate('index.html'))
const log = this.shadowRoot.getElementById('log')
for (const m of measurements) {
const row = await this.api.assets.getTemplate('index.html', 'row')
row.querySelector('.when').textContent = m.when
row.querySelector('.value').textContent = m.value
log.append(row)
}
}

A <template> block is inert until you copy it out, so the blocks inside index.html render nothing on their own. Each template file is fetched the first time it is asked for, and a tool may have at most 20 stylesheets and templates together, so a tool with many partials keeps them as blocks, not files. Voshi starts fetching every template while your code loads, so the first getTemplate of a view usually finds it already there.

Inside a stylesheet​

A relative url() in a stylesheet works as it does on any web page: it names a file relative to the stylesheet itself. url(media/logo.png) in styles.css is media/logo.png in your folder, and url(../fonts/inter.woff2) in css/theme.css is fonts/inter.woff2.

@import does not work. Voshi adopts each stylesheet on its own, and an @import inside one is ignored. List every stylesheet in app.styles instead, in the order they should apply.

Why not fetch them yourself​

Every upload and every published version of your tool is served from a different address, named by the content of the files. A URL you wrote into your code, or saved in storage, is the address of one particular upload. api.assets.url() is always the address of the files that are running. And styles.css and index.html have already been fetched once, by Voshi; fetching them again is a second round trip the learner waits on.

Importing another JavaScript module of your own with a relative import './lib/cards.js' is fine, and the normal way to split a tool into files — the browser resolves it against the running copy.