From 7d9e04dcfafd7325d268f379b064c46fee7e582d Mon Sep 17 00:00:00 2001 From: bnz183 Date: Tue, 9 Jun 2026 12:19:01 +0200 Subject: [PATCH] GitHub API polish and early Studio UI improvements --- README.md | 2 + apps/studio/server/media.ts | 6 +- apps/studio/server/posts.ts | 7 +- apps/studio/server/publish.ts | 3 +- apps/studio/src/App.tsx | 41 +-- apps/studio/src/components/AdapterStatus.tsx | 23 +- .../studio/src/components/ArticlePipeline.tsx | 70 +++-- .../studio/src/components/AstroMdxPreview.tsx | 43 ++- apps/studio/src/components/CommandBar.tsx | 6 +- .../studio/src/components/EditorWorkspace.tsx | 4 +- .../src/components/FrontmatterInspector.tsx | 22 +- apps/studio/src/components/LoginScreen.tsx | 29 ++- apps/studio/src/components/MediaDropzone.tsx | 81 ++++-- apps/studio/src/components/PublishGate.tsx | 72 +++-- apps/studio/src/index.css | 127 +++++++-- apps/studio/src/lib/media.ts | 6 +- apps/studio/src/lib/posts.ts | 11 +- apps/studio/src/lib/publish.ts | 6 +- docs/configuration.md | 2 + docs/github-publishing.md | 44 +++- docs/project-status.md | 12 +- package.json | 2 +- packages/github-publisher/package.json | 4 +- .../github-publisher/src/githubErrors.test.ts | 92 +++++++ packages/github-publisher/src/githubErrors.ts | 177 +++++++++++++ .../github-publisher/src/githubPaths.test.ts | 16 ++ packages/github-publisher/src/githubPaths.ts | 11 + .../github-publisher/src/githubPublisher.ts | 245 ++++++++++++------ packages/github-publisher/src/index.ts | 9 + pnpm-lock.yaml | 3 + 30 files changed, 935 insertions(+), 241 deletions(-) create mode 100644 packages/github-publisher/src/githubErrors.test.ts create mode 100644 packages/github-publisher/src/githubErrors.ts create mode 100644 packages/github-publisher/src/githubPaths.test.ts create mode 100644 packages/github-publisher/src/githubPaths.ts diff --git a/README.md b/README.md index 1ed4a19..71e9523 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,8 @@ The GitHub token never reaches the browser. It is read from `.env` on the server Details: [docs/github-publishing.md](docs/github-publishing.md) · [docs/media.md](docs/media.md) +v0.1 uses the GitHub Contents API — suitable for typical blogs; very large content folders are a known MVP limitation. + ## Quickstart Requirements: Node.js 22+, pnpm 11+ diff --git a/apps/studio/server/media.ts b/apps/studio/server/media.ts index 7f129e3..887bf28 100644 --- a/apps/studio/server/media.ts +++ b/apps/studio/server/media.ts @@ -257,12 +257,16 @@ export async function uploadMedia( path: repoPath, contentBase64: parsed.buffer.toString("base64"), message: `Upload media: ${repoFilename}`, + purpose: "media", }); if (!result.ok) { return { status: 502, - body: { ok: false, error: result.error }, + body: { + ok: false, + error: result.error || "Media upload to GitHub failed.", + }, }; } diff --git a/apps/studio/server/posts.ts b/apps/studio/server/posts.ts index 21c4fbc..e0b5114 100644 --- a/apps/studio/server/posts.ts +++ b/apps/studio/server/posts.ts @@ -175,11 +175,11 @@ export async function listPosts( ): Promise<{ status: number; body: PostsListResponse }> { const publisher = createPublisher(env); const contentDir = normalizeContentDir(env.contentDir); - const listed = await publisher.listFiles({ path: contentDir }); + const listed = await publisher.listFiles({ path: contentDir, contentDir }); if (!listed.ok) { return { - status: 502, + status: listed.status === 404 ? 404 : 502, body: { ok: false, error: listed.error }, }; } @@ -246,8 +246,9 @@ export async function loadPost( const loaded = await publisher.readFile({ path: safe.path }); if (!loaded.ok) { + const status = loaded.status === 404 ? 404 : 502; return { - status: loaded.status === 404 ? 404 : 502, + status, body: { ok: false, error: loaded.error }, }; } diff --git a/apps/studio/server/publish.ts b/apps/studio/server/publish.ts index 8756682..423a9e2 100644 --- a/apps/studio/server/publish.ts +++ b/apps/studio/server/publish.ts @@ -100,12 +100,13 @@ export async function publishArticle( path, content, message: `Publish: ${article.slug}`, + purpose: "post", }); if (!result.ok) { const errorBody: PublishErrorResponse = { ok: false, - error: result.error, + error: result.error || "Publish to GitHub failed.", }; if (result.status !== undefined) { diff --git a/apps/studio/src/App.tsx b/apps/studio/src/App.tsx index 1b3d03c..09e24bf 100644 --- a/apps/studio/src/App.tsx +++ b/apps/studio/src/App.tsx @@ -256,7 +256,9 @@ function App() { } const action = result.created ? "Created" : "Updated"; - setPublishSuccess(`${action} ${result.path} (${result.commitSha.slice(0, 7)})`); + setPublishSuccess( + `${action} output file ${result.path} (commit ${result.commitSha.slice(0, 7)}).`, + ); setEditingPath(result.path); await refreshPosts(); } catch { @@ -269,7 +271,9 @@ function App() { if (!authChecked) { return (
-

Checking session...

+

+ Checking session… +

); } @@ -292,14 +296,16 @@ function App() { {view === "overview" && (
{loadPostError && ( -

- {loadPostError} -

+
+

Could not open post

+

{loadPostError}

+
)} { void refreshPosts(); }} @@ -320,10 +326,13 @@ function App() {
{editingPath && ( -

- Editing {editingPath}. Publishing will update this - file in GitHub. -

+
+

Editing an existing post

+

+ Changes will update output file{" "} + {editingPath} on GitHub. +

+
)} {fieldErrors.body && ( @@ -349,7 +358,7 @@ function App() {
-

Publishing configuration

+

Settings

- Paths and categories from sourcedraft.config.json. GitHub - target from .env. + Project paths from config · GitHub target from .env

- Publishing requires a GitHub token with write access to the - target repository. The token is read only by the server when you - publish. + These values are read-only here. Edit{" "} + sourcedraft.config.json for folders and categories, and{" "} + .env for GitHub credentials. The token never reaches + the browser.

diff --git a/apps/studio/src/components/AdapterStatus.tsx b/apps/studio/src/components/AdapterStatus.tsx index 7625acf..0ac8fb4 100644 --- a/apps/studio/src/components/AdapterStatus.tsx +++ b/apps/studio/src/components/AdapterStatus.tsx @@ -21,29 +21,30 @@ export function AdapterStatus({ githubOwner.trim().length > 0 && githubRepo.trim().length > 0; const rows: StatusRow[] = [ - { label: "Adapter", value: adapter, state: "ok" }, - { label: "Output path", value: contentDir, state: "ok" }, + { label: "Output format", value: adapter, state: "ok" }, + { label: "Content folder", value: contentDir, state: "ok" }, { - label: "GitHub target", + label: "GitHub repository", value: githubReady ? `${githubOwner}/${githubRepo}` : "Not configured", state: githubReady ? "idle" : "off", }, { label: "GitHub token", - value: "Checked server-side on publish", - state: "idle", + value: "Used on the server when you publish", + state: githubReady ? "idle" : "off", }, - { label: "Auth", value: "Local password session", state: "ok" }, ]; return ( -
+
-

Publishing setup

+

+ Publishing setup +

{githubReady - ? "GitHub repo configured in .env" - : "Set GITHUB_OWNER and GITHUB_REPO in .env to publish"} + ? "Connected to your GitHub repository" + : "Finish GitHub setup in .env to publish"}

@@ -56,7 +57,7 @@ export function AdapterStatus({ className={`adapter-status__dot adapter-status__dot--${row.state}`} aria-hidden="true" /> - {row.value} + {row.value}
))} diff --git a/apps/studio/src/components/ArticlePipeline.tsx b/apps/studio/src/components/ArticlePipeline.tsx index 4d4783c..4cec052 100644 --- a/apps/studio/src/components/ArticlePipeline.tsx +++ b/apps/studio/src/components/ArticlePipeline.tsx @@ -4,6 +4,7 @@ type ArticlePipelineProps = { posts: PostSummary[]; loading: boolean; error: string | null; + githubReady: boolean; onRefresh: () => void; onEdit: (path: string) => void; }; @@ -12,16 +13,19 @@ export function ArticlePipeline({ posts, loading, error, + githubReady, onRefresh, onEdit, }: ArticlePipelineProps) { return ( -
+
-

Articles

-

+

+ Your posts +

+

{loading - ? "Loading from GitHub..." + ? "Loading posts from GitHub…" : `${posts.length} post${posts.length === 1 ? "" : "s"} in your content folder`}

+ {loading && ( +
+

Loading posts…

+

+ Fetching posts from your GitHub content folder. This may take a moment + for larger sites. +

+
+ )} + + {!githubReady && !loading && ( +
+

GitHub is not configured yet

+

+ Set GITHUB_OWNER and GITHUB_REPO in{" "} + .env, then open Settings to confirm the + target repository. You can still write drafts locally. +

+
+ )} + {error && ( -

{error}

+
+

Could not load posts

+

{error}

+

+ Check your GitHub token, repository settings, and{" "} + contentDir in Settings. Use Refresh list after fixing + configuration. +

+
)} {!loading && !error && posts.length === 0 && (
-

No posts yet

+

No posts found

- Use New Article to write your first post. After you - publish, it will show up here so you can edit it later. + {githubReady + ? "Nothing matched your content folder yet. Open Write to draft a post, then publish to GitHub. Published posts appear here for editing." + : "Configure GitHub in Settings, then open Write to create your first post."}

)} - {posts.length > 0 && ( + {!loading && posts.length > 0 && (
- - - - - + + + + @@ -65,7 +101,7 @@ export function ArticlePipeline({ @@ -77,7 +113,7 @@ export function ArticlePipeline({ : "article-pipeline__badge article-pipeline__badge--ready" } > - {post.draft ? "Draft" : "Published"} + {post.draft ? "Draft" : "Live"}
TitleDateCategoryStatus + TitleDateCategoryStatus + Actions +
{post.title} - {post.path} + {post.path} {post.pubDate} {post.category} diff --git a/apps/studio/src/components/AstroMdxPreview.tsx b/apps/studio/src/components/AstroMdxPreview.tsx index 5aa94b2..2643ce9 100644 --- a/apps/studio/src/components/AstroMdxPreview.tsx +++ b/apps/studio/src/components/AstroMdxPreview.tsx @@ -12,7 +12,7 @@ type AstroMdxPreviewProps = { }; function previewLabel(adapter: string): string { - return adapter === "markdown" ? "Markdown output" : "Astro MDX output"; + return adapter === "markdown" ? "Markdown preview" : "MDX preview"; } export function AstroMdxPreview({ @@ -40,20 +40,22 @@ export function AstroMdxPreview({ : null; return ( -
+
-

{previewLabel(adapter)}

+

+ {previewLabel(adapter)} +

{valid - ? "Preview of the file that will be committed" - : "Fix validation issues to preview output"} + ? "Review the file that will be saved to GitHub" + : "Complete post details and body to preview"}

{valid && resolvedOutputPath && fileOutput ? (
- Output path + Output file {resolvedOutputPath}
@@ -61,14 +63,27 @@ export function AstroMdxPreview({
           
) : ( -
    - {issues.map((issue) => ( -
  • - {issue.field} - {issue.message} -
  • - ))} -
+
+ {issues.length === 0 ? ( +

+ Add a title, description, dates, category, and body to continue. +

+ ) : ( + <> +

+ Fix these items before publishing: +

+
    + {issues.map((issue) => ( +
  • + {issue.field} + {issue.message} +
  • + ))} +
+ + )} +
)}
); diff --git a/apps/studio/src/components/CommandBar.tsx b/apps/studio/src/components/CommandBar.tsx index 46e3a12..402cad6 100644 --- a/apps/studio/src/components/CommandBar.tsx +++ b/apps/studio/src/components/CommandBar.tsx @@ -7,8 +7,8 @@ type CommandBarProps = { }; const NAV_ITEMS: { id: View; label: string }[] = [ - { id: "overview", label: "Overview" }, - { id: "new-article", label: "New Article" }, + { id: "overview", label: "Posts" }, + { id: "new-article", label: "Write" }, { id: "settings", label: "Settings" }, ]; @@ -23,7 +23,7 @@ export function CommandBar({ SD

SourceDraft Studio

-

Markdown / MDX writing workspace

+

Write, preview, and publish to GitHub

diff --git a/apps/studio/src/components/EditorWorkspace.tsx b/apps/studio/src/components/EditorWorkspace.tsx index bab1f08..9da1647 100644 --- a/apps/studio/src/components/EditorWorkspace.tsx +++ b/apps/studio/src/components/EditorWorkspace.tsx @@ -7,8 +7,8 @@ export function EditorWorkspace({ body, onBodyChange }: EditorWorkspaceProps) { return (
-

Body

-

MDX / Markdown

+

Write

+

Markdown or MDX body