diff --git a/apps/api/.env.example b/apps/api/.env.example
index af3fdeccf..9ab8297a6 100644
--- a/apps/api/.env.example
+++ b/apps/api/.env.example
@@ -9,12 +9,12 @@ NEXT_PUBLIC_API_URL=http://localhost:8080
# === CRON Secret for Internal API Calls ===
CRON_SECRET=your-secure-cron-secret-key
-# === Content Preview Secret ===
+# === Content Preview Secret (optional) ===
# Signs the preview links that let a reviewer read an unpublished record
-# without an account. The signature is the *only* access control on those
-# links, so this is required whenever a content type has `editorial.preview`
-# enabled: at least 32 random bytes, or the API refuses to boot in production
-# and preview stays switched off everywhere else.
+# without an account. Optional: leave it unset and the API boots normally with
+# preview switched off - minting a link answers 503 naming this variable, and
+# opening one answers 404. Set at least 32 random bytes to switch it on, because
+# the signature is the *only* access control those links have.
#
# openssl rand -base64 32
#
diff --git a/apps/docs/.env.example b/apps/docs/.env.example
index 726860ed7..7e09103d8 100644
--- a/apps/docs/.env.example
+++ b/apps/docs/.env.example
@@ -7,12 +7,12 @@ NEXT_PUBLIC_WEB_URL=http://localhost:3000
# === CRON Secret for Internal API Calls ===
CRON_SECRET=your-secure-cron-secret-key
-# === Content Preview Secret ===
+# === Content Preview Secret (optional) ===
# Signs the preview links that let a reviewer read an unpublished record
-# without an account. The signature is the *only* access control on those
-# links, so this is required whenever a content type has `editorial.preview`
-# enabled: at least 32 random bytes, or the API refuses to boot in production
-# and preview stays switched off everywhere else.
+# without an account. Optional: leave it unset and the API boots normally with
+# preview switched off - minting a link answers 503 naming this variable, and
+# opening one answers 404. Set at least 32 random bytes to switch it on, because
+# the signature is the *only* access control those links have.
#
# openssl rand -base64 32
#
diff --git a/apps/docs/content/docs/dev/content-engine/admin-form-layouts.mdx b/apps/docs/content/docs/dev/content-engine/admin-form-layouts.mdx
new file mode 100644
index 000000000..19f28d053
--- /dev/null
+++ b/apps/docs/content/docs/dev/content-engine/admin-form-layouts.mdx
@@ -0,0 +1,285 @@
+---
+title: Dialog or page, and custom layouts
+description: Choose how a content type's create and edit forms appear - and rearrange them without giving up a single line of the generated behaviour.
+icon: LayoutPanelLeft
+---
+
+The generated create and edit forms open in a dialog. That is right for most
+records and wrong for the ones people spend an hour inside, so a content type can
+say which it wants - and, separately, a plugin can decide where the fields go.
+
+The two are independent. Page mode with no layout is a perfectly good screen; a
+custom layout inside a dialog works too.
+
+## Dialog or page
+
+```ts title="src/content/article.ts"
+admin: {
+ label: { plural: "Articles", singular: "Article" },
+
+ create: { mode: "page" },
+ edit: { mode: "page" },
+}
+```
+
+`"dialog"` and `"page"`, and **`"dialog"` is the default** - a content type
+written before this existed behaves exactly as it did, and nothing about it
+changes until somebody adds those two lines. Each action is independent: a
+content type can create on a page and edit in a dialog.
+
+```ts
+// @ts-expect-error - only "dialog" and "page" are presentation modes
+create: { mode: "drawer" }
+```
+
+### The URLs
+
+Page mode is served by the **same** catch-all route as the list. There is no
+second router, and no file to add:
+
+```text
+/admin/content/blog/post list
+/admin/content/blog/post/create create page
+/admin/content/blog/post/42/edit edit page
+```
+
+The Create button becomes a link rather than a dialog trigger - none of the
+form's JavaScript is downloaded until the page it points at is requested - and
+the pencil in each table row becomes a link too. Typing either URL works, which
+is the point of checking permissions on the server rather than on the button.
+
+