StoryLark
← All guides

Publishing Stories

A few ways to get a story onto a live site: the CLI (full control, including narration), the admin portal (browser upload for new stories, or editing an existing chapter in place), or a push from your own publishing system through the content API.

Format first

Stories are plain markdown — see authoring-stories.md for the full format. No custom parser needed for the common case.

CLI publish (full pipeline: text + audio + timings)

node packages/pipeline/publish.mjs --brand <id> --source <path-to-your-content>

This is the complete pipeline: parses your markdown, synthesizes word-synced narration (28 free on-device voices, or Azure premium voices with your own key), uploads everything, and updates the manifest. Add --no-audio if you don't have TTS set up yet — the app falls back to on-device Web Speech for listen mode. Add --storage azure-blob if you're deployed on Azure. Full flag reference: content-pipeline.md.

Re-publishing is cheap: only chapters whose content actually changed are re-processed (content-hash diffing), and re-narration is per block, not per chapter — editing one paragraph re-synthesizes just that paragraph, not the whole book.

Because the deployment stores its own copy of your source, a portal edit and a CLI publish can drift out of sync. --pull fetches the deployment's current source into your working tree before parsing, and every publish checks the live content against what it's about to write — a real conflict (exit code 2) names the chapter and refuses rather than silently overwriting someone's portal edit; --force publishes anyway.

Editing an existing chapter (admin portal)

Open your site's /admin, sign in with your operator account, and open any book to edit its chapters directly — a markdown editor with live preview, upload/download of the .md, image insertion, five-version history with one-click revert, and Up/Down reordering. See admin-guide.md for the full picture, including the "managed externally" case for content synced in from elsewhere. A portal save publishes text instantly; narration catches up through the bulk narration queue, automatically.

If a chapter was already published from a CLI run, its contentHash at save time is checked against the live version — editing a stale copy (say, two browser tabs open at once) is refused with an offer to reload the live text rather than silently clobbering it.

New story upload (admin portal, text-only)

Open your site's /admin, sign in with your operator account, and use the "Publish a story" form: book id, title, author, and the markdown text. This is the single-chapter shorthand format — one story, no chapters to split.

Under the hood this doesn't reimplement the pipeline in the browser or the Worker — it commits your markdown to the site's own repo (via the GitHub API) and triggers publish.yml, which runs the exact same packages/pipeline/publish.mjs the CLI uses. There's only ever one publish pipeline; the portal is just a front door to it.

Narration depends on what the publish workflow has configured. If AZURE_SPEECH_KEY/AZURE_SPEECH_REGION are set as repo secrets, the uploaded story gets full narration. If not, it publishes text-only — the portal's confirmation message says which happened, honestly, every time. Requires GITHUB_REPO and GITHUB_DEPLOY_TOKEN configured on the deployment (see admin-guide.md); without them, upload is disabled and the portal tells you why.

Pushing from your own publishing system

If you already have a CMS or a release step of your own, /api/content/v1 is a documented, versioned HTTP contract it can call directly — single chapters, whole books, or a zip/batch import for onboarding a catalogue at once. Content pushed this way shows in the portal as read-only ("managed externally"), naming your system, so nobody accidentally edits a copy your system is about to overwrite on its next push. Full reference: content-api.md in the engine repo.

Removing or changing a story

Edit the markdown and re-run publish.mjs (CLI), edit it directly in the portal's content manager, or DELETE it through the content API — there's no delete button in the portal UI yet, only edit and reorder. See content-pipeline.md for how the manifest and content-hash state track what's live.


Found a gap? StoryLark is open source — improve these docs on GitHub.