Skip to content

Commit 29bccf3

Browse files
authored
Rework blog-post skill for the new blog home (#2133)
* Rework blog-post skill for the open-source-website blog home Posts are now authored in posit-dev/open-source-website: frontmatter is scaffolded from its blog archetype via hugo new (mirroring /new-post), docs links become absolute quarto.org URLs, thumbnails move to 1920x1080 (Typst templates rescaled and re-verified), and the old quarto.org scaffolding, categories, publish-date.ts, and backport sections are dropped. Release-post specifics move out of the general sections into references/post-types.md. * Read docs from the repo, link to quarto.org, flag pending-merge links Drafting ahead of a release means the docs may only exist on the prerelease branch, not yet on quarto.org. Direct the skill to source terminology from quarto-web (working tree, then origin/prerelease), derive absolute quarto.org URLs from repo paths, verify each link, and flag links that won't resolve until the release-time merge. * Add release-post prose guidance from the 1.10 draft run Document the summary-paragraph convention (one short noun phrase per major feature) and lean feature prose: cut why-this-matters clauses, scope claims precisely, cover only the headline change. * Document how to display literal shortcode syntax in posts Verified against the ported Quarto posts and scratch renders: displayed shortcode examples need a code context (never prose), a Quarto-side escape (triple braces or shortcodes="false" on the fence), and the site's escape-shortcodes Lua filter, which ported Quarto posts get via _metadata.yml and new posts opt into via frontmatter. Escapes in prose reach Hugo live - a known name silently renders, an unknown name fails the site build. * Advise on tags as the home for the old categories vocabulary The port moved each post's quarto.org categories into its tags, so tags carry that vocabulary forward (Releases, Quarto X.Y, Features, Learn, ...). Reuse existing corpus tags, skip a bare Quarto tag that would duplicate source/software, and give release posts Quarto X.Y + Releases.
1 parent 6979d02 commit 29bccf3

6 files changed

Lines changed: 260 additions & 125 deletions

File tree

.claude/skills/blog-post/SKILL.md

Lines changed: 166 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,90 @@
11
---
22
name: quarto-blog-post
3-
description: Use when writing, drafting, or editing blog posts for quarto.org, creating Quarto feature or release announcements, or reviewing blog post drafts for the Quarto website.
3+
description: Use when writing, drafting, or editing Quarto blog posts, creating Quarto feature or release announcements, or reviewing Quarto blog post drafts. Quarto posts publish on the Posit Open Source blog (posit-dev/open-source-website), not on quarto.org.
44
---
55

66
# Quarto Blog Post
77

8-
Write blog posts for `quarto.org/blog` matching the voice, structure, and conventions
9-
from 40+ existing posts.
8+
Write Quarto blog posts matching the voice, structure, and conventions of 40+
9+
prior posts. Since May 2026 the Quarto blog lives on the
10+
[Posit Open Source website](https://opensource.posit.co/blog/q/quarto/):
11+
posts are authored and published in the
12+
[`posit-dev/open-source-website`](https://github.com/posit-dev/open-source-website)
13+
repo, not in quarto-web. This skill carries the Quarto-specific knowledge
14+
(post types, voice, thumbnails); defer to that repo's own guides for its
15+
mechanics.
1016

1117
## Setup
1218

13-
1. `ls docs/blog/posts/` — browse existing posts for reference
14-
2. Read `docs/blog/posts/_metadata.yml` — inherited by all posts (Giscus comments,
15-
title-block-banner, left TOC, signup widget, `search: false`)
16-
3. If the post covers a Quarto feature, read the relevant docs page for accurate
17-
terminology and linking.
19+
1. Locate a local clone of `posit-dev/open-source-website` — conventionally a
20+
sibling of this repo (`../open-source-website`). Clone it if missing.
21+
Check `git status` before branching.
22+
2. Read, in its `content/blog/` directory:
23+
- `CLAUDE.md` — frontmatter schema and taxonomies
24+
- `_authoring-guide.md` — format choice, rendering, preview, and PR flow
25+
3. Browse `content/blog/ported/quarto/` for prior Quarto posts. Recent ones
26+
(e.g. `2026-03-24-1.9-release/`) show the current frontmatter and structure
27+
conventions.
28+
4. If the post covers a Quarto feature, read its docs from this repo — not
29+
from quarto.org — so drafting works even when the docs aren't live yet.
30+
Docs for an unreleased version live on the `prerelease` branch: check the
31+
working tree first, then `git fetch origin prerelease` and
32+
`git show origin/prerelease:docs/<path>`. quarto.org serves `main`, so
33+
anything prerelease-only won't be live until the release-time merge — see
34+
§ Links for how to link to it anyway.
1835

1936
## File Structure
2037

38+
In the open-source-website clone, on a branch named `blog/<slug>`:
39+
2140
```
22-
docs/blog/posts/YYYY-MM-DD-slug/
23-
index.qmd # The post (required)
24-
thumbnail.png # Listing card image (required)
41+
content/blog/<slug>/
42+
index.qmd # Source (required)
43+
index.md # Rendered output — always commit alongside the source
44+
thumbnail.png # Hero + listing card image, 1920×1080 (required)
2545
*.png, *.jpg # Additional images
26-
_contribs.md # Contributor list (release posts only)
2746
```
2847

29-
Directory name: `YYYY-MM-DD-slug` — date matches frontmatter, slug is short kebab-case.
48+
The folder name is the URL slug: short kebab-case, no date prefix
49+
(e.g. `quarto-1-10`). The published URL becomes `/blog/YYYY-MM-DD_<slug>/`
50+
from the frontmatter `date`. Never scaffold under `content/blog/ported/`
51+
that tree is for migrated legacy posts.
3052

3153
## Frontmatter
3254

33-
```yaml
34-
---
35-
title: "Post Title"
36-
description: |
37-
One to three sentences for listing cards and social sharing.
38-
author: Author Name
39-
date: "YYYY-MM-DD"
40-
categories:
41-
- Category1
42-
- Category2
43-
image: thumbnail.png
44-
image-alt: "Descriptive alt text for the thumbnail."
45-
---
46-
```
55+
Don't write the frontmatter from memory — scaffold it from the repo's Hugo
56+
archetype (`archetypes/blog.md`, the source of truth for the schema; see
57+
Workflow step 2) and fill it in. Field meanings and taxonomies are documented
58+
in open-source-website's `content/blog/CLAUDE.md`.
4759

48-
**title**: Short. Release posts: `"Quarto X.Y"`. Backtick code spans OK.
60+
Quarto-post specifics when filling in the scaffold:
4961

50-
**description**: Self-contained summary (makes sense without the title). Always `|` block scalar.
62+
**source: quarto** — required, and not in the archetype. Places the post on
63+
the [Quarto project listing](https://opensource.posit.co/blog/q/quarto/),
64+
where old quarto.org blog URLs redirect. Set `software: quarto` too.
5165

52-
**author**: Plain string for staff (`Charlotte Wickham`). Two authors: `Name and Name`.
53-
Guest authors use structured form with `name:` and optional `url:`.
66+
**date**: a future date schedules the post — Hugo hides it until a daily
67+
build (8 AM UTC) on the publish date, so set the date the PR is expected to
68+
merge (or later). No date-fixing script needed.
5469

55-
**date**: ISO 8601 `"YYYY-MM-DD"`. Must match directory name.
70+
**image / image-alt**: set `image: thumbnail.png`, 1920×1080 (16:9) — used as
71+
both the hero and the listing card. `image-alt` is mandatory.
72+
See `references/thumbnail-guide.md` for the decision between the two
73+
production paths (Typst, or HTML+SVG) and the HTML+SVG flow. For the Typst
74+
path, see `references/typst-thumbnail.md`.
5675

57-
**categories**: 2-3 per post. Use existing values — scan recent posts to check. Common:
58-
`Releases`, `Quarto X.Y`, `Features`, `Authoring`, `Learn`, `Workshop`, `Conference`,
59-
`Tip`, `Extensions`, `Tables`, `Teaching`, `Jupyter`.
76+
**people**: full names, individuals only — never a team name.
6077

61-
**image / image-alt**: `thumbnail.png` preferred. `image-alt` is mandatory.
62-
See `references/thumbnail-guide.md` for the decision between the two production
63-
paths (Typst, or HTML+SVG) and the HTML+SVG flow. For the Typst path, see
64-
`references/typst-thumbnail.md`.
78+
**tags**: freeform — this is where the old quarto.org `categories` vocabulary
79+
lives on (`Releases`, `Quarto X.Y`, `Features`, `Authoring`, `Learn`,
80+
`Workshop`, `Tip`, ...; the port moved each post's categories into its tags).
81+
Reuse tags already present in `content/blog/ported/quarto/` sources rather
82+
than coining synonyms, and skip a bare `Quarto` tag — `source` and `software`
83+
already cover it.
6584

66-
**Optional**: `lightbox: true` (many screenshots), `draft: true` (while developing).
67-
Do not use `subtitle:` (phased out after 2023).
85+
Never add `ported_from` or `port_status` (migration metadata), and don't carry
86+
over the old quarto.org schema: no `categories`, no `subtitle`, no
87+
`_metadata.yml` inheritance.
6888

6989
## Post Types
7090

@@ -103,8 +123,9 @@ Every image must have `fig-alt=` text — non-negotiable accessibility standard.
103123
![](screenshot.png){fig-alt="Description of what the screenshot shows."}
104124
```
105125

106-
Multi-image layouts use Quarto's layout system (`{layout-ncol="2"}`).
107-
For many images, add `{.lightbox group="name"}`.
126+
Multi-image layouts use Quarto's layout system (`{layout-ncol="2"}`) — a Lua
127+
filter in open-source-website converts these for Hugo. Don't use `.lightbox`;
128+
that's a quarto.org HTML feature with no equivalent in the Hugo pipeline.
108129

109130
### Code Blocks
110131

@@ -119,29 +140,101 @@ project:
119140

120141
### Links
121142

122-
Every feature mentioned links to its docs page. Pattern: explain briefly, show example,
123-
then link. Use site-root-relative paths: `[Feature](/docs/path.qmd)`.
143+
**Docs links must be absolute**: `https://quarto.org/docs/...`, never a
144+
`.qmd` source path or a site-root-relative path — the post no longer lives on
145+
quarto.org. Derive the URL from the repo path:
146+
`docs/<path>/<page>.qmd``https://quarto.org/docs/<path>/<page>.html`.
147+
Every feature mentioned links to its docs page. Pattern: explain briefly,
148+
show example, then link.
149+
150+
**Verify every quarto.org link resolves** (e.g.
151+
`curl -s -o /dev/null -w '%{http_code}' <url>`). A page whose docs exist only
152+
on the `prerelease` branch will 404 until the release-time
153+
`prerelease``main` merge. Keep the quarto.org URL anyway — never
154+
substitute the prerelease site's domain — and **flag every such
155+
pending-merge link when handing off the draft** so it's re-checked before the
156+
post publishes.
157+
158+
**Other blog posts**: link with the permalink pattern `/blog/YYYY-MM-DD_slug/`
159+
(see the authoring guide), never content-directory paths.
124160

125161
### Callouts
126162

127163
Use sparingly: `.callout-tip` for post origin context, `.callout-warning` for caveats,
128-
`.callout-note` for prerequisites. Release posts typically skip callouts.
164+
`.callout-note` for prerequisites. Quarto callouts render correctly through
165+
the Hugo pipeline.
129166

130167
### Shortcodes
131168

132-
- `{{< prerelease-callout X.Y type="blog" >}}` — pre-release banner (auto-disappears)
133169
- `{{< video URL >}}` — video embed
134170
- `{{< include file.md >}}` — include generated content
135171

172+
Both are processed by Quarto at render time and work in open-source-website.
173+
174+
**Displaying literal shortcode syntax** (showing readers what to type) takes
175+
three ingredients — this is how every such example in the ported Quarto posts
176+
works (verified empirically):
177+
178+
1. **Put the syntax in code** — a fenced block or inline code, never prose.
179+
An escape in prose reaches Hugo as a live shortcode: a name Hugo knows
180+
silently renders its output in place of your example; an unknown name
181+
fails the entire site build.
182+
2. **Escape it so Quarto doesn't execute it**: triple braces
183+
(`{{{< meta state >}}}`), or the `shortcodes="false"` attribute on a
184+
fenced block (`{.markdown shortcodes="false"}`). Unescaped syntax in code
185+
is swallowed at the Quarto stage, leaving empty code.
186+
3. **Opt into the site's escape filter** in the post frontmatter:
187+
188+
```yaml
189+
filters:
190+
- escape-shortcodes
191+
```
192+
193+
The filter (`content/_extensions/escape-shortcodes`) rewrites `{{<` to
194+
Hugo's comment escape in code contexts, so Hugo displays the syntax
195+
instead of executing it. Ported Quarto posts get it automatically via
196+
`content/blog/ported/quarto/_metadata.yml`; new posts must opt in.
197+
198+
In prose, refer to shortcodes by name instead ("the `video` shortcode").
199+
200+
**`{{< prerelease-callout >}}` does NOT exist there** — it's a quarto-web
201+
extension. For a post about an unreleased feature, write a static callout
202+
instead, and remove it once the version ships:
203+
204+
```markdown
205+
::: {.callout-note}
206+
This feature is available in the
207+
[pre-release version of Quarto](https://quarto.org/docs/download/prerelease.html)
208+
and will be part of the upcoming X.Y release.
209+
:::
210+
```
211+
136212
## Workflow
137213

138214
1. **Identify post type** → read `references/post-types.md` for that type
139-
2. **Create directory**: `docs/blog/posts/YYYY-MM-DD-slug/`
140-
3. **Write frontmatter** per schema above
141-
4. **Draft body** following type-specific structure
215+
2. **Branch and scaffold** in the open-source-website clone, off up-to-date
216+
`main`, mirroring its `/new-post` command:
217+
218+
```bash
219+
git checkout -b blog/<slug>
220+
hugo new blog/<slug>/index.md
221+
mv content/blog/<slug>/index.md content/blog/<slug>/index.qmd
222+
```
223+
224+
`hugo new` fills the frontmatter from `archetypes/blog.md`, so the schema
225+
stays in sync with the site. If `hugo` isn't installed, copy the
226+
frontmatter from `archetypes/blog.md` by hand instead — same source of
227+
truth. (In a Claude session started in that repo, the `/new-post` command
228+
runs this flow interactively.)
229+
3. **Fill in the frontmatter**: replace the placeholder `title`, `people`,
230+
and `description`; trim `topics`, `languages`, and `tags` to what applies;
231+
delete unused optional fields; and set the Quarto specifics above
232+
(`source: quarto`, `image`/`image-alt`, `date`)
233+
4. **Draft body** following type-specific structure. Body headings start at
234+
`##` — the H1 comes from `title`.
142235
5. **Add images** with `fig-alt=` on every one
143-
6. **Link to docs** for every feature mentioned
144-
7. **Create thumbnail**:
236+
6. **Link to docs** with absolute quarto.org URLs for every feature mentioned
237+
7. **Create thumbnail** (1920×1080):
145238
1. Read `references/thumbnail-guide.md` § Choosing your path
146239
2. **Always present both options as a question to the user**, even if one
147240
path is the obvious recommendation. State the recommendation with
@@ -151,21 +244,27 @@ Use sparingly: `.callout-tip` for post origin context, `.callout-warning` for ca
151244
3. Read the chosen path's reference and execute
152245
(`references/typst-thumbnail.md` for Typst, rest of `thumbnail-guide.md`
153246
for HTML+SVG)
154-
8. **Review**: direct opener? code blocks fenced with language? all images alt-texted?
155-
docs links present? closing matches type convention? categories correct?
247+
8. **Render**: from the post directory, `quarto render index.qmd` — this
248+
produces the `index.md` Hugo builds from. Commit `index.qmd`, `index.md`,
249+
and any generated outputs together.
250+
9. **Validate**: from the open-source-website root:
251+
252+
```bash
253+
uv run scripts/validate-blog-posts.py content/blog/<slug>/index.md
254+
```
255+
256+
In a Claude session started in that repo, the `/check-post` and
257+
`/review-post` commands run validation and a content review interactively.
258+
10. **Review**: direct opener? code blocks fenced with language? all images
259+
alt-texted? absolute docs links, each verified live or flagged as
260+
pending-merge? closing matches type convention? frontmatter complete
261+
(`people`, `topics`, `source: quarto`)?
156262

157263
## Publishing
158264

159-
When ready to publish, set the post date to today and rename the directory:
160-
161-
```bash
162-
quarto run _tools/publish-date.ts docs/blog/posts/YYYY-MM-DD-slug
163-
```
164-
165-
This updates `date:` in frontmatter and renames the directory to match. Run it
166-
on the day you intend to merge — avoids manual date edits if the publish date slips.
167-
168-
## PR Workflow
169-
170-
PR to `main` first. On merge, auto-backport creates cherry-pick PR to `prerelease`.
171-
Push branches to `upstream` remote (quarto-dev/quarto-web), not `origin`.
265+
Open a PR against `main` in `posit-dev/open-source-website` — never push
266+
directly to `main`. Posit org members push branches to the repo itself (no
267+
fork needed); a Netlify preview is posted on the PR automatically, and merging
268+
requires one approving review from someone with Write access. To schedule a
269+
post, set a future `date` and merge — it goes live on that date's morning
270+
build. Details: `content/blog/_authoring-guide.md` § Publishing your post.

.claude/skills/blog-post/assets/thumbnail-diagram.typ

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
#import "@preview/cetz:0.3.4"
22

33
#set page(
4-
width: 1200pt,
5-
height: 630pt,
6-
margin: 50pt,
4+
width: 1920pt,
5+
height: 1080pt,
6+
margin: 80pt,
77
fill: rgb("#447099"),
88
)
99

@@ -15,17 +15,17 @@
1515
#align(center)[
1616
#stack(
1717
dir: ltr,
18-
spacing: 16pt,
19-
align(horizon)[#image("quarto-logo-trademark-light.svg", height: 90pt)],
20-
align(horizon)[#text(baseline: -8pt, size: 90pt, weight: "bold")[2]],
18+
spacing: 26pt,
19+
align(horizon)[#image("quarto-logo-trademark-light.svg", height: 144pt)],
20+
align(horizon)[#text(baseline: -13pt, size: 144pt, weight: "bold")[2]],
2121
)
2222

23-
#v(10pt)
24-
#text(size: 56pt, weight: "bold")[Replace this title]
23+
#v(16pt)
24+
#text(size: 90pt, weight: "bold")[Replace this title]
2525

26-
#v(10pt)
26+
#v(16pt)
2727

28-
#cetz.canvas(length: 45pt, {
28+
#cetz.canvas(length: 72pt, {
2929
import cetz.draw: *
3030

3131
// Ellipse node — white fill, dark blue label.
@@ -38,12 +38,12 @@
3838
stroke: white,
3939
name: name,
4040
)
41-
content((x, y), text(fill: rgb("#1f3a52"), weight: "bold", size: 20pt)[#label])
41+
content((x, y), text(fill: rgb("#1f3a52"), weight: "bold", size: 32pt)[#label])
4242
}
4343

4444
// Edge — white line connecting two named nodes.
4545
let edge(a, b) = {
46-
line(a, b, stroke: (paint: white, thickness: 1.5pt))
46+
line(a, b, stroke: (paint: white, thickness: 2.5pt))
4747
}
4848

4949
// Replace the placeholder graph below with your own nodes + edges.
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#set page(
2-
width: 1200pt,
3-
height: 630pt,
4-
margin: 50pt,
2+
width: 1920pt,
3+
height: 1080pt,
4+
margin: 80pt,
55
fill: rgb("#447099"),
66
)
77

@@ -13,14 +13,14 @@
1313
#align(center + horizon)[
1414
#stack(
1515
dir: ltr,
16-
spacing: 16pt,
17-
align(horizon)[#image("quarto-logo-trademark-light.svg", height: 90pt)],
18-
align(horizon)[#text(baseline: -8pt, size: 90pt, weight: "bold")[2]],
16+
spacing: 26pt,
17+
align(horizon)[#image("quarto-logo-trademark-light.svg", height: 144pt)],
18+
align(horizon)[#text(baseline: -13pt, size: 144pt, weight: "bold")[2]],
1919
)
2020

21-
#v(20pt)
22-
#text(size: 56pt, weight: "bold")[Replace this title]
21+
#v(32pt)
22+
#text(size: 90pt, weight: "bold")[Replace this title]
2323

24-
#v(10pt)
25-
#text(size: 32pt)[Replace this subtitle (optional)]
24+
#v(16pt)
25+
#text(size: 52pt)[Replace this subtitle (optional)]
2626
]

0 commit comments

Comments
 (0)