• 9 min read

Building a Live Blog Post Editor Canvas with GitHub Copilot

I was tired of starting my entire Hugo site just to check one post before a PR. So I asked GitHub Copilot to build me a canvas that lists my posts, previews them with every image and video, and lets me edit them live right next to my chat.

Ever since I rebuilt this blog as a static Hugo site with GitHub Copilot CLI, every post is just a Markdown file in a git repo. I love that! But there was one thing that kept bugging me: to see what a post actually looks like, I had to start the whole site with hugo server, wait for it to build, find the post, and then go back and forth between my editor and the browser. And I wanted to do that before sending a pull request, not after the preview deployment came back.

What I really wanted was an elegant little place to pick a post, edit it, and watch it render with all of its images, videos, and embeds. It didn't need to look exactly like the site. It just needed to show me the content on a light or dark background, so I could catch a broken image or a wonky screenshot before anyone else did.

So I asked Copilot to build it for me as a canvas, and this post is the story of how that went. Oh, and every screenshot in this post is of the canvas editing... this post. 🤯

The Post Workbench canvas showing this post's Markdown on the left and its live preview on the right.
Post Workbench editing this post in a split view.

What is a canvas anyway?

A canvas is a panel that a Copilot extension can open right next to your chat. It is plain web content: the extension starts a small local web server, hands the host a URL, and the host shows it in a panel. The extension can also give the agent actions, so Copilot can drive the canvas for you ("open my newest post", "switch to dark mode").

Extensions can live in a few places. I picked workspace scope, which means the canvas lives in my blog repo at .github/extensions/post-workbench/. That has a really nice side effect: the canvas always works on the checkout that contains it, so every branch or worktree sees its own posts, its own edits, and its own git status. That is exactly what I want when I am working on a post in a branch before a PR.

Building it with the create-canvas skill

I started with the create-canvas skill and this (typo-filled, as usual) prompt:

i would like to create a canvas for visualizing our posts, and and allow liveing editing and previeing in the current branch. i want to be able to visualize the images/videos and such like that. it doesn't need to be actually what it woudl look like on the site jsut a preview on a light or dark backgroudn (with toggle)

Just like with my site migration, the best thing I did was let Copilot plan first. It went off and explored the repo: how my posts are laid out, how Hugo mounts static/ and static-case/ for images, which shortcodes I use for YouTube, Vimeo, Fireside, and gists, and how the canvas SDK actually works. Then it asked me a few questions before writing any code:

  • Where should it live? Workspace scope, so it ships with the repo.
  • How should Markdown be rendered? A vendored copy of marked, so there is no npm install and no build step.
  • Should it save automatically? Yes! Autosave on by default, with the option to turn it off.

From there it wrote a plan with 16 todos, I approved it, and it got to work.

Taking a tour

Let's take a look at what came out the other side.

All of my posts, at a glance

The canvas opens on a grid of every post with its cover image, date, tags, and how many images and videos it has. I can search, filter to drafts, or filter to Changed, which shows only the posts that differ from my main branch or have uncommitted edits. That one is basically "what am I about to put in this PR?"

The Post Workbench post list showing a grid of blog post cards with cover images, dates, tags, and media counts.
Every post in the repo, newest first.

Edit and preview side by side

Pick a post and you get the Markdown on one side and a live preview on the other. As I type, the preview updates, and with Sync scroll on, both sides stay together. Autosave writes the file a moment after I stop typing, and it keeps the file's line endings intact so my diffs stay clean.

Split view with the Markdown source of this post on the left and the rendered preview with a screenshot on the right, on a light background.
Split view on a light background.

Here is the toggle I really wanted: one click and the preview flips to a dark background. This is super handy for screenshots with transparent backgrounds or light borders that look great in one theme and disappear in the other. There is even a Transparency option that puts a checkerboard behind transparent images.

The same split view with the preview on a dark background.
Same post, dark background.

Every image, video, and embed

The Media tab lists every image, video, audio file, and embed in the post, with its source line and a status: found, missing, too large, or a case mismatch. That last one is sneaky: Windows doesn't care if your file is Screenshot.png and your post says screenshot.png, but the Linux build on GitHub Actions absolutely does!

The Media tab listing this post's screenshots as tiles, each with a found status and a line number.
The Media tab checks every reference before the build does.

Click any tile to open it in a larger viewer with its size and file details, and jump straight to the line in the source where it is used. YouTube, Vimeo, and Fireside embeds show a card first, and only load the player when you click Load player.

A screenshot opened in the Post Workbench media viewer, with its dimensions, file size, and a Show in source button.
The media viewer, with a quick way back to the source line.

Copilot can drive it, too

Because the canvas exposes actions, Copilot can use it without me touching the panel. I can ask it to list the posts I changed on this branch, open a specific post at a line, check a post's media for problems, or switch the preview to dark. That makes "hey, go check the images in my new post" a real request.

For example, here are the results when Copilot calls get_post_media on this post: every image comes back as found, with its line number, file size, and git status. Then it calls open_post with a line number and set_preview_theme with dark, and the canvas jumps right there:

The Post Workbench canvas on a dark background, scrolled to the A peek under the hood section of this post after Copilot opened it at that line.
The canvas after Copilot opened this post at a line and switched the preview to dark.

A peek under the hood

The canvas is just an ES module named extension.mjs that uses the Copilot SDK. Here is the shape of it, trimmed down to one action:

import { CanvasError, createCanvas, joinSession } from "@github/copilot-sdk/extension";

const canvas = createCanvas({
  id: "montemagno-post-workbench",
  displayName: "Post Workbench",
  description: "Browse, preview and live-edit this blog's Markdown posts.",
  actions: [
    {
      name: "set_preview_theme",
      description: "Switch the preview background between light, dark, or the system setting.",
      inputSchema: {
        type: "object",
        properties: { theme: { type: "string", enum: ["light", "dark", "system"] } },
        required: ["theme"],
      },
      handler: async (ctx) => registry.require(ctx.instanceId).actions.setPreviewTheme(ctx.input),
    },
  ],
  // Start (or reuse) a local server for this panel and give the host its URL.
  open: async (ctx) => registry.open(ctx.instanceId, ctx.input),
  onClose: async (ctx) => registry.close(ctx.instanceId),
});

await joinSession({ canvases: [canvas] });

open starts a small web server on 127.0.0.1 with a random port and returns its URL, and calling it again for the same panel reuses that server, so my unsaved edits survive. There is no package.json and nothing to install: the runtime provides the SDK.

Everything else lives in sibling folders: a Node side in lib/ that indexes posts, reads git status, and saves files, and a plain JavaScript browser app in web/ with no build step. A few details I really appreciated:

  • Saves are safe. Files are written to a temporary file and then swapped in, so a crash can't leave half a post. If the file changes on disk while I have unsaved edits (say, Copilot edited it in chat), I get to choose which version wins instead of silently losing one.
  • Post content is treated as untrusted. The preview is rebuilt from an allowlist of safe HTML, a content security policy backs that up, and embeds run in sandboxed frames.
  • Preferences stay out of the repo. Things like the light or dark choice are stored under my Copilot home folder, not in git.

One gotcha worth knowing if you build your own: never use console.log in an extension. Standard output is how the extension talks to the host, so stray logging can break the connection. Use session.log() instead.

Testing it for real

This is where working with an agent really shines. It didn't stop at "it compiles." Copilot wrote more than 140 tests for the Node side and the rendering, ran them on Node 20, 22, and 26, and then drove the canvas in a real browser to check editing, autosave, conflicts, keyboard navigation, narrow panels, and dark mode. It even rendered all of my posts in the preview to make sure none of them broke it.

That testing caught real bugs. The Skip to content link was quietly closing the open post, because the canvas uses the URL hash for navigation. A test that passed on my machine flaked under load because of a timing race. And the test command itself behaved differently on Node 20 and Node 22.

Then I had Copilot run a separate code review, and it found the scariest one of all: if a post briefly disappeared or couldn't be read (think git stash, switching branches, or Windows holding a lock on the file), the canvas treated it as deleted and threw away my unsaved edits. Yikes! Now only a file that is truly gone counts as removed, the edits are kept as a draft, and editing picks right back up when the file comes back. All of these got fixed before I ever hit them, which is exactly what I want from my AI pair programmer.

Try it or build your own

If you have a Markdown blog or docs site in a repo, this is a fun one to build for yourself. Start with the create-canvas skill, tell it what you want to see and edit, and let it plan before it codes. Keep it in your repo with workspace scope, so it follows whatever branch you're on.

As for this post, it was previewed and checked in the canvas, right next to my chat, with no hugo server in sight. That is exactly the workflow I was after before sending a PR!

James Montemagno
Author

James Montemagno

James Montemagno is a Principal Lead Program Manager for Developer Community at Microsoft. He has been a .NET developer since 2005, working in a wide range of industries including game development, printer software, and web services. Live, Love, Bike, and Code.

Milli the dog peeking up from the bottom of the page