Skip to content

manifest.webmanifest

The manifest feature generates a /manifest.webmanifest file in your build output during astro build. It is configured through the manifest option on the integration. The output is a JSON file serialized from the options you provide.

Chromium PWA requirements (name or short_name, display or display_override, and start_url) are enforced as TypeScript compile-time constraints rather than runtime errors. Setting prefer_related_applications: true is a type error.

If manifest is omitted from your integration config, a warning is logged recommending you add it. If manifest: false is set, no file is generated and no warning is logged.

WebManifestOptions is a TypeScript intersection type. At least one of name or short_name is required, and at least one of display or display_override is required. Setting both name: never and short_name: never is a compile error.

optiontypedefaultrequireddescription
namestring-Yes*Full name of the web application. Required unless short_name is provided.
short_namestring-Yes*Short name for use in contexts where name is too long. Required unless name is provided.
start_urlstring-YesURL loaded when the app is launched.
displaystring-Yes**Display mode for the app. Common values: "standalone", "fullscreen", "minimal-ui", "browser". Required unless display_override is provided.
display_overridestring[]-Yes**Ordered list of display mode candidates. Required unless display is provided.
iconsWebManifestIconItem[]-NoIcons to represent the app. If omitted and the icons feature is active, auto-populated from generated 192 and 512 icons.
prefer_related_applicationsfalse-NoMust be false or omitted. Setting true is a TypeScript error.
descriptionstring-NoShort description of the app’s purpose.
background_colorstring-NoBackground color for the splash screen before the stylesheet loads.
theme_colorstring-NoDefault theme color for the OS title bar or browser chrome.
scopestring-NoNavigation scope for the app. URLs outside the scope open in the browser.
orientationstring-NoDefault screen orientation. Common values: "portrait", "landscape", "any".
idstring-NoUnique string identity for the app across installs and updates.
categoriesstring[]-NoApp store categories the app belongs to.
screenshotsWebManifestScreenshotItem[]-NoScreenshots shown in app stores or install prompts.
shortcutsWebManifestShortcutItem[]-NoApp shortcuts exposed through OS context menus or long-press actions.
related_applicationsWebManifestRelatedApplication[]-NoNative app alternatives to this web app.
file_handlersWebManifestFileHandler[]-NoFile types the app can open as a registered file handler.
protocol_handlersWebManifestProtocolHandler[]-NoCustom URL protocols the app handles.
share_targetWebManifestShareTarget-NoConfiguration for receiving shared content from the OS share sheet.
launch_handlerWebManifestLaunchHandler-NoControls the client behavior when the app is launched.
note_taking{ new_note_shortcut?: { url: string } }-NoConfiguration for note-taking shortcut integration.
scope_extensionsArray<{ origin: string }>-NoAdditional origins that extend the app’s navigation scope.
serviceworkerWebManifestServiceWorker-NoService worker registration metadata.

* At least one of name or short_name is required by TypeScript.

** At least one of display or display_override is required by TypeScript.

fieldtyperequireddescription
srcstringYesPath to the icon image file.
sizesstringNoOne or more sizes as space-separated <width>x<height> values, for example "192x192", or "any" for SVGs.
typestringNoMIME type of the icon, for example "image/png" or "image/svg+xml".
purposestringNoOne or more space-separated purpose keywords: "any", "maskable", "monochrome".
fieldtyperequireddescription
srcstringYesPath to the screenshot image.
sizesstringNoDimensions in <width>x<height> format.
typestringNoMIME type of the screenshot.
labelstringNoAccessible label for the screenshot.
form_factorstringNoDisplay context hint. Common values: "narrow", "wide".
platformstringNoTarget platform hint, for example "windows", "android".
fieldtyperequireddescription
namestringYesName displayed for the shortcut.
urlstringYesURL activated when the shortcut is selected.
short_namestringNoAbbreviated name for constrained display.
descriptionstringNoDescription of what the shortcut does.
iconsWebManifestIconItem[]NoIcons to represent the shortcut.
fieldtyperequireddescription
platformstringYesPlatform the related app is hosted on.
urlstringNoURL to the related app’s store page.
idstringNoApp identifier on the platform.
fieldtyperequireddescription
actionstringYesURL that handles file open requests.
acceptRecord<string, string[]>YesMap of MIME type to accepted file extensions.
fieldtyperequireddescription
protocolstringYesThe custom protocol to handle, for example "web+example".
urlstringYesURL template with %s substituted for the full protocol URL.
fieldtyperequireddescription
actionstringYesURL that receives the shared data.
methodstringNoHTTP method, "GET" or "POST". Defaults to "GET".
enctypestringNoEncoding type for POST requests.
paramsRecord<string, string>NoMapping of share data fields to URL or form parameters.
fieldtyperequireddescription
client_modestring | string[]NoHow the browser client handles an app launch. Common value: "navigate-existing".
fieldtyperequireddescription
srcstringYesPath to the service worker script.
scopestringNoScope of the service worker.
typestringNoModule type. Use "module" for ES module service workers.
update_via_cachestringNoCache behavior for service worker updates.
astro.config.mjs
eminence({
manifest: {
name: "My App",
start_url: "/",
display: "standalone",
icons: [
{ src: "/icon-192x192.png", sizes: "192x192", type: "image/png" },
{ src: "/icon.png", sizes: "512x512", type: "image/png" },
],
},
});

Output at /manifest.webmanifest:

{
"name": "My App",
"start_url": "/",
"display": "standalone",
"icons": [
{ "src": "/icon-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon.png", "sizes": "512x512", "type": "image/png" }
]
}

When the icons integration is configured, you can omit icons from manifest entirely. The default icon set includes icon-192.png (192x192) and icon.png (512x512), both marked for manifest inclusion, and are automatically added.

eminence({
icons: {
source: "src/assets/logo.svg",
},
manifest: {
name: "My App",
start_url: "/",
display: "standalone",
},
});

The generated manifest.webmanifest will include:

{
"name": "My App",
"start_url": "/",
"display": "standalone",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon.png", "sizes": "512x512", "type": "image/png" }
]
}

Customizing manifest icons from the icons integration

Section titled “Customizing manifest icons from the icons integration”

Manifest icon auto-population scans the file-keyed icons entries. Any entry with manifest: true or a ManifestIconOptions object is added to manifest.webmanifest. The src defaults to the file’s public path, sizes and type are inferred from the icon definition, and purpose is taken from the manifest options.

Set default entries to false to exclude them from the output when you want a focused result:

eminence({
icons: {
source: "src/assets/logo.svg",
"icon-192.png": false,
"icon.png": false,
"icon-192x192.png": { size: 192, tag: { rel: "icon" }, manifest: true },
"icon-512.png": {
size: 512,
tag: { rel: "icon" },
manifest: { purpose: "maskable" },
},
"badge.png": {
size: 96,
tag: { rel: "icon" },
manifest: { src: "/brand/badge.png", purpose: "monochrome" },
},
},
manifest: {
name: "My App",
start_url: "/",
display: "standalone",
},
});

Resulting icons array in manifest.webmanifest:

[
{ "src": "/icon-192x192.png", "sizes": "192x192", "type": "image/png" },
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/brand/badge.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "monochrome"
}
]

You can also provide manifest.icons directly. Those entries are merged over icons resolved from the integration using src as the key, so matching src values override auto-populated metadata:

eminence({
icons: {
source: "src/assets/logo.svg",
"icon-192.png": false,
"icon.png": false,
"icon-192x192.png": { size: 192, tag: { rel: "icon" }, manifest: true },
"icon-512.png": { size: 512, tag: { rel: "icon" }, manifest: true },
},
manifest: {
name: "My App",
start_url: "/",
display: "standalone",
icons: [
{ src: "/icon-192x192.png", sizes: "200x200", type: "image/webp" },
{ src: "/custom.png", sizes: "1024x1024", type: "image/png" },
],
},
});

Resulting icons array in manifest.webmanifest:

[
{ "src": "/icon-192x192.png", "sizes": "200x200", "type": "image/webp" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/custom.png", "sizes": "1024x1024", "type": "image/png" }
]
eminence({
manifest: {
short_name: "App",
start_url: "/",
display_override: ["window-controls-overlay", "standalone"],
icons: [{ src: "/icon-192x192.png", sizes: "192x192", type: "image/png" }],
},
});
eminence({
manifest: {
name: "My App",
short_name: "App",
start_url: "/",
display: "standalone",
icons: [
{ src: "/icon-192x192.png", sizes: "192x192", type: "image/png" },
{ src: "/icon.png", sizes: "512x512", type: "image/png" },
],
description: "A great web app",
background_color: "#ffffff",
theme_color: "#1a1a2e",
scope: "/",
orientation: "portrait",
id: "my-app",
categories: ["productivity"],
screenshots: [
{
src: "/screenshots/home.png",
sizes: "1280x720",
type: "image/png",
form_factor: "wide",
},
],
shortcuts: [{ name: "New Note", url: "/notes/new" }],
related_applications: [{ platform: "play", id: "com.example.app" }],
prefer_related_applications: false,
file_handlers: [
{ action: "/open-file", accept: { "text/plain": [".txt"] } },
],
protocol_handlers: [{ protocol: "web+myapp", url: "/handle?url=%s" }],
share_target: {
action: "/share",
method: "POST",
enctype: "multipart/form-data",
},
launch_handler: {
client_mode: "navigate-existing",
},
note_taking: { new_note_shortcut: { url: "/notes/new" } },
scope_extensions: [{ origin: "https://example.com" }],
serviceworker: { src: "/sw.js" },
},
});
eminence({
manifest: false,
});

No file is generated and no warning is logged.

PWA requirements are enforced by TypeScript, not at runtime

Section titled “PWA requirements are enforced by TypeScript, not at runtime”

Chromium’s required manifest members — at least one of name or short_name, at least one of display or display_override, and start_url — are modeled as TypeScript intersection and union types. Invalid configurations are caught at authoring time without runtime overhead or confusing build errors.

prefer_related_applications is typed as false only, making true a compile-time error. This reflects the Chromium requirement that the field must be absent or explicitly false for the app to be installable.

icons is optional when the icons integration is active

Section titled “icons is optional when the icons integration is active”

When icons is omitted from manifest, the integration scans the configured file-keyed icons entries and automatically injects any files marked with manifest: true or a ManifestIconOptions object. This avoids requiring you to repeat file paths that the integration already knows about.

manifest.icons is merged over generated manifest icons. Generated entries are added first, then explicit manifest.icons entries override by matching src.

Auto icon population is controlled per icon entry

Section titled “Auto icon population is controlled per icon entry”

Rather than adding separate manifest-icon config to the manifest options, the icons integration owns the source of truth for which generated files contribute to the manifest. Each keyed icon entry can carry optional manifest metadata (sizes, type, purpose, src) or a simple manifest: true opt-in.

This keeps manifest icon paths and generation paths in sync automatically when overrides change.

Icons not marked for manifest are excluded

Section titled “Icons not marked for manifest are excluded”

Icons that do not set manifest do not produce manifest entries. This keeps favicon-only assets, Apple touch icons, and other auxiliary files out of manifest.webmanifest unless you explicitly opt them in.

File is not overwritten if it already exists

Section titled “File is not overwritten if it already exists”

If /manifest.webmanifest is already present in the build output, the integration logs a warning and disables further generation for that build. This preserves hand-crafted or CMS-generated manifests without silently clobbering them.

Passing manifest: false disables the feature cleanly. Omitting manifest logs a recommendation warning so new projects are nudged to make an intentional choice.

manifest.ts
import { constants } from "node:fs";
import { access, mkdir, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import type { IntegrationRuntimeContext } from "..";
import { resolveManifestIconsFromIconsOptions } from "./generate-icons";
export type WebManifestIconItem = {
src: string;
sizes?: string;
type?: string;
purpose?: string;
};
export type WebManifestScreenshotItem = {
src: string;
sizes?: string;
type?: string;
label?: string;
form_factor?: string;
platform?: string;
};
export type WebManifestShortcutItem = {
name: string;
url: string;
short_name?: string;
description?: string;
icons?: WebManifestIconItem[];
};
export type WebManifestRelatedApplication = {
platform: string;
url?: string;
id?: string;
};
export type WebManifestFileHandler = {
action: string;
accept: Record<string, string[]>;
};
export type WebManifestProtocolHandler = {
protocol: string;
url: string;
};
export type WebManifestShareTarget = {
action: string;
method?: string;
enctype?: string;
params?: Record<string, string>;
};
export type WebManifestLaunchHandler = {
client_mode?: string | string[];
};
export type WebManifestServiceWorker = {
src: string;
scope?: string;
type?: string;
update_via_cache?: string;
};
type NameOrShortName =
| { name: string; short_name?: string }
| { short_name: string; name?: never };
type DisplayOrDisplayOverride =
| { display: string; display_override?: string[] }
| { display_override: string[]; display?: never };
type WebManifestBase = {
start_url: string;
icons?: WebManifestIconItem[];
prefer_related_applications?: false;
description?: string;
background_color?: string;
theme_color?: string;
scope?: string;
orientation?: string;
id?: string;
categories?: string[];
screenshots?: WebManifestScreenshotItem[];
shortcuts?: WebManifestShortcutItem[];
related_applications?: WebManifestRelatedApplication[];
file_handlers?: WebManifestFileHandler[];
protocol_handlers?: WebManifestProtocolHandler[];
share_target?: WebManifestShareTarget;
launch_handler?: WebManifestLaunchHandler;
note_taking?: { new_note_shortcut?: { url: string } };
scope_extensions?: Array<{ origin: string }>;
serviceworker?: WebManifestServiceWorker;
};
export type WebManifestOptions = NameOrShortName &
DisplayOrDisplayOverride &
WebManifestBase;
export const WEB_MANIFEST_RECOMMENDATION =
"Recommendation: follow eminence-astro-suite.xeffen25.com/recommendations/when-you-should-add-a-manifest-webmanifest to learn when you should add a manifest.webmanifest.";
export const WEB_MANIFEST_RELATIVE_PATH = "/manifest.webmanifest";
const resolveManifestInput = (
input: WebManifestOptions,
options: IntegrationRuntimeContext["options"],
): WebManifestOptions => {
const autoIcons = resolveManifestIconsFromIconsOptions(options.icons);
if (autoIcons.length === 0 && input.icons === undefined) {
return input;
}
if (input.icons === undefined) {
return {
...input,
icons: autoIcons,
};
}
const iconsBySrc = new Map<string, WebManifestIconItem>();
for (const icon of autoIcons) {
iconsBySrc.set(icon.src, icon);
}
for (const icon of input.icons) {
iconsBySrc.set(icon.src, icon);
}
return {
...input,
icons: Array.from(iconsBySrc.values()),
};
};
const buildManifest = (options: WebManifestOptions): string => {
return `${JSON.stringify(options, null, 2)}\n`;
};
const exists = async (path: string): Promise<boolean> => {
try {
await access(path, constants.F_OK);
return true;
} catch (error) {
if (
error &&
typeof error === "object" &&
"code" in error &&
error.code === "ENOENT"
) {
return false;
}
throw error;
}
};
export async function generateManifest({
dir,
options,
logger,
}: IntegrationRuntimeContext): Promise<void> {
const input = options.manifest;
const outputPath = join(fileURLToPath(dir), "manifest.webmanifest");
const outputExists = await exists(outputPath);
if (input === false) {
if (outputExists) {
logger.info(
`No "${WEB_MANIFEST_RELATIVE_PATH}" file was generated nor modified because it already exists.`,
);
} else {
logger.info(
`No "${WEB_MANIFEST_RELATIVE_PATH}" file exists and no file was generated.`,
);
}
return;
}
if (input === undefined) {
logger.warn(
`No manifest.webmanifest file was generated because manifest is undefined. ${WEB_MANIFEST_RECOMMENDATION}`,
);
return;
}
if (typeof input !== "object" || input === null) {
logger.error(
"Invalid manifest configuration: expected an object with required PWA fields.",
);
throw new Error(
"Invalid manifest configuration: expected an object with required PWA fields.",
);
}
if (outputExists) {
logger.warn(
`Could not generate "${WEB_MANIFEST_RELATIVE_PATH}" because it already exists. Disabling manifest generation for this build.`,
);
options.manifest = false;
return;
}
try {
const normalizedInput = resolveManifestInput(input, options);
const content = buildManifest(normalizedInput);
await mkdir(dirname(outputPath), { recursive: true });
await writeFile(outputPath, content, "utf-8");
logger.info(`Generated "${WEB_MANIFEST_RELATIVE_PATH}"`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error(
`Failed to generate "${WEB_MANIFEST_RELATIVE_PATH}": ${message}`,
);
throw error;
}
}