Write-up format
This is an internal document, published openly. When every post invents its own shape, readers have to re-learn the structure each time — and, more importantly, it becomes easy to skip the redaction pass.
New posts live at src/content/articles/<lang>/<slug>.md. The Korean and English versions must share the same slug or the language switcher has nothing to point at.
Frontmatter
The schema lives in src/content.config.ts and is .strict(). An undefined key fails the build immediately. That is deliberate: a typo’d key that gets silently ignored is worse than a broken build.
The example below is fictional, for shape only. The values are not a real report.
---
title: "Internal API prefix bypassed gateway authentication"
description: "The router special-cased a single public prefix, so a similarly named internal namespace was proxied without authentication. Reproduction and impact, as of the patched build."
pubDate: 2026-09-19
updatedDate: 2026-09-22
lang: en
tags: ["web", "authorization", "code-audit"]
author: chwrld
severity: high
target: "Example Gateway 1.4.2"
cve: ["CVE-2026-00000"]
bounty: "$750"
draft: false
---
Field by field:
| Field | Required | Rules |
|---|---|---|
title | yes | Used for the card and the <title>. Name the flaw, not the vendor |
description | yes | Meta description, RSS summary and card excerpt in one. A single paragraph, no line breaks. One or two sentences |
pubDate | yes | 2026-09-19 — unquoted, so YAML parses it as a date |
updatedDate | no | Only for meaningful revisions. Typo fixes do not count |
lang | yes | ko or en, and it must match the directory the file sits in |
tags | no | Lowercase kebab-case. These become URLs (/en/articles/tags/web/) |
author | no | Defaults to chwrld. Match a handle in src/data/team.ts |
draft | no | true keeps it out of the production index, RSS and sitemap. The dev server shows it with a DRAFT badge |
severity | no | critical / high / medium / low / info. Omit on non-vulnerability posts |
target | no | Product and version. A target without a version is meaningless six months later |
cve | no | An array, and only identifiers actually assigned. Requested-but-unassigned does not go here |
bounty | no | Exactly as the vendor stated it. Do not estimate or convert currency |
heroImage | no | Absolute path from public/ (/og/gateway.png) |
Do not add a slug key. The filename is the only source of a slug.
draft versus the _ prefix
Both hide a post, for different reasons.
draft: true— the page is still built; it is only absent from listings. Anyone with the URL can read it, which makes it the right tool for sharing a review link.- Naming the file
_wip-foo.md— it is never loaded into the collection at all, and does not have to satisfy the schema. Use this for drafts that do not even have frontmatter yet.
A draft containing details of an unpatched issue belongs behind the _ prefix, not behind draft: true. Drafts are reachable by URL.
Body
The layout renders the <h1> from title. Body content starts at ##.
Code blocks
Always tag the language. Shiki needs it, and without it you get an undifferentiated grey block.
```http
GET /internalapi/v1/namespaces/default/config HTTP/1.1
Host: gateway.example
Authorization: REDACTED
```
Use http for requests and responses, bash for shell, yaml / json for config. For patch diffs and decompiler output, diff gets you coloured + and - lines.
Trim long output. Rather than pasting a 200-line stack trace, keep the frames that matter and mark the cut with [...]. Trimming without marking it leaves whoever reproduces the bug wondering what they are missing.
Images
- Store them under
public/articles/<slug>/and reference them as/articles/<slug>/foo.png. - Alt text is mandatory. Describe what the reader is supposed to notice, not the word “screenshot”.
- Prefer a code block over a screenshot wherever possible. Text is searchable, copyable and diffable; a PNG is none of those.
- When a screenshot is genuinely necessary, capture the region, not the whole browser window. Full windows come with other tab titles, bookmarks and notifications attached.
Tables
Tables are for comparison — behaviour across versions, responses across parameters — not for procedures. Numbered lists read better for steps. Keep it to four columns; beyond that it scrolls sideways on a phone.
Quotes
Cite the source when quoting an advisory or a commit message.
> The quoted text.
> <cite>Vendor advisory SA-2026-001</cite>
Pre-publication checklist
This list is checked by someone other than the author. Finding your own mistakes in your own prose does not work reliably.
Credentials and tokens
- No live session cookies, bearer tokens or API keys in the prose, code blocks or screenshots
- Anything removed is replaced with
REDACTEDor an obvious dummy (eyJhbG...TRUNCATED) - Nothing is only partially masked — a JWT with the first eight characters covered is still a working token
- No passwords for accounts used during testing
- Vendor-internal hostnames and IPs are confirmed harmless to publish
Third-party data
- No other user’s email, phone number, name or order reference in the example responses
- Nothing personal in screenshot margins, tab titles or autocomplete dropdowns
- Internal identifiers (UUIDs, account IDs) do not resolve to a real user
Payload scope
- The proof of concept stops at demonstrating the flaw exists
- It is not a finished script that takes over an account or destroys data when run as-is
- No automation is attached — no mass scanners, no account enumeration tooling
Clearance
- The vendor has shipped a patch
- The vendor agreed to publication, or an advisory is already out
- No program or platform terms restrict disclosure
- No still-unpatched variant from the same codebase has leaked into the text
That last item is the genuinely dangerous one. A second flaw found during variant analysis has a way of ending up inside the first post’s casual sentence about how “this pattern appears elsewhere too”. Unpatched variants stay unmentioned until their own report closes.
The reasoning behind clearance decisions is in the responsible disclosure policy.