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.

option type default required description
name string - Yes* Full name of the web application. Required unless short_name is provided.
short_name string - Yes* Short name for use in contexts where name is too long. Required unless name is provided.
start_url string - Yes URL loaded when the app is launched.
display string - Yes** Display mode for the app. Common values: "standalone", "fullscreen", "minimal-ui", "browser". Required unless display_override is provided.
display_override string[] - Yes** Ordered list of display mode candidates. Required unless display is provided.
icons WebManifestIconItem[] - No Icons to represent the app. If omitted and the icons feature is active, auto-populated from generated 192 and 512 icons.
prefer_related_applications false - No Must be false or omitted. Setting true is a TypeScript error.
description string - No Short description of the app’s purpose.
background_color string - No Background color for the splash screen before the stylesheet loads.
theme_color string - No Default theme color for the OS title bar or browser chrome.
scope string - No Navigation scope for the app. URLs outside the scope open in the browser.
orientation string - No Default screen orientation. Common values: "portrait", "landscape", "any".
id string - No Unique string identity for the app across installs and updates.
categories string[] - No App store categories the app belongs to.
screenshots WebManifestScreenshotItem[] - No Screenshots shown in app stores or install prompts.
shortcuts WebManifestShortcutItem[] - No App shortcuts exposed through OS context menus or long-press actions.
related_applications WebManifestRelatedApplication[] - No Native app alternatives to this web app.
file_handlers WebManifestFileHandler[] - No File types the app can open as a registered file handler.
protocol_handlers WebManifestProtocolHandler[] - No Custom URL protocols the app handles.
share_target WebManifestShareTarget - No Configuration for receiving shared content from the OS share sheet.
launch_handler WebManifestLaunchHandler - No Controls the client behavior when the app is launched.
note_taking { new_note_shortcut?: { url: string } } - No Configuration for note-taking shortcut integration.
scope_extensions Array<{ origin: string }> - No Additional origins that extend the app’s navigation scope.
serviceworker WebManifestServiceWorker - No Service 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.

field type required description
src string Yes Path to the icon image file.
sizes string No One or more sizes as space-separated <width>x<height> values, for example "192x192", or "any" for SVGs.
type string No MIME type of the icon, for example "image/png" or "image/svg+xml".
purpose string No One or more space-separated purpose keywords: "any", "maskable", "monochrome".
field type required description
src string Yes Path to the screenshot image.
sizes string No Dimensions in <width>x<height> format.
type string No MIME type of the screenshot.
label string No Accessible label for the screenshot.
form_factor string No Display context hint. Common values: "narrow", "wide".
platform string No Target platform hint, for example "windows", "android".
field type required description
name string Yes Name displayed for the shortcut.
url string Yes URL activated when the shortcut is selected.
short_name string No Abbreviated name for constrained display.
description string No Description of what the shortcut does.
icons WebManifestIconItem[] No Icons to represent the shortcut.
field type required description
platform string Yes Platform the related app is hosted on.
url string No URL to the related app’s store page.
id string No App identifier on the platform.
field type required description
action string Yes URL that handles file open requests.
accept Record<string, string[]> Yes Map of MIME type to accepted file extensions.
field type required description
protocol string Yes The custom protocol to handle, for example "web+example".
url string Yes URL template with %s substituted for the full protocol URL.
field type required description
action string Yes URL that receives the shared data.
method string No HTTP method, "GET" or "POST". Defaults to "GET".
enctype string No Encoding type for POST requests.
params Record<string, string> No Mapping of share data fields to URL or form parameters.
field type required description
client_mode string | string[] No How the browser client handles an app launch. Common value: "navigate-existing".
field type required description
src string Yes Path to the service worker script.
scope string No Scope of the service worker.
type string No Module type. Use "module" for ES module service workers.
update_via_cache string No Cache 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;
}
}