[]previous and next page

File conventions

These files are read from the top level of src/ (a file inside routes/ is a page, not a convention).

For agentsUpdated

These files are read from the top level of src/ (a file inside routes/ is a page, not a convention). The social images are the exception: opengraph-image and twitter-image, as a static file or a module, are also read in routes/ and in every route folder (see Static social images and Dynamic social images).

File Served at What it exports
sitemap.ts /sitemap.xml a default function that returns the entries
sitemap.ts with generateSitemaps /sitemap.xml (the index) and /sitemap/<id>.xml generateSitemaps() returns [{ id }]; the default takes { id }
robots.ts /robots.txt a default function that returns the rules
manifest.ts /manifest.webmanifest a default function that returns the manifest
opengraph-image.tsx, twitter-image.tsx (.ts, .js, ...) /opengraph-image, /twitter-image at the top; under a route folder's URL, with its params, in a folder (/blog/:slug/opengraph-image) a default function that returns the image's tree; alt, size, contentType, fonts and resvgWasm
opengraph-image.png, .jpg, .jpeg or .gif, and the same for twitter-image /opengraph-image.<ext> and /twitter-image.<ext>, under the folder's URL in a route folder the file, embedded with the type its header says; the pages carry its tags
icon.svg, icon.png, icon.ico /icon the file, embedded with its type
apple-icon.png /apple-icon the file
favicon.ico /favicon.ico the file

Each default function receives the request event, so the answer can depend on the host. The functions are answered by the helpers of the metadata routes above, so the limits of those apply. A file that is embedded is at most 256 KiB.

src/sitemap.tsts
import type { SitemapEntry } from "@shadow-js/dusk/server";

export default function sitemap(): SitemapEntry[] {
  return [{ url: "https://example.com/", changeFrequency: "daily" }];
}

A build fails, naming the files, when two files define one convention (sitemap.ts and sitemap.tsx), or when a convention's URL is already the URL of a route. A dynamic opengraph-image.tsx (or twitter-image.tsx) below routes/ is not a page: it is the image of its folder, served at the folder's URL plus its name (see Dynamic social images).

Static social images#

An image file named opengraph-image or twitter-image (extension .png, .jpg, .jpeg or .gif) gives the pages below it their og:image or twitter:image tags, as in Next.js. It is found at the top of src/, at the top of src/routes/ (the same root) and in any route folder, including folders of dynamic segments and groups:

text
src/
  opengraph-image.png            the card of the whole site
  opengraph-image.alt.txt        its alt text (optional)
  routes/
    _layout.tsx
    index.tsx
    blog/
      _layout.tsx
      opengraph-image.jpg        the card of /blog and everything below it
      opengraph-image.alt.txt
      [slug]/
        twitter-image.png        served at /blog/:slug/twitter-image.png, for every slug

What the build does with each file:

Step Result
It reads the header The width, height and MIME type are the file's own: PNG (IHDR), JPEG (the first frame header) and GIF (logical screen). The type comes from the signature, not from the name. A header it cannot read (an unknown format, a file cut short, a zero side) fails the build and names the file.
It serves the file At /<folder>/opengraph-image.<ext> (/opengraph-image.png at the root), embedded in the server like the icons, so it is at most 256 KiB. The route is the plain path.
It writes the tags og:image (or twitter:image) with og:image:type, og:image:width, og:image:height and, when the .alt.txt file is there, og:image:alt (twitter:image:alt for Twitter). The URL ends in ? and a short hash of the file's bytes, so a changed file is fetched again. The hash is on the tag's URL only.

The alt text is the content of opengraph-image.alt.txt (or twitter-image.alt.txt) in the same folder, without the spaces and line breaks at its ends. An empty file is no alt text.

Which image a page gets follows Next.js:

  • The folder nearest the page wins. A page in blog/ gets the image of blog/; a page in a folder without one gets the image of the nearest folder above it. A page file named after a segment is the page of that segment: blog.tsx takes the image of blog/, and blog/[slug].tsx takes the image of blog/[slug]/. Each kind decides on its own: a twitter-image in blog/ does not hide the site's opengraph-image. A folder of dynamic segments (blog/[slug]/) fills the segments from the page's params, so the tag points at that page's own URL.
  • A layer that sets openGraph.images (or twitter.images) in its own metadata or generateMetadata keeps them: the file of its folder is not added for that block. A layer that sets openGraph without images still gets the file's image, and the other fields stay.
  • A card with only an opengraph-image also gives Twitter its image, with the same alt text, as twitter:image falls back to the Open Graph image.
  • A dynamic opengraph-image.tsx (or .ts, .js) in the same folder wins over the static file, whichever way the names sort. The static file of that kind is then neither served nor written to a page.
  • The image of a page is relative to metadataBase like any other URL: set metadataBase in the root layout so the tags hold absolute URLs, which crawlers need.

A build fails, naming the files, for two images of one kind in one folder (opengraph-image.png and opengraph-image.jpg), a file over 256 KiB, a folder the router refuses (@slot), and an image in a private folder (a name that starts with _: the route is never served, so the image would not be either). Folders that start with . or - and node_modules are not route folders.

Dynamic social images#

A module named opengraph-image or twitter-image (.ts, .tsx, .js, .jsx, .mts or .mjs) draws the image from the page's params. It is found where a static file is found (the top of src/, the top of routes/ and every route folder, dynamic and group folders included), and it is not a page: the image is served at the folder's URL plus its name, with the folder's params. A routes/blog/[slug]/opengraph-image.tsx answers /blog/:slug/opengraph-image, and the pages of that folder carry a tag that points at that URL, with the page's own slug in it.

src/routes/blog/[slug]/opengraph-image.tsts
import { h, type OgFont } from "@shadow-js/dusk/og";
import type { RequestEvent } from "@shadow-js/dusk/server";

const sans = new Uint8Array(0);
const wasm = new Uint8Array(0);

export const alt = "A post card";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export const fonts: OgFont[] = [{ name: "Sans", data: sans, weight: 400 }];
export const resvgWasm = wasm;

/** The event carries the folder's params (`[slug]`), decoded, as every route's does. */
export default function image(event: RequestEvent) {
  const slug = event.params["slug"] ?? "";
  return h("div", { style: { display: "flex", width: "100%", height: "100%", fontSize: 64 } }, slug);
}

The default export is the tree. The build answers it with an ImageResponse and passes that the module's size, fonts and resvgWasm. The exports alt, size and contentType also feed the tags:

Export When it is a literal in the module Tag and response
alt A string or a template without substitutions og:image:alt (twitter:image:alt); an empty or blank value is no alt text.
size { width, height } with whole numbers from 1 to 4096 og:image:width and og:image:height; the response is that size.
contentType "image/png" (the only type an ImageResponse answers) og:image:type. Any other literal is a build error.

An export the build cannot read (a call, a variable, a re-export) takes the runtime default: 1200 by 630, image/png, no alt text. The response still uses the module's own values, so write the export as a literal when the tag must say what the image is. The tag's URL carries a hash of the module's bytes (? and eight hex digits), so a changed module is fetched again, as a static file's is.

  • A module with HTTP method handlers (GET), without a default export, or with generateImageMetadata is a build error that names the file. Use one image per folder, whose default export is the tree. A module with a syntax error is reported with its position.
  • A dynamic image in a private folder (a name starting with _) is a build error, as a static one is.
  • A sibling opengraph-image.alt.txt is not read for a module: the alt export is its alt text.
  • The prerender writes pages only, so a dynamic image is drawn by the server when it is asked for. An app that deploys only static files has no such image; keep dynamic images on an app that has a server.
  • The response has the ImageResponse headers: content-type: image/png, x-content-type-options: nosniff, and cache-control: public, max-age=3600, stale-while-revalidate=86400, the default of ImageResponse (see Icons and social images). A module cannot set another header in 1.0.0.