Agentic Quarto: Shipping a Personal Site with GitHub, Actions, and Grok Bot

quarto
r
How I run Data Science Bytes on Quarto + GitHub Actions + renv, publish the rendered site to Netlify from CI, and use a Cursor Pro Grok Bot team (plus a Voice of Javier skill) to draft and review posts without giving up human judgment.
Author
Published

September 14, 2026

The publishing stack was never the hard part

I have rebuilt this site more times than I like to admit. R Markdown plus blogdown, then distill, then Quarto. Each stack was a genuine upgrade. None of them fixed the thing that kept posts from going live.

The bottleneck was shipping. I could draft a post on a Sunday night, render it locally, and still leave it sitting in a folder because the last mile felt like chores: freeze files, listings, a preview image, a pull request, a deploy that I only half trusted.

This week I finally wired the last mile in a way I will actually use. Quarto and GitHub stay in front. Agents are optional, and they stay behind a human gate. If you only take one thing from this post, make it that order.

I am writing this as a practitioner who still publishes from a laptop. The setup is not a platform pitch. It is a shipping loop I can explain to a colleague over coffee.

Why Quarto still wins for a personal site

I recommend Quarto for a personal data science site because it lets me keep the writing next to the code. A post can be prose, a dplyr pipeline, a plot, and a bibliography without me switching tools. If you already think in .Rmd or Jupyter, the jump is small.

It is also honest about formats. The same project can be a website, a slide deck, a PDF, or a dashboard. I do not need a CMS for a site that is mostly essays and talks.

The other reason is community. The people who maintain it keep showing up. That matters more than a prettier theme.

TipKeep the site boring on purpose

I want the publishing machinery to be the least interesting part of the week. Quarto plus git plus a pull request is boring in the best way. Fancy editors are optional. The source of truth is still a folder of .qmd files.

If you are choosing a stack in 2026, I would start here even if you never touch an agent. Agents only help if the site is already simple enough to review.

The foundation: a git-backed Quarto blog, with CI as the publisher

The live site is Data Science Bytes. The source lives at JavOrraca/quarto-blog. That repo is the whole system: posts, _freeze/, theme, renv.lock, and the Actions workflow.

The pattern I would copy tomorrow is the one this repo uses:

  • Commit .qmd source and _freeze/
  • Gitignore /_site/
  • Pin Quarto 1.10.18 and R 4.6.1 in GitHub Actions; restore packages from renv.lock
  • On pull requests: render, check drafts, upload a _site artifact. Do not publish.
  • On main: the same render, then publish that already-rendered _site to Netlify
  • Stop Netlify’s own builds so production comes from Actions, not Netlify’s build bot

It matches the Quarto CI continuum (local execution, freeze, CI render and publish) and the Posit / Quarto Netlify guidance: commit _freeze/, gitignore _site/, publish from Actions.

The workflow uses quarto-dev/quarto-actions to install the pinned Quarto version and r-lib/actions to set up R. Packages come from renv.lock via r-lib/actions/setup-renv@v2. There is no hand-maintained install list in the YAML.

The workflow is named Quarto Render and Publish. On pull requests it renders, runs scripts/check-draft-posts.py against the rendered output, and uploads a _site artifact.

On a push to main, or workflow_dispatch from main, the same render and checks run, then quarto-dev/quarto-actions/publish@v2 publishes to Netlify with target: netlify and render: false. The token is NETLIFY_AUTH_TOKEN.

That last flag matters. scripts/inject-og-meta.py is a project post-render, so we publish the _site we already built, not a second render that would skip those tags.

_publish.yml points at the Netlify site for https://www.javierorracadeatcu.com. netlify.toml ignores git-triggered Netlify builds (ignore = "exit 0"). In the Netlify UI, Build status should be Stopped builds.

A shortened version of the job looks like this:

# .github/workflows/quarto-render.yml
name: Quarto Render and Publish

on:
  pull_request:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  render:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: quarto-dev/quarto-actions/setup@v2
        with:
          version: 1.10.18
      - uses: r-lib/actions/setup-r@v2
        with:
          r-version: "4.6.1"
      - uses: r-lib/actions/setup-renv@v2
      - run: quarto render
      # PRs stop after render + draft checks + artifact.
      # Publish only from main (push or workflow_dispatch).
      - uses: quarto-dev/quarto-actions/publish@v2
        if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
        with:
          target: netlify
          render: false
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}

The pull request is the human gate. Agents can open the PR. I still read the diff, check the preview image, and decide whether draft: true comes off. CI on the PR tells me the site renders. Merge to main is what publishes.

Posts inherit freeze: auto from posts/_metadata.yml, including drafts. _freeze/ is committed. Do not set freeze: false on a draft. Freeze only controls whether knitr re-runs, not whether HTML is written.

Local quarto::quarto_render(as_job = FALSE) is still how I refresh freeze when I draft by hand.

This site uses Quarto website.draft-mode: unlinked. A draft: true post still gets a live URL with Quarto’s Draft banner (#quarto-draft-alert) after CI publishes main. It stays off the Blog listing, search, and sitemap.

CI verifies that banner in the rendered _site. _site/ is gitignored.

What “agentic” means here: harness, orchestrator, tools

I do not think of this as “the AI writes my blog.” I think of it as three layers that I can name.

The harness is the thing that actually edits the repo. In my case that is Cursor Cloud Agents. They clone the repo onto an isolated VM, work on a branch, run commands, and open a pull request. They are the intern with a laptop.

The orchestrator is a small Grok Bot team. One coordinator routes the work. Specialists own lanes: drafting in my voice, reviewing the YAML and Quarto conventions, checking that CI renders and that main is what publishes, keeping agents out of files they should not touch. Bots can talk to each other. They can also kick a cloud agent the same way I would.

The tools are ordinary. GitHub. The Actions workflow. Skills. Repo guides. A handful of MCP connectors when a task needs email or a calendar, which a blog post usually does not.

That split comes straight out of how Grok Bot is designed. Matt Palmer’s writeup is the clearest version I have read: the bot is the outer loop that writes the prompt I would have written. The cloud agent is the inner loop that builds in a clean environment. Mixing those jobs in one window is how you get a draft that also tries to rewrite _quarto.yml.

Cursor Pro is about $20 a month, and that seat is what unlocked this for me. Grok Bot and Cloud Agents are included on paid Cursor plans. The weekly usage is not unlimited. The entry price is no longer an Ultra-only experiment.

TipSpecialists beat one mega-bot

A coordinator that can delegate is more useful than a single bot that tries to remember every rule. Keep the drafter focused on prose. Keep the reviewer focused on front matter, links, and the files that are off limits. Let the cloud agent own the branch and the PR.

The coordinator does not merge. The specialist does not deploy. The cloud agent does not remove draft: true unless I say so. Those are product decisions, not model settings.

Voice of Javier, and the Quarto-specific guide

The easiest way to get a bad draft is to ask a general model to “write like me” with no artifacts. My fix is a shared Voice of Javier skill. It is a portable playbook: first person, no emoji, no em dashes, short paragraphs, practitioner-first, warm closing, concrete claims instead of sincerity filler.

Cloud agents can use it because skills sync. Grok Bot can invoke it the same way.

That skill is about how it sounds. It is not about how this repo is laid out.

The Quarto specifics live in posts/CLAUDE.md. Folder names, YAML, inherited fields, the {target="_blank"} rule, known categories, the preview image line, the quality checklist. The root CLAUDE.md is even narrower: what agents may touch, what they must not touch, and the reminder that a full quarto render is not a casual command.

I like that split. Voice travels with me across projects. Repo guides stay next to the files they protect.

If you steal one idea, steal the protected-path list. _quarto.yml, _freeze/, _extensions/, and the top-level pages are easy for an eager agent to “helpfully” edit. _site/ is generated output. Agents should not commit it, and they should not hand-edit HTML.

Skills, memory, and repo guides are not the same drawer

I kept stuffing everything into one instructions file. That does not scale.

Skills are on-demand playbooks. Cursor discovers them from .cursor/skills/ or ~/.cursor/skills/, and the agent loads one when the task matches. Agent Skills are progressive on purpose. A “draft this post” skill can be long. It should not sit in every chat about a CSS tweak.

Memory is durable facts across sessions: my name, that this site is git-backed, that Actions publishes the rendered _site to Netlify, that _site/ is gitignored. It is a poor place for a 200-line style guide. Memory drifts. A skill is a file you can diff.

Repo guides are always-on project context. CLAUDE.md, posts/CLAUDE.md, an AGENTS.md if you want something tool-agnostic. These are the “do not touch _quarto.yml” rules. They belong in git so every cloud agent sees them, not only the bot that happened to remember.

My rule of thumb:

  • If every session on this repo needs it, put it in a repo guide.
  • If only some tasks need it, make a skill.
  • If it is a fact about me that should survive a new chat, that can be memory.
  • If it has to happen every time no matter what the model thinks, that is CI or a hook, not a paragraph of advice.

The Voice of Javier skill is the second bucket. The freeze policy is the first. “Javier prefers a Monday closing” can live in memory. “Render must pin 1.10.18” belongs in the workflow file, where a test can fail.

The lifecycle: idea, draft, PR, review, publish

Here is the loop I want, including on weeks I am tired.

  1. Idea. A URL, a talk, a note from SoCal RUG, or a sentence I cannot stop repeating. I send that to the coordinator, not to a blank editor.
  2. Draft. A specialist invokes the voice skill, reads posts/CLAUDE.md, and asks a cloud agent to create posts/YYYY-MM-DD-slug/index.qmd with draft: true. Freeze inherits auto. Preview image in the same folder. No inherited YAML fields duplicated.
  3. PR. The cloud agent opens a pull request. Actions renders, checks draft banners on the rendered _site, and uploads an artifact I can inspect. Nothing is live yet.
  4. Review. I read it like I would read a colleague’s draft. Does the H1 differ from the title? Do external links open in a new tab? Is the claim in my voice, or did the model get confident about a library I have not used? I leave comments. The agent can patch. I can also just edit the .qmd.
  5. Publish. I take draft: true off when I am ready, then I merge. Actions publishes the already-rendered _site to Netlify. Do not commit _site/.

The important part is which person is in which step. Agents are great at 2 and 3. They are useful in 4 if you treat them like a copy editor. Step 5 is mine.

I would rather ship a slightly late post than let an agent undraft something I have not sat with.

A real week: Actions, freeze, and a DuckDB refresh

This is the loop on a week I actually used it.

First the Actions workflow. Pin Quarto, restore R from renv.lock, render, check drafts, publish from main. Without that job, every agent-rendered site is a local snowflake.

Then the docs pass: when to render locally, when freeze is auto, why a draft should not set freeze: false. Agents need that in the repo, not in a chat I already closed.

Then the DuckDB work. I had a Quack draft that needed to catch ducklake at 1.5.5 and the DuckDB 2.0 story. Cloud agents refreshed the post, fixed freeze, and opened the PR.

You can read the result as One DuckDB, Many Analysts. The technical content is mine. The shipping muscle was the loop: undraft, merge, CI publishes.

Pitfalls I would budget for

A few of these bit me. The rest I have watched other people hit.

Credentials, the host, and freeze. Forget NETLIFY_AUTH_TOKEN and main renders a beautiful artifact that never leaves GitHub.

Leave Netlify on Active builds and a git-triggered build can blank production before Actions finishes. Stop Netlify builds, and keep ignore = "exit 0" in netlify.toml.

Forget to commit _freeze/ after knitr actually re-ran and CI will try to re-execute code it may not be ready for.

Freeze looks like a cache bug when it is a policy bug. freeze: true never re-knits on a project render. auto re-knits when source changes. Drafts inherit auto. Do not set freeze: false just because a post is a draft. If an agent “helpfully” copies freeze from a neighbor post, you will debug the wrong layer.

Agents love to edit the wrong files. Give them a short allow-list. Posts live under posts/. Site config is not a draft. _site/ is generated. Do not commit it.

Voice drift is real. Without the skill, I get emoji, em dashes, and a tone that sounds like a product blog. With the skill, I still have to read the H1 and the claims. A style guide is not a fact checker.

Context mixing. If the same agent both researches a URL and writes the .qmd, it will smuggle in leftover instructions. Outer loop for research and prompting. Inner loop for the branch. Human for merge.

Cost and attention. Pro at about $20 a month is the seat. Cloud agent tokens and Grok Bot weekly usage are extra if you lean on them all day. Start with one specialist and one coordinator, not a simulated department.

Drafts are not private, they are unlinked. This site uses Quarto draft-mode: unlinked. A draft can still have a live URL with a banner after main publishes. That is enough for a personal blog.

Who this is for

This is for people who already write, or want to write, in Quarto (or R Markdown) and are tired of the last mile.

It is a good fit if you:

  • Keep a personal site next to real analysis work, not as a separate WordPress hobby
  • Are comfortable with git, even if you are not a full-time platform engineer
  • Want agents as drafters and reviewers, not as the author of record
  • Care enough about voice that a shared skill is worth maintaining

It is a weaker fit if you need a CMS with non-technical editors, or if you want fully automatic publishing. I would not point a student at Grok Bot before they can render a Quarto project locally. The agents make a working loop faster. They do not replace knowing what a pull request is.

If you are an R user still on a 2019 blogdown site, start with Quarto and GitHub. Gitignore /_site/ from day one. Add CI that publishes from Actions before you add bots.

posit::conf(2026), and why I am watching Quarto 2

posit::conf(2026) runs September 14 through 16 in Houston, with a virtual track. Workshop day is today. If you are there, Charlotte Wickham and Mine Çetinkaya-Rundel’s Level up your Quarto session is the one I would have put on my calendar first: patterns, brands, Positron, and practical LLM use in a Quarto workflow.

I am also watching What’s next: Quarto 2 from Carlos Scheidegger. Quarto 2 is a Rust rewrite of the CLI, with a long-term bet on a collaborative editor and better performance. The April post is careful, and I appreciate that. Quarto 1 is not going anywhere for a while. There is no public 2.x I would pin a personal site to today.

My interest is selfish. A faster render and better editor diagnostics would make the CI loop nicer. Collaborative editing might matter more for workshop notes than for this blog. I would rather keep my source in .qmd files that a future CLI can still read.

Nothing in this post depends on Quarto 2. The shipping loop should survive a major version, the same way it survived blogdown to distill to Quarto 1.

A template if you want the outer loop

If you want to copy this shipping loop without starting from a blank repo, start from my Quarto Grok Bot public template. It is built for the same pattern: draft a post, open a PR via a coding agent, let Actions render, then publish the already-rendered _site to Netlify.

Gitignore /_site/. Keep freeze committed when knitr actually re-ran. Leave draft: true until a human undrafts. The template is polyglot-friendly: R and renv are optional, not required. It is Netlify-first, with room for an equivalent host if that is what you already run.

Learn More

If you want the outer-loop framing in more detail, Matt Palmer’s Grok Bot 101 is the piece I keep sending people.


This post was prepared by the Grok Bot setup I described: a coordinator, a voice skill, repo guides, and a Cursor cloud agent that opened the pull request. I still read every paragraph. The merge button stays mine.

If you try a version of this and you find a sharper split between skills and repo guides, I would like to hear it. The stack should stay boring. The posts should not.

Happy Monday, and happy coding!