Coding - James Montemagno https://montemagno.com/tag/coding/ https://montemagno.com/content/images/2017/08/MonogramIcon_Orange_NoBuffer.png Coding - James Montemagno https://montemagno.com/tag/coding/ Hugo -- gohugo.io Sat, 17 Oct 2026 17:26:05 GMT 60 Rebuilding 13 Years of Blog Posts as a Static Site with GitHub Copilot CLI https://montemagno.com/rebuilding-my-blog-as-a-static-site-with-github-copilot-cli/ https://montemagno.com/rebuilding-my-blog-as-a-static-site-with-github-copilot-cli/ Fri, 16 Oct 2026 16:00:00 GMT

If you are reading this, you are looking at a brand-new montemagno.com. It looks a lot like the old one on purpose, but underneath it is a completely different animal: no database, no server, no admin panel. Just Markdown files in a private GitHub repo, built by Hugo, verified by GitHub Actions, and served from GitHub Pages.

I have been blogging here since 2012 (hi, Tumblr) and on Ghost since 2017. Ghost was great, but over the years my wishlist kept growing: I wanted to write in my editor, keep images next to my posts in git, have dark mode, get rid of jQuery and ad scripts, and most of all own the whole thing as plain files. The thing that always stopped me was the sheer size of the job: 274 posts, a thousand images, nine years of URLs that people and search engines link to, and a theme I really liked.

So this time I did it differently. I opened a terminal, started GitHub Copilot CLI, and said roughly this:

I want to completely remake my site to be a static site with full backwards compat with the format/slugs, images, import of data, easily writing things in markdown and then having it displayed, easy image management. Here is my Ghost export and my existing design system. Do deep research on montemagno.com and let's plan it all out.

About an afternoon later the initial version of the site you are looking at existed, every one of 2,172 legacy URLs was verified to resolve, and a CI pipeline was standing guard. This post is the story of that migration, the follow-up work to get it launch-ready, and what I learned about working with an agent on a project this size.

Timeline of the migration session
The initial migration session, wall-clock. Planning took longer than the migration scripts.

Step 0: Plan first, code second

The best decision I made was starting in plan mode. Instead of diving into code, Copilot CLI spent the first chunk of time doing research:

  • It parsed the Ghost export JSON and reported exactly what was in it: 298 posts (274 published, 13 drafts), 10 pages, 323 tags, ~1,330 image references, which posts were stored as Ghost "mobiledoc" Markdown cards versus the newer Lexical editor, how many used gists, YouTube, Fireside podcast players, and so on.
  • It spun up a research sub-agent that crawled the live site and came back with a precise inventory: URL patterns (/slug/, /tag/slug/, /page/N/, /rss/, /tag/slug/rss/, /author/james-montemagno/), posts per page, every <meta> and JSON-LD tag Ghost emits, the fonts and colors in the Cedar theme, the third-party scripts, and even the fact that /slug/amp/ 301s back to the canonical URL.
  • It dug through my old montemagno.com-assets repo and found the Cedar theme source, the brand guidelines, and a redirects.json with 293 legacy redirects going back to the Tumblr days.
Charts of how Ghost stored content and which embeds were converted
Knowing what was actually in the export made the converter a lot simpler to design.

Then it asked me a handful of structured questions: which static site generator, where to host, how faithful to keep the design, what to do about comments, newsletters, ads, and the little dog that pops up at the bottom of the page. I pushed back on its first suggestion (Astro), it gave me an honest comparison of Hugo, Eleventy, Statiq, Zola, and Jekyll, and we landed on Hugo on GitHub Pages with an "evolve the Cedar look" design direction.

The output of plan mode was a 16-item plan with dependencies. Only after I approved it did anything get written to disk. That alone saved me from a dozen wrong turns.

The architecture

Here is what we ended up with. Every box is files in one repository.

Architecture diagram of the new site
Sources on the left, one-time migration tooling in the middle, the Hugo repo, then Actions and Pages.

A few things worth calling out:

  • Images keep their exact URLs. Ghost served images from /content/images/YYYY/MM/name.png. The new site puts them at static/content/images/YYYY/MM/name.png, so every hotlink, Google Images result, and old tweet still works. New posts just drop images into the current month's folder.
  • Redirects are front matter. GitHub Pages has no server-side redirects, so each legacy URL becomes an aliases: entry on the post it should point to. Hugo emits a tiny HTML stub with <meta http-equiv="refresh"> and a canonical link. Search engines treat it as permanent.
  • Tags are real pages. content/tags/<slug>/_index.md holds the display name (so xamarin-forms renders as "Xamarin.Forms"), and a few near-duplicate tags (vs-code, visual-studio-code) were merged into vscode with aliases for the old URLs and their RSS feeds.
  • RSS at /rss/ is still XML. Feed readers have been hitting montemagno.com/rss/ for years. Hugo now writes the feed to /rss/index.html (what GitHub Pages serves for that directory) with a Ghost-compatible structure: full content:encoded, media:content feature images, categories, and the original Ghost post IDs as GUIDs so nobody's reader shows 274 "new" items.
  • No build-time API calls. The Apps page (more on that below) reads data/github.json, which a scheduled Action refreshes daily. Builds are deterministic and fast.

The migration pipeline

Seven step migration pipeline
Each step is a small, re-runnable Node script in tools/migrate.

Convert: HTML to Markdown, for real

Ghost's export includes rendered HTML for every post, which is what we converted (the mobiledoc/lexical JSON is an implementation detail). The converter uses Turndown with a set of custom rules that Copilot wrote after sampling the awkward cases in my content:

  • Fenced code blocks with the language preserved (and a few typos like language-csharo normalized).
  • Heading IDs preserved as {#id} attributes so in-page anchors keep working.
  • Ghost image cards with captions become {{< figure >}} shortcodes; plain images become plain Markdown.
  • <script src="https://gist.github.com/..."> becomes {{< gist user id >}} (118 of them!), YouTube iframes become {{< youtube id >}}, Fireside players become {{< fireside url >}}.
  • Every ad block (AdSense, Amazon affiliate widgets, DeveloperMedia) and tracking script is stripped.
  • __GHOST_URL__ placeholders and storage.ghost.io CDN URLs are rewritten to root-relative paths.

It also found and fixed things I had never noticed: a page with a literal </3> closing tag, posts where Markdown image syntax had been saved as plain text, and four image URLs that differed from a sibling only by case (github.png vs github.PNG), which is fine on Linux and a collision on Windows. That last one only surfaced when the first CI run on Ubuntu failed the URL check, and the fix was a second static-case/ mount folder.

Verify: 2,172 URLs, every build

This is the part I am most happy about. A script builds a manifest of every URL the old site served: all post slugs, tag pages and their RSS feeds, pages, images, the 250 usable redirect sources, plus whatever the live sitemap-*.xml files listed. A second script checks that every one of them exists in the Hugo output, case-sensitively, and fails the build if not.

Bar chart of verified URL categories
The manifest is committed to the repo, so the check runs on every push forever.

It caught real bugs: the case-collision above, a redirect that pointed at a typo'd /serach/, and a pair of duplicate rules in the original redirects.json that pointed the same URL at two different places.

The design: Cedar, evolved

I told Copilot "evolve the Cedar look" and that is what it did. The full-viewport cover, the big condensed headlines, the sticky header that hides on scroll, the slide-in drawer, the alternating post list: all there. But it is rebuilt with modern CSS custom properties, no jQuery, no Font Awesome (inline SVG icons instead), system-aware dark mode with a toggle, a copy button on code blocks, responsive embeds, and prev/next and related-post cards at the bottom of every article. Fonts still come from my Adobe Fonts kit, but via a single CSS link instead of a JavaScript loader.

Before and after comparison table
Fewer moving parts everywhere.

Search moved from GhostHunter (jQuery plus a Ghost API key) to Pagefind, which indexes the built HTML after Hugo runs and searches entirely in the browser. And yes, Milli still pops up from the bottom of the page when you find her.

The Apps page, powered by GitHub

Once the migration was done I asked for something fun: "For Apps, do you think we could do something really cool with my GitHub profile?"

The result is the new Apps page. A curated list of apps lives in data/apps.yaml (name, tagline, store links, platforms, and an optional repo). Everything else comes from GitHub: a daily Action runs a small Node script that pulls my profile, every repository, pinned items, and contribution stats (REST plus GraphQL, using the workflow's own token), and commits data/github.json only when something changed. Hugo then renders:

  • Featured app cards enriched with live stars, last-commit date, and language.
  • A "by the numbers" strip: 12,000+ stars, 291 original repos, followers, contributions in the past year, years on GitHub.
  • Language bars and fun facts (most starred, most forked, first-ever repo from April 2012, how many repos I created this year).
  • Pinned repos with their social-preview images, the most-starred open source, and "what I'm hacking on right now."

No client-side API calls, no rate limits, and it stays fresh without me touching it.

Fit and finish: from migrated to launch-ready

At that point I had the content, the design, and even a shiny new Apps page. But getting the migration working in an afternoon was not quite the same as being ready to send everyone to it. In a follow-up session, I asked Copilot: "What still needs to happen before I actually publish this?"

That question turned into 19 GitHub issues, which was a lot more useful than a vague list of things to remember. We split independent fixes across Copilot CLI sub-sessions in separate git worktrees: accessibility, feeds, search, content cleanup, and build checks. Each session had its own branch, and the work came back as focused pull requests that we reviewed and merged once their checks passed. More work could happen in parallel, but I still needed to review it and make the decisions.

The interesting bugs were not missing pages. The homepage needed its paginator initialized before Hugo rendered the head metadata. Posts made almost entirely of embeds needed their titles included in search, and related-post recommendations needed the taxonomy index wired up correctly. Even the code-copy button had a surprise: copying rendered text could double the newlines. None of those would have stopped the migration, but they are exactly the things you notice once you start using a site.

We also spent time on the things that make it easier for everyone to use. Text and button contrast improved without losing the orange branding, keyboard focus stayed inside the mobile drawer and image lightbox, and long headings and the Apps profile stopped overflowing narrow screens. The image pass added or improved 130 alt texts and supplied dimensions for 33 raw image tags. That was a targeted pass over recent and important content, not a claim that thirteen years of images had suddenly been fully audited.

The feeds were a good reminder that "the URL works" is only the beginning. An old tag-feed URL returning an HTML redirect is not much help to a feed reader expecting XML. We repaired all eight merged legacy tag feeds and checked their contents, not just whether the files existed. GitHub Pages still serves those directory-style feeds with a text/html content type, so we documented an optional Cloudflare response-header rule. A successful local build cannot fix the hosting platform's response headers.

As we fixed things, we taught CI to catch them next time: redirect destinations, metadata and headings, sitemap consistency, preserved static files, and search assets all joined the legacy URL checks. A page marked noindex should not quietly slip into the sitemap, and an old image URL should still serve the original file. We added 30 offline tests for the checker rules themselves, too. If I am relying on an agent to build my safety net, I want evidence that the net actually catches something.

Not everything belonged in that pipeline. External sites started blocking link checks, so we narrowed that audit to important pages and recent posts, throttled requests, and treated blocked responses as unverified rather than broken. We refreshed evergreen copy and repaired confirmed dead links, but kept the broad external crawl out of CI. Image optimization, older-image alt text, and browser tests in CI stayed on the follow-up list. There is a difference between ready to launch and nothing left to improve.

Then we rehearsed deployment on staging.montemagno.com without touching production. The hosted check passed all 2,172 legacy URLs, confirmed a 274-page search index, and checked HTTPS, redirects, real 404s, robots.txt, and the sitemap. The feed content-type warning stayed visible, and production DNS cutover remained a separate step for me to handle. With that rehearsal behind us, the site is now live at montemagno.com.

That second pass is what gave me the confidence to ship it. The first afternoon showed that we could build the site; the follow-up turned the rough edges into small, reviewed fixes and repeatable checks.

What it's like to work this way

Between the initial migration and that launch pass, a few things stood out about working with an agent in the terminal:

Plan mode is the killer feature. Research first, questions second, code third. The plan had dependencies, the agent tracked its own todo list, and when I changed my mind ("don't publish yet, I unpublished it") it kept deploys on a manual trigger while we were getting ready to launch.

It verifies its own work. It took Playwright screenshots of the home page, a code-heavy post, the tag page, the mobile drawer, and dark mode, looked at them, and fixed what was wrong (a squished avatar, a blurry upscaled dog, trailing whitespace from a render hook). It parsed the RSS output as XML to prove it was valid. It ran the URL verifier after every change.

It hits real-world walls and climbs them. Windows path restrictions on alias names, Hugo's new template layout system, the HTML minifier corrupting the XML feed served from an .html file, GitHub Pages being case-sensitive when my laptop is not. Each one showed up as a failure, got diagnosed, and got fixed with a small, explained change.

You still steer. I made every product decision: Hugo over Astro, no comments, RSS instead of a newsletter, keep the dog, "Follow" instead of "Newsletter & Feed" in the nav, weight the tag cloud toward what I write about now. The agent did the research that made those decisions easy and then executed them.

Writing a post now

After all that migration and launch work, this is the part I actually wanted: writing the next post without thinking about any of it.

hugo new posts/my-next-post.md

Fill in the front matter, write Markdown, drop images into static/content/images/2026/10/, and set draft: false when the post is ready. Push and open a pull request. Once the post is merged to main, CI builds the site, rebuilds the search index, runs the compatibility and launch checks, and deploys to montemagno.com. The Hugo build itself takes about six seconds.

---
title: "My Next Post"
slug: my-next-post
date: 2026-10-16T09:00:00-07:00
draft: false
description: "One or two sentences for search and social."
image: /content/images/2026/10/cover.png
tags: [github-copilot, dotnet]
---

If you have been putting off a migration like this because of the sheer volume of stuff that could break, I would encourage you to try the plan-first approach. Hand Copilot the export, point it at the live site, tell it what must not change, and let it build the safety net before it builds anything else. Then make time for that second question: what still needs to happen before I publish?

Explore GitHub Copilot

I used GitHub Copilot CLI for this project because I wanted to work in the terminal, but that is only one way to get started. You can explore GitHub Copilot in your editor, in the CLI, in the Copilot app, and on GitHub itself. Pick the place you are most comfortable working and give it a real task you have been putting off. It does not have to be thirteen years of blog posts!

If you are new to it, check out my First Steps with GitHub Copilot guide. And if you would rather follow along with a video, here is my new one:

Huge thanks to the Ghost team for nine great years. Here's to the next thirteen as plain markdown files.

]]>
5 Ways I Use Background Coding Agents Every Day https://montemagno.com/5-ways-i-use-background-coding-agents-every-day/ 68d814b64825cf00016b2e62 Wed, 01 Oct 2025 17:46:09 GMT

Coding agents are different from in-editor chat and agent modes for AI assisted programming. They run asynchronously—often in the cloud—tackling tasks while you focus locally. I rely heavily on the GitHub Copilot coding agent and its VS Code integration to spin up multiple parallel workflows. Below are five everyday patterns I use to offload work, speed up development, and keep my repos in top shape.


1. Assign Issues Directly to GitHub Copilot

When a new issue or idea lands, I immediately hand it off to Copilot so it can start coding while I tackle higher-level planning or work on any tasks. I have worked on huge issues, small bugs, and complete re-designs with this flow. What is great is that you can give it a lot of context in the issue to help guide it along. You can see everything directly in VS Code, stop it, ask it for changes, and even get a diff view without leaving the code editor.

GitHub issue #179, Skeleton Loading for Heavy Sections on Admin, with the Assign for Copilot button circled under Assignees

By assigning an issue to the coding agent:

  • Checks out the base branch (usually main) and creates a feature branch named after your issue.
  • It installs dependencies, runs any setup workflows, and begins implementing the requested feature or fix.
  • A pull request pops up automatically, complete with commit history, a description of what was done, and live session logs you can follow.
  • I can review diffs right in VS Code’s Agent sessions view or directly on GitHub before merging.

VS Code Chat Sessions view listing coding agent PRs, with one session's summary and its 11 changed files open

Key benefits:

  • Parallelize work: start multiple issues at once without context-switching overhead.
  • Instant feedback: see what Copilot built, ask follow-up questions in the same session.
  • Consistency: every branch follows your CI/CD and code-style rules from day one.

Easy setup:

Learn more: • GitHub Copilot Coding Assistant overview: https://docs.github.com/en/copilot/concepts/agents/coding-agent • VS Code integration with GitHub Pull Requests & Issues: https://code.visualstudio.com/docs/copilot/copilot-coding-agent (archived)


2. Delegate Ad-Hoc Tasks from VS Code Chats

Spotted an improvement in your code or UI? Instead of stopping to implement it, or open an issues, you can simply delegate from your chat panel or from GitHub.com and get back to flow. This is maybe one of my favorite ways to use coding agents as I can even spin up a

How it works:

  • In VS Code, open Chat Sessions (enable via Settings > Chat: Agent Sessions View Location and set to view).
  • Under
  • Type a request like “When user clicks Hacker News, auto-load articles in content-feed.” or whatever else you want it to do. You can even reference files and documents.
  • Click Delegate to coding agent send button —Copilot spins up a background session, generates code, and opens a pull request.
  • Monitor progress in the Chat Sessions view or jump into the PR on GitHub.

VS Code Chat Sessions view: the + button opens a new coding agent chat with a Delegate to @copilot prompt box

Why I love it:

  • Lightweight delegation and works of a specific branch that you choose.
  • Rich context: chat history and your codebase give Copilot full visibility.
  • Notification-driven: get pinged when the PR is ready, so you only interrupt flow once.

Tip: You can also trigger these tasks from the mobile apps, GitHub.com on your repo, or the Copilot dashboard at https://github.com/copilot/agents without hopping into each repo.

Agent tasks panel on GitHub.com with a prompt to auto-load Hacker News, plus repository and main branch pickers


3. Continue Work Across Branches and Chat Sessions

Sometimes I am working on a bug or new features and I have it half-implemented locally. Or sometimes I ideate in Ask mode and don't really want to open up an issue or work on it right now. Well, you can simply push your local changes and chat context into a new agent session so Copilot can finish while you move on.

VS Code chat after local edits, with the cursor on the Delegate to Coding Agent button beside the send button

Step by step:

  1. On your feature branch (or main), start a Copilot chat to outline next steps (e.g., add docs, refactor helpers).
  2. Make local edits but don’t commit everything.
  3. Click Delegate to coding agent, choose Include changes, and let Copilot fork a sibling branch with your pending code.
  4. Copilot completes the work—documentation, tests, cleanup—and issues a pull request ready for review.

Advantages:

  • Seamless handoff: your uncommitted work becomes the starting point for the agent.
  • Branch-of-branch workflow: keep your main branches tidy while Copilot wraps up.
  • No more “oh right, I forgot to finish that” moments—wake up to a completed PR.

4. Iterate on Pull Requests with Interactive Comments

When I work with the coding agents it isn't always going to get it right the first time. Sometimes it is the coding agent, sometimes it is because I didn't give it enough context, and sometimes I just ended up wanting it a bit different . I like to treat Copilot like a teammate and file a review comment and then watch it push fixes in real time.

Workflow:

  • Open a PR that Copilot has been working on in GitHub or in the chat sessions in VS Code and comment @copilot with error messages or changes you want.
  • Copilot launches a new agent session, applies fixes or enhancements, and updates the same branch.
  • You can reply to the agent’s comments, request tweaks, or approve and merge when satisfied.

PR comment asking @copilot to center a checkbox as a toggle, followed by Copilot's commit and reply saying it's fixed

Perks:

  • Instant turn-around on feedback.
  • Team-like collaboration: follow the same comment-reply flow you’d use with a colleague (or yourself in my case often).
  • Full history: every change is tracked in the PR conversation and commit log.

Docs to explore: • Using Copilot in pull requests: https://docs.github.com/en/copilot/how-tos/use-copilot-agents/cloud-agent/use-cloud-agent-on-github#asking-copilot-to-make-changes


5. Keep Dependencies Secure and Up-to-Date with Dependabot

Dependabot isn’t AI-driven, but it embodies the same “background agent” principle. It quietly opens PRs to bump libraries and fix vulnerabilities.

dotnet/aspire pull requests filtered to author:app/dependabot, showing merged Bump tmp and form-data dependency PRs

Setup highlights:

  • In your GitHub repos Settings > Security > Advanced Secuirty, enable Dependency graph, Dependabot alerts, and Dependabot security updates.
  • Create a .github/dependabot.yml to specify package ecosystems (npm, NuGet, etc.), directory paths, and update schedules.
  • Dependabot will open pull requests whenever a new version is available or a security issue is discovered.
  • Review, merge, or auto-merge based on your repo’s policies.

Why it matters:

  • You don’t have to remember every library you depend on—they get updated for you.
  • Security vulnerabilities get flagged and patched without manual intervention.
  • Your CI/CD stays green with minimal overhead.

Get started: https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically


Setting Up Your Environment for Background Coding Agents

Before you reap these benefits, configure both GitHub and your local IDE:

  1. Enable the Copilot Coding Agent
  2. Install VS Code Extensions
  3. Activate Chat & Agent Sessions
    • In VS Code Settings, search “Agent Sessions View Location” and switch from Disabled to View.
    • This adds the Chat Sessions icon for monitoring all local and active background tasks.
  4. Add Copilot Setup Workflows (optional but recommended)
  5. Configure Dependabot
    • Add a .github/dependabot.yml specifying ecosystems, directories, and schedules.
    • Enable security updates and auto-merge rules under Settings > Advanced Security.

With those pieces in place, you’ll have a fleet of coding agents— GitHub Copilot and Dependabot—working around the clock to keep your code moving forward.


How are you leveraging background agents in your workflow? Hit me on X or leave a comment on YouTube and let me know.

]]>
How to Convert a Portable Class Library to .NET Standard and Keep Git History https://montemagno.com/how-to-convert-a-pcl-library-to-net-standard-and-keep-git-history/ 5a78e84dda32ce0022cbd5fe Tue, 13 Feb 2018 22:00:00 GMT I have had a lot of developers telling me that they have wanted to switch to .NET Standard Libraries, but that they are worried about losing git history. I have also heard fears of compatibility with existing packages. Do not fear! It is so simple to switch over to .NET Standard and takes minutes!

Why .NET Standard 2.0

There is simply no reason not to be using .NET Standard 2.0 at this point (you can still target 1.4 if you need older UWP support, but just do 2.0!). It supports all of your favorite platforms including every single Xamarin platform, offers 20K+ more APIs than 1.X, and has a 70% API compatibility with NuGet packages today (it will also consume non-.NET Standard libraries in a compat mode). On top of that it is the future! It replaces PCL and Shared Projects (with multi-targeting, which I will save for another blog post) and will make your life as a cross-platform developer much more enjoyable.

Easy as 1,2,3,4 🐒

  1. Unload your PCL project (right click -> unload), and start editing it (right -> click edit)
  2. Delete Everything in the csproj and insert this:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <!--<PackageReference Include="" Version=""/>-->
  </ItemGroup>

</Project>
  1. Add back NuGets (simply open packages.config, and add the package references above, or via the NuGet package manager.
  2. Delete AssemblyInfo.cs (this is now in the csproj) and packages.config (also in csproj via PackageReference)

That is it! You are done! Welcome to the wonderful world of .NET Standard 2.0! If you had any reference to other projects you will want to add those back in!

So Easy Let Me Show You

That is right, it is so easy that I shot an entire 6 minute video where I do this in real time!

Learn More

To learn more about .NET Standard head over to the documentation and to follow along with my Git commits you can find the source code to the library I converted in the video over on my GitHub.

Additionally, Frank and I discussed .NET Standard on Merge Conflict:

]]>
One Month of Live Coding! https://montemagno.com/one-month-of-live-coding/ 598df8f2c121b60019bf2ffd Fri, 11 Sep 2015 22:16:50 GMT Just a little over a month ago I decided to start a brand new live video series called Motz Codes Live. The idea was to cover a random mobile topic that I was investigating or interested in or perhaps a cover from YOU that you wanted to see, and I have to say that so far things have gone AMAZING with a bunch of awesome developers showing up each week and thousands of video views already. 

Since the launch I have also started a Motz Codes Live GitHub repo where I upload all of my slides and any sample code during the live session.  In case you missed any of the episodes live don’t worry as they are all recorded and published immediately on my YouTube channel. Here is a recap of the last few episodes:

Pull to Refresh

Take a look live of implementing pull to refresh in Xamarin.Forms and also my new PullToRefreshLayout!

Integrating Mobile App Preferences and Settings

Let’s create a SQLite Database!

Be sure to subscribe to my youtube channel for all the latest updates

Be sure to follow my Google+ or Twitter account for new episodes.

]]>