CHANGELOG TEMPLATE // BOT INTERFACE SPEC#
> THIS PAGE IS NOT A CHANGELOG.> THIS PAGE IS THE STENCIL THE BOT USES TO MAKE ONE.
This page defines the exact structure scripts/apply-changelog.js builds whenever the Discord bot fires a changelog update. Each collection's release is a small clickable card; clicking it opens a popup (a native <dialog>) with the full changes, instead of a long scrolling page. It's a reference for whoever maintains the bot integration — not a page end users need to read.
How this actually works
The bot (utils/siteChangelogDispatcher.js in the ncrbot repo) fires a repository_dispatch event with a JSON payload. A GitHub Action (.github/workflows/changelog-dispatch.yml) runs apply-changelog.js, which:
- Looks for that payload's
collection_slugamong the cards already onchangelog/index.md. - If found: pulls that ONE collection's old card+popup out, re-IDs it, and moves it into that same collection's own section on
changelog/archive.md— every other collection's section is left completely untouched. - If not found (first post ever for that collection): builds a whole new collapsible section for it on
index.md— nothing to archive yet. - Builds the fresh card+popup from the payload and puts it in
index.md.
Nothing on index.md or archive.md should be hand-edited except by that script.
Multiple collections, tracked independently
index.md can hold any number of "current" cards at once — one per collection currently being tracked (e.g. your CPE collection, Sub2, E33, ...). Each collection gets its own collapsible section (a <details class="pt-changelog-collection">), open by default, so visitors can collapse a collection they don't care about without affecting the others. An update to one collection only ever touches that collection's section. There's no shared "Supported Game Version" summary any more (that only ever made sense for a single collection) — each card/popup shows its own game version instead.
Archive is grouped by collection too
changelog/archive.md mirrors the same pattern — each collection has its own collapsible section holding only its own past releases (newest first), instead of one long flat grid mixing every collection together. A collection's archive section is created automatically the first time one of its releases gets archived.
Payload fields#
| Field | Type | Description |
|---|---|---|
collection_slug | string | Nexus collection slug — used bot-side to gate which collections push to the site at all (SITE_CHANGELOG_SLUGS), and site-side as the key that identifies which card an update replaces |
version | string | Collection version/revision tag, e.g. CPE-14 |
game_version | string | Cyberpunk 2077 version this release targets |
date | string | ISO date, YYYY-MM-DD |
author | string | Bot identity that posted it, e.g. Preem Team Bot |
source_channel | string | Origin channel/group name, e.g. #Sub2 |
added_items | markdown list | Bullet list of new additions, or "" if none |
updated_items | markdown list | Bullet list of version bumps/changes, or "" if none |
removed_items | markdown list | Bullet list of removals, or "" if none |
No 'Fixed' category
There's no fixed_items field — the bot's diff data only knows Added/Updated/Removed (it's a straight revision-to-revision mod diff, not a curated changelog), so there's nothing to source a "Fixed" section from. A category with no data ("") is simply omitted — no empty chip, no empty heading.
Card + popup markup#
Each entry is a <button> (the card) immediately followed by its matching <dialog> (the popup), sharing an id via data-pt-changelog-open="…" / id="…". Both need markdown="1" on themselves, not just the innermost content div — md_in_html treats an entire subtree as opaque raw HTML the moment it hits any ancestor without that attribute, however deep the real markdown="1" div sits.
<button type="button" class="pt-changelog-card" data-pt-changelog-open="{{ dialog_id }}">
<img class="pt-changelog-card-thumb" src="assets/{{ thumbnail_file }}" alt="">
<span class="pt-changelog-card-body">
<span class="pt-changelog-card-version">{{ version }}</span>
<span class="pt-changelog-card-date">{{ date }} · CP2077 {{ game_version }}</span>
<span class="pt-changelog-card-chips">{{ chips }}</span>
</span>
</button>
<dialog class="pt-changelog-modal" id="{{ dialog_id }}" markdown="1">
<div class="pt-changelog-modal-inner" markdown="1">
<button type="button" class="pt-changelog-modal-close" data-pt-changelog-close aria-label="Close changelog">×</button>
## {{ version }}
{{ chips }}
`{{ date }}` · Game Version `{{ game_version }}` · Posted by **{{ author }}** · Synced from `{{ source_channel }}`
### Added {: .pt-changelog-h-added }
{{ added_items }}
### Updated
{{ updated_items }}
### Removed {: .pt-changelog-h-removed }
{{ removed_items }}
</div>
</dialog>
{{ dialog_id }}— onchangelog/index.mdthis ischangelog-current-{{ collection_slug }}(one fixed, unique id per tracked collection). Onchangelog/archive.md, where many old releases from every collection coexist, it'schangelog-{{ slugified version }}instead (e.g.changelog-v4-3-0), with a-2,-3, ... suffix added if that id's already taken (a collection re-posting an identical version string, most likely while testing, would otherwise collide with an already-archived entry).{{ thumbnail_file }}— looked up fromcollection_slugvia theCOLLECTION_IMAGESmap at the top ofapply-changelog.js. A slug with no entry there just gets a card with no image — nothing breaks. To add a new collection's thumbnail: drop a.webpintodocs/changelog/assets/and add one line to that map.{{ chips }}— one<span class="pt-chip pt-chip--{added|updated|removed}">…</span>per non-empty category, nothing for empty ones.- The
{: .pt-changelog-h-added }/{: .pt-changelog-h-removed }bits areattr_listsyntax — that's what letsstylesheets/changelog.csscolour those two headings green/red. "Updated" needs no class —<h3>is already yellow site-wide. - Omit a whole
### Heading {{ items }}block entirely for any empty category — don't post an empty section or its chip.
Where new entries go#
changelog/index.md — each tracked collection has its own collapsible section (<details class="pt-changelog-collection" open>), and inside that section its one "current" card is wrapped in a marker pair keyed by slug:
<!-- CHANGELOG:CURRENT:START -->
<details class="pt-changelog-collection" open markdown="1" data-collection-slug="{{ collection_slug }}">
<summary>{{ collection display name }}</summary>
<div class="pt-changelog-cards" markdown="1">
<!-- CHANGELOG:CURRENT:ENTRY:{{ collection_slug }}:START -->
...card + dialog for this one collection...
<!-- CHANGELOG:CURRENT:ENTRY:{{ collection_slug }}:END -->
</div>
</details>
...one more <details> section per other tracked collection...
<!-- CHANGELOG:CURRENT:END -->
apply-changelog.js only ever replaces the one ENTRY:{{ collection_slug }} block matching the incoming payload — every other collection's <details> section (and its entry) is left byte-for-byte untouched. If no entry exists yet for that slug, a whole new <details> section is built and inserted at the top of the list instead. The display name comes from the COLLECTION_NAMES map at the top of apply-changelog.js (falls back to the raw slug if a collection isn't listed there).
changelog/archive.md — mirrors the same pattern: each collection gets its own <details class="pt-changelog-collection"> holding only that collection's past releases, newest first, inserted right after its own marker comment (which stays put for next time):
<!-- CHANGELOG:ENTRIES:START -->
<details class="pt-changelog-collection" open markdown="1" data-collection-slug="{{ collection_slug }}">
<summary>{{ collection display name }}</summary>
<div class="pt-changelog-cards" markdown="1">
<!-- CHANGELOG:ARCHIVE:{{ collection_slug }}:PREPEND_HERE -->
...new entry for this collection gets inserted right here...
...older entries for this same collection...
</div>
</details>
...one more <details> section per other collection with archived history...
<!-- CHANGELOG:ENTRIES:END -->
The first time a collection's release ever gets archived, its <details> section (with its own PREPEND_HERE marker) is created and added to the top of the list — after that, every later archive for that same collection just prepends inside its existing section.