docs/latest/advanced/app-wrapper.md
The app wrapper is the outermost component in Fresh's rendering hierarchy. It
defines the <html>, <head>, and <body> tags that every page shares. It is
only rendered on the server.
Use an app wrapper when you need to:
<html lang="en">)<meta> tags, fonts, or stylesheets<body> class or data attributeIf you're using file-based routing, create a
routes/_app.tsx file. Otherwise, register it programmatically with
app.appWrapper().
import { define } from "../utils.ts";
export default define.page(({ Component, url }) => {
return (
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<Component />
</body>
</html>
);
});
When building your app with new App() instead of file-based routing:
function AppWrapper({ Component }) {
return (
<html lang="en">
<head>
<meta charset="utf-8" />
<title>My App</title>
</head>
<body>
<Component />
</body>
</html>
);
}
app.appWrapper(AppWrapper);
Only one app wrapper is supported per App instance.
When Fresh renders a page, the components nest like this:
_app.tsx) - outermost, provides <html>/<head>/<body>_layout.tsx) - shared page chrome
(nav, sidebar, footer)The app wrapper wraps everything. Layouts sit inside it and wrap the page.
The app wrapper receives the same props as page components - url, state,
params, and more. This is useful for conditional logic:
import { define } from "../utils.ts";
export default define.page(({ Component, url, state }) => {
return (
<html lang="en" data-theme={state.theme ?? "light"}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
<meta property="og:url" content={url.href} />
<link rel="canonical" href={url.href} />
</head>
<body>
<Component />
</body>
</html>
);
});
Some routes may need to bypass the app wrapper entirely - for example, API
routes that return JSON, or pages that need a completely different HTML
structure. Use skipAppWrapper in the route config:
import { type RouteConfig } from "fresh";
import { define } from "../utils.ts";
export const config: RouteConfig = {
skipAppWrapper: true,
};
export default define.page(() => {
return (
<html>
<head>
<title>Embed</title>
</head>
<body>
<div id="widget">Embeddable widget</div>
</body>
</html>
);
});
When using programmatic layouts, pass skipAppWrapper as an option:
app.layout("/embed", EmbedLayout, { skipAppWrapper: true });