Skip to content

Discussion Bridge for Astro Machine Manual

This is the exact, reusable implementation memory for Discussion Bridge for Astro. Use it with the Human Manual to generate a site-specific runbook. It may name variables, paths, endpoints, and scopes; it must never contain real secret values.

product: Discussion Bridge for Astro
package: astro-discussion-bridge
package_root: packages/astro-discussion-bridge
cli_source: packages/astro-discussion-bridge/src/cli.ts
sync_source: packages/astro-discussion-bridge/src/sync/index.ts
import_source: packages/astro-discussion-bridge/src/import-existing.ts
public_docs: docs
default_starlight_dir: src/content/docs
default_astro_dir: src/content

Package commands run from the consuming Astro project root:

Terminal window
npx astro-discussion-bridge --help
npx astro-discussion-bridge check-discourse [options]
npx astro-discussion-bridge publish-new [docsDir] [options]
npx astro-discussion-bridge sync-existing [docsDir] [options]
npx astro-discussion-bridge publish-and-sync [docsDir] [options]
npx astro-discussion-bridge import-existing [docsDir] --topic URL[,URL] [options]

sync is a backward-compatible alias for publish-new; do not use it in new runbooks because the side effect is less obvious.

Collect and validate before generating a runbook:

site_name: required
repository: required
project_root: required
environment: dev | staging | live | placeholder
framework_preset: astro | starlight
site_url: required absolute HTTPS URL for live use
discourse_url: required absolute URL
post_as: preferred request actor for live writes
legacy_api_username: optional compatibility fallback
publishing_key_location: private reference only; never key value
diagnostics_key_location: private reference only; never key value
active_target: optional logical target name
comments_display: simple | full | fullInteractive
deployment_target: optional, e.g. Cloudflare Pages
verification_urls: required for live lanes
lanes:
- name: required
docs_dir: required
route_base: optional
source_mode: astro-managed | discourse-managed | discourse-imported
category_id: required for create/update intent
tags: []
listed: true | false
managing_page_rule: exactly one managing Astro page per topic
recovery_owner: required for production

Reject or pause generation when source mode, route base, public URL, destination forum, category, or managing page is unknown.

Use for publish-new, sync-existing, and publish-and-sync.

Exact settled granular scopes:

categories:list
categories:show
posts:edit
posts:list
search:show
tags:list
topics:write
topics:update
topics:read
topics:status

Known allowed-parameter entries from the Discourse granular-key UI:

categories show: id=any
posts edit: id=any
search show: q=any, page=any
topics write: topic_id=any
topics update: topic_id=any, category_id=any
topics read: topic_id=any, external_id=any
topics status: topic_id=any, category_id=any, status=any, enabled=any

The API user also needs the forum/category authority required by the action. Retitling replied topics and changing listing status may require staff or moderator capability even when the key has the matching endpoint scope.

Current fallback: global/admin-capable, read-oriented setup key. Use through check-discourse only; that command performs the applicable site metadata, capability, and setup reads. Do not use this key for publishing, reconciliation, or routine runtime/deploy paths.

Target future state: a granular diagnostics/read key after required Discourse site metadata scopes are confirmed.

Every key-creation interaction MUST present the applicable block with the key description, user, scope, and permissions. The operator MUST copy the block into the respective protected credential file above the secret value. Never request, echo, log, or write the real key outside the protected credential store.

Publishing key:

Purpose: Runtime publishing granular key
Use: publish-new, sync-existing, publish-and-sync, check-discourse basic limits
Bot user role: Admin currently; intended future runtime posture is non-admin or least-privilege
Key scope: Granular
Operational rule: Use this to validate the minimum permissions needed for normal bridge publishing.

Diagnostics key:

Purpose: Diagnostics/setup key
Use: check-discourse only
Bot user role: Admin
Key scope: Global or admin-read capable
Operational rule: Do not use in CI/build unless explicitly intended

The complete credential-file templates, including the settled publishing scopes and secret placeholder, are in docs/KEY_MANAGEMENT.md.

DISCOURSE_URL
SITE_URL
DISCOURSE_POST_AS
DISCOURSE_API_USERNAME
DISCOURSE_API_KEY
DISCOURSE_DIAGNOSTICS_API_KEY
DISCOURSE_CATEGORY_ID
DISCOURSE_TAGS
DISCUSSION_TARGET
DISCUSSION_PAGE_URL
DISCOURSE_NOTIFY_RECIPIENTS
DISCOURSE_TITLE_MIN_LENGTH
DISCOURSE_MAX_TOPIC_TITLE_LENGTH
DISCOURSE_MAX_POST_LENGTH
DISCOURSE_MAX_TAGS_PER_TOPIC
DISCOURSE_MAX_TAG_LENGTH

Resolution behavior:

  • CLI values override environment values.
  • check-discourse prefers --diagnostics-api-key or DISCOURSE_DIAGNOSTICS_API_KEY, then falls back to the publishing key.
  • --dry-run publish/sync does not require API username/key.
  • live publish/sync/import requires DISCOURSE_API_USERNAME and DISCOURSE_API_KEY.
request_actor:
preferred_inputs:
environment: DISCOURSE_POST_AS
cli: --post-as
publishOnBuild_lane_or_default: [postAs, postAsEnv]
legacy_fallbacks:
environment: DISCOURSE_API_USERNAME
cli: --api-username
publishOnBuild_lane_or_default: [apiUsername, apiUsernameEnv]
precedence:
- explicit_or_lane_postAs
- named_postAsEnv
- DISCOURSE_POST_AS
- legacy_apiUsername_controls
request_header: Api-Username
live_output: "Post as: USER"
diagnostics_output: "Request actor"
changes_existing_topic_owner: false
api_key_authority:
user_level:
All_Users: may_act_for_supplied_Api_Username
Single_User: bound_to_selected_user
scope:
values: [Global, Read-only, Granular]
purpose: endpoint_and_action_authority
invariant: user_level_and_scope_are_independent
service_identity:
collision_rule: no_actual_or_visually_ambiguous_nonhuman_identity_across_forums
naming_rule: role_plus_origin_system
candidates:
- { username: editorbridgeforum, public_name: Discussion Bridge Forum Editor }
- { username: editorcanforum, public_name: CAN Forum Editor }
verify_before_create: [normalized_username, site_length_limits, availability]
preserved_source_identity: obbba-bot
inventory_group:
name: special-admin
grants_admin_status: false
grants_API_rights: false
grants_category_permissions: false
discourse_mermaid:
official_existing_option:
type: theme_component
meta: https://meta.discourse.org/t/discourse-mermaid/218242
repository: https://github.com/discourse/discourse-mermaid-theme-component
distinct_paths:
- existing_official_theme_component
- fork_or_extension_of_theme_component
- separate_Discussion_Bridge_for_Discourse_plugin
- upstream_Discourse_change
forbidden_term: Discussion_Bridge_Mermaid_plugin
tier_1_API_only_dependency: none
Source mode Required operational guard Writable by publish/sync?
astro-managed explicit ownership decision yes
discourse-managed discussionSync: false no
discourse-imported discussionSync: false until explicit promotion no

Current implementation facts:

  • discussionSync: false is read and enforced by sync preflight.
  • A guarded page reports skipped with reason discussionSync is false.
  • Frontmatter parse/update paths accept LF and CRLF boundaries and preserve the source file’s existing line-ending style when writing updates.
  • Sync preflight remains guard-driven, while the reviewed import path generates discussionSourceMode: discourse-imported and boolean discussionSync: false.
  • Import preserves the Discourse topic ID and URL.

Therefore, the required Alpha import procedure is:

  1. run import-existing --dry-run;
  2. run the live import only after reviewing the destination;
  3. verify the generated source mode, boolean sync guard, topic ID, and URL;
  4. review frontmatter before any directory-wide sync;
  5. remove the guard only after explicit promotion to Astro ownership.

Line-ending invariant:

accepted_frontmatter_boundaries:
- LF
- CRLF
update_behavior: preserve existing source line-ending style
required_regression_case: discussionSync false is enforced for CRLF frontmatter
required_write_regression: frontmatter updates preserve CRLF source style

Publishing may write:

discussionTarget: "community" # only when a target is active
discourseTopicId: 123
discourseTopicUrl: "https://forum.example.com/t/example/123"
discussionSourceHash: "sha256-value"
discussionLastSyncedAt: "ISO-8601 timestamp"

Import currently writes:

discussionTarget: "community" # optional
discourseTopicId: 123
discourseTopicUrl: "https://forum.example.com/t/example/123"
discussionSourceHash: "sha256-value"
discussionImportedAt: "ISO-8601 timestamp"
discussionCommentsDisplay: "full" # only when requested

Operator-supplied controls and overrides include:

discussionSync: false
discussionCommentsDisplay: simple # simple | full | fullInteractive
discussionSummary: |
Curated Discourse-safe companion content.
discourseCategoryId: 5
discussionTags: "product, docs"

Reconciliation contract:

explicit_existing_topic:
input: discourseTopicId
embed_config: '{ topicId }'
page_url_reconciliation_required: false
verification: read and validate the explicit topic directly
url_owned_topic:
input: no discourseTopicId
embed_config: '{ discourseEmbedUrl: embedUrl }'
page_url_reconciliation_required: true

Keep embed-info/exact-URL-search results in diagnostics even for explicitly linked topics, but do not treat a 404 or no URL owner as a topic failure when the explicit topic is healthy.

Read-only endpoints may include:

/site/settings.json
/site.json
/categories.json
/tags.json
/embed/info?embed_url=...
exact URL search fallback

Canonical command:

Terminal window
npx astro-discussion-bridge check-discourse \
--discourse-url https://forum.example.com \
--category-id 5 \
--tags product,docs \
--page-url https://docs.example.com/example-page/

Expected output sections:

Discourse URL
Site settings
Site capabilities
Limits
Tag capabilities
Category
Requested tags / Tag inventory / Tag issues
Setup issues / Setup warnings
Reconciliation lookup (when page URL supplied)

Exit code is nonzero when tag issues or setup issues exist. Unavailable metadata may be reported as warnings/unknown values.

Creates only missing companion topics and skips linked pages.

Terminal window
npx astro-discussion-bridge publish-new src/content/docs --dry-run --details
npx astro-discussion-bridge publish-new src/content/docs

Expected per-page statuses include dry-run-create, created, skipped, and failure output. A live create writes topic link and sync metadata to frontmatter.

Updates only pages with discourseTopicId; skips missing links and guarded pages.

Terminal window
npx astro-discussion-bridge sync-existing src/content/blog \
--route-base blog --dry-run --details
npx astro-discussion-bridge sync-existing src/content/blog --route-base blog

Expected statuses include dry-run-update, updated, unchanged, and skipped. --force rewrites the managed first post even when the source hash is unchanged. --unlist changes topic visibility and may require staff authority.

Creates missing topics and syncs linked topics in one explicit run:

Terminal window
npx astro-discussion-bridge publish-and-sync src/content/releases \
--route-base releases --dry-run --details

Use only after checking that every non-Astro-managed page in the directory has discussionSync: false.

Terminal window
npx astro-discussion-bridge import-existing src/content/docs \
--topic https://forum.example.com/t/example/123 \
--source-mode discourse-managed \
--site-url https://docs.example.com \
--comments-display full \
--dry-run

Hero options:

--hero-image PATH|URL
--hero-alt TEXT

Validation contract:

pairing: bidirectional_required
hero_alt: non_empty_after_trim
explicit_errors:
- bare option
- inline-empty value
- whitespace-only alt
- hero image without alt
- hero alt without image
output:
placement: leading image before raw body
image_syntax: angle-wrapped destination
alt_escaping: supported
internal_path_spaces: supported

Prune option:

--prune-profile community-call-to-action
profile: community-call-to-action
mode: opt_in
removal_scope: trailing block after horizontal rule
markers_all_required:
- Join the Conversation Today
- /signup
- Please share how
- /c/stories/
failure_before_io:
- no verified boundary
- unknown profile
- duplicate profile
- bare option
- empty value
success_metadata:
discussionImportPolicy: pruned:community-call-to-action

Accepted topic references: numeric ID or URL whose host matches DISCOURSE_URL. Expected statuses: dry-run-import, dry-run-overwrite, imported, skipped. Existing files are skipped unless --overwrite is used. --source-mode accepts discourse-imported or discourse-managed, defaults to imported, and rejects astro-managed. Generated frontmatter always includes the selected discussionSourceMode and discussionSync: false. Manifest entries use sourceMode; the same validation is applied before write.

selection_modes:
curated:
input: explicit topics or manifest
ordering: preserve caller-supplied order
next_in_category:
category_discovery: list available categories and subcategories
category_selector: ID or unambiguous slug/name
category_selection: required before queue preview
default_order: created_at ascending
tie_breaker: topic ID ascending
filters_optional:
- tags
- created_at range
- open/closed status
- limit
ordering_optional:
- oldest by created_at
- newest by created_at
- natural topic title/name for numbered collections
forbidden_order_fields:
- bumped_at
- last reply
- latest activity
pre_import:
preview_selected_category_queue: required
preview_candidates: required
exclude_already_imported_topics: required
alpha_requirement: true

The stable comparison key for the default queue is (created_at, topic_id). Community replies must never reorder publishing candidates.

Manifest refresh gate:

command: import-existing --manifest PATH --overwrite
purpose: deterministic multi-page refresh with per-topic policy
schema:
format: strict_json
top_level_keys: [version, imports]
manifest_entry_fields:
- topic
- requiredTags
- output
- commentsDisplay
- heroImage
- heroAlt
- pruneProfiles
ordering: preserve caller-supplied manifest order
reject:
- duplicate_topics
- manifest_and_direct_option_mixing
validation:
- runner_revalidation
- dry_run_collision_and_path_preflight
- path_containment_before_writes
write_model:
- stage_all_entries_before_writes
- exclusive_atomic_creation_without_overwrite
- atomic_overwrite_with_rollback
regression_coverage:
- slug_drift
- destination_race
- zero_byte_output
- path_traversal
blanket_update_all_safe: false
package_gate: pass_51_of_51
obbba_site_live_proof: pass_topics_434_747_751_752_753

Every import route has two validated contracts:

WHEREFROM:
discourse_base_or_target: required
topic_identity_or_curated_manifest_order: required
category: required_when_lane_uses_one
required_tags_and_filters: validate_against_live_source_before_write
WHERETO:
docs_dir: deterministic Astro content root
output: safe relative .md or .mdx file
public_identity: site_url + route_base
navigation: Astro navigation lane
invariants: [deterministic, reviewable, path_contained, never_latest_activity]
manifest_v1_mapping:
wherefrom: [topic, requiredTags]
whereto: [docsDir, output, site-url, route-base, astro_navigation_lane_map]
future_schema_note: nested from/to may be considered later; do not redesign v1 now

requiredTags comparisons are case-insensitive assertions against live Discourse tags and fail before any write. Discourse topic tags may arrive as strings or objects. output must be a safe relative .md or .mdx path; nested parent directories are created only after the entire destination passes validation and containment checks.

Stock Starlight docsSchema() may strip custom bridge fields, and imported Markdown pages do not contain a hand-written <Discussion> component. A Starlight consumer must:

  1. extend docsSchema with the Discussion Bridge frontmatter fields;
  2. install src/components/MarkdownContent.astro at the page boundary;
  3. wire it through starlight.components.MarkdownContent;
  4. remove per-page explicit <Discussion> instances when the boundary override is active, preventing duplicate discussions.

Verification requires exactly one discussion instance per linked page with the expected topic ID and display mode.

--dry-run
--details
--target NAME
--route-base PATH
--discourse-url URL
--site-url URL
--api-username USER
--api-key KEY
--tags TAG[,TAG]
--category-id ID
--force
--unlist
--notify-on-failure
--notify-recipients USER[,USER]
--title-min-length N
--max-topic-title-length N
--max-post-length N
--max-tags-per-topic N
--max-tag-length N
--skip-title-validation

Do not put real keys on a command line in a runbook; use protected environment variables so keys do not enter shell history.

URL derivation joins siteUrl, normalized routeBase, and the Markdown path relative to docsDir, with index collapsed to its parent path.

Examples:

docsDir=src/content/blog
file=src/content/blog/content-lanes.md
routeBase=blog
result=https://docs.example.com/blog/content-lanes/
docsDir=src/content/releases
file=src/content/releases/2_1.md
routeBase=releases
result=https://docs.example.com/releases/2_1/

Pre-write invariants:

  • one managing Astro source page per Discourse topic in a run;
  • no duplicate managed page URL in a run;
  • active discussionTarget must match --target/DISCUSSION_TARGET;
  • display-only comparison pages use discussionSync: false;
  • category and tags are checked before live writes;
  • computed URLs are reviewed with --details.
discussionBridge({
provider: "discourse",
discourseUrl: "https://forum.example.com",
siteUrl: "https://docs.example.com",
comments: {
display: "full", // simple | full | fullInteractive
embedHeight: "800px",
className: "discussion-bridge-embed",
},
replies: {
refreshOnPageLoad: true,
refreshEndpoint: "/api/discourse/topics/{topicId}.json",
renderMermaid: true,
},
});
  • simple: Discourse embeddable-comments script; limited display control and metadata.
  • full: Astro-rendered replies; interaction remains in Discourse; browser refresh needs CORS or same-origin proxy.
  • fullInteractive: Discourse full-app iframe; requires full-app embedding and compatible sign-in/cookie configuration.

Rendering boundary:

fullInteractive:
owner: cross_origin_Discourse_iframe
host_Astro_transforms_or_CSS_cross_boundary: false
mermaid_and_table_styling_owner: Discourse
ordinary_topic_434_mermaid_theme_result: SVG
full_app_embed_topic_434_mermaid_result: raw_code
full_app_embed_theme_component_js_loaded: false
table_parse: pass
table_presentation: weak_until_embedded_CSS
immediate_table_path: Discourse common/embedded.scss targeted by embed class
mermaid_embed_solution: open_extension_plugin_or_upstream
full:
owner: Astro_rendered_bridge_component
parity_review: pass_d7800d7_code_boss_final
renderMermaid_default: true
renderMermaid_opt_out: false
mermaid_version: 11
security_level: strict
loading: lazy_when_reply_contains_mermaid
failure: preserve_source_and_allow_module_load_retry
tables: readable_borders_padding_and_horizontal_overflow
repeated_components: claim_each_replies_container_once
embed_class_hooks:
comments.className: forwarded_to_window.DiscourseEmbed.className
embedClassName_components: [Discussion, DiscourseDiscussion, DiscourseComments]

The lazy Mermaid chunk is emitted during a full-capable build and may trigger Vite’s greater-than-500-kB warning, but browsers fetch it only when a full-mode reply contains Mermaid.

Verification matrix:

simple: embed loads, full discussion link works
full: build/render fetch works, browser refresh works, unavailable state works
fullInteractive: logged-out load, logged-in reply/like/quote, mobile height/CSP

For a live signed-in interaction item, record sanitized evidence:

topic_id: expected topic
post_number: created test post number
direct_post_url: public verification URL
content_marker: non-sensitive test text
post_count_after: observed count
astro_embed_signature_after:
topic_id: expected topic
full_app: true
retired_renderer: absent
private_account_identifier: omit

Creating the reply is an intentional browser-session action, not a package publish/sync/import operation. Verify the forum result read-only afterward.

Fresh-import gate matrix:

cases:
- { hero: false, prune: false }
- { hero: true, prune: false, alt_text: required }
- { hero: false, prune: true }
- { hero: true, prune: true, alt_text: required }
per_case_assertions:
- discussionSourceMode == discourse-imported
- discussionSync == false
- topic ID and URL preserved
- build/deploy/live render pass
- comments pass
- no writeback

Do not use an existing-page migration or signed-in interaction result as a substitute for this matrix.

Component propagation invariant:

Discussion.astro -> DiscourseDiscussion.astro -> DiscourseComments.astro

If topicId is supplied, every layer must preserve it and the browser embed configuration must use { topicId }. Regression tests must cover this path.

Shipped browser declaration contract:

type DiscourseEmbedConfig =
| { topicId: number; discourseEmbedUrl?: never }
| { discourseEmbedUrl: string; topicId?: never };
// Both alternatives may also include:
// fullApp?: boolean
// embedHeight?: string

Do not model topicId and discourseEmbedUrl as simultaneously required or freely co-present; they represent alternative ownership/linking paths.

Planned boundary-credit contract (not implemented in the current reviewed artifact):

comments_credit:
configurable: required
candidate_text:
- Discussion connection by Discussion Bridge
- Discourse connection by Discussion Bridge
canonical_product_link: required
visually_secondary: required
accessible: required
supported_modes: [simple, full, fullInteractive]
hard_coded_in_site_content: false
final_default_and_schema: unresolved

10. Cloudflare Pages / Domain Verification

Section titled “10. Cloudflare Pages / Domain Verification”

CDN-backed Discourse compatibility evidence

Section titled “CDN-backed Discourse compatibility evidence”
verified_at: 2026-07-22
forum: https://forum.repealobbba.org
forum_edge: Cloudflare_CDN
status: PASS_for_exercised_production_workflows
exercised:
- check_discourse_and_API_reads
- Discourse_topic_imports_to_Astro
- target_topic_reconciliation
- protected_source_topic_links
- fullInteractive_comments
- signed_in_reply_behavior
- five_live_source_disclosures
- source_and_cross_target_no_writeback
- canonical_Astro_production_pages
claim_limit: no_guarantee_for_every_CDN_WAF_cache_configuration
preserve_through_CDN:
- Discourse_API_paths
- JSON_endpoints
- embed_and_full_app_routes
- authentication_and_cookies
- websocket_behavior
diagnostic_rule: compare_edge_and_direct_origin_then_investigate_cache_WAF

Do not record private Cloudflare account labels, rule identifiers, or access values. If edge and origin results differ, capture sanitized path/status/cache evidence and route the CDN/WAF configuration issue to Ops.

Cloudflare-specific values are site inputs, not package constants. Record:

repository: canonical repository URL
production_branch: main
root_directory: consuming Astro project path
build_command: npm run build
output_directory: site-specific Astro output, commonly dist
custom_domain: exact public hostname
astro_site: exact public URL
discussion_bridge_site_url: exact public URL
discourse_embed_host: exact hostname
account_id: explicit deployment account identifier in approved site config
worker_name: exact Worker name when using Workers
worker_version: deployment evidence
workers_dev_endpoint: deployment verification endpoint

When more than one Cloudflare account is available, pin the intended account_id in the approved deployment configuration so CLI/CI selection is deterministic. Manuals and support bundles should describe the placement without copying private account labels or login email addresses.

Verification:

  1. Pages deployment corresponds to the intended commit and canonical source.
  2. Custom domain resolves and HTTPS is valid.
  3. Astro site, Discussion Bridge siteUrl, CLI SITE_URL, and the public hostname agree.
  4. Discourse allows that exact embed hostname.
  5. A lane page and its companion topic resolve in both directions.
  6. The selected comments mode works on desktop and mobile.
  7. Cache bypass or narrowly scoped purge is tested before declaring stale output a failed sync/deploy.
  8. The Worker endpoint and canonical domain serve the intended candidate.
  9. Generated HTML contains the expected package/topic signature and does not contain the retired renderer signature.

Before deployment commits, inspect the consuming site’s worktree. Preserve and report unrelated pre-existing changes; do not absorb them into deployment-only commits.

Build the exact tracked candidate from a clean checkout or detached worktree before release. A dirty local deletion can hide a stale tracked page or asset reference and make the working-tree build pass when the committed candidate fails. Isolate the correction in its own reviewed commit, rebuild that exact commit, and leave unrelated unstaged changes and untracked artifacts untouched.

Failure Detection Recovery
Missing credentials CLI missing-configuration message Set protected env vars; rerun dry run.
Site metadata 403 check-discourse unavailable/warning Use diagnostics key or explicit preflight limits.
Title/body/tag validation Local preflight failure Correct content/settings; rerun dry run.
Embed URL already taken Publish error or reconciliation output Prove ownership, resolve with diagnostics, then link/retry.
Page URL has no embed owner but topicId is explicit Embed-info 404 or exact search has no result Validate the explicit topic and generated { topicId }; do not auto-relink.
Wrong category/route --details or post-write verification Stop writes; correct lane; assess/relink affected topic explicitly.
Active target mismatch skipped with target reason Use the matching --target; do not remove labels casually.
discussionSync is false guarded page skipped Expected for display/import/Discourse-owned pages; remove only on approved promotion.
Topic deleted topic read failure Do not auto-recreate; decide restore, relink, or replacement.
First post deleted missing-first-post failure Restore or choose explicit repair; do not silently replace.
Discourse offline clear network/API failure Preserve Astro shell; retry after service recovery.
Stale CDN output source/topic correct but public view stale Verify commit/topic, bypass cache, then purge relevant cache only.

CLI/build output is authoritative. Failure-notification PMs are best effort and must not be treated as the only failure record.

1. Confirm intended package build/version.
2. Confirm lane inputs and source modes.
3. Run check-discourse.
4. Run intended operation with --dry-run --details.
5. Review every page URL, target, topic ID, category, tag, and reason.
6. Run live command without changing other arguments.
7. Verify Discourse topic, first post, metadata, tags, and listing state.
8. Verify Astro source/frontmatter and local build.
9. Verify deployed URL, comments mode, and cache state.
10. Sanitize logs before sharing; update manuals with newly confirmed facts.

13. Multi-Target Frontmatter And Execution

Section titled “13. Multi-Target Frontmatter And Execution”

The frontmatter values are YAML scalars. CSV fields preserve target order; the binding map is serialized JSON:

discussionSourceMode: discourse-imported
discussionSync: false
discussionTarget: repeal-obbba
discussionSourceTarget: repeal-obbba
discussionTargets: repeal-obbba,citizen-activist
discussionPublishTargets: citizen-activist
discussionPrimaryTarget: repeal-obbba
discussionTargetBindings: '{"repeal-obbba":{"topicId":434,"topicUrl":"https://forum.repealobbba.org/t/434","status":"synced"},"citizen-activist":{"status":"failed","lastError":"sanitized target error","lastAttemptedAt":"2026-07-22T00:00:00.000Z"}}'

Operate one target per CLI invocation:

Terminal window
npx astro-discussion-bridge publish-and-sync src/content/docs --target citizen-activist --dry-run
npx astro-discussion-bridge publish-and-sync src/content/docs --target citizen-activist

discussionSync: false does not authorize source writeback. The named source remains protected for discourse-imported and discourse-managed; only a target listed in discussionPublishTargets is writable. New targeted imports set both legacy discussionTarget and discussionSourceTarget. A legacy imported page without the latter protects its legacy discussionTarget.

Run ordered publishOnBuild.lanes sequentially when build-time publishing is intended. Each lane needs targetName and may override discourseUrl, apiKey/apiUsername, or named apiKeyEnv/apiUsernameEnv. Do not put secret values in frontmatter, manuals, or committed configuration.

On failure, inspect only the active target’s binding. Preserve other successful bindings and retry with the failed --target. A stored error is whitespace- normalized, truncated, and paired with lastAttemptedAt. A reconciled 422 embed/title collision records the discovered owning topic in the active binding and clears its failure state. Invalid binding JSON or shape must fail before network access.

implementation:
commit: a9d2097
review: Code_Boss_PASS
tests: 68_of_68_PASS
package_dry_run_exports:
- dist/source.*
- DiscussionSource.astro
- package_root_helpers_and_types
- astro-discussion-bridge/source
public_api:
component: astro-discussion-bridge/DiscussionSource.astro
helper: resolveDiscussionSourceNotice
types: [DiscussionSourceMode, DiscussionSourceNotice]
rendering:
placement: near_article_start
separate_from: [comments_boundary, Discussion_Bridge_credit]
aria_label: Content source
prefix: "Source:"
default_source_label: Discourse
modes:
discourse-imported: This page originated in {sourceLabel} and was imported here for publication.
discourse-managed: This page is managed in {sourceLabel} and published here for easier reading.
astro-managed: no_notice
unknown: no_notice
source_url_precedence:
- explicit_sourceUrl_prop
- discussionImportedFrom
- protected_discussionSourceTarget_binding
- legacy_discussionTarget_binding_fallback
- legacy_discourseTopicUrl
accepted_link_protocols: [http, https]
unsafe_or_malformed_candidates: skip
no_safe_url: render_notice_without_link
multi_target_rule: provenance_follows_protected_source_not_publication_target
custom_props: [sourceLabel, message, linkLabel, class, mode, sourceUrl, targetBindings, frontmatter]
site_wiring:
plain_Astro: canonical_BlogPost_layout
Starlight: MarkdownContent_override
OBBBA:
adoption_status: complete
adoption_commit: aa7846d
artifact: vendor/astro-discussion-bridge-0.1.0-alpha-a9d2097-f3fbb73e.tgz
artifact_sha256: F3FBB73E95D52B5799FBEBE5221298040FD32292EDA8BD76C257C0C19E4267B2
Code_Boss_adoption_review: PASS
clean_detached_npm_ci_build: PASS
publishOnBuild: false
Discourse_writes: none
worker_version: 005b9ff2-c880-43e4-b759-31ec2d02bed5
live_routes_verified: 5
per_route:
Content_source_asides: 1
source_links: 1
source_forum: forum.repealobbba.org
source_label: Repeal OBBBA Forum
wording: exact_imported_default_with_custom_label
discussion_boundary_preserved: true
citizen_activist_topology_gate: separate_and_open
release_scope_doctrine:
alpha: nearly_feature_complete_for_declared_product_promise
scope_model: cumulative
source_of_truth: existing_dashboard_and_build_launch_checklists
new_gates_displace_existing_gates: false
removal_authority: explicit_Phil_direction
beta_primary_work:
- usability
- compatibility
- reliability
- performance
- packaging
- documentation
- installation
- recovery
- support
- presentation
beta_missing_capability_from_user_evidence: allowed
planned_major_pillar_deferral_to_beta: prohibited_by_default
long_term_and_layer_3_outside_declared_promise: later
tiers:
tier_1:
transport: Discourse_API
plugin_required: false
role: free_self_serve_floor
optional_discourse_plugin:
alpha_vertical_slice_status: accepted_Alpha_goal_pending_implementation_and_live_proof
product_repo: separate_from_Astro_package_per_Boss_routing
installability: supported_stock_current_Discourse
removal_and_rollback_docs: required
CAN_normal_topic_baseline: official_Discourse_Mermaid_theme_component
v0_1: [full_app_embed_Mermaid, table_parity, explicit_embed_context_detection, automated_tests]
ordinary_topic_regression: prohibited
live_proof: forum.citizenactivist.network_full_app_embed
not_v0_1: [full_control_plane, post_as_user, PM_automation, general_many_to_many]
tier_1_dependency: false
logical_workspace_path: DiscussionBridge/plugins/discourse-discussion-bridge
physical_github_repo: unresolved_Boss_folder_decision
multi_target_implementation:
commit: 60e41e1
review: Code_Boss_PASS
tests: 62_of_62_PASS
package_check: PASS
package_dry_run: PASS
frontmatter:
discussionTargets: ordered_CSV_target_names
discussionPublishTargets: explicit_writable_subset_CSV
discussionSourceTarget: explicit_protected_source
discussionTargetBindings: JSON_scalar_map_by_target
discussionPrimaryTarget: required_when_multiple_linked
cli: one_explicit_--target_per_run
publish_on_build: ordered_sequential_lanes
recovery: retain_success_retry_failed_target_idempotently
malformed_binding_state: fail_before_network
public_export: astro-discussion-bridge/targets
connection_job_product_doctrine:
public_lead: Every connection has a job
explicit_human_purpose: required_product_behavior_not_yet_implemented
visible_label_and_CTA_match_purpose: required
independent_replies_silently_merged: prohibited
primary_vs_additional: explicit
later_scope: [relay, promotion, summary]
configuration_design_status: pending_review_not_final_not_implemented
candidate_fields: [role_or_purpose, audience, callToAction, description, visibility_context, direction_source_ownership, presentation]
candidate_roles: [primary-community, public-community, chapter, internal-review, expert-feedback, source-wiki, syndication]
bidirectional_CAN_contract:
directions:
- Astro_managed_page_to_CAN_companion_topic
- CAN_topic_to_Discourse_managed_or_imported_Astro_page
separate_page_topic_pairs: required
explicit_source_ownership: required
same_item_writable_both_directions: prohibited
loop_prevention: single_writer_per_item
chapter_national_future_design:
status: proposed_not_implemented
public_frame: Local ownership. National reach.
topology: governed_hub_and_spoke_federation
supported_directions_to_design:
- local_topic_to_mapped_national_category
- national_guidance_to_selected_local_categories
required_fields:
- source_forum_and_topic
- source_chapter_identity
- parent_child_relationship
- mapped_source_and_destination_categories
- required_tags_region_chapter_metadata
- automatic_or_moderator_approved_promotion
- public_private_eligibility
- source_attribution_and_return_link
- target_specific_title_CTA_description
- one_way_first_post_sync_direction
- independent_reply_streams
- target_specific_failure_retry_idempotency
- moderation_ownership_at_both_levels
optional_later: post_as_chapter_identity
silent_merge_or_ambiguous_writeback: prohibited
current_full_forum_to_forum_orchestration_claim: false
alpha_topology_proof:
status: bounded_OBBBA_to_Citizen_Activist_live_proof_complete
verified_at: 2026-07-22
adoption_commit: 36df91c98a35251edd6ddd657cca42ddf0acdafa
adoption_files:
- src/components/MarkdownContent.astro
- src/content.config.ts
- src/content/docs/title i/10101-impact.mdx
Code_Boss_review: PASS
clean_detached_build: PASS
worker_version: 632db326-bc0d-4047-b74b-7e74d3588dbf
canonical_page: https://onebigbeautifulbill.us/title-i/10101-impact/
canonical_page_HTTP: 200
production_obbba_target: https://forum.repealobbba.org
citizen_activist_target:
canonical: https://forum.citizenactivist.network
public_description: A community of activists
hostname_policy: literal_forum_prefix_identity_in_copy
cloudflare_account_ownership: unresolved_Ops_decision
bounded_demo_target: https://forum.discussionbridge.dev
exact_matrix:
- { from: same_selected_onebigbeautifulbill.us_page, to: https://forum.repealobbba.org }
- { from: same_selected_onebigbeautifulbill.us_page, to: https://forum.citizenactivist.network }
- { from: bounded_demo_credit_pages, to: https://forum.discussionbridge.dev }
- { from: multiple_Astro_public_sites, to: https://forum.repealobbba.org }
proof_meaning:
one_page_multiple_forums: first_two_edges
multiple_sites_one_forum: fourth_edge
contract:
source_target_distinct_from_publication_discussion_targets: required
target_list: explicit_ordered
binding_fields: [topic_id, topic_url, source_hash, last_synced_at, status, last_error, last_attempted_at]
presentation_policy: explicit_primary_plus_accessible_additional_links
source_no_writeback: preserve_for_discourse_imported_and_discourse_managed
comments_presentation: explicit_primary_plus_additional_linked_or_rendered
silent_primary_selection: prohibited
write_semantics: recoverable_partial_success
successful_bindings_on_other_target_failure: retain
retry: target_specific_idempotent_no_duplicate_topics
target_specific_surfaces: [diagnostics, dry_run, CLI_output, manuals, live_proof]
required_gate:
- { target_diagnostics: PASS }
- { target_specific_dry_run: PASS }
- { publish_and_independent_bindings: PASS }
- { unchanged_idempotent_retry: PASS }
- { clean_build_deploy_live_binding: PASS }
- { protected_source_no_writeback: PASS }
exercised_binding:
source_target: repeal-obbba
source_topic_id: 434
source_mode: discourse-imported
discussionSync: false
primary_display: fullInteractive
primary_fullApp: true
publication_target: citizen-activist
publication_topic_id: 9
publication_topic_url: https://forum.citizenactivist.network/t/sec-10101-re-evaluation-of-thrifty-food-plan-impact/9
publication_category: { id: 5, name: One Big Beautiful Bill, slug: one-big-beautiful-bill, public: true }
publication_tags: [discussionbridge, obbba, impact, title-i]
additional_presentation: one_accessible_link
publication_interaction:
posts_count: 2
highest_post_number: 2
last_poster: discourseadmin
Astro_primary_topic_remained: 434
reachability: accessible_Additional_discussions_link
evidence_scope: interaction_and_presentation_only
source_post_count: 12
source_first_post_updated_at: 2026-07-01T01:35:12Z
lane:
docsDir: src/content/docs/title i
route_base: title-i
active_target: citizen-activist
broad_root_dry_run: rejected_before_write_due_to_wrong_index_and_route
dependency_audit:
total: 10
low: 1
moderate: 1
high: 8
resolved: false
action: dependency_review_no_automatic_audit_fix
known_notices: [Starlight_docs_to_404, chunk_size, punycode_deprecation]
temporary_detached_worktree: removed_after_deployment
protected_credentials:
records_exist: true
public_paths_or_values: prohibited
legacy_record_format_cleanup: protected_vault_task
claim: bounded_same_page_OBBBA_to_Citizen_Activist_topology
later_scope: general_many_to_many_administration
release_channel:
Astro_package:
alpha: GitHub_prerelease_plus_npm_prerelease_same_commit
semver_example: 0.1.0-alpha.1
exact_first_version: release_gate_decision
npm_dist_tag: alpha
public_install: npm install astro-discussion-bridge@alpha
latest_reserved_for: first_stable_release
beta_tag: consciously_chosen_beta_next_or_other_prerelease
repo_tarball: development_recovery_fallback
Discourse_plugin:
npm_distribution_decision_applies: false
alpha_feature_lock:
status: locked
cumulative_scope_source: docs/BUILD_LAUNCH_CHECKLISTS.md
allowed_new_work:
- close_existing_promise_or_gate
- fix_exercised_behavior
- explicitly_approved_scope_change
software_tracks:
astro_package: free_open_source
optional_light_discourse_plugin: free_open_source
public_docs_and_community_support: free_open_source
paid_value:
- implementation_help
- handholding
- managed_hosting_and_operations
- customization
- support
- consulting
third_party_infrastructure: operator_paid
discussionbridge_dev_dogfood:
reviewed_import_support:
commit: 1731547
code_boss: PASS
tests: 72/72
source_modes: [discourse-imported, discourse-managed]
default: discourse-imported
rejected: [astro-managed]
manifest_field: sourceMode
discussionSync: false
artifact: astro-discussion-bridge-0.1.0-alpha-1731547-7d8951d1.tgz
sha256: 7d8951d15f4b0a4a4f14e238665bc41c28255c6f2cdcb1979105926ba6f4affb
node_engine: ">=22.12.0"
guide_dry_run:
source_forum: https://forum.discussionbridge.dev
source_topic_id: 36
destination_route: /guides/how-to-choose-a-discussion-bridge-source-mode/
status: passed
live_gate:
status: complete_bounded_two_direction_proof
apex_commit: d68ffc4
code_boss: PASS
clean_detached_build_routes: 5
deployed_at: 2026-07-23T09:00:47-07:00
apex_http: 200
pages_hostname: https://discussionbridge-dev.pages.dev
pages_hostname_redirect: 301_to_apex
blog:
route: /blog/every-connection-has-a-job/
http: 200
topic_id: 37
category_id: 5
wiki: false
tags: [discussionbridge, community, product]
guide:
route: /guides/how-to-choose-a-discussion-bridge-source-mode/
http: 200
topic_id: 36
category_id: 6
wiki: true
tags: [discussionbridge, source-mode, guide]
source_disclosure: verified
primary_comments: fullInteractive
sync_existing_dry_run: skipped_discourse_managed_no_writeback
npm_audit: { high: 1, automatic_fix: prohibited_not_run }
known_warning: Mermaid_chunk_over_500k
topic_36_editor_ownership_acceptance:
status: open
required:
- change_first_post_owner_from_discourseadmin_to_selected_bridge_forum_editor
- edit_wiki_as_selected_editor
- import_refresh_discourse_managed_guide_with_overwrite
- verify_content_and_source_ownership
- build_deploy_and_verify_live_markers
- prove_no_Astro_writeback
source_author_provenance:
import_fields:
- discussionSourceAuthorUsername
- discussionSourceAuthorName
overwrite_refresh: update_from_current_Discourse_first_post_author
preserved_on_refresh:
- discussionSourceMode
- discussionSync_false
- discourseTopicId
rendered_notice: "Source author: Display Name (@username)"
profile_link: same_forum_base_preserving_safe_/u/username
subfolder_example:
topic: https://example.com/forum/t/example/123
profile: https://example.com/forum/u/editorbridgeforum
unsafe_username_behavior: omit_author_metadata_and_link
request_actor_regressions:
- real_CLI_execution_with_--post-as
- dry-run_actor_output
package_suite: 79/79
category_contract:
generated_source_field: discussionSourceCategoryId
comparison_source: existing_opening_YAML_frontmatter_only
parser_safety: [LF, CRLF, BOM, ignore_body_and_code_block_lookalikes]
change_reason: "source category changed: OLD -> NEW; Astro route/navigation unchanged"
evaluated_during:
- existing_file_skip
- dry_run_overwrite
- live_overwrite
overwrite_effect:
WHEREFROM: refresh_source_category_metadata
WHERETO: preserve_output_file_public_route_and_Astro_navigation_lane
manifest_contract:
- direct_and_strict_atomic_flows
- preserve_reason_in_dry_run_and_live_overwrite
- preserve_stable_output_path
- preserve_source_mode
- preserve_discussionSync_false
- preserve_topic_ID_and_source_protections
CLI: print_change_reason
example_schemas: [Astro, Starlight]
astro_managed:
configured_category: authoritative_sync_corrects_drift
no_configured_category: preserve_manual_Discourse_category
discourse_managed_or_imported_source:
category_writeback: prohibited
multi_target:
target_categories: independent
evidence_record: docs/evidence/DISCUSSIONBRIDGE_DEV_TWO_WAY_DOGFOOD_2026-07-23.md
count_boundary:
apex_generated_public_routes: 5
docs_synced_sources: 20
docs_generated_html_pages: 21
direction_1:
source: discussionbridge.dev_Astro_managed_blog
destination: forum.discussionbridge.dev_public_companion_topic
writer: Astro
direction_2:
source: forum.discussionbridge.dev_community_wiki
destination: discussionbridge.dev_durable_public_guide
source_mode: discourse-managed
discussionSync: false
source_provenance: required
refresh: deterministic_reviewed_source
destination_contract: explicit_public_route_and_Astro_navigation_lane
primary_discussion: source_forum_topic
writer: Discourse_wiki
reply_streams: independent_never_merged
public_outcome: The site starts conversations. The community develops durable knowledge. The site publishes what the community learns.
support:
formal_work: GitHub Issues
setup_and_field_reports: Discourse Alpha Support category
email_intake: [email protected] -> Discourse
private_help: paid implementation/migration
attribution_and_licensing:
implementation:
script: scripts/check-attribution.mjs
inventory: docs/THIRD_PARTY_PROVENANCE.json
reviewed_override_evidence: docs/third-party-licenses/khroma-2.1.0-MIT.txt
fresh_checkout_sync:
executable: process.execPath
shell: false
regression: passed
full_package_gate:
command: npm run check:attribution
included_in_package_suite: exact_candidate_total_required
status: PASS
checks:
- root_package_MIT_parity_and_holder
- package_license_metadata
- production_lock_dependency_license_allowlist_or_reviewed_override
- npm_pack_required_contents
- README_non_affiliation_and_rendered_links
- tracked_media_provenance
- protected_path_scan
docs_scope_gate:
command: npm run check:attribution --docs-scope
required_status: PASS (docs scope)
npm_package_contents: SKIPPED (requires built release candidate)
docs_result: exact_candidate_synced_source_and_HTML_counts_required
reporting_invariant: full_package_gate_and_docs_scope_gate_are_not_interchangeable
manual_boss:
required_result: "Attribution and Licensing: PASS / FAIL / N/A"
reviewed_paths: required
sanitized_exact_release_record: required
current_status: PASS
exact_candidate: b09dbce
commit_chain: [7127eb1, 462b3ae, b09dbce]
remaining_findings: 0
record: docs/evidence/ATTRIBUTION_LICENSING_REVIEW_B09DBCE_2026-07-23.md
release_prerequisites:
- npm package name, ownership, and publisher authority confirmed
- npm account 2FA or trusted publishing confirmed
- exact reviewed semver prerelease selected
- npm pack --dry-run and tarball contents inspected
- clean install from packed artifact in plain Astro and Starlight
- clean registry install from @alpha in plain Astro and Starlight
- exports, Astro components, CLI bin/help, import/sync, comments, source disclosure, and multi-target helpers verified
- LICENSE, README, repository, bugs, and homepage metadata verified
- credentials, fixtures, local paths, and unintended files absent
- Code Boss pass/fail result recorded against the exact release candidate
- blocking code-review edits complete and re-reviewed where required
- Bridge Boss technical verification complete
- Manual Boss quality review complete
- Human and Machine Manuals ready for the exact release
- Product Boss documentation sign-off recorded
- Product Boss release approval recorded
- live Alpha Support category verified
- email route verified
- README, docs, metadata, demos, and release notes agree
- GitHub prerelease and npm artifact resolve to the same commit
- npm view, dist-tags, and clean consumer install verified
- rollback, deprecation, and yank response documented for immutable versions
- automatic npm audit fix prohibited
- package tests and demo build pass
- dry-run CLI checks pass
- at least one controlled live smoke sync passes

Support bundles may include package/framework versions, sanitized commands and output, lane name, public URLs, category ID, tags, key type, and dry-run result. They must not include key values, credentials, private account data, or production secrets.

When implementation confirms or changes a command, field, scope, endpoint, failure, recovery path, or deployment invariant:

  1. update this machine manual;
  2. update the corresponding human instruction when operator behavior changes;
  3. update the focused guide or checklist;
  4. route implementation gaps to Bridge Boss and manual-quality review to Manual Boss.