# @spacefast/zero-compat-emdash

> Translates an EmDash CMS project into a Spacefast Zero content model, entirely on the CLI side.

Latest version **0.5.0** (published 2026-09-29) · MIT license · 0 weekly downloads

## Install

```sh
npm install @spacefast/zero-compat-emdash
pnpm add @spacefast/zero-compat-emdash
yarn add @spacefast/zero-compat-emdash
bun add @spacefast/zero-compat-emdash
```

## Health

**Score 60/100 (C)** — status: active.

Positive: esm support; no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads; no types; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.5.0 |
| Published | 2026-09-29 |
| First published | 2026-09-10 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | none |
| Module format | ESM |
| Node | >=20 |
| Dependencies | 2 |
| Unpacked size | 75.5 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | bi |

## Links

- npm: https://www.npmjs.com/package/@spacefast/zero-compat-emdash
- Repository: https://github.com/spacefast/monorepo
- Homepage: https://github.com/spacefast/monorepo#readme
- Issues: https://github.com/spacefast/monorepo/issues
- npm.io page: https://npm.io/package/@spacefast/zero-compat-emdash

## Dependencies (2)

- [zod](https://npm.io/package/zod.md) ^4.3.6
- [@spacefast/common](https://npm.io/package/@spacefast/common.md) 0.5.0

## Recent versions

- 0.5.0 (latest) — 2026-09-29
- 0.4.1 — 2026-09-10

## README

# @spacefast/zero-compat-emdash

Translates an [EmDash](https://github.com/emdash-cms/emdash) project into Spacefast Zero content.

This is a CLI-side translation, not a server-side adapter. Spacefast runs Zero and only Zero;
nothing here ships to the platform, and there is no EmDash compatibility layer at runtime. You
point it at an EmDash seed, you get Zero content and a report of exactly what happened to your
site.

> **Two emit targets; the CLI takes the authored one.** `sf zero import emdash` reads
> `translateEmDashSeed`'s mapped model and lets `@spacefast/zero-compile` print the capsule
> content module the author edits next — that authoring lane has landed. This package's own
> `translateEmDashProject` still emits the compiled `spacefast.zero-wordpress`
> ContentModelRelease plus the Markdown files its sync bindings name, for callers that want the
> build output directly. The split is what makes carrying both cheap: `translate.ts` maps an
> EmDash seed onto a source-agnostic model and knows nothing about either target; only
> `emit-content-model.ts` is about the compiled one.

## Input

An EmDash site declares its whole schema and optional content in one seed document, resolved
from `.emdash/seed.json`, `package.json#emdash.seed`, or `seed/seed.json`. Export a whole
project with:

```bash
npx emdash export-seed --with-content > seed.json
```

## Use

```ts
import { readFile } from "node:fs/promises";
import { formatTranslationReport, translateEmDashProject } from "@spacefast/zero-compat-emdash";

const seed = JSON.parse(await readFile("seed.json", "utf8"));
const { contentModel, files, report } = await translateEmDashProject(seed);

console.log(formatTranslationReport(report));
for (const [path, markdown] of Object.entries(files)) {
  await writeFile(path, markdown);
}
```

`contentModel` is already validated against `contentModelReleaseV1Schema`, revision included.
`files` are the repo Markdown files, keyed by the exact path each sync binding names.

## The report

Silent loss is the one thing a compatibility translator must never do, so every part of the
source project leaves a record:

| status       | meaning                                                                                  |
| ------------ | ---------------------------------------------------------------------------------------- |
| `mapped`     | carried over with its meaning intact                                                     |
| `renamed`    | carried over under a different name, because the original is not a legal Zero identifier |
| `downgraded` | carried over, but the target expresses less than the source did                          |
| `refused`    | not carried over; Zero has no counterpart                                                |

A translation is faithful exactly to the extent the `refused` list is empty. It usually is not,
and that is the useful part.

## What carries over

| EmDash field type | Zero                         | note                                       |
| ----------------- | ---------------------------- | ------------------------------------------ |
| `string`, `url`   | `text` (single line)         |                                            |
| `text`            | `text` (multiline)           |                                            |
| `number`          | `number`                     |                                            |
| `integer`         | `number`                     | **downgraded** — integrality is lost       |
| `boolean`         | `boolean`                    |                                            |
| `datetime`        | `datetime`                   |                                            |
| `select`          | `enum`                       | refused when the field declares no options |
| `multiSelect`     | `enum` (multiple)            | refused when the field declares no options |
| `portableText`    | `blocks` + a Markdown source |                                            |
| `image`, `file`   | `media`                      |                                            |
| `reference`       | `reference`                  | refused when the target is not declared    |
| `slug`            | WordPress post slug          | native, so not a content model field       |
| `json`            | —                            | **refused** — needs a Lakebed table        |
| `repeater`        | —                            | **refused**                                |

A WordPress content model always declares exactly one `posts`, `pages`, and `media` resource. The
seed's own `posts` and `pages` collections are adopted onto the natives; any that the seed does
not declare are synthesized. A seed collection named `media` is refused — the media library is
the platform's.

Every translated collection gets `list` and `get` queries and a `save` mutation, so the SDK's
invalidation graph is real: `save` writes the resource its own reads read. Synthesized natives
the seed never declared stay read-only.

## What does not

Refused with a reason, never dropped quietly: `menus`, `widgetAreas`, `sections`, `redirects`,
`bylines`, `taxonomies`, `settings`, and any localization (`defaultLocale`, per-entry `locale`,
`translationOf`). None have a counterpart in a Zero content model.

Content data is the subtle one. A ContentModelRelease describes a content _schema_; the only content
it carries is Markdown reached through a sync binding. So an entry's `portableText` field
becomes `content/<collection>/<slug>.md` (or `content/<collection>/<slug>.<field>.md` when the
collection declares more than one), and every other value on that entry — `title`,
`excerpt`, `featured_image`, taxonomy assignments, bylines, draft status — is refused by name.
Importing that data is a separate job from compiling a content model.

## Portable Text

Rich text becomes Markdown, because Markdown is what a sync binding reconciles against a
WordPress block field. The WordPress php-toolkit owns Markdown-to-blocks and back; nothing here
produces blocks directly.

Paragraphs, `h1`–`h6`, blockquotes, bullet and numbered lists, `strong`, `em`, `code`,
`strike-through`, links, and image nodes convert. Anything else — a custom node type, an unknown
decorator, an unresolvable mark definition — is refused by name. Real EmDash templates ship
custom node types (`marketing.hero` and friends), and a converter that quietly flattened one
into its text would corrupt a page while reporting success.

A full Portable Text converter, including custom node types mapped onto registered Zero
components, is follow-up work.

## Fixture

`src/fixtures/emdash-blog-seed.json` is the unmodified `templates/blog/seed/seed.json` from
[emdash-cms/emdash](https://github.com/emdash-cms/emdash) at commit
`1717d31b351164a5f78e95fe004ee582c7c50f40` (MIT).

---
_Source: https://npm.io/package/@spacefast/zero-compat-emdash · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
