diff --git a/apps/api/.env.example b/apps/api/.env.example
index af3fdeccf..1b0847218 100644
--- a/apps/api/.env.example
+++ b/apps/api/.env.example
@@ -9,17 +9,18 @@ NEXT_PUBLIC_API_URL=http://localhost:8080
# === CRON Secret for Internal API Calls ===
CRON_SECRET=your-secure-cron-secret-key
-# === Content Preview Secret ===
-# 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.
+# === Content Preview Secret (optional override) ===
+# Signs the preview links that let a reviewer read an unpublished record without
+# an account. Nothing to set: the install generates 32 random bytes on first use
+# and stores them in `core_secrets`, so preview works out of the box.
+#
+# Set this only for the two things a generated key cannot do - revoking every
+# outstanding link at once, and sharing a key between deployments that do not
+# share a database. Anything under 32 bytes is ignored with a warning.
#
# openssl rand -base64 32
#
-# Rotating this value revokes every outstanding preview link at once.
-CONTENT_PREVIEW_SECRET=
+# CONTENT_PREVIEW_SECRET=
# === AI (Vercel AI SDK) ===
# Gateway (default): one key for Anthropic, OpenAI, Google, etc. via `provider/model`
diff --git a/apps/docs/.env.example b/apps/docs/.env.example
index 726860ed7..3bfef1532 100644
--- a/apps/docs/.env.example
+++ b/apps/docs/.env.example
@@ -7,17 +7,18 @@ NEXT_PUBLIC_WEB_URL=http://localhost:3000
# === CRON Secret for Internal API Calls ===
CRON_SECRET=your-secure-cron-secret-key
-# === Content Preview Secret ===
-# 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.
+# === Content Preview Secret (optional override) ===
+# Signs the preview links that let a reviewer read an unpublished record without
+# an account. Nothing to set: the install generates 32 random bytes on first use
+# and stores them in `core_secrets`, so preview works out of the box.
+#
+# Set this only for the two things a generated key cannot do - revoking every
+# outstanding link at once, and sharing a key between deployments that do not
+# share a database. Anything under 32 bytes is ignored with a warning.
#
# openssl rand -base64 32
#
-# Rotating this value revokes every outstanding preview link at once.
-CONTENT_PREVIEW_SECRET=
+# CONTENT_PREVIEW_SECRET=
# === Docker Database Postgres ===
POSTGRES_USER=root
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..c76d88871
--- /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.
+
+