Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,16 @@ add_filter('post_content_to_markdown/post_allowed', function ($allowed, $post) {

**Default:** `true`

#### `post_content_to_markdown/emit_vary`

Controls whether the plugin appends `Vary: Accept` to every front-end response. Enabled by default so that downstream caches (browsers, proxies, CDNs) key on the Accept header and don't cross-serve an HTML response to a Markdown request (or vice versa). Disable if you don't want the plugin touching HTML response headers — Markdown responses will still include `Vary: Accept` regardless, so content negotiation stays correct for direct Markdown hits.

```php
add_filter('post_content_to_markdown/emit_vary', '__return_false');
```

**Default:** `true`

### Feed filters

#### `post_content_to_markdown/feed_post_types`
Expand Down
85 changes: 77 additions & 8 deletions post-content-to-markdown.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/**
* Plugin Name: Post Content to Markdown
* Description: Serve post content as Markdown via Accept headers or query parameters.
* Version: 1.4.0
* Version: 1.5.0
* Author: roots.io
* Requires PHP: 8.1
*/
Expand All @@ -26,21 +26,69 @@
}

/**
* Check if Markdown format is requested via Accept header or query parameter
* Return the q-value for a media type in an Accept header, picking the q of
* the most specific matching entry (exact > type/* > *\/*) per RFC 9110.
*/
function isMarkdownRequested()
function acceptQuality($accept, $type, $subtype)
{
// Check Accept header
if (isset($_SERVER['HTTP_ACCEPT']) && str_contains($_SERVER['HTTP_ACCEPT'], 'text/markdown')) {
return true;
$best = 0.0;
$bestSpecificity = -1;

foreach (explode(',', $accept) as $entry) {
$entry = trim($entry);
if ($entry === '') {
continue;
}

$parts = explode(';', $entry);
[$t, $s] = array_pad(explode('/', strtolower(trim(array_shift($parts))), 2), 2, '');

$specificity = match (true) {
$t === $type && $s === $subtype => 2,
$t === $type && $s === '*' => 1,
$t === '*' && $s === '*' => 0,
default => -1,
};
if ($specificity < 0) {
continue;
}

$q = 1.0;
foreach ($parts as $param) {
$param = trim($param);
if (stripos($param, 'q=') === 0) {
$q = (float) substr($param, 2);
}
}

if ($specificity > $bestSpecificity || ($specificity === $bestSpecificity && $q > $best)) {
$best = $q;
$bestSpecificity = $specificity;
}
}

// Check query parameter
return $best;
}

/**
* Check if Markdown is preferred over HTML via Accept header q-values,
* or requested explicitly via ?format=markdown.
*/
function isMarkdownRequested()
{
if (isset($_GET['format']) && $_GET['format'] === 'markdown') {
return true;
}

return false;
$accept = $_SERVER['HTTP_ACCEPT'] ?? '';
if ($accept === '') {
return false;
}

$markdown = acceptQuality($accept, 'text', 'markdown');
$html = acceptQuality($accept, 'text', 'html');

return $markdown > 0.0 && $markdown > $html;
}

/**
Expand All @@ -53,6 +101,23 @@ function isMarkdownRequested()
}
});

/**
* Advertise that responses vary by Accept header so caches key on it. Applied to
* every front-end response (not just Markdown) so an HTML response cached first
* can't be served to a later Markdown request for the same URL.
*/
add_action('send_headers', function () {
if (is_admin()) {
return;
}

if (! apply_filters('post_content_to_markdown/emit_vary', true)) {
return;
}

header('Vary: Accept', false);
});

/**
* Serve content as Markdown based on Accept header or query parameter
*/
Expand Down Expand Up @@ -90,6 +155,7 @@ function isMarkdownRequested()
}

header('Content-Type: text/markdown; charset='.get_option('blog_charset'));
header('Vary: Accept', false);
echo '# '.strip_tags($post->post_title)."\n\n".contentToMarkdown($post->post_content);
exit;
}
Expand All @@ -112,6 +178,7 @@ function outputMarkdownFeed()
$cached_feed = get_transient($cache_key);
if ($cached_feed !== false) {
header('Content-Type: text/markdown; charset='.get_option('blog_charset'));
header('Vary: Accept', false);
echo $cached_feed;
exit;
}
Expand Down Expand Up @@ -204,6 +271,7 @@ function outputMarkdownFeed()
set_transient($cache_key, $feed_content, apply_filters('post_content_to_markdown/feed_cache_duration', HOUR_IN_SECONDS));

header('Content-Type: text/markdown; charset='.get_option('blog_charset'));
header('Vary: Accept', false);
echo $feed_content;
exit;
}
Expand All @@ -214,6 +282,7 @@ function outputMarkdownFeed()
function outputSinglePostCommentFeed($post)
{
header('Content-Type: text/markdown; charset='.get_option('blog_charset'));
header('Vary: Accept', false);

echo '# '.strip_tags($post->post_title)."\n\n";

Expand Down
Loading