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. +

+ + + +