Task: Inline hashtag links
Date: 2026-11-04
Workflow: Direct
Feedback: PF-20261104-F78
Source: https://zenmemes.com/threads/thread-20261104182608-cc730e8b
Status: Local implementation complete; merge and deployment pending

Scope:
Render canonical lowercase hashtags as ordinary /tags/{tag} links in post bodies and replies, including offline snapshots. Preserve authored bytes, line breaks, code examples, existing links and URL fragments. No schema, dependency, styling or tag-assignment changes.

Reuse:
src/ForumRewrite/View/TemplateRenderer.php already routes full post bodies through MediaEmbedRenderer; extend that path for plain text. PublicMarkdownRenderer::inline already protects generated links and code with tokens; add hashtag linking only in post mode. Share the PHP link rule in View/PostHashtags.php, with a matching rule in the existing offline reader. Keep titles and list excerpts as plain text. LocalWriteService::extractThreadLabelsFromBody defines the lowercase ASCII letters/digits and single internal hyphen tag syntax; do not change ingestion semantics.

Completion:
Open a thread or post with #meta and follow its body link to /tags/meta, including replies, Markdown disabled and JavaScript disabled. Check Markdown code/links and media remain intact. Offline thread bodies use the same links and existing offline tag navigation. Check phone/desktop and theme rendering. Run smoke plus affected renderer, docs, offline and route checks; do not run the full suite. Submit a PR; deployment and production acceptance remain pending.

Checks:
PHP 8.3 and the existing pinned playwright-core 1.58.1 were installed locally for verification. No project dependencies, manifests or lockfiles changed. TMPDIR=/private/tmp avoids macOS symlink paths rejected by ImageStore.

Passed: ./v3 test MediaEmbedRendererTest PostMarkdownRendererTest PlatformDocsPageTest OfflineSnapshotThreadPresentationTest OfflineSnapshotPresentationTest WriteApiSmokeTest::testThreadPagesRenderMarkdownOnlyForMarkdownPostsAndRespectTheFlag (57/57).
Passed: ForteBoardReaderTest (4/4) and LocalAppSmokeTest::testForteThreadDetailApiRendersOneRequestedThread (1/1).
Passed: tests/browser/inline_hashtags.mjs in Brave: plain/Markdown roots and replies; links to actual tag routes; JavaScript enabled/disabled; 390px/1280px; light, dark, Word97, Chicago and Midnight. Offline snapshot tag navigation also passed at both widths. Browser test uses an isolated fixture, no authenticated user profile or production writes.
Passed: ./v3 feedback check; node --check public/assets/offline_reader.js; git diff --check.

Smoke limitation: ./v3 test --smoke MediaEmbedRendererTest PostMarkdownRendererTest PlatformDocsPageTest OfflineSnapshotThreadPresentationTest OfflineSnapshotPresentationTest WriteApiSmokeTest::testThreadPagesRenderMarkdownOnlyForMarkdownPostsAndRespectTheFlag ran 470 tests: 424 passed, 46 failed. macOS lacks /usr/bin/setsid, required by BoundedProcess for signing/transaction tests; the first failure emits a PHP warning and causes subsequent header/output assertions to fail. A separate tag case-variant test also assumes a case-sensitive filesystem. SingleRequestHttpTest::testHttpSubmissionAndRecovery, SignedPostTransactionTest::testFailureRollbackAndAcceptedCommit and LocalAppSmokeTest::testTagBrowsingCombinesCaseVariantsWithoutDuplicateThreads reproduce their failures on unchanged base 937dde4. Affected rendering tests pass in isolation. Full suite not run, per AGENTS.md. A Linux smoke run remains for the maintainer/CI.

Deployment:
No migration or post rewrite. Existing rendered/static pages must be regenerated with the new application release, using the normal publication flow. Offline readers need the updated hashed reader asset; the snapshot schema is unchanged. Production acceptance is pending; no production messages were posted.

Follow-up: Feature flag and metadata label links (2026-11-04)
Workflow: Direct; small extension of the existing renderer and flag contracts.
Feedback: PF-20261104-F78 (matching item; deployment acceptance remains pending).
Scope: Add site-mutable FORUM_TAG_LINKS_ENABLED, on by default, controlling automatic body hashtag links only. Header/footer metadata labels always link, per the user’s follow-up clarification. Keep tag assignment, authored links and stored records unchanged. Apply to shared thread/root/continuation cards, post pages, Forte and the offline reader body path.
Reuse: FeatureFlagRegistry and FeatureFlagEvaluator provide operator controls and environment overrides. TemplateRenderer passes effective rendering options to MediaEmbedRenderer and PublicMarkdownRenderer. Extend PostHashtags for escaped metadata label lists; reuse it from templates/partials/thread_root_card.php, post_card.php and thread_card.php. Pass the flag into the offline shell before rendering; reuse the existing offline body renderer. No new component, schema, dependency or navigation destination.
Completion: Follow labels to actual tag pages; disable and re-enable through the existing flag contract, verifying plain/Markdown roots and replies, continuation metadata, shared views and offline bodies. Check JS on/off, five affected themes, phone/desktop and overflow. Run smoke plus affected tests; full suite excluded. Existing static HTML and saved offline shells require regeneration/refresh through normal publication after deployment.
Private configuration: Register the environment setting in PrivateConfigSchema with null default and no template default, matching site-mutable flags.
Checks:
Passed: ./v3 test --smoke MediaEmbedRendererTest PostMarkdownRendererTest PublicMarkdownRendererTest PlatformDocsPageTest OfflineSnapshotThreadPresentationTest OfflineSnapshotPresentationTest FeatureFlagEvaluatorTest FeatureFlagsBehaviorTest PrivateConfigSchemaTest PrivateConfigCommandTest ForteBoardReaderTest LocalAppSmokeTest::testForteThreadDetailApiRendersOneRequestedThread WriteApiSmokeTest::testThreadPagesRenderMarkdownOnlyForMarkdownPostsAndRespectTheFlag WriteApiSmokeTest::testTagLinksCanBeDisabledAndRestoredWithoutChangingLabels WriteApiSmokeTest::testCreateThreadWithHashtagsWritesThreadLabelRecordAndRendersLabels WriteApiSmokeTest::testCreateReplyWithHashtagsWritesThreadLabelRecordForTargetThread (497/497 including smoke).
Passed: ./v3 test PrivateConfigSchemaTest WriteApiSmokeTest::testCodexHandoffRequiresDevelopmentTagOrThreadLabelInApiAndUi WriteApiSmokeTest::testCodexHandoffAllowsDevelopmentThreadLabelsOnRootPost WriteApiSmokeTest::testCodexHandoffReplyAddedThreadLabelShowsRootButtonOnly WriteApiSmokeTest::testLabeledWriteIncrementalLabelFailureFallsBackToFullRebuildAndKeepsReadModelHealthy LocalAppSmokeTest::testThreadMergesSameAuthorQuickRepliesIntoContinuations (10/10). This rerun covers the added private-config template exclusion assertion.
Passed: CHROMIUM_PATH=/usr/bin/chromium-browser node tests/browser/inline_hashtags.mjs, repeated with FORUM_TAG_LINKS_ENABLED=false. Both cover plain/Markdown roots and replies, 390px/1280px, JS on/off, light/dark/Word97/Chicago/Midnight, no overflow and loaded theme assets. Initial verification before clarification: enabled metadata links navigated to actual tag pages; disabled metadata and bodies stayed text. Superseded by the body-only flag scope below. Offline bodies respect both settings. The initial browser selector assumed labels stayed in the root; the existing continuation merger moves them to the last reply. Corrected the check to find the Labels byline and reran both modes successfully. Screenshots inspected at phone and desktop widths: /tmp/inline-hashtags-mkfTCC/online-390.png and online-1280.png.
Passed: node --check public/assets/offline_reader.js; git diff --check; ./v3 feedback check (0 problems, 21 existing source-overlap reviews).
Limitations: Full suite not run. No production deployment, physical-device check or production acceptance. Offline has no metadata label list today; this change gates its existing body links and does not add a new list. No dependency, manifest, lockfile, canonical-record or schema changes.
Status: Follow-up locally complete; uncommitted; deployment and production acceptance pending.

Clarification: 2026-11-04
Request: Header/footer labels always link; gate only hashtags inside post bodies.
Reuse: Keep the shared PostHashtags::renderLabels helper and three existing card consumers; remove its flag parameter and conditional. Existing renderer options still gate body hashtags, including offline. Rename the flag UI label to Hashtag links in posts; keep its configuration key.
Verification: ./v3 test --smoke MediaEmbedRendererTest PostMarkdownRendererTest FeatureFlagEvaluatorTest FeatureFlagsBehaviorTest OfflineSnapshotThreadPresentationTest ForteBoardReaderTest WriteApiSmokeTest::testTagLinksCanBeDisabledAndRestoredWithoutChangingLabels passed 455/455. Updated integration coverage asserts metadata links remain with either canonical flag value, while body hashtag links toggle independently. Repeated CHROMIUM_PATH=/usr/bin/chromium-browser node tests/browser/inline_hashtags.mjs with FORUM_TAG_LINKS_ENABLED=false and with the default: both passed plain/Markdown thread/post reads and footer label navigation at 390px/1280px, JS on/off, five themes, overflow checks and offline body gating. Inspected disabled phone screenshot /tmp/inline-hashtags-WCbWL5/online-390.png: body hashtags are text and footer labels stay linked. Node/PHP syntax and git diff checks passed. Full suite not run. Earlier results above describe the prior flag scope.
Status: Clarified behavior locally complete; uncommitted and not deployed. No dependency changes.

Integration: 2026-11-05
Authorization: User requested merge into main and push. Preserve the verified changes in a feature branch, integrate current origin/main, then run smoke and affected checks before pushing. Deployment and production acceptance remain outside this integration.
Integration verification: Merged origin/main e269f96c into feat/hashtag-link-controls cleanly. Inspected the merged shared renderer, registry and card changes. ./v3 test --smoke MediaEmbedRendererTest PostMarkdownRendererTest FeatureFlagEvaluatorTest FeatureFlagsBehaviorTest OfflineSnapshotThreadPresentationTest ForteBoardReaderTest PrivateConfigSchemaTest PrivateConfigCommandTest WriteApiSmokeTest::testTagLinksCanBeDisabledAndRestoredWithoutChangingLabels WriteApiSmokeTest::testThreadPagesRenderMarkdownOnlyForMarkdownPostsAndRespectTheFlag passed 471/471. FORUM_TAG_LINKS_ENABLED=false CHROMIUM_PATH=/usr/bin/chromium-browser node tests/browser/inline_hashtags.mjs passed across five themes, phone/desktop, JS on/off, footer label navigation and offline body gating. git diff --check passed. Full suite not run; no dependency changes.
Integration status: Verified and ready to advance main and push under the user's authorization. Deployment and production acceptance remain pending.