diff --git a/.claude/rules/docs-style.md b/.claude/rules/docs-style.md new file mode 100644 index 0000000..301c438 --- /dev/null +++ b/.claude/rules/docs-style.md @@ -0,0 +1,50 @@ +# Docs and copy style + +## Tone + +Professional open-source developer tool: clean, technical, precise, +trustworthy. Not generic SaaS marketing, not AI-generated filler, not startup +hype, not fake enterprise. Write like the maintainer of a tool people trust +with their repositories and credentials. + +## Positioning to emphasize + +- Git-owned content and portability (plain `.md`/`.mdx` in the user's repo) +- Adapter/publisher architecture (one schema, many targets) +- Local/private control; credentials stay server-side +- Publishing confidence: validation, exact output preview, content QA, + publish checklist, demo mode + +## Competitors + +Respect Decap CMS, TinaCMS, CloudCannon, GitCMS, WordPress, and Ghost. State +plainly where they are stronger (maturity, hosting, ecosystems, visual +editing) and where SourceDraft differs. Never attack, never imply they are +bad choices, never invent their weaknesses. + +## Evidence rules + +- Feature claims must match shipped code; check `docs/project-status.md`. +- Acceptable sources: this repository, official docs of the tools discussed, + reputable open-source ecosystem guidance, neutral UX/product principles. +- Not acceptable as neutral evidence: vendor marketing pages, SEO listicles, + fabricated statistics, competitor sales pages. Real user/founder reviews may + be cited only as clearly-marked anecdotes. +- No fake screenshots, metrics, benchmarks, or testimonials. + +## UX writing principles + +Optimize docs and UI text for: fast first success, low cognitive load, clear +system status, obvious next action, recognition over recall, error +prevention, user control and recovery, progressive disclosure, transparent +limitations, and trust around credentials and publishing. The reader is +technical but may be new to Git-based CMS workflows. + +## Mechanics + +- Sentence-case headings, short paragraphs, tables for matrices. +- Every doc links onward to the next logical doc. +- Commands shown must actually work from the repo root (or state the cwd). +- Mark experimental/partial features inline, not only in a footnote. +- Keep "MVP password auth is intended for local/private use" warnings wherever + exposure to the public internet could be implied. diff --git a/.claude/rules/no-scope-creep.md b/.claude/rules/no-scope-creep.md new file mode 100644 index 0000000..b57ead2 --- /dev/null +++ b/.claude/rules/no-scope-creep.md @@ -0,0 +1,41 @@ +# No scope creep + +SourceDraft launches first as a genuinely useful free and open-source AGPL +project. The free version must not be artificially crippled, and monetization +is **not** implemented now. + +## Forbidden in the current phase + +Do not implement, scaffold, stub, or document as available: + +- Paywalls, billing, SaaS plans, license gates, or feature flags that gate + open-source functionality +- Telemetry or analytics collection of any kind +- OAuth, user accounts, team accounts, or RBAC +- Hosted / multi-tenant Studio +- Plugin marketplace +- AI writing tools +- Large UI redesigns + +Also forbidden: fake screenshots, fake metrics, fake benchmarks, and +production/enterprise overclaims in any doc or UI string. + +## Allowed to mention (roadmap only) + +Future commercial possibilities (hosted SourceDraft Cloud, managed onboarding, +OAuth/team accounts, RBAC, managed media, premium support, agency workspaces, +migration services, dual licensing) may appear in `docs/roadmap.md` as +clearly-labeled future options — never as current features, never as code. + +## Decision filter + +Classify every proposed change as one of: + +1. **True launch blocker** — broken, misleading, insecure, or missing piece + that would embarrass the project on day one. Do it. +2. **High-value polish** — improves first success, clarity, or trust with low + risk. Do it if cheap and in scope. +3. **Later roadmap** — useful but not now. Write it down in `docs/roadmap.md` + or an issue; do not implement. +4. **Explicitly not now** — anything on the forbidden list. Refuse, even if + requested casually, and point to this file. diff --git a/.claude/rules/release-gates.md b/.claude/rules/release-gates.md new file mode 100644 index 0000000..88979c5 --- /dev/null +++ b/.claude/rules/release-gates.md @@ -0,0 +1,45 @@ +# Release gates + +Before tagging a release, merging release-related PRs, or recommending any +public promotion, all gates below must pass. See `RELEASE_CHECKLIST.md` and +`docs/public-launch-checklist.md` for the full operator checklists. + +## Automated gates + +```bash +pnpm install --frozen-lockfile +pnpm build +pnpm test +pnpm test:e2e # required for releases and any UI/auth/publish change +``` + +- CI (`.github/workflows/ci.yml`) green: build, unit tests, studio e2e +- CodeQL: no open high-severity alerts on the release PR + +## Repository hygiene gates + +- `LICENSE` is AGPL-3.0-or-later; no stray MIT references anywhere +- `.env` / `.env.local` gitignored and not committed +- No-secrets scan clean on tracked files (tokens, passwords, private keys): + `git grep -nIiE 'ghp_[A-Za-z0-9]|gho_[A-Za-z0-9]|BEGIN [A-Z]+ PRIVATE KEY' -- ':!*.example*'` +- No QuBrite hardcoding in `*.ts` / `*.tsx` app logic + +## Honesty gates + +- README, `docs/project-status.md`, and `CHANGELOG.md` agree on shipped vs + experimental vs not-shipped +- Stated limitations still accurate: MVP password auth, in-memory sessions, + Contents API scale limits, `s3-compatible` upload not implemented, no post + list for Bitbucket/WordPress/Ghost +- No screenshots showing tokens, real repo secrets, or personal data +- No production/SaaS/enterprise claims anywhere + +## Manual gates (release only) + +- Demo mode walkthrough passes (`docs/manual-acceptance-test.md`) +- Real publish against a **test** GitHub repository: direct commit and + pull-request mode both verified +- Screenshots regenerated (`pnpm screenshots:generate`) if UI changed + +If any gate fails, the release stops. Document the failure; do not waive +gates silently. diff --git a/.claude/rules/source-draft-project-rules.md b/.claude/rules/source-draft-project-rules.md new file mode 100644 index 0000000..6387ad7 --- /dev/null +++ b/.claude/rules/source-draft-project-rules.md @@ -0,0 +1,38 @@ +# SourceDraft project rules + +SourceDraft is an open-source publishing Studio for Markdown, MDX, and +Git-backed content workflows, with an adapter/publisher architecture for +multiple static-site frameworks and CMS targets. License: AGPL-3.0-or-later. + +## Product identity + +- Target users: solo developers, technical bloggers, documentation-site + maintainers, Astro/Next.js/Hugo/Docusaurus/MkDocs/Nuxt Content users, and + small teams that want Git-owned content. +- Core promise: Git-owned, portable content; secrets server-side; publishing + confidence (validation, preview of exact output path/file, content QA). +- SourceDraft is **not** WordPress, not a site builder, not a hosted CMS. +- QuBrite.com is the origin story only. Never hardcode QuBrite (or any single + site) into core logic, defaults, or fixtures. + +## Engineering rules + +- Universal article schema lives in `@sourcedraft/core`; adapters and + publishers consume it through `adapterRegistry` / `publisherRegistry`. +- Secrets are read from `.env` in `apps/studio/server` only. Browser code must + never import publisher/media packages or see credential values. +- Keep modules typed, small, and testable. Prefer boring reliable code. +- No unnecessary comments, no unrelated refactors, no dependency additions + without explicit justification. +- Errors returned to Studio must be clear and actionable, without leaking + secret values. + +## Honesty rules + +- Docs and UI describe only what is implemented. Shipped vs experimental vs + not-shipped follows `docs/project-status.md` — update it when status + changes, and keep README/CHANGELOG consistent with it. +- No fake analytics, fake charts, fake metrics, fake screenshots, placeholder + features, or production/enterprise overclaims. +- Known limitations (MVP auth, in-memory sessions, Contents API scale limits, + S3 upload not implemented) stay visibly documented until fixed. diff --git a/.claude/worktrees/project-guidance b/.claude/worktrees/project-guidance new file mode 160000 index 0000000..e791f54 --- /dev/null +++ b/.claude/worktrees/project-guidance @@ -0,0 +1 @@ +Subproject commit e791f54723e8a630ae5316dbd6a14e0763924b51 diff --git a/.github/ISSUE_TEMPLATE/adapter_request.md b/.github/ISSUE_TEMPLATE/adapter_request.md new file mode 100644 index 0000000..60fc9ec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/adapter_request.md @@ -0,0 +1,36 @@ +--- +name: Adapter request +about: Request or propose a new file adapter (framework/static-site generator) +title: "adapter: " +labels: enhancement, adapter +assignees: "" +--- + +## Framework / generator + + + +## Frontmatter format + + + +## Field mapping + + + +## File path conventions + + + +## Example post file + +```markdown + +``` + +## Are you willing to implement it? + + diff --git a/.github/ISSUE_TEMPLATE/publisher_request.md b/.github/ISSUE_TEMPLATE/publisher_request.md new file mode 100644 index 0000000..96f78b2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/publisher_request.md @@ -0,0 +1,37 @@ +--- +name: Publisher request +about: Request or propose a new publish target (Git host or CMS API) +title: "publisher: " +labels: enhancement, publisher +assignees: "" +--- + +## Target + + + +## Kind + + + +## Auth model + + + +## Capabilities expected + +- Publish new post: yes / no +- Update existing post: yes / no (how is the post identified?) +- Upload media: yes / no +- List/read posts: yes / no + +## Why not a plugin? + + + +## Are you willing to implement it? + + diff --git a/.github/ISSUE_TEMPLATE/security_hardening.md b/.github/ISSUE_TEMPLATE/security_hardening.md new file mode 100644 index 0000000..1239bea --- /dev/null +++ b/.github/ISSUE_TEMPLATE/security_hardening.md @@ -0,0 +1,30 @@ +--- +name: Security hardening +about: Propose a hardening improvement (NOT for reporting vulnerabilities) +title: "hardening: " +labels: security +assignees: "" +--- + + + +## Area + + + +## Current behavior + + + +## Proposed hardening + + + +## Trade-offs + + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..dd9dfd3 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,30 @@ +## What and why + + + +## Type of change + + + +- [ ] Bug fix +- [ ] Feature (adapter / publisher / Studio / server) +- [ ] Documentation +- [ ] Tests / CI +- [ ] Refactor (no behavior change) + +## Checklist + +- [ ] `pnpm build` passes +- [ ] `pnpm test` passes +- [ ] `pnpm test:e2e` passes (required if Studio UI, auth, demo mode, or publish flows changed) +- [ ] Tests added/updated for logic changes +- [ ] Docs updated if behavior or configuration changed (including compatibility matrices) +- [ ] No secrets in code, fixtures, tests, or screenshots; nothing credential-related in browser code +- [ ] No new dependencies (or justified below) +- [ ] In scope — no monetization, telemetry, OAuth/RBAC, or hosted features (see docs/roadmap.md "Explicitly not now") + +## Test notes + + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f1950b9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,90 @@ +# CLAUDE.md + +Guidance for AI assistants working in this repository. Detailed rules live in +`.claude/rules/`. + +## What SourceDraft is + +An open-source (AGPL-3.0-or-later) publishing Studio for Markdown, MDX, and +Git-backed content workflows. A local React Studio plus a server-side publish +API commit content and media to the user's own target — GitHub, GitLab, +Bitbucket, WordPress, or Ghost — through an adapter/publisher architecture. + +- **Adapters** render a validated universal article into platform-specific + file output (Astro MDX, Hugo, Docusaurus, MkDocs, Nuxt Content, …). +- **Publishers** send content to a target (Git file commit or remote CMS API). +- Content stays portable: plain `.md`/`.mdx` files in the user's repository. + +Status: early local/private MVP. Honest about limitations — see +`docs/project-status.md`. QuBrite.com is the origin story only, never a +dependency or hardcoded target. + +## Repository layout + +| Path | Contents | +|------|----------| +| `apps/studio` | React Studio UI + Express publish API (`server/`) | +| `packages/core` | Universal article schema and validation | +| `packages/adapter-*` | One package per file adapter (8 shipped) | +| `packages/adapters` | `adapterRegistry` | +| `packages/publishers` | `publisherRegistry` (GitLab, Bitbucket, WP, Ghost) | +| `packages/github-publisher` | GitHub Contents API client | +| `packages/media-providers` | Git media, Cloudinary, S3 (config-only) | +| `packages/plugins` | Server-side plugin loader | +| `packages/setup` | Setup wizard + config validation CLI | +| `examples/*` | Folder-layout integration examples (not runnable sites) | +| `docs/` | All user and contributor documentation | + +## Commands + +```bash +pnpm install # workspace install (pnpm 11+, Node 22+) +pnpm dev # Studio UI + publish API +pnpm build # build everything incl. studio server TS +pnpm test # unit tests across packages + studio +pnpm test:e2e # Playwright smoke tests (demo mode, no credentials) +pnpm setup # guided config wizard +pnpm validate:config # validate sourcedraft.config.json + .env +``` + +Run `pnpm build` and `pnpm test` before finishing any code change. Run +`pnpm test:e2e` when Studio UI, auth, demo mode, or publish flows change. + +## Hard rules + +1. **Secrets stay server-side.** Tokens, passwords, and API keys are read from + `.env` in `apps/studio/server` only. Never import publisher packages or + reference credentials in browser code. Never commit `.env`/`.env.local`. +2. **License is AGPL-3.0-or-later** everywhere. No MIT references. +3. **No scope creep** — see `.claude/rules/no-scope-creep.md`. No billing, + paywalls, telemetry, OAuth, RBAC, team accounts, hosted/multi-tenant + Studio, plugin marketplace, or AI writing features. +4. **No fabrication.** No fake screenshots, metrics, benchmarks, testimonials, + or placeholder features. Docs must describe what the code actually does. +5. **No new dependencies** unless absolutely necessary and justified in the + PR description. +6. **Honest status.** SourceDraft is an early local/private MVP, not a hosted + SaaS or production multi-user product. Keep all docs consistent with + `docs/project-status.md` (shipped / experimental / not shipped). +7. **No QuBrite hardcoding** in app logic. Generic core, per-site config. +8. **Do not push to `main`.** PRs only; CI and CodeQL must pass. + +## Code style + +- TypeScript, small typed modules, boring reliable code over clever + abstractions. +- New adapters/publishers go through the registries; follow the interfaces in + `docs/compatibility-roadmap.md`. +- Unit tests with `node --test` next to the source (`*.test.ts`). +- Match existing comment density (low); no comments that restate code. + +## Docs style + +See `.claude/rules/docs-style.md`. Short version: precise, technical, +trustworthy; respectful toward Decap/Tina/CloudCannon/WordPress/Ghost; +no SaaS hype, no overclaims; every feature claim must match shipped code. + +## Release gates + +See `.claude/rules/release-gates.md` and `RELEASE_CHECKLIST.md` before +tagging or promoting anything publicly. diff --git a/README.md b/README.md index 27dbd43..cc4ac9c 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,9 @@ Your static site still builds and deploys exactly as before. SourceDraft creates ## What it does not do yet - Host your website or run your Astro build -- OAuth, user accounts, or role-based access +- OAuth, user accounts, role-based access, or hosted multi-tenant Studio - 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 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. @@ -82,6 +81,14 @@ Details: [docs/publishers.md](docs/publishers.md) · [docs/git-publishers.md](do 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) +## For writers and bloggers + +If someone already installed SourceDraft for your publication, you only need the **Studio link** and **password**. You can write articles, upload images, preview the generated file, and send posts to your blog without touching GitHub manually. + +If you are setting it up yourself, start with **demo mode** or the guided **`pnpm setup`** wizard. Demo mode lets you explore Studio safely — no real posts are published and sample content resets when the server restarts. + +Plain-language overview for non-technical users: [docs/non-technical-overview.md](docs/non-technical-overview.md). + ## Quickstart Requirements: Node.js 22+, pnpm 11+ @@ -117,7 +124,7 @@ pnpm dev Sign in, click **New post**, preview the output, publish. The file lands at `contentDir/.mdx` or `.md` depending on your adapter (default: `src/content/blog/`). -**Try without GitHub:** set `SOURCEDRAFT_DEMO_MODE=true` in `.env`, or leave GitHub vars empty and click **Explore demo mode** on the sign-in screen. Demo content reloads from repository fixtures on each API start. See [docs/demo-mode.md](docs/demo-mode.md). +**Try without GitHub:** set `SOURCEDRAFT_DEMO_MODE=true` in `.env`, or leave GitHub vars empty and click **Try demo mode** on the sign-in screen. Demo content reloads from repository fixtures on each API start. See [docs/demo-mode.md](docs/demo-mode.md). Validate config: `pnpm validate:config` · Wizard details: [docs/setup-wizard.md](docs/setup-wizard.md) · Full walkthrough: [docs/getting-started.md](docs/getting-started.md) @@ -187,6 +194,9 @@ Issues and pull requests are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.md) fo - [Architecture](docs/architecture.md) - [Adapters](docs/adapters.md) - [Project status](docs/project-status.md) +- [How SourceDraft compares](docs/comparison.md) — Decap, Tina, CloudCannon, WordPress, Ghost +- [Roadmap](docs/roadmap.md) +- [Contributing roadmap](docs/contributing-roadmap.md) — good first issues - [Manual acceptance test](docs/manual-acceptance-test.md) - [Smoke tests (Playwright)](docs/getting-started.md#smoke-tests-playwright) - [Release checklist](RELEASE_CHECKLIST.md) diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md index afbb18e..2ecc8d0 100644 --- a/RELEASE_CHECKLIST.md +++ b/RELEASE_CHECKLIST.md @@ -1,6 +1,8 @@ # SourceDraft release checklist -Use this before tagging a public release or promoting the repository. +Use this before tagging a public release or promoting the repository. For +first-time public promotion (announcements, demos, screenshots), also run +[docs/public-launch-checklist.md](docs/public-launch-checklist.md). ## Automated checks @@ -32,7 +34,9 @@ pnpm test:e2e - [ ] Docs state: early local/private MVP, not hosted SaaS, not production multi-user auth - [ ] GitHub Contents API limits documented - [ ] `mediaDir` vs `publicMediaPath` documented -- [ ] Issue templates present under `.github/ISSUE_TEMPLATE/` +- [ ] Issue templates present under `.github/ISSUE_TEMPLATE/` (bug, feature, adapter, publisher, security hardening) +- [ ] `.github/pull_request_template.md` present +- [ ] `SECURITY.md` reporting instructions current ## Screenshots @@ -88,4 +92,4 @@ Only tag after automated checks pass and manual acceptance is satisfactory. - Post list in Studio for Bitbucket, WordPress, and Ghost - Git Trees API indexer for very large repos -Roadmap: [docs/compatibility-roadmap.md](docs/compatibility-roadmap.md) +Roadmap: [docs/roadmap.md](docs/roadmap.md) · [docs/compatibility-roadmap.md](docs/compatibility-roadmap.md) diff --git a/SECURITY.md b/SECURITY.md index f4012b1..b84444f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,5 +1,44 @@ # Security Policy -Do not report real secrets in public issues. +SourceDraft is an early open-source MVP intended for **local or private** +deployments. Its threat model and current limitations are documented in +[docs/security.md](docs/security.md) — read that first. -GitHub tokens and publishing credentials must stay server-side. +## Reporting a vulnerability + +Please do **not** open a public issue for security vulnerabilities. + +Report privately via [GitHub Security Advisories](https://github.com/bnz183/SourceDraft/security/advisories/new) +("Report a vulnerability" on the repository's Security tab). + +Include where practical: + +- Affected area (Studio UI, publish API, a specific publisher/adapter/media provider) +- Reproduction steps or proof of concept +- Impact assessment (what an attacker gains) + +You should receive an initial response within 7 days. This is a small +maintainer-run project — fixes are best-effort but security reports are +prioritized over feature work. + +## Scope notes + +Known, documented MVP limitations are not considered vulnerabilities on their +own (but bypasses of the documented protections are): + +- Single shared password auth with in-memory sessions (local/private use only) +- No CSRF tokens — `Sec-Fetch-Site`/`Origin` checks instead +- Studio is not hardened for public internet exposure + +## Handling secrets + +- GitHub/GitLab/Bitbucket tokens, WordPress/Ghost/Cloudinary credentials, and + the admin password live in `.env` and are read **server-side only**. +- Never commit `.env` or `.env.local`; never paste tokens, passwords, or + private repository details into issues, PRs, or screenshots. +- If you accidentally expose a token, revoke and rotate it immediately. + +## Automated checks + +CI runs build, unit tests, and Playwright smoke tests; GitHub CodeQL analyzes +JavaScript/TypeScript and Actions workflows on pushes and PRs to `main`. diff --git a/apps/studio/e2e/helpers.ts b/apps/studio/e2e/helpers.ts index 1fd7362..9e35d5d 100644 --- a/apps/studio/e2e/helpers.ts +++ b/apps/studio/e2e/helpers.ts @@ -42,8 +42,10 @@ export async function enterDemoMode(page: Page): Promise { attachPageErrorLogging(page); await waitForStudioRoot(page); await expect(page.getByRole("heading", { name: "SourceDraft Studio" })).toBeVisible(); - await page.getByRole("button", { name: "Explore demo mode" }).click(); - await expect(page.getByText("Demo mode — no GitHub commits are made")).toBeVisible(); + await page.getByRole("button", { name: "Try demo mode" }).click(); + await expect( + page.getByText("Demo mode — explore without connecting a real blog"), + ).toBeVisible(); } export function ensureScreenshotDir(): void { diff --git a/apps/studio/e2e/screenshots.spec.ts b/apps/studio/e2e/screenshots.spec.ts index 6ba9fd4..19a531c 100644 --- a/apps/studio/e2e/screenshots.spec.ts +++ b/apps/studio/e2e/screenshots.spec.ts @@ -62,7 +62,7 @@ test.describe("release screenshots", () => { path: screenshotPath("preview.png"), }); - await page.getByRole("button", { name: "New post" }).click(); + await page.getByRole("button", { name: "New article" }).click(); await postTitleInput(page).fill("Screenshot publish example"); await postDescriptionInput(page).fill( "Summary used for automated publish-success screenshot.", @@ -71,14 +71,14 @@ test.describe("release screenshots", () => { page, "# Screenshot publish example\n\nBody for release screenshot capture.", ); - await page.getByRole("button", { name: "Simulate publish" }).click(); - await expect(page.getByText("Publish simulated")).toBeVisible({ timeout: 10_000 }); + await page.getByRole("button", { name: "Simulate send to blog" }).click(); + await expect(page.getByText("Send simulated")).toBeVisible({ timeout: 10_000 }); await page.locator(".publish-bar").screenshot({ path: screenshotPath("publish-success.png"), }); await page.getByRole("button", { name: "Settings" }).click(); - await expect(page.getByRole("heading", { name: "Setup health" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Publishing readiness" })).toBeVisible(); await page.locator(".setup-health").screenshot({ path: screenshotPath("setup-health.png"), }); diff --git a/apps/studio/e2e/smoke.spec.ts b/apps/studio/e2e/smoke.spec.ts index fc1808a..93d0635 100644 --- a/apps/studio/e2e/smoke.spec.ts +++ b/apps/studio/e2e/smoke.spec.ts @@ -14,18 +14,19 @@ test.describe("Studio smoke", () => { attachPageErrorLogging(page); await waitForStudioRoot(page); await expect(page.getByRole("heading", { name: "SourceDraft Studio" })).toBeVisible(); - await expect(page.getByRole("button", { name: "Explore demo mode" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Try demo mode" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Your writing dashboard" })).toBeVisible(); }); test("overview/post list renders in demo mode", async ({ page }) => { await enterDemoMode(page); - await expect(page.getByRole("heading", { name: "Posts" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Articles" })).toBeVisible(); await expect(page.getByText("Getting started with SourceDraft")).toBeVisible(); }); test("new post form and editor accept text", async ({ page }) => { await enterDemoMode(page); - await page.getByRole("button", { name: "New post" }).click(); + await page.getByRole("button", { name: "New article" }).click(); await postTitleInput(page).fill("Smoke test post"); await postDescriptionInput(page).fill( "A short summary for the smoke test post.", @@ -36,7 +37,7 @@ test.describe("Studio smoke", () => { test("toolbar inserts Markdown", async ({ page }) => { await enterDemoMode(page); - await page.getByRole("button", { name: "New post" }).click(); + await page.getByRole("button", { name: "New article" }).click(); await fillPostBody(page, "Selected text"); await page.keyboard.press("Control+A"); await page.getByRole("button", { name: "Bold" }).click(); @@ -45,7 +46,7 @@ test.describe("Studio smoke", () => { test("autosave status appears after edits", async ({ page }) => { await enterDemoMode(page); - await page.getByRole("button", { name: "New post" }).click(); + await page.getByRole("button", { name: "New article" }).click(); await postTitleInput(page).fill("Autosave smoke test"); await expect(page.getByText("Unsaved changes", { exact: false })).toBeVisible({ timeout: 5000, @@ -61,16 +62,17 @@ test.describe("Studio smoke", () => { test("settings setup health renders", async ({ page }) => { await enterDemoMode(page); await page.getByRole("button", { name: "Settings" }).click(); + await expect(page.getByRole("heading", { name: "Welcome to SourceDraft" })).toBeVisible(); await expect(page.getByRole("heading", { name: "Setup detection" })).toBeVisible(); await expect(page.getByRole("heading", { name: "Content audit" })).toBeVisible(); - await expect(page.getByRole("heading", { name: "Setup health" })).toBeVisible(); - await expect(page.getByText("Admin password")).toBeVisible(); - await expect(page.getByText("GitHub token (server-side)")).toBeVisible(); + await expect(page.getByRole("heading", { name: "Publishing readiness" })).toBeVisible(); + await expect(page.getByText("Studio password")).toBeVisible(); + await expect(page.getByText("GitHub connection")).toBeVisible(); }); test("publish checklist renders in demo mode", async ({ page }) => { await enterDemoMode(page); - await page.getByRole("button", { name: "New post" }).click(); + await page.getByRole("button", { name: "New article" }).click(); await postTitleInput(page).fill("Checklist smoke test"); await postDescriptionInput(page).fill("Summary for checklist smoke test."); await fillPostBody(page, "# Checklist\n\nBody content."); @@ -81,19 +83,19 @@ test.describe("Studio smoke", () => { test("publish success can be simulated in demo mode", async ({ page }) => { await enterDemoMode(page); - await page.getByRole("button", { name: "New post" }).click(); + await page.getByRole("button", { name: "New article" }).click(); await postTitleInput(page).fill("Demo publish smoke test"); await postDescriptionInput(page).fill( "Summary for demo publish smoke test.", ); await fillPostBody(page, "# Demo publish\n\nBody content."); - await page.getByRole("button", { name: "Simulate publish" }).click(); - await expect(page.getByText("Publish simulated")).toBeVisible({ timeout: 10_000 }); + await page.getByRole("button", { name: "Simulate send to blog" }).click(); + await expect(page.getByText("Send simulated")).toBeVisible({ timeout: 10_000 }); }); test("publish mode selector renders in demo mode", async ({ page }) => { await enterDemoMode(page); - await page.getByRole("button", { name: "New post" }).click(); + await page.getByRole("button", { name: "New article" }).click(); await postTitleInput(page).fill("Publish mode smoke test"); await postDescriptionInput(page).fill( "Summary for publish mode smoke test.", @@ -104,15 +106,15 @@ test.describe("Studio smoke", () => { await expect(modeSelect).toBeVisible(); await modeSelect.selectOption("pull-request"); await expect(page.getByText("PR branch")).toBeVisible(); - await page.getByRole("button", { name: "Simulate PR publish" }).click(); - await expect(page.getByText("Pull request simulated")).toBeVisible({ + await page.getByRole("button", { name: "Simulate review request" }).click(); + await expect(page.getByText("Review request simulated")).toBeVisible({ timeout: 10_000, }); }); test("source mode toggle preserves raw body", async ({ page }) => { await enterDemoMode(page); - await page.getByRole("button", { name: "New post" }).click(); + await page.getByRole("button", { name: "New article" }).click(); await fillPostBody(page, "\n\n## Heading"); await page.getByRole("button", { name: "Source", exact: true }).click(); const source = page.getByTestId("post-body-source"); diff --git a/apps/studio/server/setupHealth.ts b/apps/studio/server/setupHealth.ts index 86f8d25..2352bea 100644 --- a/apps/studio/server/setupHealth.ts +++ b/apps/studio/server/setupHealth.ts @@ -65,19 +65,19 @@ function publisherCredentialChecks(activePublisher: string): SetupHealthCheck[] return [ { id: "gitlab-token", - label: "GitLab token (server-side)", + label: "GitLab connection", ok: tokenOk, detail: tokenOk - ? "GITLAB_TOKEN is present on the server. The value is never sent to the browser." - : "Set GITLAB_TOKEN in .env for GitLab publishing.", + ? "GitLab is connected on the server. Credentials never appear in the browser." + : "Publishing to GitLab is not connected yet. Add a GitLab token in .env or ask the person who installed SourceDraft.", }, { id: "gitlab-project", label: "GitLab project", ok: projectOk, detail: projectOk - ? "GITLAB_PROJECT_ID or GITLAB_PROJECT_PATH is configured." - : "Set GITLAB_PROJECT_ID or GITLAB_PROJECT_PATH in .env.", + ? "SourceDraft knows which GitLab project to update." + : "SourceDraft does not know which GitLab project stores your site yet.", }, ]; } @@ -89,27 +89,27 @@ function publisherCredentialChecks(activePublisher: string): SetupHealthCheck[] return [ { id: "bitbucket-token", - label: "Bitbucket token (server-side)", + label: "Bitbucket connection", ok: tokenOk, detail: tokenOk - ? "BITBUCKET_TOKEN is present on the server. The value is never sent to the browser." - : "Set BITBUCKET_TOKEN in .env for Bitbucket publishing.", + ? "Bitbucket is connected on the server. Credentials never appear in the browser." + : "Publishing to Bitbucket is not connected yet. Add a Bitbucket token in .env or ask the person who installed SourceDraft.", }, { id: "bitbucket-workspace", label: "Bitbucket workspace", ok: workspaceOk, detail: workspaceOk - ? "BITBUCKET_WORKSPACE is configured." - : "Set BITBUCKET_WORKSPACE in .env.", + ? "SourceDraft knows which Bitbucket workspace owns your blog." + : "SourceDraft does not know which Bitbucket workspace to use yet.", }, { id: "bitbucket-repo", label: "Bitbucket repository", ok: repoOk, detail: repoOk - ? "BITBUCKET_REPO_SLUG is configured." - : "Set BITBUCKET_REPO_SLUG in .env.", + ? "SourceDraft knows which Bitbucket repository stores your site." + : "SourceDraft does not know which Bitbucket repository stores your site yet.", }, ]; } @@ -121,27 +121,27 @@ function publisherCredentialChecks(activePublisher: string): SetupHealthCheck[] return [ { id: "wordpress-api-url", - label: "WordPress API URL", + label: "WordPress site address", ok: apiOk, detail: apiOk - ? "WORDPRESS_API_URL is configured." - : "Set WORDPRESS_API_URL in .env (e.g. https://example.com/wp-json).", + ? "SourceDraft knows where your WordPress site lives." + : "SourceDraft does not know your WordPress site address yet.", }, { id: "wordpress-username", - label: "WordPress username", + label: "WordPress sign-in", ok: userOk, detail: userOk - ? "WORDPRESS_USERNAME is configured." - : "Set WORDPRESS_USERNAME in .env.", + ? "A WordPress username is configured for publishing." + : "SourceDraft needs a WordPress username before it can publish posts.", }, { id: "wordpress-app-password", - label: "WordPress app password (server-side)", + label: "WordPress app password", ok: passwordOk, detail: passwordOk - ? "WORDPRESS_APP_PASSWORD is present on the server. The value is never sent to the browser." - : "Set WORDPRESS_APP_PASSWORD in .env.", + ? "WordPress publishing credentials are stored on the server only." + : "Publishing to WordPress is not connected yet. Add an app password in .env or ask the person who installed SourceDraft.", }, ]; } @@ -152,19 +152,19 @@ function publisherCredentialChecks(activePublisher: string): SetupHealthCheck[] return [ { id: "ghost-admin-url", - label: "Ghost site URL", + label: "Ghost site address", ok: urlOk, detail: urlOk - ? "GHOST_ADMIN_URL is configured." - : "Set GHOST_ADMIN_URL in .env (site root, no /ghost path).", + ? "SourceDraft knows where your Ghost site lives." + : "SourceDraft does not know your Ghost site address yet.", }, { id: "ghost-admin-api-key", - label: "Ghost Admin API key (server-side)", + label: "Ghost Admin connection", ok: keyOk, detail: keyOk - ? "GHOST_ADMIN_API_KEY is present on the server. The value is never sent to the browser." - : "Set GHOST_ADMIN_API_KEY in .env.", + ? "Ghost publishing credentials are stored on the server only." + : "Publishing to Ghost is not connected yet. Add a Ghost Admin API key in .env or ask the person who installed SourceDraft.", }, ]; } @@ -175,47 +175,72 @@ function publisherCredentialChecks(activePublisher: string): SetupHealthCheck[] return [ { id: "github-owner", - label: "GitHub owner", + label: "GitHub account or organization", ok: ownerOk, detail: ownerOk - ? "GITHUB_OWNER is configured." - : "Set GITHUB_OWNER in .env.", + ? "SourceDraft knows which GitHub account owns your blog." + : "SourceDraft does not know which GitHub account owns your blog yet.", }, { id: "github-repo", - label: "GitHub repository", + label: "Blog repository", ok: repoOk, - detail: repoOk ? "GITHUB_REPO is configured." : "Set GITHUB_REPO in .env.", + detail: repoOk + ? "SourceDraft knows which repository stores your site." + : "SourceDraft does not know which repository stores your site yet.", }, { id: "github-token", - label: "GitHub token (server-side)", + label: "GitHub connection", ok: tokenOk, detail: tokenOk - ? "GITHUB_TOKEN is present on the server. The value is never sent to the browser." - : "Set GITHUB_TOKEN in .env for GitHub publishing.", + ? "GitHub is connected on the server. Credentials never appear in the browser." + : "Publishing to GitHub is not connected yet. Add a GitHub token in .env or ask the person who installed SourceDraft.", }, ]; } function publisherSetupMessage(activePublisher: string): string { if (activePublisher === "gitlab") { - return "Complete GitLab setup in .env (GITLAB_TOKEN, GITLAB_PROJECT_ID or GITLAB_PROJECT_PATH) or use demo mode to explore without GitLab."; + return "Finish connecting SourceDraft to your GitLab project, or try demo mode to explore without publishing."; } if (activePublisher === "bitbucket") { - return "Complete Bitbucket setup in .env (BITBUCKET_TOKEN, BITBUCKET_WORKSPACE, BITBUCKET_REPO_SLUG) or use demo mode to explore without Bitbucket."; + return "Finish connecting SourceDraft to your Bitbucket repository, or try demo mode to explore without publishing."; } if (activePublisher === "wordpress") { - return "Complete WordPress setup in .env (WORDPRESS_API_URL, WORDPRESS_USERNAME, WORDPRESS_APP_PASSWORD) or use demo mode to explore without WordPress."; + return "Finish connecting SourceDraft to your WordPress site, or try demo mode to explore without publishing."; } if (activePublisher === "ghost") { - return "Complete Ghost setup in .env (GHOST_ADMIN_URL, GHOST_ADMIN_API_KEY) or use demo mode to explore without Ghost."; + return "Finish connecting SourceDraft to your Ghost site, or try demo mode to explore without publishing."; } - return "Complete GitHub setup in .env (GITHUB_TOKEN, GITHUB_OWNER, GITHUB_REPO) or use demo mode to explore without GitHub."; + return "Finish connecting SourceDraft to your GitHub blog repository, or try demo mode to explore without publishing."; +} + +function adapterDisplayName(adapter: string): string { + switch (adapter) { + case "astro-mdx": + return "Astro"; + case "markdown": + return "Markdown"; + case "nextjs-mdx": + return "Next.js"; + case "hugo-markdown": + return "Hugo"; + case "eleventy-jekyll-markdown": + return "Eleventy or Jekyll"; + case "docusaurus-mdx": + return "Docusaurus"; + case "mkdocs-markdown": + return "MkDocs"; + case "nuxt-content-markdown": + return "Nuxt Content"; + default: + return adapter; + } } export function getSetupHealth(): SetupHealthReport { @@ -247,71 +272,71 @@ export function getSetupHealth(): SetupHealthReport { const checks: SetupHealthCheck[] = [ { id: "admin-password", - label: "Admin password", + label: "Studio password", ok: adminPasswordConfigured, detail: adminPasswordConfigured - ? "SOURCEDRAFT_ADMIN_PASSWORD is set on the server." - : "Set SOURCEDRAFT_ADMIN_PASSWORD in .env for normal sign-in.", + ? "Writers can sign in with the Studio password." + : "Add a Studio password on the server, or use demo mode to explore without signing in.", }, ...publisherCredentialChecks(activePublisher), { id: "content-dir", - label: "Content directory", + label: "Article folder", ok: contentDirConfigured, detail: contentDirConfigured - ? `contentDir: ${contentDir}` - : "Configure contentDir in sourcedraft.config.json.", + ? `Articles will be saved in ${contentDir}.` + : "SourceDraft does not know where your articles should be saved yet.", }, { id: "media-dir", - label: "Media directory", + label: "Image folder", ok: mediaDirConfigured, detail: mediaDirConfigured - ? `mediaDir: ${mediaDir}` - : "Configure mediaDir in sourcedraft.config.json.", + ? `Uploaded images will be saved in ${mediaDir}.` + : "SourceDraft does not know where uploaded images should be saved yet.", }, { id: "public-media-path", - label: "Public media path", + label: "Public image path", ok: publicMediaPathConfigured, detail: publicMediaPathConfigured - ? `publicMediaPath: ${publicMediaPath}` - : "Configure publicMediaPath in sourcedraft.config.json or CMS_PUBLIC_MEDIA_PATH.", + ? `Images will appear on your site under ${publicMediaPath}.` + : "SourceDraft does not know the web path for images on your site yet.", }, { id: "adapter", - label: "Adapter", + label: "Blog type", ok: adapterValid, detail: adapterValid - ? `Using ${adapter} adapter.` - : `Unsupported adapter "${rawAdapter}". Use a built-in adapter id from docs/adapters.md.`, + ? `SourceDraft is writing for ${adapterDisplayName(adapter)}.` + : "Choose the blog type SourceDraft should write for, such as Astro, Hugo, or Next.js.", }, { id: "publisher", - label: "Publisher", + label: "Publishing destination", ok: publisherValid, detail: publisherValid - ? `Using ${publisher} publisher.` - : `Unsupported publisher "${rawPublisher}". Use a built-in publisher id from docs/configuration.md.`, + ? `Finished articles are sent through ${publisher}.` + : "Choose where SourceDraft should send finished articles.", }, { id: "demo-mode", label: "Demo mode", ok: true, detail: demoModeForced - ? "SOURCEDRAFT_DEMO_MODE=true — remote commits are disabled." + ? "Demo mode is forced on. Explore safely — nothing is sent to a real blog." : !isPublisherConfigured() - ? "Publisher is not fully configured — Studio uses demo content and simulated publish." - : "Demo mode is off. Publishing is enabled when credentials are valid.", + ? "Your blog is not fully connected yet. Studio shows sample articles and simulates publishing." + : "Publishing to your connected blog is available when the checks above pass.", }, ]; const nextAction = demoModeForced - ? "Demo mode is active. Explore Studio locally or configure your publisher and disable SOURCEDRAFT_DEMO_MODE for real publishing." + ? "You are in demo mode. Explore Studio safely, then ask your technical helper to connect a real blog when you are ready." : !adminPasswordConfigured && demoModeAvailable - ? "Enter demo mode from the sign-in screen or set SOURCEDRAFT_ADMIN_PASSWORD for password sign-in." + ? "Try demo mode from the sign-in screen, or ask your technical helper to add a Studio password." : !adminPasswordConfigured - ? "Set SOURCEDRAFT_ADMIN_PASSWORD in .env and restart the API server." + ? "Ask your technical helper to add a Studio password on the server." : !publisherReady ? publisherSetupMessage(activePublisher) : null; @@ -329,25 +354,26 @@ export function getSetupHealth(): SetupHealthReport { if (!validation.ok && validation.missingEnvVars.length > 0) { checks.push({ id: "config-validation", - label: "Configuration validation", + label: "Full setup check", ok: false, - detail: `Missing: ${validation.missingEnvVars.join(", ")}. Run pnpm validate:config for details.`, + detail: + "Some server settings are still missing. Ask your technical helper to run the guided setup or check the README.", }); } else if (validation.warnings.length > 0) { checks.push({ id: "config-validation", - label: "Configuration validation", + label: "Full setup check", ok: true, - detail: validation.warnings[0] ?? "Configuration validated with warnings.", + detail: validation.warnings[0] ?? "Setup looks mostly ready, with a few warnings to review.", }); } else { checks.push({ id: "config-validation", - label: "Configuration validation", + label: "Full setup check", ok: validation.ok, detail: validation.ok - ? "Adapter, publisher, and media provider look compatible." - : "Run pnpm validate:config locally for details.", + ? "Blog type, publishing destination, and image storage look compatible." + : "Ask your technical helper to review the setup guide in the README.", }); } diff --git a/apps/studio/src/components/AdapterStatus.tsx b/apps/studio/src/components/AdapterStatus.tsx index 0ac8fb4..5d629f0 100644 --- a/apps/studio/src/components/AdapterStatus.tsx +++ b/apps/studio/src/components/AdapterStatus.tsx @@ -43,8 +43,8 @@ export function AdapterStatus({

{githubReady - ? "Connected to your GitHub repository" - : "Finish GitHub setup in .env to publish"} + ? "Connected to your blog repository" + : "Finish blog connection in Settings before sending articles"}

diff --git a/apps/studio/src/components/AppBar.tsx b/apps/studio/src/components/AppBar.tsx index 290085b..2b86f12 100644 --- a/apps/studio/src/components/AppBar.tsx +++ b/apps/studio/src/components/AppBar.tsx @@ -45,13 +45,13 @@ export function AppBar({ }: AppBarProps) { const repoLabel = githubReady ? `${githubOwner}/${githubRepo}` - : "GitHub not configured"; + : "Blog not connected"; return (
SourceDraft - Git-backed writing studio + Writing dashboard
@@ -60,7 +60,7 @@ export function AppBar({ className={ githubReady ? "app-bar__badge" : "app-bar__badge app-bar__badge--muted" } - title={githubReady ? "Target repository" : "Set GITHUB_OWNER and GITHUB_REPO in .env"} + title={githubReady ? "Connected blog repository" : "Open Settings to connect your blog"} > {repoLabel} diff --git a/apps/studio/src/components/AstroMdxPreview.tsx b/apps/studio/src/components/AstroMdxPreview.tsx index d2779eb..66fdc68 100644 --- a/apps/studio/src/components/AstroMdxPreview.tsx +++ b/apps/studio/src/components/AstroMdxPreview.tsx @@ -65,8 +65,8 @@ export function AstroMdxPreview({

{valid - ? "File that will be saved to GitHub" - : "Complete post details to preview output"} + ? "See the generated article file before you send it to your blog" + : "Complete article details to preview the generated output"}

- - {demoAvailable && ( -
-

- Explore Studio with sample posts. No GitHub token required. -

- -
- )} ); diff --git a/apps/studio/src/components/PostSidebar.tsx b/apps/studio/src/components/PostSidebar.tsx index 7482567..38618bc 100644 --- a/apps/studio/src/components/PostSidebar.tsx +++ b/apps/studio/src/components/PostSidebar.tsx @@ -63,15 +63,15 @@ export function PostSidebar({ } return ( -