Skip to content

manifest.webmanifest

The manifest feature writes /manifest.webmanifest into the build output during astro build. The JSON output contains the options you provide.

If manifest is omitted, Eminence logs a recommendation. Set manifest: false to disable generation and silence it.

The TypeScript type requires:

  • name or short_name
  • display or display_override
  • start_url

prefer_related_applications, when present, may only be false.

Manifest icons are explicit. Public favicon discovery does not add entries because 32×32 favicons and 180×180 Apple touch icons are not substitutes for installable application icons.

Place appropriately sized files in public/ and list them in manifest.icons:

eminence({
manifest: {
name: "Example",
short_name: "Example",
start_url: "/",
display: "standalone",
icons: [
{ src: "/icon-192.png", sizes: "192x192", type: "image/png" },
{
src: "/icon-512.png",
sizes: "512x512",
type: "image/png",
purpose: "any maskable",
},
],
},
});
option type required description
name string Yes* Full application name; optional when short_name is present.
short_name string Yes* Compact name; optional when name is present.
start_url string Yes URL loaded when the application launches.
display string Yes* Display mode; optional with display_override.
display_override string[] Yes* Ordered display candidates; optional with display.
icons WebManifestIconItem[] No Explicit installable-application icons.
description string No Application description.
background_color string No Splash-screen background color.
theme_color string No Default browser or OS chrome color.
scope string No Navigation scope.
orientation string No Preferred screen orientation.
id string No Stable application identity.
categories string[] No Application categories.
screenshots WebManifestScreenshotItem[] No Install and store screenshots.
shortcuts WebManifestShortcutItem[] No OS shortcut menu entries.
related_applications WebManifestRelatedApplication[] No Related native applications.
prefer_related_applications false No May only be false.
file_handlers WebManifestFileHandler[] No File associations.
protocol_handlers WebManifestProtocolHandler[] No Custom protocol handlers.
share_target WebManifestShareTarget No OS share target.
launch_handler WebManifestLaunchHandler No Launch behavior.
note_taking { new_note_shortcut?: { url: string } } No Note-taking integration.
scope_extensions Array<{ origin: string }> No Additional navigation origins.
serviceworker WebManifestServiceWorker No Service worker metadata.

If a manifest already exists in the output directory, Eminence leaves it untouched and logs a warning.

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 "..";
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 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 content = buildManifest(input);
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;
}
}