Skip to main content

Content Register

The content register (content-inventory.csv) is the lifecycle and identity source of truth for id86.net content. Markdown is the pre-publication content source; after publication, WordPress Gutenberg is the live-content source of truth. The register records what exists, its status, when it publishes, and where its original source lives.

Location & Folder Layout

The register lives in the local control/recap mirror, separate from the canonical repo and from this guide:

/home/rezriz/github/seo-content/web-project/id86.net/
├── content/ # actual web content (Markdown), grouped by category folder
├── content-inventory.csv # control register — this document's subject
├── keyword-research.csv # keyword opportunity register (Keyword ID)
├── schema-seo/ # optional PID-identified JSON-LD schema overrides
├── neuronwriter/ # briefs and draft evaluations
├── research/ # one human/AI research directory per content PID
├── scripts/ # register tooling (PID assignment, validation)
└── README.md # mirror-local operating notes
PathPurpose
content/Cheatsheets, references, checklists, glossaries, and premium types (prompts, snippets, patterns, templates, runbooks) in Markdown. First folder = WordPress category
content-inventory.csvOne row per document: status, category, tags, target keyword, priority, source path
keyword-research.csvKeyword opportunities with Keyword ID; selected rows link through Target Post PID
schema-seo/Optional PID-identified JSON-LD files linked by the register's Schema Path column
neuronwriter/NeuronWriter Markdown briefs and JSON evaluations linked by the register's NeuronWriter columns
research/Human or AI research synthesis, one directory per content PID, linked by Research Path
scripts/assign-pids.pyAuto-assigns PID values (never by hand)
scripts/validate-inventory.pyValidates the register against Markdown source; refreshes auto columns with --fix

The canonical content repository is github.com/donnyaw/ID86-instant-documentation; this folder is the local control mirror. One row per document, one status per row, and nothing hand-typed that is derivable from the Markdown source.

Keyword Research Register

keyword-research.csv is an opportunity register, not a content-PID register. Its Keyword ID values (KW-######) identify keyword opportunities. Target Post PID is empty until an opportunity is selected for a content idea, then links that keyword to the PID owned by content-inventory.csv. Import any compatible export with scripts/import-keywords.py; it normalizes common headers, assigns stable keyword IDs, and skips duplicate keyword/category/topic combinations. Run scripts/assign-keyword-ids.py for missing IDs and scripts/validate-keyword-research.py to verify exact keywords, search intent, content types, and links; scripts/assign-pids.py never processes this file.

Column Reference

ColumnTypeMeaning / Rules
No.autoSequential row number (1, 2, 3, …). Auto-generated with VisiData rn, never hand-typed
PIDautoPrimary identifier, format <PREFIX>-NNNNNN. Per-category counter, zero-padded to 6 digits. See PID Rules
StatusenumLifecycle status: idea / draft / scheduled / published. See Status Lifecycle
Publish DatedateGo-live date (YYYY-MM-DD, date only). Required for scheduled rows; when the date arrives the publish script sends the post to WordPress and flips Status to published
TitlemanualPost title; becomes the WordPress H1 and acts as the main target keyword. Entered by a human or AI-generated
DescriptionmanualSEO meta description (WordPress/SEOPress). Concise summary of the page, ~150–160 characters. See Description Usage
CategoryenumSingle select: code / devops / ai / security (main); demo is hidden system. See Categories
TagsmultiComma-separated WordPress tags; maximum 5 per post. See Tags Usage
Content TypeenumSingle select: cheatsheet / reference / checklist / glossary (free) + prompt / snippet / pattern / template / runbook (premium). See Content Types
Target KeywordtextSearch query this page targets
PriorityP1–P4Opportunity tier after keyword research: P1 = highest opportunity, publish first; P4 = lowest. Empty until keyword research is done
Source PathpathPath of the Markdown source, relative to content/. See Source Path Usage
UpdatedautoLast-modified date of the source file. Refreshed by validate-inventory.py from the file timestamp
Schema Pathpath, optionalRelative path to a PID-identified manual JSON-LD file in schema-seo/. Leave empty for generated or SEOPress-managed schema
NeuronWriter Query IDtext, optionalNeuronWriter analysis identity for this document
NeuronWriter Brief Pathpath, optionalRelative path to the Markdown brief under neuronwriter/briefs/
NeuronWriter Statusenum, optionalEmpty / not_started / brief_ready / evaluated / approved / failed
Research Pathpath, optionalRelative PID directory under research/, for example research/DEV-000008/

Description Usage

The Description column is the page's SEO meta description. It is copied to the Markdown frontmatter description field and synchronized to WordPress/SEOPress, where it becomes the post's <meta name="description"> automatically — see SEO Metadata Automation.

  • Write one concise summary of the answer or task the page provides.
  • Include the target keyword naturally; never keyword-stuff.
  • Write it manually or generate with AI, then review by hand.
  • Aim for approximately 150–160 characters; clarity matters more than exact length.
  • Keep the CSV value and Markdown frontmatter value identical.
  • Update it whenever the page's focus or target keyword changes.

Example:

Follow this practical SEO checklist to find, evaluate, and map keywords to useful content.

Tags Usage

The Tags column is multi-select — comma-separated WordPress tags. Limit is 5 tags maximum per post.

  • Comma-separated, no # prefix, lowercase by convention.
  • Pick tags that describe the post's topic and format (e.g. cheatsheet, git, version-control).
  • Keep them consistent with the Markdown frontmatter tags list — the validate script enforces the match.
  • Write them manually or generate with AI, then review by hand.

Source Path Usage

The Source Path column holds the location of the Markdown source file, relative to the content/ folder. It is the canonical link between the register and the actual document.

  • Always relative to content/ — for example content/devops/DEV-000003-docker-volumes-cheat-sheet.md.
  • New documents use the PID-title.md naming convention (see New Document Workflow).
  • The first path segment must match the Category column (the validate script enforces this).
  • Must exist — the validate script fails if the file is missing.
  • Is never derivable from another column; it is the record's address.

Status Lifecycle

Status is one of exactly four values:

StatusMeaning
ideaCaptured concept, not written
draftBeing written / edited, not final
scheduled.md is final, with a Publish Date set. When that date arrives, the publish script auto-sends it to WordPress and flips the row to published
publishedLive on the site (maps to WordPress status published)

Decision note: draft was deliberately chosen over in progress — it is the WordPress-native term and covers both "being written" and "paused, unfinished".

The lifecycle drives two scripts in wp-ops (01-operations/content-management/):

ScriptCSV StatusEffect
id86-publish-scheduled.shscheduledSends the Markdown to WordPress directly (via WP-CLI bridge) when Publish Date <= today, then flips the row to published
id86-push-published.shpublishedArchives or distributes the Markdown record; it must not overwrite the published Gutenberg post

Content Types

Content Type is one of exactly nine values across two tiers — free (cheatsheet / reference / checklist / glossary) and premium (prompt / snippet / pattern / template / runbook). Premium types are gated by SureMembers and are the monetization surface; free types remain public and indexable. The full definitions, best-fit categories, examples, and the mental model for choosing a type live on their own page:

Content Types →

Categories

Category is single select. The main categories are the site's content lanes, published and shown on the homepage:

CategoryTierHomepage
codeMainShown
devopsMainShown
aiMainShown
securityMainShown

demo is a hidden system category for styling verification fixtures only.

Note on seo vs security: the register and the live site currently hold a seo category (SEO-000001), while the content model lists security as a main lane. Migrating the live seo category/post to security (or vice versa) is a separate follow-up; this page documents the register as it exists.

PID Rules

The PID is the primary identifier, format <PREFIX>-NNNNNN. Uniqueness is the goal, so a per-category counter is used instead of an embedded date.

CategoryPrefixExample
aiAI-AI-000001
codeCOD-COD-000001
devopsDEV-DEV-000001
securitySEC-SEC-000001
demoDEM-DEM-000001
  • Zero-padded 6-digit per-category counter (000001–999999).
  • The next value is max(existing in that category) + 1, so renumbering or deleting rows never causes reuse.
  • Generated by scripts/assign-pids.py, never by hand:
python3 scripts/assign-pids.py # fill empty PIDs
python3 scripts/assign-pids.py --fix-existing # re-key PIDs whose prefix mismatches Category
python3 scripts/assign-pids.py --dry-run # preview without writing
  • Stored on the WordPress post as _id86_doc_id; post matching uses _id86_source_path, so re-keying PIDs is safe.
  • The PID is identity only — date information lives in the Publish Date and Updated columns, not in the ID.

New Document Workflow (scaffold)

scripts/new-doc.py is the single entry point for creating a new document. One command generates both halves of the contract: the Markdown file (named PID-title.md) with complete frontmatter, and the matching CSV row. It then runs assign-pids.py + validate-inventory.py so the row and file are verified consistent before you start authoring.

File naming convention

New documents use the PID-title.md pattern inside the category folder:

content/<category>/<PID>-<slug>.md

Examples:

content/code/COD-000003-docker-volumes-cheat-sheet.md
content/devops/DEV-000004-tmux-split-panes-cheat-sheet.md
content/security/SEC-000001-ssh-hardening.md
  • PID is the next free per-category counter (e.g. DEV-000003 after DEV-000002), assigned by the script using the same logic as assign-pids.py.
  • slug is derived from the title (kebab-case, lowercased), e.g. "Docker Volumes Cheat Sheet" → docker-volumes-cheat-sheet.
  • Duplicate slugs are auto-suffixed (-2, -3, …) so a title collision never overwrites an existing file.

What the scaffold creates

1. The Markdown file content/<category>/<PID>-<slug>.md:

---
id: DEV-000003
slug: tmux-split-panes-cheat-sheet
title: Tmux Split Panes Cheat Sheet
description: Split, resize, and manage panes in tmux.
category: devops
tags: [tmux, terminal]
content_type: cheatsheet
status: draft
---

## Overview

Author this document: state what the page answers and when to use it.

## Topic

Fill in the body per the article body contract (`03-content/article-body-elements`).

The body is a skeleton only — author the real content yourself (with AI) after scaffolding. The frontmatter is fully populated and publish-ready (minus the body).

2. The CSV row appended to content-inventory.csv, with PID, No., Source Path, and Updated already filled:

8,DEV-000003,draft,,Tmux Split Panes Cheat Sheet,"Split, resize, and manage panes in tmux.",devops,"tmux, terminal",cheatsheet,tmux split pane,,content/devops/DEV-000003-tmux-split-panes-cheat-sheet.md,2026-08-08,,,,

3. A verification pass — the script runs assign-pids.py (skips the pre-filled PID) and validate-inventory.py (confirms file exists, category matches, frontmatter matches the CSV row, and the frontmatter content_type matches the CSV Content Type).

The slug: frontmatter field

Because the file is named PID-title.md, the frontmatter carries an explicit slug: field. This is required so WordPress URLs stay clean: sync.py falls back to path.stem when slug: is absent, which would produce /devops/DEV-000003-tmux-split-panes-cheat-sheet/. With slug: set, the URL stays /devops/tmux-split-panes-cheat-sheet/.

Usage (flags)

cd /home/rezriz/github/seo-content/web-project/id86.net

# Draft cheatsheet (default status)
python3 scripts/new-doc.py --title "Docker Volumes Cheat Sheet" --category devops \
--type cheatsheet --status draft \
--description "Manage named volumes, bind mounts, and persistence." \
--tags "docker, volumes, compose" --keyword "docker volumes"

# Scheduled document (requires publish date)
python3 scripts/new-doc.py --title "SSH Hardening Guide" --category security \
--type reference --status scheduled --publish-date 2026-09-01 \
--description "Harden SSH: key auth, fail2ban, and port lockdown." \
--tags "ssh, security" --keyword "ssh hardening"

# Preview without writing anything
python3 scripts/new-doc.py --title "Docker Volumes Cheat Sheet" --category devops \
--type cheatsheet --dry-run

Usage (interactive)

Run without --title and the script prompts for each missing field:

python3 scripts/new-doc.py
Title: Docker Volumes Cheat Sheet
Category (code/devops/ai/security/demo): devops
Content Type (cheatsheet/reference/checklist/glossary/prompt/snippet/pattern/template/runbook): cheatsheet
Description (SEO meta): Manage named volumes, bind mounts, and persistence.
Tags (comma-separated): docker, volumes, compose

CLI reference

FlagRequiredValues / Notes
--titleyes (or interactive)Post title; becomes the slug and the WordPress H1
--categoryyes (or interactive)code / devops / ai / security / demo
--typeyes (or interactive)cheatsheet / reference / checklist / glossary / prompt / snippet / pattern / template / runbook
--statusnoidea / draft (default) / scheduled. scheduled requires --publish-date
--publish-datewhen scheduledYYYY-MM-DD; written to frontmatter date: and CSV Publish Date
--descriptionnoSEO meta description; copied to frontmatter and CSV. Keep CSV + frontmatter identical
--tagsnoComma-separated, max 5; lowercase. Written to frontmatter list and CSV
--keywordnoTarget keyword → CSV Target Keyword column
--dry-runnoPreview frontmatter + CSV row, write nothing

What the scaffold fills vs what you fill

Auto-filled by the scaffoldYou provide / author
PID (next per-category counter)Title
No. (next row number)Description (SEO meta)
Updated (today)Tags (max 5)
Source Path (content/<category>/<PID>-<slug>.md)Target Keyword (optional)
slug: frontmatter (keeps URLs clean)Body content (skeleton provided)
File name PID-title.md

Behavior notes

  • The script never overwrites an existing file — it errors if the target already exists.
  • The PID is written into the CSV row, so assign-pids.py leaves it untouched (it only fills empty PIDs).
  • validate-inventory.py passes immediately because title/description/category/tags are written identically to both the frontmatter and the CSV row.
  • If you scaffold with an empty --description or --tags, the script warns that sync.py requires both non-empty — fill them before publishing.
  • For --status scheduled, frontmatter status becomes publish and date: is set to the publish date; the row keeps CSV Status = scheduled until the publish script sends it.

Auto Columns & Validation Tooling

The No. column is auto-numbered in VisiData with the rn command (adds a leftmost no column, 1-based, contiguous), then renamed to No. with ^. Never hand-type row numbers.

The Updated column is maintained by scripts/validate-inventory.py, never by hand. Run it before committing to catch drift:

python3 scripts/validate-inventory.py # report
python3 scripts/validate-inventory.py --fix # refresh auto columns

An hourly systemd user timer (id86-inventory-refresh) runs --fix automatically, so Updated refreshes from file save-times without manual steps:

systemctl --user status id86-inventory-refresh.timer
systemctl --user list-timers id86-inventory-refresh.timer

The validator checks, per row: the source file exists, category matches the first path segment, frontmatter (title, description, category, tags) matches the CSV, and Updated is current.

If Schema Path is populated, it also checks that the JSON-LD file exists inside the register directory and contains valid JSON. The register currently records the relationship; the content sync pipeline does not yet inject these files into WordPress.

For NeuronWriter, the validator checks the allowed status, requires a query ID for an active workflow, and verifies that a populated brief path exists under neuronwriter/briefs/. Scores, timestamps, project IDs, and full API responses stay in the NeuronWriter artifacts rather than this CSV.

Research Path and PID Ownership

content-inventory.csv is the only register that assigns content PIDs. Keyword research uses Keyword ID values such as KW-000001; its Target Post PID remains empty until a keyword is selected for a content idea. When an idea is created with scripts/new-idea.py, Python assigns the content PID, adds the idea row, and creates research/<PID>/research.md, serp-analysis.md, brief.md, and sources/ in one operation. Supplying --keyword-id also updates the selected keyword row and rejects mismatched or already-linked keywords.

The research file may be written by a human, AI, or both. It is the synthesis layer between keyword/NeuronWriter data and the content draft. See Research Workflow.

Structured Data Usage

Use schema-seo/<PID>.json for manually authored schema that cannot be derived reliably from the Markdown or supplied by SEOPress. The filename must be exactly <PID>.json; put the content name and SEO keywords inside the JSON. Do not copy ordinary page metadata or generated glossary schema into these files.

Example register value:

schema-seo/SEO-000001.json

The JSON file must be valid JSON-LD and should be validated with Google's Rich Results Test after deployment. Glossary DefinedTermSet data remains a sync-time generation task as described in Glossary SEO.

Current Inventory

Generated from content-inventory.csv — the CSV is authoritative; do not edit this table by hand.

PIDTitleCategoryTypeWP Link
COD-000001Git Quick ReferencecodeCheatsheethttps://id86.net/code/git-cheat-sheet/
COD-000002Python Quick ReferencecodeCheatsheethttps://id86.net/code/python-cheat-sheet/
DEV-000001Docker Compose EssentialsdevopsCheatsheethttps://id86.net/devops/docker-compose/
DEV-000002Plocate Quick ReferencedevopsCheatsheethttps://id86.net/devops/plocate-cheat-sheet/
SEC-000001English Verb Tenses ReferencesecurityReferencehttps://id86.net/security/english-tenses/
SEO-000001Keyword Research ChecklistseoChecklisthttps://id86.net/seo/keyword-research/
DEM-000001Article Body Styling SampledemoReferencehttps://id86.net/demo/styling-sample/
DEM-000002Tmux CheatsheetdemoCheatsheethttps://id86.net/demo/tmux-cheatsheet/
DEM-000003Tmux ReferencedemoReferencehttps://id86.net/demo/tmux-reference/
DEM-000004Tmux Setup ChecklistdemoChecklisthttps://id86.net/demo/tmux-setup-checklist/
DEM-000005Tmux GlossarydemoGlossaryhttps://id86.net/demo/tmux-glossary/
DEM-000007Checklist UX DemodemoChecklisthttps://id86.net/demo/checklist-ux-demo/

Editing Content (how updates reach WordPress)

Markdown is the source of truth before publication; WordPress Gutenberg is the source of truth after publication. The CSV register remains the source of truth for lifecycle and identity. To update a post:

  1. For an unpublished post, edit Markdown in content/ and validate it.
  2. Publish it once through the scheduled publishing or sync workflow.
  3. For a published post, edit the body and editorial metadata directly in WordPress Gutenberg. Do not edit Markdown expecting it to update the live post.
  4. Use the CSV for lifecycle, PID, keyword, and audit identity fields.
FieldWhen to edit
Body contentMarkdown before publication; Gutenberg after publication
titleMarkdown before publication; Gutenberg after publication
descriptionMarkdown before publication; WordPress/SEOPress after publication
tagsMarkdown before publication; WordPress after publication
dateBefore publication only; change published dates in WordPress
id / category / slug / statusDo not change casually; identity and live URL changes require a WordPress operations decision

Two different dates:

FieldFormatMeaningWho updates it
Frontmatter dateYYYY-MM-DD (date only)WordPress publish date shown in the postYou, only when re-dating
CSV Updated columnYYYY-MM-DDWhen the file was last editedAuto — refreshed by validate-inventory.py from the file timestamp