diff --git a/quartz/components/frames/DefaultFrame.tsx b/quartz/components/frames/DefaultFrame.tsx
new file mode 100644
index 000000000..d46c120d4
--- /dev/null
+++ b/quartz/components/frames/DefaultFrame.tsx
@@ -0,0 +1,61 @@
+import { PageFrame, PageFrameProps } from "./types"
+import HeaderConstructor from "../Header"
+
+const Header = HeaderConstructor()
+
+/**
+ * The default page frame — three-column layout with left sidebar, center
+ * content (header + body + afterBody), and right sidebar, followed by a footer.
+ *
+ * This is the original Quartz layout, extracted from renderPage.tsx.
+ */
+export const DefaultFrame: PageFrame = {
+ name: "default",
+ render({
+ componentData,
+ header,
+ beforeBody,
+ pageBody: Content,
+ afterBody,
+ left,
+ right,
+ footer: Footer,
+ }: PageFrameProps) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ )
+ },
+}
diff --git a/quartz/components/frames/FullWidthFrame.tsx b/quartz/components/frames/FullWidthFrame.tsx
new file mode 100644
index 000000000..f7748a15b
--- /dev/null
+++ b/quartz/components/frames/FullWidthFrame.tsx
@@ -0,0 +1,51 @@
+import { PageFrame, PageFrameProps } from "./types"
+import HeaderConstructor from "../Header"
+
+const Header = HeaderConstructor()
+
+/**
+ * Full-width page frame — no sidebars. The center content area spans the
+ * full width of the page. Header, beforeBody, body, afterBody, and footer
+ * are all rendered in a single column.
+ *
+ * Useful for page types like Canvas, presentations, or dashboards that
+ * need maximum horizontal space.
+ */
+export const FullWidthFrame: PageFrame = {
+ name: "full-width",
+ render({
+ componentData,
+ header,
+ beforeBody,
+ pageBody: Content,
+ afterBody,
+ footer: Footer,
+ }: PageFrameProps) {
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ )
+ },
+}
diff --git a/quartz/components/frames/MinimalFrame.tsx b/quartz/components/frames/MinimalFrame.tsx
new file mode 100644
index 000000000..7a95f7dc7
--- /dev/null
+++ b/quartz/components/frames/MinimalFrame.tsx
@@ -0,0 +1,23 @@
+import { PageFrame, PageFrameProps } from "./types"
+
+/**
+ * Minimal page frame — no sidebars, no header/footer chrome. Only the
+ * page body is rendered with a thin wrapper, plus the footer for legal/link
+ * obligations.
+ *
+ * Useful for immersive page types like full-screen canvases, kiosks,
+ * or custom landing pages that want complete control of the viewport.
+ */
+export const MinimalFrame: PageFrame = {
+ name: "minimal",
+ render({ componentData, pageBody: Content, footer: Footer }: PageFrameProps) {
+ return (
+ <>
+
+
+
+
+ >
+ )
+ },
+}
diff --git a/quartz/components/frames/index.ts b/quartz/components/frames/index.ts
new file mode 100644
index 000000000..b772fdfba
--- /dev/null
+++ b/quartz/components/frames/index.ts
@@ -0,0 +1,40 @@
+import { PageFrame } from "./types"
+import { DefaultFrame } from "./DefaultFrame"
+import { FullWidthFrame } from "./FullWidthFrame"
+import { MinimalFrame } from "./MinimalFrame"
+
+export type { PageFrame, PageFrameProps } from "./types"
+export { DefaultFrame } from "./DefaultFrame"
+export { FullWidthFrame } from "./FullWidthFrame"
+export { MinimalFrame } from "./MinimalFrame"
+
+/**
+ * Registry of built-in page frames. Page types can reference these by name
+ * via their `frame` property, and YAML config can override via
+ * `layout.byPageType..template`.
+ *
+ * The "default" frame reproduces the original three-column Quartz layout.
+ */
+const builtinFrames: Record = {
+ default: DefaultFrame,
+ "full-width": FullWidthFrame,
+ minimal: MinimalFrame,
+}
+
+/**
+ * Resolve a frame by name. Returns the DefaultFrame if the name is not found,
+ * logging a warning for unknown frame names.
+ */
+export function resolveFrame(name: string | undefined): PageFrame {
+ if (!name || name === "default") {
+ return DefaultFrame
+ }
+ const frame = builtinFrames[name]
+ if (!frame) {
+ console.warn(
+ `Unknown page frame "${name}", falling back to "default". Available frames: ${Object.keys(builtinFrames).join(", ")}`,
+ )
+ return DefaultFrame
+ }
+ return frame
+}
diff --git a/quartz/components/frames/types.ts b/quartz/components/frames/types.ts
new file mode 100644
index 000000000..22d745975
--- /dev/null
+++ b/quartz/components/frames/types.ts
@@ -0,0 +1,43 @@
+import { JSX } from "preact"
+import { QuartzComponent, QuartzComponentProps } from "../types"
+
+/**
+ * Props passed to a PageFrame's render function.
+ * Contains the resolved layout components and the shared component data.
+ */
+export interface PageFrameProps {
+ /** Component data shared across all components on the page */
+ componentData: QuartzComponentProps
+ /** The Head component (rendered in ) — NOT used by frames, included for completeness */
+ head: QuartzComponent
+ /** Header slot components (rendered inside ) */
+ header: QuartzComponent[]
+ /** Components rendered before the page body */
+ beforeBody: QuartzComponent[]
+ /** The page body component (Content) */
+ pageBody: QuartzComponent
+ /** Components rendered after the page body */
+ afterBody: QuartzComponent[]
+ /** Left sidebar components */
+ left: QuartzComponent[]
+ /** Right sidebar components */
+ right: QuartzComponent[]
+ /** Footer component */
+ footer: QuartzComponent
+}
+
+/**
+ * A PageFrame defines the inner HTML structure of a page inside the
+ * `` shell. Different frames can produce completely
+ * different layouts (e.g. with/without sidebars, horizontal scroll, etc.)
+ * while the outer shell (html, head, body, quartz-root) remains stable
+ * for SPA navigation.
+ */
+export interface PageFrame {
+ /** Unique name for this frame (e.g. "default", "full-width", "minimal") */
+ name: string
+ /** Render the inner page structure. Returns a JSX tree to be placed inside Body > #quartz-body. */
+ render: (props: PageFrameProps) => JSX.Element
+ /** Optional CSS string to include when this frame is active */
+ css?: string
+}