@open-press/core
Workspace
The root component of every OpenPress project. Holds one or more <Press> children. Single-document projects use a Workspace with one Press; multi-document projects (proposal + pitch deck + social) use a Workspace with several.
press/index.tsx default-exports a
<Workspace>. Number of <Press> children = number of
documents. Uniform shape means single-doc → multi-doc growth is just "add another Press"; no
restructuring needed.
Live preview: see what the Workspace gallery looks like (static mock of three Presses).
Root of every OpenPress project. Single-doc workspaces hold one Press; multi-doc workspaces hold several. Workspace carries shared theme tokens, media library, and any data its children import via plain ES imports.
import { Workspace } from "@open-press/core"; <Workspace
name? // project label (tab bar, PDF metadata)
theme? // workspace-level shared theme dir, default "./theme"
media? // workspace-level shared media dir, default "./media"
>
<Press ... /> // 1 child = single-doc project
<Press ... /> // N children = multi-doc project
</Workspace> Props
| Name | Type | Default | Description |
|---|---|---|---|
name | string | Optional workspace label. Surfaced in the reader tab bar and in PDF metadata as the project name. | |
theme | string | Path to a workspace-level theme directory. Children inherit unless they set their own theme prop. Default: "./theme". | |
media | string | Path to a workspace-level media directory. Default: "./media". | |
children required | Press[] | One or more <Press> children. Each must have a unique slug prop. |
Project layout
my-paper/
├── package.json ← deploy adapter goes here (optional)
└── press/
├── index.tsx ← <Workspace><Press ...>...</Press></Workspace>
├── chapters/ ← MDX content
├── theme/ ← brand tokens
├── components/ ← workspace-local React components
└── media/ ← images, vectors my-launch/
├── package.json ← deploy adapter goes here
└── press/
├── index.tsx ← <Workspace> with three <Press> children
├── theme/ ← shared brand tokens (default)
├── media/ ← shared image library (default)
├── data.ts ← shared facts / figures (plain ES module)
├── proposal/
│ ├── index.tsx ← default-exports <Press title="..." page="a4" ...>
│ ├── chapters/ ← MDX
│ └── theme/ ← optional per-doc override
├── pitch-deck/
│ ├── index.tsx ← default-exports <Press title="..." page="slide-16-9" ...>
│ └── slides/
└── social/
├── index.tsx ← default-exports <Press page="social-square" ...>
└── cards/ import { Workspace, Press, Frame, mdxSource } from "@open-press/core";
import { Sections, Toc } from "@open-press/core/manuscript";
export default function Project() {
return (
<Workspace>
<Press
title="Transport models in dense networks"
page="a4"
sources={[
mdxSource({ id: "story", preset: "section-folders", root: "chapters" }),
]}
>
<Frame frameKey="cover" role="document.cover"><Cover /></Frame>
<Toc source="story" />
<Sections source="story" />
</Press>
</Workspace>
);
} import { Workspace } from "@open-press/core";
import Proposal from "./proposal";
import PitchDeck from "./pitch-deck";
import Social from "./social";
export default function Launch() {
return (
<Workspace name="Series A launch">
<Proposal slug="proposal" />
<PitchDeck slug="pitch-deck" />
<Social slug="social" />
</Workspace>
);
} import { Press, Frame, mdxSource } from "@open-press/core";
import { Sections, Toc } from "@open-press/core/manuscript";
export default function Proposal({ slug }: { slug?: string }) {
return (
<Press
slug={slug}
title="Series A 提案書"
page="a4"
sources={[
mdxSource({ id: "story", preset: "section-folders", root: "chapters" }),
]}
>
<Frame frameKey="cover" role="document.cover"><Cover /></Frame>
<Toc source="story" />
<Sections source="story" />
</Press>
);
} What workspace mode unlocks
Reader / build
| Name | Type | Default | Description |
|---|---|---|---|
Per-doc routes | behavior | Reader URL has one path per slug — /proposal, /pitch-deck, /social. The root / shows a workspace index with cards for each doc. | |
Tab bar | behavior | The workbench shows a tab bar across docs (workspace name on the left, doc tabs to the right). | |
Shared theme tokens | behavior | Workspace-level theme/tokens.css applies to every doc unless that doc sets its own theme prop. | |
Per-doc build artifacts | behavior | public/openpress/<slug>/document.json per doc, plus a top-level public/openpress/workspace.json manifest. |
CLI behavior changes
| Name | Type | Default | Description |
|---|---|---|---|
npm run build | behavior | Builds every doc in the workspace. Validation aborts the whole build if any doc has structural issues. | |
npm run openpress:pdf | behavior | Generates one PDF per doc into dist-react/<slug>.pdf. Pass --doc=<slug> to build a single PDF. | |
npm run openpress:deploy | behavior | Deploys the whole workspace as one site. The deploy adapter receives dist-react/ with multi-doc routes intact. | |
Tier 3 tools (search, replace, inspect) | behavior | All accept --doc=<slug> to scope to one document; default is workspace-wide. |
When NOT to merge into one Workspace
<Workspace> itself is always present — the question is whether to put multiple
docs into one Workspace or use separate Workspaces (separate package.json projects).
Keep them separate when:
- Separate brands or unrelated content — two docs with nothing in common except living in the same git repo. Give them separate Workspaces in a monorepo.
- Versioned docs of the same content — use git branches / tags, not multiple Press children of one Workspace. A Workspace is a coherent product, not an archive.
- Different deploy targets — if two docs go to different deploy adapters or different Cloudflare projects, they want separate Workspaces (deploy config is workspace-level).
Sharing data between docs
Workspace doesn't introduce a special data API. The recommended pattern is plain ES module
imports — a press/data.ts exports facts / figures / dates, and each
press/<slug>/index.tsx imports what it needs. Updating a number once
propagates to every doc that imports it.
// press/data.ts
export const RAISE = {
amount: "$8M",
round: "Series A",
closeDate: "2026-09-30",
};
// press/proposal/chapters/01-overview.mdx
import { RAISE } from "../../data";
We are raising {RAISE.amount} in our {RAISE.round}, closing {RAISE.closeDate}.
// press/pitch-deck/slides/03-ask.mdx
import { RAISE } from "../../data";
Ask: {RAISE.amount} ({RAISE.round}). Related
- Press — each child document.
- Workspace config — operational settings in
package.json. - Themes — workspace-level vs per-doc theme directories.