File conventions
These files are read from the top level of src/ (a file inside routes/ is a page, not a convention).
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.
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:
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 ofblog/; 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.tsxtakes the image ofblog/, andblog/[slug].tsxtakes the image ofblog/[slug]/. Each kind decides on its own: atwitter-imageinblog/does not hide the site'sopengraph-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(ortwitter.images) in its ownmetadataorgenerateMetadatakeeps them: the file of its folder is not added for that block. A layer that setsopenGraphwithoutimagesstill gets the file's image, and the other fields stay. - A card with only an
opengraph-imagealso gives Twitter its image, with the same alt text, astwitter:imagefalls 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
metadataBaselike any other URL: setmetadataBasein 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.
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 withgenerateImageMetadatais 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.txtis not read for a module: thealtexport 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
ImageResponseheaders:content-type: image/png,x-content-type-options: nosniff, andcache-control: public, max-age=3600, stale-while-revalidate=86400, the default ofImageResponse(see Icons and social images). A module cannot set another header in 1.0.0.