Skip to content

Markdown Feeds: Serve comment feeds and add comments to singular documents - #1121

Merged
dkotter merged 12 commits into
WordPress:developfrom
ekamran:shifteq/markdown-comment-feeds
Oct 8, 2026
Merged

dkotter merged 12 commits into
WordPress:developfrom
ekamran:shifteq/markdown-comment-feeds

Conversation

@ekamran

@ekamran ekamran commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

What?

Closes #1120

Renders comments in the site-wide comment feed instead of the post list, adds a post's approved comments to its Markdown document (?output_format=markdown), and answers 404 for the never-intended /<post>/feed/markdown/ URL.

Why?

add_feed( 'markdown', ... ) registers the format for every feed context, so /comments/feed/markdown/ and /<post>/feed/markdown/ resolve, but do_feed_markdown() always used the post renderer: those URLs returned the latest posts with full content, while the core comment feeds at the same URLs list comments. Details and sample output are in #1120. Review settled the per-post case differently from core: the singular Markdown entry point is ?output_format=markdown (#855), so that document carries the comments, and the singular feed URL, a side effect of add_feed(), is not served.

How?

  • New Markdown_Comment_Renderer. do_feed_markdown() uses it for the site-wide comment feed (/comments/feed/markdown/, ?feed=markdown&withcomments=1 and other non-singular comment feed contexts); the post feed is unchanged otherwise.
  • Markdown_Singular_Renderer adds a comments section after the content when the post has approved comments: ## Comments, then one block per comment. The query follows the Discussion settings: comment order, and with paging on only the first page in that order. With threading on, a page holds comments-per-page top-level comments, each followed by its replies, oldest first as in the core comment list (not nested). It skips pingbacks, trackbacks and notes, skips the query for posts without a comment count unless that filter is in use, and is filterable through wpai_markdown_singular_comments_args; the whole section is removable through wpai_markdown_singular_sections. Posts without comments keep today's document, byte for byte.
  • /<post>/feed/markdown/ (and ?p=N&feed=markdown) answers 404 with the theme's not-found page. The URL only exists because add_feed() registers the format for every feed rewrite rule, the per-post comment feed included. Other feed formats of the post (/<post>/feed/), the main Markdown feed and the singular document are untouched.
  • Feed requests are never negotiated. is_singular() is true on a post's comment feed, so /post/feed/ previously got Vary: Accept and could be served the post's Markdown on Accept: text/markdown or ?output_format=markdown. Both gates now also require ! is_feed().
  • The site-wide document opens with # Comments for <site name> and the site URL.
  • Each approved comment is one block: a heading (## Comment on <post> by <author> site-wide, ### By: <author> in a post's document, the same wording core uses for its comment feed item titles), a metadata list with the comment link and date, and the comment text run through the comment_text filters and converted to Markdown, so <strong> and auto-linked URLs come out as Markdown. Markdown syntax in the comment text is escaped, so a comment cannot add headings, lists or other structure to the document. The site-wide comment feed renders through WordPress' comment loop, while singular Markdown documents query approved comments for the post and set the comment and post globals before applying comment_text, so those filters see the same context as in the core comment feeds.
  • Comments on password-protected posts are listed without their text, as core does in its comment feeds.
  • In the feed, which comments and how many come from the core query (comment_feed_* filters, posts_per_rss), so moderation, order and the limit match the core feeds. Last-Modified, ETag and 304 answers come from core as before.
  • A filter wpai_markdown_comment_sections mirrors wpai_markdown_feed_item_sections for comment blocks in both places. Documented in docs/experiments/markdown-feeds.md together with the comment feed URLs.

Sample, /<post>/?output_format=markdown with this PR (the document as before, then its comments):

# Feed Probe Post

- Link: http://127.0.0.1:8974/feed-probe/
- Published: 2026-10-07T17:20:21+00:00
- Author: admin

Probe body text.

## Comments

### By: Commenter 1

- Link: http://127.0.0.1:8974/feed-probe/#comment-507
- Published: 2026-10-01T10:00:00+00:00

Probe comment 1.

### By: Commenter 2

- Link: http://127.0.0.1:8974/feed-probe/#comment-508
- Published: 2026-10-02T10:00:00+00:00

Probe comment 2.

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code and Codex
Used for: Investigation, implementation, tests, verification, and PR wording. I reviewed the reasoning and test results, and I take responsibility for the contribution.

Testing Instructions

  1. Enable the Markdown Feeds experiment and approve a comment on a post.
  2. Open /comments/feed/markdown/. On develop it lists posts. With this PR it lists the comments, newest first, each with a link and date.
  3. Open /<post-slug>/?output_format=markdown. The document as before, then a ## Comments section with one block per approved comment, in the Discussion settings' order. With threading on, replies follow their parent, oldest first, also when newer comments are shown first. A post without comments gives exactly the same output as on develop.
  4. Open /<post-slug>/feed/markdown/. On develop it renders the post as a one-item feed; with this PR it is a 404 page. /<post-slug>/feed/ (RSS) still works.
  5. Open /feed/markdown/. It still lists posts.
  6. Optional: add a comment to a password-protected post. Its block appears in /comments/feed/markdown/ without the comment text.
  7. With the Accept setting on, curl -sI -H 'Accept: text/markdown' <post URL>feed/ returns application/rss+xml without a Vary: Accept header (on develop it returns the post's Markdown).

Automated:

  • npm run test:php -- --filter 'Markdown_' runs the 95 Markdown Feeds tests; all pass here.
  • The full PHP suite passes on single site and on multisite (1900 tests).
  • composer lint and PHPStan pass.

Changelog Entry

Fixed - Markdown Feeds: the site-wide comment feed lists comments instead of the post list.
Added - Markdown Feeds: a post's Markdown document (?output_format=markdown) ends with its approved comments.
Changed - Markdown Feeds: /<post>/feed/markdown/, never an intended URL, answers 404; the post's document carries the comments instead.
Fixed - Markdown Feeds: Feed requests are no longer served the post's Markdown by Accept negotiation or ?output_format=markdown.

Open WordPress Playground Preview

add_feed() registers the markdown format for every feed context, so
/comments/feed/markdown/ and /<post>/feed/markdown/ resolve, but
do_feed_markdown() always used the post renderer. Those URLs returned the
post list instead of comments, unlike the core comment feeds at the same
URLs.

Comment feed contexts now use Markdown_Comment_Feed_Renderer: a heading for
the site or the post, then one block per approved comment with the author
(and the post, in the site-wide feed), the comment link and date, and the
comment text converted to Markdown. Comments on password-protected posts
are listed without their text, as in core. A new filter,
wpai_markdown_comment_feed_item_sections, mirrors the post item filter.
@ekamran
ekamran requested a review from a team October 6, 2026 13:45
@ekamran
ekamran requested a review from jeffpaul as a code owner October 6, 2026 13:45
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: ekamran <ekamran@git.wordpress.org>
Co-authored-by: dkotter <dkotter@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@codecov

codecov Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.71069% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.11%. Comparing base (ecc9ecd) to head (95f903a).

Files with missing lines Patch % Lines
...ments/Markdown_Feeds/Markdown_Comment_Renderer.php 94.59% 6 Missing ⚠️
...udes/Experiments/Markdown_Feeds/Markdown_Feeds.php 82.60% 4 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             develop    #1121      +/-   ##
=============================================
+ Coverage      81.95%   82.11%   +0.16%     
- Complexity      3350     3394      +44     
=============================================
  Files            135      136       +1     
  Lines          13026    13178     +152     
=============================================
+ Hits           10675    10821     +146     
- Misses          2351     2357       +6     
Flag Coverage Δ
unit 82.11% <93.71%> (+0.16%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread includes/Experiments/Markdown_Feeds/Markdown_Comment_Feed_Renderer.php Outdated
…ents

The comment feed renderer iterated the comments array directly, so the
comment_text filters ran without the global comment and with the main
query's post as the current post. Use the comment loop and set up each
comment's post before filtering, like the core comment feeds do, and reset
the post data after the loop.
…omment-feeds

# Conflicts:
#	tests/Integration/Includes/Experiments/Markdown_Feeds/Markdown_FeedsTest.php
$this->send_header( 'Content-Type: text/markdown; charset=' . get_option( 'blog_charset' ) );

$renderer = new Markdown_Feed_Renderer();
$renderer = is_comment_feed() ? new Markdown_Comment_Feed_Renderer() : new Markdown_Feed_Renderer();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies but testing again, this does not seem to work the way I'd expect. With these changes in place, the main post markdown feed only renders comments, not the actual post itself. That seems broken, shouldn't there be a specific comment feed that is used instead?

I also think header negotiation is broken for these feeds so that needs looked at

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, agreed on all of these.

For the per-post feed shape: I read core's /post/feed/ as the post's comment feed, which is why I switched /post/feed/markdown/ to comments. But I agree this changes the Markdown feature's existing behaviour, where that URL rendered the post itself.

The header negotiation problem is real and pre-existing: is_singular() is true on a post's comment feed, so /post/feed/ got Vary: Accept and was served the post's Markdown on Accept: text/markdown or ?output_format=markdown. Fixed here so feed requests are never negotiated, plus the smaller renderer fixes.

For the route shape, my suggested direction is to keep /post/feed/markdown/ rendering the post as before, keep /comments/feed/markdown/ for the site-wide comment feed, and add a separate explicit path or query for per-post comments only if you want that supported. Happy to go whichever way you prefer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read core's /post/feed/ as the post's comment feed, which is why I switched /post/feed/markdown/ to comments. But I agree this changes the Markdown feature's existing behaviour, where that URL rendered the post itself.

Hmm.. I didn't realize this but in testing, does seem the post singular feed shows comments, if any, not the content of the post. That seems not great to me but I'm sure there's a reason. But this particular feature was built to show a markdown representation of the post and so I don't think we should change our feed to only be comments, even if that means we aren't matching Core (though open to a discussion on that if people feel that's the wrong approach).

For the route shape, my suggested direction is to keep /post/feed/markdown/ rendering the post as before, keep /comments/feed/markdown/ for the site-wide comment feed, and add a separate explicit path or query for per-post comments only if you want that supported. Happy to go whichever way you prefer.

For the first two questions, I think that's all correct.

For the last (separate path for a per-post-comment feed), I'd say we either don't support that for now or any thoughts on just rendering comments in the per-post feed, alongside the post content?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works for me. I will keep /post/feed/markdown/ as a post document first, matching the feature's existing purpose, and append approved comments under a ## Comments section when comments exist. Posts without comments will keep the same output as before.

/comments/feed/markdown/ and ?withcomments=1 will stay as the site-wide comment feed.

I will push that update today.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In thinking about this a bit more and testing on some default WordPress sites, I think that supporting /<post>/feed/markdown/ was unintentional coming out of #855. Any thoughts on removing that singular post markdown feed here?

The main entry points from that PR were for global feeds to support /markdown but for singular content, they only serve markdown if you add the query param (like https://example.com/this-is-an-article?output_format=markdown). So I think it was unintentional that /<post>/feed/markdown/ exists/works.

So my thoughts are:

  1. Remove the singular markdown feed URL support
  2. For the singular markdown query param, update the output there to render comments
  3. Ensure global feeds (like /comments/feed/markdown/) do all work

This also matches how WP.org is currently working. For example, if you go to https://make.wordpress.org/core/2026/10/07/proposal-minor-updates-to-the-wordpress-core-css-coding-standards/?output_format=markdown, you'll see comments rendered there. If you go to https://make.wordpress.org/core/2026/10/07/proposal-minor-updates-to-the-wordpress-core-css-coding-standards/feed/markdown, that will 404.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on all three.

The singular feed URL exists because add_feed() registers the format for every feed rewrite rule, the per-post comment feed included, so the clean way to remove it is a 404 for is_singular() plus feed=markdown. That covers ?p=N&feed=markdown too.

For the comments in the singular document: a ## Comments section after the content, one block per approved comment (### By: author, link, date, text), ordered by the Discussion settings' comment order and capped at its comments-per-page value, so a post with 2,000 comments does not become a 2,000-comment document. Pingbacks and trackbacks are left out, and the section can be removed through the existing sections filter. Posts without comments keep today's document unchanged.

Global feeds stay as in this PR: /comments/feed/markdown/ and ?withcomments=1 list comments, the others list posts.

Pushing the update shortly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed as f2b8aff: the singular feed URL answers 404, the ?output_format=markdown document ends with the approved comments (Discussion settings for order and count, pingbacks excluded, removable through the sections filter), global feeds unchanged. PR title and description updated.

Comment thread includes/Experiments/Markdown_Feeds/Markdown_Comment_Renderer.php
Comment thread includes/Experiments/Markdown_Feeds/Markdown_Comment_Renderer.php
Comment thread includes/Experiments/Markdown_Feeds/Markdown_Comment_Feed_Renderer.php Outdated
Comment thread includes/Experiments/Markdown_Feeds/Markdown_Comment_Feed_Renderer.php Outdated
A post's comment feed is a singular request too, so "/post/feed/" was
getting a "Vary: Accept" header and was served the post's Markdown when
the client sent "Accept: text/markdown" or "?output_format=markdown".
Both gates now also require the request not to be a feed.

In the comment feed renderer, escape the comment author in the item
heading, rewind the comment loop after rendering, and drop a redundant
assignment.
A post's own Markdown feed renders the post document as before and, when
the post has approved comments, ends with a "## Comments" section holding
one block per comment under a "### By:" heading. Posts without comments
produce the same document as before. The site-wide comment feed is
unchanged.
A post's Markdown document (?output_format=markdown) now ends with a
"## Comments" section listing its approved comments, following the
Discussion settings for order and count, pingbacks excluded. Posts
without comments keep the same document as before.

The Markdown feed of a single post was never a supported URL: its
rewrite rule only exists because add_feed() registers the format for
every feed context. It now answers 404 with the theme's not-found page.
The site-wide comment feed is unchanged.

The comment renderer is renamed Markdown_Comment_Renderer and its
sections filter wpai_markdown_comment_sections; a new
wpai_markdown_singular_comments_args filter controls the comment query.
@ekamran ekamran changed the title Markdown Feeds: Render comments in comment feed contexts Markdown Feeds: Serve comment feeds and add comments to singular documents Oct 7, 2026
…ent can't manipulate the markdown output. Ensure we handle threaded comments and ensure the number of comments we output matches core settings. Slightly better approach for marking the singular markdown feed as a 404. Minor performance improvements on setting global post
dkotter
dkotter previously approved these changes Oct 8, 2026
@dkotter

dkotter commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

@ekamran I've made some last changes here, let me know if you see any issues with those though. Thanks!

@dkotter dkotter added this to the 1.5.0 milestone Oct 8, 2026
@ekamran

ekamran commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @dkotter, the escaping, notes exclusion and threading changes look good to me, and CI is green.

One thing I found while testing: with Discussion set to show newer comments first, replies inside a thread also come out newest first. Core's comment list keeps replies oldest first and only reverses the top level: comments_template() queries ASC, and wp_list_comments() sets reverse_top_level from comment_order. With that setting the threaded query here uses DESC and the replies inherit it, so a reply can appear above the comment it answers.

Tested on 7.2-alpha with two levels of replies:

Order Core This PR
asc A, A.1, A.1.1, A.1.2, A.2, B, B.1 same
desc B, B.1, A, A.1, A.1.1, A.1.2, A.2 B, B.1, A, A.2, A.1, A.1.2, A.1.1

Reversing child comments level by level when the query order is DESC matches core in both orders. Happy to push that with a test here if you want it in this PR, or open a follow-up.

@dkotter

dkotter commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Reversing child comments level by level when the query order is DESC matches core in both orders. Happy to push that with a test here if you want it in this PR, or open a follow-up.

Ah, good find. And yeah, might as well fix that up in this PR so feel free to push that change when you have time. Thanks!

With Discussion set to show newer comments first, the threaded query
returns every level newest first, so a reply could appear above the
comment it answers. Core's comment list keeps replies oldest first and
only reverses the top level: comments_template() queries ASC and
wp_list_comments() sets reverse_top_level from comment_order.

When the query order is DESC, reverse the replies at each level. The
order is read the way WP_Comment_Query reads it, anything but ASC is
DESC, so a filter that drops the order gets the same result.
@ekamran

ekamran commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Pushed in fbbe201. When the query order is DESC, replies are now reversed at each level, so they stay oldest first under their parent, as in core's comment list. The order is read the way WP_Comment_Query reads it, so a filter that drops order gets the same result.

The new tests cover both comment orders and the dropped-order case, and they fail on the previous head. On a test site the document now matches wp_list_comments() in both orders. I also added "oldest first" to the docs line, and updated the PR description for your threading, notes and escaping changes.

One small thing in your commit: the comment above the early return says "posts with not comments". Happy to fix that here too if you want.

@dkotter
dkotter merged commit b4b9419 into WordPress:develop Oct 8, 2026
36 checks passed
@ekamran
ekamran deleted the shifteq/markdown-comment-feeds branch October 9, 2026 08:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Markdown Feeds: comment feed URLs return the post list instead of comments

2 participants