From 9537867e79a991e65dd6b697c7afa8ff7caf1ea4 Mon Sep 17 00:00:00 2001 From: bnz183 Date: Tue, 9 Jun 2026 12:19:01 +0200 Subject: [PATCH] Setup wizard, config validation, and SEO metadata --- README.md | 7 +- apps/studio/package.json | 5 +- apps/studio/server/publish.ts | 2 + apps/studio/server/setupHealth.ts | 47 +++ .../src/components/CompatibilityPanel.tsx | 72 ++++ .../src/components/PostDetailsPanel.tsx | 8 + .../studio/src/components/SeoSharingPanel.tsx | 173 ++++++++ apps/studio/src/components/SettingsPanel.tsx | 8 +- .../src/components/SetupHealthPanel.tsx | 3 + apps/studio/src/index.css | 121 ++++++ apps/studio/src/lib/articleForm.ts | 49 +++ apps/studio/src/lib/autosave.ts | 18 +- apps/studio/src/lib/seoValidation.test.ts | 23 + apps/studio/src/lib/seoValidation.ts | 21 + apps/studio/src/lib/setupHealth.ts | 11 + docs/adapters.md | 2 +- docs/compatibility-roadmap.md | 2 +- docs/configuration.md | 14 +- docs/getting-started.md | 21 +- docs/ghost.md | 6 +- docs/non-technical-overview.md | 4 +- docs/seo-fields-roadmap.md | 35 -- docs/seo-fields.md | 72 ++++ docs/setup-wizard.md | 75 ++++ docs/wordpress.md | 13 + .../blog/getting-started-with-sourcedraft.mdx | 4 + .../getting-started-with-sourcedraft.mdx | 4 + package.json | 4 +- .../adapter-astro-mdx/src/toAstroMdx.test.ts | 7 + packages/adapter-astro-mdx/src/toAstroMdx.ts | 3 +- .../src/toDocusaurusMdx.ts | 45 +- .../src/toEleventyJekyllMarkdown.ts | 39 +- .../src/toHugoMarkdown.ts | 68 +-- packages/adapter-markdown/src/toMarkdown.ts | 3 +- .../src/toMkdocsMarkdown.ts | 39 +- .../adapter-nextjs-mdx/src/toNextjsMdx.ts | 47 +-- .../src/toNuxtContentMarkdown.ts | 39 +- .../adapters/src/registerBuiltInAdapters.ts | 28 +- packages/core/src/article.ts | 6 + packages/core/src/frontmatterSeo.ts | 120 ++++++ packages/core/src/index.ts | 17 + packages/core/src/seo.test.ts | 72 ++++ packages/core/src/seo.ts | 102 +++++ packages/core/src/validation.test.ts | 20 + packages/core/src/validation.ts | 50 ++- packages/core/tsconfig.json | 2 +- packages/publishers/package.json | 1 + .../src/ghost/ghostPublisher.test.ts | 31 ++ .../publishers/src/ghost/ghostPublisher.ts | 39 +- packages/publishers/src/types.ts | 2 + .../src/wordpress/wordpressPublisher.test.ts | 29 ++ .../src/wordpress/wordpressPublisher.ts | 32 ++ .../src/wordpressPublisherAdapter.ts | 31 ++ packages/setup/package.json | 32 ++ packages/setup/src/cli.ts | 61 +++ packages/setup/src/connectionChecks.ts | 227 ++++++++++ packages/setup/src/envFile.test.ts | 71 ++++ packages/setup/src/envFile.ts | 105 +++++ packages/setup/src/envRequirements.ts | 322 ++++++++++++++ packages/setup/src/index.ts | 52 +++ packages/setup/src/maskSecrets.ts | 27 ++ packages/setup/src/paths.ts | 25 ++ packages/setup/src/validateConfig.test.ts | 102 +++++ packages/setup/src/validateConfig.ts | 269 ++++++++++++ packages/setup/src/wizard.test.ts | 101 +++++ packages/setup/src/wizard.ts | 392 ++++++++++++++++++ packages/setup/tsconfig.json | 23 + pnpm-lock.yaml | 31 ++ 68 files changed, 3327 insertions(+), 209 deletions(-) create mode 100644 apps/studio/src/components/CompatibilityPanel.tsx create mode 100644 apps/studio/src/components/SeoSharingPanel.tsx create mode 100644 apps/studio/src/lib/seoValidation.test.ts create mode 100644 apps/studio/src/lib/seoValidation.ts delete mode 100644 docs/seo-fields-roadmap.md create mode 100644 docs/seo-fields.md create mode 100644 docs/setup-wizard.md create mode 100644 packages/core/src/frontmatterSeo.ts create mode 100644 packages/core/src/seo.test.ts create mode 100644 packages/core/src/seo.ts create mode 100644 packages/setup/package.json create mode 100644 packages/setup/src/cli.ts create mode 100644 packages/setup/src/connectionChecks.ts create mode 100644 packages/setup/src/envFile.test.ts create mode 100644 packages/setup/src/envFile.ts create mode 100644 packages/setup/src/envRequirements.ts create mode 100644 packages/setup/src/index.ts create mode 100644 packages/setup/src/maskSecrets.ts create mode 100644 packages/setup/src/paths.ts create mode 100644 packages/setup/src/validateConfig.test.ts create mode 100644 packages/setup/src/validateConfig.ts create mode 100644 packages/setup/src/wizard.test.ts create mode 100644 packages/setup/src/wizard.ts create mode 100644 packages/setup/tsconfig.json diff --git a/README.md b/README.md index 2a48c5e..a8aae05 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,12 @@ Requirements: Node.js 22+, pnpm 11+ git clone https://github.com/bnz183/SourceDraft.git cd SourceDraft pnpm install +pnpm setup # guided wizard — or copy example files manually (below) +``` + +Or copy example files manually: +```bash cp sourcedraft.config.example.json sourcedraft.config.json cp .env.example .env ``` @@ -104,7 +109,7 @@ Sign in, click **New post**, preview the output, publish. The file lands at `con **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). -Full walkthrough: [docs/getting-started.md](docs/getting-started.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) ## Beginner path diff --git a/apps/studio/package.json b/apps/studio/package.json index 9185a27..54a8bc1 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -4,8 +4,8 @@ "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/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/github-publisher --filter @sourcedraft/config build", + "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\"", "dev:web": "vite", "dev:server": "tsx watch server/index.ts", @@ -33,6 +33,7 @@ "@sourcedraft/core": "workspace:*", "@sourcedraft/github-publisher": "workspace:*", "@sourcedraft/media-providers": "workspace:*", + "@sourcedraft/setup": "workspace:*", "busboy": "^1.6.0", "dotenv": "^16.5.0", "express": "^5.1.0", diff --git a/apps/studio/server/publish.ts b/apps/studio/server/publish.ts index 44e3c3d..ba95701 100644 --- a/apps/studio/server/publish.ts +++ b/apps/studio/server/publish.ts @@ -77,6 +77,8 @@ function toCmsPayload(article: Article): CmsArticlePayload { : {}), ...(article.canonicalUrl !== undefined ? { canonicalUrl: article.canonicalUrl } : {}), ...(article.socialImage !== undefined ? { socialImage: article.socialImage } : {}), + ...(article.coverImageAlt !== undefined ? { coverImageAlt: article.coverImageAlt } : {}), + ...(article.noindex === true ? { noindex: true } : {}), }; } diff --git a/apps/studio/server/setupHealth.ts b/apps/studio/server/setupHealth.ts index 0099058..0c64790 100644 --- a/apps/studio/server/setupHealth.ts +++ b/apps/studio/server/setupHealth.ts @@ -1,5 +1,6 @@ import { isAdapterId } from "@sourcedraft/adapters"; import { isPublisherId } from "@sourcedraft/publishers"; +import { validateConfig } from "@sourcedraft/setup"; import { isAuthConfigured } from "./auth.js"; import { loadProjectConfig, loadPublicConfig } from "./config.js"; import { @@ -32,6 +33,15 @@ export type SetupHealthCheck = { detail: string; }; +export type SetupCompatibilityReport = { + adapter: string; + publisher: string; + mediaProvider: string; + validationOk: boolean; + missingEnvVars: string[]; + warnings: string[]; +}; + export type SetupHealthReport = { ok: boolean; adminPasswordConfigured: boolean; @@ -46,6 +56,7 @@ export type SetupHealthReport = { demoModeForced: boolean; demoModeAvailable: boolean; githubReady: boolean; + compatibility: SetupCompatibilityReport; checks: SetupHealthCheck[]; nextAction: string | null; }; @@ -314,6 +325,41 @@ export function getSetupHealth(): SetupHealthReport { nextAction = null; } + const validation = validateConfig(); + const compatibility: SetupCompatibilityReport = { + adapter: validation.adapter, + publisher: validation.publisher, + mediaProvider: validation.mediaProvider, + validationOk: validation.ok, + missingEnvVars: validation.missingEnvVars, + warnings: validation.warnings, + }; + + if (!validation.ok && validation.missingEnvVars.length > 0) { + checks.push({ + id: "config-validation", + label: "Configuration validation", + ok: false, + detail: `Missing: ${validation.missingEnvVars.join(", ")}. Run pnpm validate:config for details.`, + }); + } else if (validation.warnings.length > 0) { + checks.push({ + id: "config-validation", + label: "Configuration validation", + ok: true, + detail: validation.warnings[0] ?? "Configuration validated with warnings.", + }); + } else { + checks.push({ + id: "config-validation", + label: "Configuration validation", + ok: validation.ok, + detail: validation.ok + ? "Adapter, publisher, and media provider look compatible." + : "Run pnpm validate:config locally for details.", + }); + } + return { ok: publisherReady || demoModeAvailable, adminPasswordConfigured, @@ -328,6 +374,7 @@ export function getSetupHealth(): SetupHealthReport { demoModeForced, demoModeAvailable, githubReady: publisherReady, + compatibility, checks, nextAction, }; diff --git a/apps/studio/src/components/CompatibilityPanel.tsx b/apps/studio/src/components/CompatibilityPanel.tsx new file mode 100644 index 0000000..bb48458 --- /dev/null +++ b/apps/studio/src/components/CompatibilityPanel.tsx @@ -0,0 +1,72 @@ +import type { SetupCompatibilityReport } from "../lib/setupHealth.js"; + +type CompatibilityPanelProps = { + compatibility: SetupCompatibilityReport; +}; + +export function CompatibilityPanel({ compatibility }: CompatibilityPanelProps) { + const statusLabel = compatibility.validationOk ? "Valid" : "Needs attention"; + const statusClass = compatibility.validationOk + ? "compatibility-panel__status--ok" + : "compatibility-panel__status--warn"; + + return ( +
+
+

+ Compatibility & status +

+

+ Read-only summary — secrets are never shown in the browser +

+
+ +
+
+
Adapter
+
{compatibility.adapter}
+
+
+
Publisher
+
{compatibility.publisher}
+
+
+
Media provider
+
{compatibility.mediaProvider}
+
+
+
Validation
+
+ + {statusLabel} + +
+
+
+ + {compatibility.missingEnvVars.length > 0 && ( +
+

Missing environment variables

+

+ Set these in .env on the server (or run{" "} + pnpm setup):{" "} + {compatibility.missingEnvVars.join(", ")} +

+
+ )} + + {compatibility.warnings.length > 0 && ( + + )} +
+ ); +} diff --git a/apps/studio/src/components/PostDetailsPanel.tsx b/apps/studio/src/components/PostDetailsPanel.tsx index 80ea399..c335674 100644 --- a/apps/studio/src/components/PostDetailsPanel.tsx +++ b/apps/studio/src/components/PostDetailsPanel.tsx @@ -2,6 +2,7 @@ import type { ValidationIssue } from "@sourcedraft/core"; import type { ArticleFormState } from "../lib/articleForm"; import { ContentQualityPanel } from "./ContentQualityPanel"; import { MediaSection } from "./MediaSection"; +import { SeoSharingPanel } from "./SeoSharingPanel"; type PostDetailsPanelProps = { values: ArticleFormState; @@ -208,6 +209,13 @@ export function PostDetailsPanel({ )} + +
; + onChange: (field: keyof ArticleFormState, value: string | boolean) => void; + validationIssues: ValidationIssue[]; +}; + +export function SeoSharingPanel({ + values, + fieldErrors, + onChange, + validationIssues, +}: SeoSharingPanelProps) { + const { readingTimeMinutes, warnings } = analyzeSeoFields(values); + const blockingFields = new Set(validationIssues.map((issue) => issue.field)); + + function fieldClass(field: string): string { + return fieldErrors[field] + ? "field__input field__input--error" + : "field__input"; + } + + return ( +
+ + SEO / Sharing + Optional metadata for search and social + + +
+

+ Leave fields blank to fall back to the post title, description, or cover + image. Soft warnings below do not block publishing. +

+ + + +