Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,6 @@ CMS_CONTENT_DIR=
CMS_MEDIA_DIR=
CMS_PUBLIC_MEDIA_PATH=
CMS_ADAPTER=

# Optional plugins — configure in sourcedraft.config.json (plugins, requiredPlugins, discoverPlugins)
# See docs/plugins.md. Plugin paths are server-only; never expose plugin secrets to the browser.
28 changes: 17 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SourceDraft

SourceDraft is a free, open-source editor for Markdown and MDX blogs backed by GitHub. You write in the browser, upload images, check your metadata, preview the generated file, and publish into your site repository.
SourceDraft is a free, open-source editor for Markdown and MDX blogs. You write in the browser, upload images, check SEO metadata, preview the generated file, and publish to a Git repository or remote CMS (WordPress, Ghost).

**Project status:** SourceDraft is an early local/private MVP for Git-backed Markdown and MDX publishing. It is usable for solo writing and GitHub commits, but it is not a hosted CMS, multi-user product, or finished SaaS. See [docs/project-status.md](docs/project-status.md) and [CHANGELOG.md](CHANGELOG.md).

Expand All @@ -18,9 +18,9 @@ More views (toolbar, autosave, media library, content quality, preview, setup he

## What is SourceDraft?

SourceDraft is not WordPress and not a hosted website builder. It is a local **Studio** (editor) plus a small **publish API** that commits content and media files to GitHub.
SourceDraft is not WordPress and not a hosted website builder. It is a local **Studio** (editor) plus a small **publish API** that commits content and media to your target — GitHub, GitLab, Bitbucket, WordPress, or Ghost.

Your static site — Astro today, others later — still builds and deploys exactly as before. SourceDraft creates or updates `.mdx` or `.md` files in the folder you configure, and can upload images to `mediaDir`.
Your static site still builds and deploys exactly as before. SourceDraft creates or updates `.mdx` or `.md` files (via adapters) in the folder you configure, or pushes posts to a remote CMS API. Images can commit to your repo, upload to Cloudinary, or (experimentally) target S3-compatible storage.

## Who is this for?

Expand Down Expand Up @@ -48,10 +48,11 @@ Your static site — Astro today, others later — still builds and deploys exac

- Host your website or run your Astro build
- OAuth, user accounts, or role-based access
- Full S3/R2 media upload (config validation only today; Cloudinary supported)
- Adapters beyond `astro-mdx` and `markdown`
- Full S3/R2 media upload (`s3-compatible` validates config only; use Cloudinary or git media today)
- Post list in Studio for Bitbucket, WordPress, and Ghost publishers
- OAuth, team accounts, or hosted multi-tenant Studio

See [docs/project-status.md](docs/project-status.md).
Eight adapters ship today — see [docs/adapters.md](docs/adapters.md). See [docs/project-status.md](docs/project-status.md) for the full shipped vs experimental list.

## How publishing works

Expand All @@ -65,12 +66,14 @@ API tokens never reach the browser. They are read from `.env` on the server when

Details: [docs/publishers.md](docs/publishers.md) · [docs/git-publishers.md](docs/git-publishers.md) · [docs/wordpress.md](docs/wordpress.md) · [docs/ghost.md](docs/ghost.md)

**Compatibility**
**Compatibility (summary)**

| Kind | Publishers | Notes |
|------|------------|-------|
| Git file | GitHub, GitLab, Bitbucket | Commit `.md`/`.mdx` to a repo; list/edit in Studio for GitHub/GitLab |
| Remote CMS | WordPress, Ghost | Server-side API connectors; updates need `remoteId` from prior publish |
| | Adapters (8) | Publishers (5) | Media (3) | Deploy hooks (4) |
|---|--------------|----------------|-----------|------------------|
| **Shipped** | Astro MDX, Markdown, Next.js MDX, Hugo, Eleventy/Jekyll, Docusaurus, MkDocs, Nuxt Content | GitHub, GitLab, Bitbucket, WordPress, Ghost | Git repo, Cloudinary, S3-compatible† | Generic, Vercel, Netlify, Cloudflare Pages |
| **Notes** | Plugin loader for custom adapters | Git: list posts (GH/GL); Bitbucket publish only; WP/Ghost API publish | †S3 upload not implemented yet | Optional `DEPLOY_HOOK_URL` after publish |

Full matrices: [adapters](docs/adapters.md) · [publishers](docs/publishers.md) · [media](docs/media.md) · [deploy-hooks](docs/deploy-hooks.md) · [quickstart recipes](docs/quickstart-recipes.md)

## Quickstart

Expand Down Expand Up @@ -156,6 +159,9 @@ Issues and pull requests are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md) fo
## Documentation

- [Getting started](docs/getting-started.md)
- [Quickstart recipes](docs/quickstart-recipes.md) — Astro+GitHub, Hugo+GitLab, WordPress, Cloudinary, deploy hooks, …
- [Plugins](docs/plugins.md)
- [SEO fields](docs/seo-fields.md)
- [Demo mode](docs/demo-mode.md)
- [Non-technical overview](docs/non-technical-overview.md) — for writers
- [Publishers overview](docs/publishers.md)
Expand Down
7 changes: 4 additions & 3 deletions apps/studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"predev": "pnpm --filter @sourcedraft/core --filter @sourcedraft/adapter-astro-mdx --filter @sourcedraft/adapter-markdown --filter @sourcedraft/adapter-nextjs-mdx --filter @sourcedraft/adapter-hugo-markdown --filter @sourcedraft/adapter-eleventy-jekyll-markdown --filter @sourcedraft/adapter-docusaurus-mdx --filter @sourcedraft/adapter-mkdocs-markdown --filter @sourcedraft/adapter-nuxt-content-markdown --filter @sourcedraft/adapters --filter @sourcedraft/publishers --filter @sourcedraft/media-providers --filter @sourcedraft/setup --filter @sourcedraft/github-publisher --filter @sourcedraft/config build",
"prebuild": "pnpm --filter @sourcedraft/core --filter @sourcedraft/adapter-astro-mdx --filter @sourcedraft/adapter-markdown --filter @sourcedraft/adapter-nextjs-mdx --filter @sourcedraft/adapter-hugo-markdown --filter @sourcedraft/adapter-eleventy-jekyll-markdown --filter @sourcedraft/adapter-docusaurus-mdx --filter @sourcedraft/adapter-mkdocs-markdown --filter @sourcedraft/adapter-nuxt-content-markdown --filter @sourcedraft/adapters --filter @sourcedraft/publishers --filter @sourcedraft/media-providers --filter @sourcedraft/setup --filter @sourcedraft/github-publisher --filter @sourcedraft/config build",
"dev": "concurrently -n web,api -c blue,gray \"vite\" \"tsx watch server/index.ts\"",
"predev": "pnpm --filter @sourcedraft/core --filter @sourcedraft/adapter-astro-mdx --filter @sourcedraft/adapter-markdown --filter @sourcedraft/adapter-nextjs-mdx --filter @sourcedraft/adapter-hugo-markdown --filter @sourcedraft/adapter-eleventy-jekyll-markdown --filter @sourcedraft/adapter-docusaurus-mdx --filter @sourcedraft/adapter-mkdocs-markdown --filter @sourcedraft/adapter-nuxt-content-markdown --filter @sourcedraft/adapters --filter @sourcedraft/publishers --filter @sourcedraft/media-providers --filter @sourcedraft/plugins --filter @sourcedraft/setup --filter @sourcedraft/github-publisher --filter @sourcedraft/config build",
"prebuild": "pnpm --filter @sourcedraft/core --filter @sourcedraft/adapter-astro-mdx --filter @sourcedraft/adapter-markdown --filter @sourcedraft/adapter-nextjs-mdx --filter @sourcedraft/adapter-hugo-markdown --filter @sourcedraft/adapter-eleventy-jekyll-markdown --filter @sourcedraft/adapter-docusaurus-mdx --filter @sourcedraft/adapter-mkdocs-markdown --filter @sourcedraft/adapter-nuxt-content-markdown --filter @sourcedraft/adapters --filter @sourcedraft/publishers --filter @sourcedraft/media-providers --filter @sourcedraft/plugins --filter @sourcedraft/setup --filter @sourcedraft/github-publisher --filter @sourcedraft/config build",
"dev": "concurrently -n web,api -c blue,gray \"vite\" \"tsx watch --ignore \\\"../../packages/**/dist\\\" --ignore \\\"node_modules\\\" --ignore \\\"dist\\\" server/index.ts\"",
"dev:web": "vite",
"dev:server": "tsx watch server/index.ts",
"build": "tsc -b && vite build && tsc -p server/tsconfig.json",
Expand All @@ -33,6 +33,7 @@
"@sourcedraft/core": "workspace:*",
"@sourcedraft/github-publisher": "workspace:*",
"@sourcedraft/media-providers": "workspace:*",
"@sourcedraft/plugins": "workspace:*",
"@sourcedraft/setup": "workspace:*",
"busboy": "^1.6.0",
"dotenv": "^16.5.0",
Expand Down
3 changes: 3 additions & 0 deletions apps/studio/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { listMedia } from "./listMedia.js";
import { listPosts, loadPost } from "./posts.js";
import { publishArticle, type PublishRequestBody } from "./publish.js";
import { requireSameSiteRequest } from "./requestProtection.js";
import { initializePlugins } from "./plugins.js";
import { getSetupHealth } from "./setupHealth.js";

const envPaths = [
Expand All @@ -38,6 +39,8 @@ for (const envPath of envPaths) {
}
}

await initializePlugins();

const port = Number(process.env.STUDIO_API_PORT ?? 8787);
const app = express();

Expand Down
46 changes: 46 additions & 0 deletions apps/studio/server/plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { dirname } from "node:path";
import { loadSourceDraftConfig, resolveConfigPath } from "@sourcedraft/config";
import { loadPlugins } from "@sourcedraft/plugins";

export async function initializePlugins(cwd = process.cwd()): Promise<void> {
const config = loadSourceDraftConfig(cwd);
const configPath = resolveConfigPath(cwd);
const configDir = configPath ? dirname(configPath) : cwd;

const hasExplicitPlugins = (config.plugins?.length ?? 0) > 0;
const discover = config.discoverPlugins === true;

if (!hasExplicitPlugins && !discover) {
return;
}

const report = await loadPlugins({
configDir,
...(config.plugins !== undefined ? { plugins: config.plugins } : {}),
...(config.requiredPlugins !== undefined
? { requiredPlugins: config.requiredPlugins }
: {}),
...(discover ? { discoverPlugins: true } : {}),
});

for (const failure of report.failures) {
const label = failure.name ?? failure.path;
if (failure.required) {
console.error(`[plugins] Required plugin failed (${label}): ${failure.error}`);
} else {
console.warn(`[plugins] Optional plugin skipped (${label}): ${failure.error}`);
}
}

if (!report.ok) {
const summary = report.failures
.filter((failure) => failure.required)
.map((failure) => `${failure.name ?? failure.path}: ${failure.error}`)
.join("; ");
throw new Error(`Required plugin(s) failed to load: ${summary}`);
}

if (report.loaded.length > 0) {
console.log(`[plugins] Loaded: ${report.loaded.join(", ")}`);
}
}
29 changes: 15 additions & 14 deletions docs/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,18 @@ Unknown adapter ids fail validation in `loadPublishEnv()` with a list of support

## Compatibility matrix

| Adapter key | Extension | Default `contentDir` | Best use case | Supported SEO fields |
|-------------|-----------|-------------------|---------------|----------------------|
| `astro-mdx` | `.mdx` | `src/content/blog` | Astro content collections | `metaTitle`, `metaDescription`, `canonicalUrl`, `socialImage` (when set on article) |
| `markdown` | `.md` | `src/content/blog` | Generic Markdown repos | same |
| `nextjs-mdx` | `.mdx` | `content/posts` | Next.js MDX blogs | same + `author` → `author`, `heroImage` → `coverImage` |
| `hugo-markdown` | `.md` | `content/posts` | Hugo static sites | same |
| `eleventy-jekyll-markdown` | `.md` | `src/posts` / `_posts` | Eleventy or Jekyll | same |
| `docusaurus-mdx` | `.mdx` | `blog` | Docusaurus blog plugin | same + `author` → `authors[]`, `heroImage` → `image` |
| `mkdocs-markdown` | `.md` | `docs` | MkDocs documentation sites | same (no `draft` in output) |
| `nuxt-content-markdown` | `.md` | `content/blog` | Nuxt Content v2 collections | same |
| Adapter key | Status | Extension | Default `contentDir` | Studio preview | SEO fields | Example |
|-------------|--------|-----------|----------------------|----------------|------------|---------|
| `astro-mdx` | Shipped | `.mdx` | `src/content/blog` | Yes | Yes | [astro-blog](../examples/astro-blog/) |
| `markdown` | Shipped | `.md` | `src/content/blog` | Yes | Yes | — |
| `nextjs-mdx` | Shipped | `.mdx` | `content/posts` | Yes | Yes | [nextjs-mdx-blog](../examples/nextjs-mdx-blog/) |
| `hugo-markdown` | Shipped | `.md` | `content/posts` | Yes | Yes | [hugo-blog](../examples/hugo-blog/) |
| `eleventy-jekyll-markdown` | Shipped | `.md` | `src/posts` / `_posts` | Yes | Yes | [eleventy-jekyll-blog](../examples/eleventy-jekyll-blog/) |
| `docusaurus-mdx` | Shipped | `.mdx` | `blog` | Yes | Yes | [docusaurus-blog](../examples/docusaurus-blog/) |
| `mkdocs-markdown` | Shipped | `.md` | `docs` | Yes (+ nav hint) | Yes (no `draft` in file) | [mkdocs-blog](../examples/mkdocs-blog/) |
| `nuxt-content-markdown` | Shipped | `.md` | `content/blog` | Yes | Yes | [nuxt-content-blog](../examples/nuxt-content-blog/) |

Custom adapters can register via [plugins.md](plugins.md). SEO fields: `metaTitle`, `metaDescription`, `canonicalUrl`, `socialImage`, `coverImageAlt`, `noindex`, computed `readingTime` — see [seo-fields.md](seo-fields.md).

Optional SEO fields (`metaTitle`, `metaDescription`, `canonicalUrl`, `socialImage`, `coverImageAlt`, `noindex`, `author`, computed `readingTime`) are emitted when present. Edit them in Studio under **SEO / Sharing**. See [seo-fields.md](seo-fields.md).

Expand Down Expand Up @@ -105,9 +107,8 @@ Does not edit `mkdocs.yml`. Studio preview shows a **nav hint** with the path to
| MkDocs | [examples/mkdocs-blog](../examples/mkdocs-blog/) |
| Nuxt Content | [examples/nuxt-content-blog](../examples/nuxt-content-blog/) |

## Future (not in repo yet)
## Remote CMS publishers (WordPress, Ghost)

- WordPress REST API
- Ghost API
Adapters still control **preview** and optional file-shaped output in Studio. When `publisher` is `wordpress` or `ghost`, the publish API sends article fields to the remote CMS — not necessarily a git commit. Use `markdown` or your site's file adapter for preview consistency.

These are planned directions, not shipped packages.
Details: [publishers.md](publishers.md) · [wordpress.md](wordpress.md) · [ghost.md](ghost.md)
43 changes: 43 additions & 0 deletions docs/assets/screenshots/ATTRIBUTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Connector documentation screenshots

This folder is for **optional** reference screenshots of third-party API documentation pages (GitLab, Bitbucket, WordPress, Ghost, Cloudinary, Cloudflare R2, etc.).

## Current status

**No third-party screenshots are committed by default.** Official documentation is linked below. Maintainers may capture screenshots locally using the capture script only after confirming the source site’s terms of use, robots policy, and brand guidelines allow it.

Do **not** hotlink images from vendor sites in this repository.

## Official documentation links

| Connector | Official docs | Owner |
|-----------|---------------|-------|
| GitLab Repository Files API | https://docs.gitlab.com/ee/api/repository_files.html | GitLab Inc. |
| Bitbucket Cloud REST API (source) | https://developer.atlassian.com/cloud/bitbucket/rest/api-group-source/ | Atlassian |
| WordPress REST API (posts) | https://developer.wordpress.org/rest-api/reference/posts/ | WordPress Foundation |
| Ghost Admin API | https://docs.ghost.org/admin-api/ | Ghost Foundation |
| Cloudinary Upload API | https://cloudinary.com/documentation/image_upload_api_reference | Cloudinary Ltd. |
| Cloudflare R2 (S3-compatible) | https://developers.cloudflare.com/r2/ | Cloudflare, Inc. |

## Capturing screenshots (maintainers)

From the repository root:

```bash
# Review legal/brand constraints first, then:
pnpm capture-doc-screenshots -- --confirm-attribution
```

Output directory: `docs/assets/screenshots/connectors/`

When committing captures, update the table below.

## Attribution log

| File | Source URL | Capture date | Owner | Usage note |
|------|------------|--------------|-------|------------|
| *(none committed)* | — | — | — | Use official links above until captures are approved |

## Placeholders in docs

Until screenshots exist, connector docs link to the official URLs above. See [connectors/README.md](connectors/README.md).
27 changes: 27 additions & 0 deletions docs/assets/screenshots/connectors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Connector screenshot placeholders

Screenshots of third-party API documentation are **not committed** by default.

## Why placeholders?

Official vendor documentation is copyrighted. SourceDraft docs link to authoritative URLs instead of hotlinking or embedding unlicensed captures.

## Adding screenshots

1. Read [../ATTRIBUTION.md](../ATTRIBUTION.md) and confirm the source site allows capture and redistribution in an open-source repo.
2. Run `pnpm capture-doc-screenshots -- --confirm-attribution` from the repo root (requires Playwright; see script help).
3. Record each file in the attribution log with URL, date, and owner.
4. Prefer cropped, readable regions — avoid logos unless brand guidelines permit.

## Expected filenames (when captured)

| Filename | Subject |
|----------|---------|
| `gitlab-repository-files-api.png` | GitLab Repository Files API overview |
| `bitbucket-source-api.png` | Bitbucket commit-upload / source API |
| `wordpress-rest-posts.png` | WordPress REST posts endpoint |
| `ghost-admin-api.png` | Ghost Admin API authentication |
| `cloudinary-upload-api.png` | Cloudinary upload API reference |
| `cloudflare-r2-s3.png` | Cloudflare R2 S3-compatible overview |

Until then, use the official links in [../ATTRIBUTION.md](../ATTRIBUTION.md).
2 changes: 1 addition & 1 deletion docs/compatibility-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Extension foundation for SourceDraft adapters and publishers.
| `adapterRegistry` | `@sourcedraft/adapters` | Article → file content, paths, frontmatter parsing |
| `publisherRegistry` | `@sourcedraft/publishers` | Publish articles, upload media, list/read posts |

Built-in connectors register on package load via `registerBuiltInAdapters()` and `registerBuiltInPublishers()`.
Built-in connectors register on package load via `registerBuiltInAdapters()` and `registerBuiltInPublishers()`. Custom adapters, publishers, and media providers can register via server-side plugins — see [plugins.md](plugins.md).

### Adapter interface

Expand Down
4 changes: 4 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Studio **Settings → Compatibility & status** mirrors validation (adapter, publ

Details: [setup-wizard.md](setup-wizard.md).

## Server-side plugins

Optional `plugins`, `requiredPlugins`, and `discoverPlugins` load custom adapters/publishers/media providers when the Studio API starts. Server-only — never loaded in the browser. See [plugins.md](plugins.md).

## Secrets vs project settings

SourceDraft uses two files on purpose:
Expand Down
16 changes: 9 additions & 7 deletions docs/deploy-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,19 @@ By default, a deploy hook failure does **not** fail the publish — your content

Set `DEPLOY_HOOK_STRICT=true` when you want publish to return an error if the hook fails. Use this only when you need atomic “publish + deploy” semantics.

## Provider notes
## Compatibility matrix

| Provider | Typical hook source |
|----------|---------------------|
| `vercel` | Project → Settings → Git → Deploy Hooks |
| `netlify` | Site → Build & deployBuild hooks |
| `cloudflare-pages` | Pages project → Settings → BuildsDeploy hooks |
| `generic` | Any CI webhook that accepts `POST` + JSON |
| Provider | `DEPLOY_HOOK_PROVIDER` | Status | Typical hook source | Auth |
|----------|------------------------|--------|---------------------|------|
| Generic | `generic` | Shipped | Any CI webhook accepting `POST` + JSON | URL secret |
| Vercel | `vercel` | Shipped | Project → Settings → GitDeploy Hooks | URL secret |
| Netlify | `netlify` | Shipped | Site → Build & deployBuild hooks | URL secret |
| Cloudflare Pages | `cloudflare-pages` | Shipped | Pages project → Settings → Builds → Deploy hooks | URL secret |

SourceDraft does not store provider API tokens for deploy hooks — the hook URL itself is the credential.

Recipe: [quickstart-recipes.md#deploy-hook-after-publish](quickstart-recipes.md#deploy-hook-after-publish)

## Security

- Treat `DEPLOY_HOOK_URL` like a password. Anyone with the URL can trigger builds.
Expand Down
Loading