Skip to content

JOURNAL // ARCHIVE_ENTRY

Rebuilding My Site with Drupal Canvas Headless, Astro, and MCP

I wanted to rebuild this site around Drupal Canvas, but I did not want Drupal rendering the public frontend. I wanted Canvas for visual page building and structured content, Astro for the frontend, and a completely static site deployed to Cloudflare Workers.

That sounds like two different architectures. Canvas is normally experienced inside Drupal, while a static Astro site is built somewhere else and has no Drupal runtime. The interesting part of this project was getting both sides to work together without reducing Canvas to a basic content API.

Canvas's best-kept secret

The key was Drupal Canvas Headless. I call it a secret because the submodule is both hidden and experimental, so it does not appear on the normal Extend screen. It arrived in Canvas 1.9 with the first-party Headless SDK, component synchronization from decoupled apps, a configuration UI for headless frontends, and support for rendering Canvas component trees outside Drupal.

This site is now running Canvas 1.10. That release added the Canvas-owned routed content endpoint at /canvas/content-api, which gives the frontend the component tree, route context, and document head for a Drupal path. The Canvas 1.10 release notes describe that endpoint and the related Headless SDK changes.

The distinction matters: 1.9 introduced the headless system, while 1.10 provided the routed endpoint this build uses. The feature is still experimental, so I expect its edges to keep moving, but it is already capable of supporting a real site.

Credit where it is due

I especially want to thank Bálint Kléri, the technical lead for JavaScript components in Drupal Canvas. Bálint has been working directly on Canvas Headless and gave me a ton of help while I put this site together. Because the feature is new and experimental, having someone explain what was intentional, what was still moving, and how the Drupal and frontend pieces were meant to connect saved me from a lot of dead ends. This implementation came together much faster because he shared his time and knowledge.

Starting with the design instead of Drupal markup

The original designs came from Google Stitch. I already had the visual direction and a component-shaped frontend, including the terminal-inspired typography, dark interface, cards, grids, timelines, and article layouts. I did not want to rebuild those screens in a Drupal theme and then build them again in Astro.

Instead, I treated the Astro component library as the design system. Each component needed a clear contract: a name, typed properties, slots for composed content, and a frontend implementation. Canvas Headless can synchronize that component metadata into Drupal, which means Canvas understands what editors can place even though Astro owns the final rendering.

This also forced some useful cleanup. Components that looked fine as one large design export had to be split into reusable pieces. Repeated content became slots or data-backed lists. Visual options became explicit properties instead of one-off markup. The same components now work in the Astro codebase, in Canvas page trees, and in the final static build.

Using Drupal MCP to assemble the site

Once Drupal knew about the component library, I used a restricted Drupal MCP server to build the Canvas pages. The server can inspect component schemas and slots, list pages, read the current page revision, create and update drafts, validate a component tree, and publish an approved revision. It cannot run arbitrary SQL, change Drupal configuration, or turn into a general-purpose remote shell.

That restriction is important. MCP is most useful here because it gives an agent a narrow, structured authoring surface. Instead of guessing at Drupal's state or clicking through every component in the Canvas UI, the agent can inspect the actual component definitions, compare them with the original design, and compose a valid page tree.

The workflow was roughly:

  1. Inspect the available components, properties, and slots.
  2. Read the current Canvas page and its revision.
  3. Translate the design into a parent-before-child component tree.
  4. Save the changes as a draft.
  5. Validate the tree before publishing it.

That is how the Home, About, Journal, and Projects pages were assembled. I also added Canvas content templates for individual Blog and Project nodes, so structured Drupal content uses the same component system instead of falling back to a separate Drupal presentation layer.

Making the listings real

One easy trap in a design-driven build is leaving sample cards in place. The first version of the journal section looked correct, but the cards were static component properties. A screenshot could pass review while the site quietly stopped reflecting new content.

I replaced those placeholders with data-backed listing components. The homepage now asks Drupal for the newest published Blog nodes during the Astro build. The Journal page builds from the real Blog collection, and the Projects page does the same for Project content. Titles, paths, dates, summaries, read times, images, and tags come from Drupal instead of being duplicated in a page layout.

Canvas still controls where the listing appears and how the surrounding page is composed. Drupal's structured content controls what appears inside the listing. That separation turned out to be much cleaner than manually adding a card to Canvas every time I publish something.

Static on purpose

The public site does not call Drupal when somebody visits a page. Astro talks to Drupal at build time, renders every known route, downloads the assets it needs, and writes static HTML, CSS, JavaScript, fonts, and media. Cloudflare Workers then serves those files without needing a live request back to the CMS.

Google Stitch -> Astro components -> Canvas pages
Drupal content + Canvas trees -> Astro build -> static files -> Cloudflare Workers

At build time, Astro fetches Canvas-managed routes through the Canvas content API and fetches Blog and Project collections through Drupal's content APIs. The build snapshots the responses and mirrors referenced assets locally. A validation step checks the generated routes and files before the deploy command sends the result to Cloudflare.

The tradeoff is intentional: publishing in Drupal does not instantly change the public site. When I want content changes to appear, I run the deployment command again. In return, the public frontend is fast, inexpensive to serve, resilient when Drupal is unavailable, and easy to roll forward as one complete artifact.

What I learned

  • Headless does not have to mean giving up visual page building. Canvas can remain the place where pages are composed while a separate framework renders them.
  • Component boundaries matter more than framework choice. Good properties and slots made the same design system useful to Astro, Canvas, and MCP.
  • Canvas pages and structured content solve different problems. Canvas handles composition; Blog and Project nodes handle repeatable content and queries.
  • MCP works best with a small permission surface. A purpose-built authoring server was enough to assemble and validate pages without exposing the rest of Drupal.
  • A static release step is a feature. It gives me an explicit moment when editorial changes become a deployable version of the site.

Where it landed

The finished architecture keeps each tool focused on what it does well. Drupal owns content modeling, revisions, editorial data, and Canvas page trees. Astro owns the frontend code and static rendering. Cloudflare owns delivery. Drupal MCP connects the design and authoring workflow without becoming part of the public runtime.

The result still feels like the site I designed, but it is no longer a set of static mockups or manually maintained cards. I can build a page visually in Canvas, manage Blogs and Projects as real Drupal content, and regenerate a fully static frontend whenever I am ready to publish the next version.