@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.

1.0 contract. Every project's 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).
Component Plan · v1.0

# <Workspace>

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

Single-doc project
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
Multi-doc project
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/
Single-doc — press/index.tsx
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>
  );
}
Multi-doc — press/index.tsx
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>
  );
}
Per-doc — press/proposal/index.tsx
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.

Shared data via import
// 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.