Skip to content
Open
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
29 changes: 3 additions & 26 deletions src/Discovery/AlternateLinkHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace MarkdownAlternate\Discovery;

use MarkdownAlternate\PostTypeSupport;
use MarkdownAlternate\Router\UrlConverter;

/**
Expand All @@ -26,30 +27,6 @@ public function register(): void {
add_action( 'wp_head', [ $this, 'output_alternate_link' ], 5 );
}

/**
* Get supported post types for markdown output.
*
* Returns an array of post types that can be served as markdown.
* Developers can extend this via the 'markdown_alternate_supported_post_types' filter.
*
* @return array List of supported post type names.
*/
private function get_supported_post_types(): array {
$default_types = [ 'post', 'page' ];
return apply_filters( 'markdown_alternate_supported_post_types', $default_types );
}

/**
* Check if a post type is supported for markdown output.
*
* @param string $post_type The post type to check.
* @return bool True if supported, false otherwise.
*/
private function is_supported_post_type( string $post_type ): bool {
return in_array( $post_type, $this->get_supported_post_types(), true );
}


/**
* Output alternate link tag for markdown version.
*
Expand All @@ -70,13 +47,13 @@ public function output_alternate_link(): void {
return;
}

// Only for published posts.
// Only for published content.
if ( get_post_status( $post ) !== 'publish' ) {
return;
}

// Only for supported post types.
if ( ! $this->is_supported_post_type( $post->post_type ) ) {
if ( ! PostTypeSupport::is_supported( $post->post_type ) ) {
return;
}

Expand Down
3 changes: 2 additions & 1 deletion src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ private function __construct() {
* @return void
*/
public static function activate(): void {
RewriteHandler::register_rules();
$handler = new RewriteHandler();
$handler->add_rewrite_rules();
flush_rewrite_rules();
}

Expand Down
40 changes: 40 additions & 0 deletions src/PostTypeSupport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php
/**
* Post type support utility for markdown output.
*
* @package MarkdownAlternate
*/

namespace MarkdownAlternate;

/**
* Shared utility for post type validation.
*
* Provides a single source of truth for determining which post types
* support markdown output.
*/
class PostTypeSupport {

/**
* Get supported post types for markdown output.
*
* Returns an array of post types that can be served as markdown.
* Developers can extend this via the 'markdown_alternate_supported_post_types' filter.
*
* @return array List of supported post type names.
*/
public static function get_supported_types(): array {
$default_types = [ 'post', 'page' ];
return apply_filters( 'markdown_alternate_supported_post_types', $default_types );
}

/**
* Check if a post type is supported for markdown output.
*
* @param string $post_type The post type to check.
* @return bool True if supported, false otherwise.
*/
public static function is_supported( string $post_type ): bool {
return in_array( $post_type, self::get_supported_types(), true );
}
}
146 changes: 61 additions & 85 deletions src/Router/RewriteHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use WP_Post;
use MarkdownAlternate\Output\ContentRenderer;
use MarkdownAlternate\PostTypeSupport;

/**
* Handles URL rewriting and markdown request processing.
Expand All @@ -22,6 +23,13 @@ class RewriteHandler {
*/
private ?WP_Post $markdown_post = null;

/**
* Cached ContentRenderer instance.
*
* @var ContentRenderer|null
*/
private ?ContentRenderer $renderer = null;

/**
* Register all hooks for URL routing.
*
Expand Down Expand Up @@ -143,7 +151,7 @@ public function parse_markdown_url(\WP $wp): void {
return;
}

if (!$this->is_supported_post_type($post->post_type)) {
if (!PostTypeSupport::is_supported($post->post_type)) {
return;
}

Expand All @@ -170,26 +178,55 @@ public function add_query_vars(array $vars): array {
}

/**
* Get supported post types for markdown output.
* Get cached ContentRenderer instance.
*
* Returns an array of post types that can be served as markdown.
* Developers can extend this via the 'markdown_alternate_supported_post_types' filter.
* @return ContentRenderer The renderer instance.
*/
private function get_renderer(): ContentRenderer {
if ($this->renderer === null) {
$this->renderer = new ContentRenderer();
}

return $this->renderer;
}

/**
* Validate post for markdown output.
*
* @return array List of supported post type names.
* @param WP_Post $post The post to validate.
* @return bool True if valid for markdown output, false otherwise.
*/
private function get_supported_post_types(): array {
$default_types = ['post', 'page'];
return apply_filters('markdown_alternate_supported_post_types', $default_types);
private function validate_post_for_markdown(WP_Post $post): bool {
if (!PostTypeSupport::is_supported($post->post_type)) {
return false;
}

if (get_post_status($post) !== 'publish') {
return false;
}

return true;
}

/**
* Check if a post type is supported for markdown output.
* Serve markdown response for a post.
*
* @param string $post_type The post type to check.
* @return bool True if supported, false otherwise.
* @param WP_Post $post The post to serve.
* @return void
*/
private function is_supported_post_type(string $post_type): bool {
return in_array($post_type, $this->get_supported_post_types(), true);
private function serve_markdown_response(WP_Post $post): void {
if (post_password_required($post)) {
status_header(403);
header('Content-Type: text/plain; charset=UTF-8');
echo 'This content is password protected.';
exit;
}

$markdown = $this->get_renderer()->render($post);

$this->set_response_headers($post, $markdown);
echo $markdown;
exit;
}

/**
Expand All @@ -201,55 +238,29 @@ private function is_supported_post_type(string $post_type): bool {
* @return void
*/
public function handle_format_parameter(): void {
// Skip if already a markdown request (URL wins over query parameter)
if (get_query_var('markdown_request')) {
return;
}

// Check for format=markdown query parameter (case-sensitive, strict equality)
$format = get_query_var('format');
if ($format !== 'markdown') {
if (get_query_var('format') !== 'markdown') {
return;
}

// Only for singular content
if (!is_singular()) {
return;
}

// Get the queried object
$post = get_queried_object();

// Validate post exists and is a WP_Post
if (!$post instanceof WP_Post) {
return;
}

// Check post type - only serve supported post types
if (!$this->is_supported_post_type($post->post_type)) {
return;
}

// Check post status - only serve published posts
if (get_post_status($post) !== 'publish') {
if (!$this->validate_post_for_markdown($post)) {
return;
}

// Check password protection
if (post_password_required($post)) {
status_header(403);
header('Content-Type: text/plain; charset=UTF-8');
echo 'This content is password protected.';
exit;
}

// Render and serve the markdown content
$renderer = new ContentRenderer();
$markdown = $renderer->render($post);

$this->set_response_headers($post, $markdown);
echo $markdown;
exit;
$this->serve_markdown_response($post);
}

/**
Expand All @@ -260,59 +271,35 @@ public function handle_format_parameter(): void {
* @return void
*/
public function handle_markdown_request(): void {
// Check if this is a markdown request
if (!get_query_var('markdown_request')) {
return;
}

// Get the request URI
$request_uri = $_SERVER['REQUEST_URI'] ?? '';

// Enforce lowercase .md extension - let WP 404 if wrong case
if (!preg_match('/\.md$/', $request_uri) && preg_match('/\.md$/i', $request_uri)) {
if (
!preg_match('/\.md$/', $request_uri) &&
preg_match('/\.md$/i', $request_uri)
) {
return;
}

// Handle trailing slash redirect: /post-slug.md/ -> /post-slug.md
if (preg_match('/\.md\/$/', $request_uri)) {
$redirect_url = rtrim($request_uri, '/');
wp_redirect($redirect_url, 301);
if (str_ends_with($request_uri, '.md/')) {
wp_redirect(rtrim($request_uri, '/'), 301);
exit;
}

// Get post - use cached post from parse_markdown_url (Nginx) or queried object (Apache)
$post = $this->markdown_post ?? get_queried_object();

// Validate post exists and is a WP_Post
if (!$post instanceof WP_Post) {
return;
}

// Check post type - only serve supported post types
if (!$this->is_supported_post_type($post->post_type)) {
return;
}

// Check post status - only serve published posts
if (get_post_status($post) !== 'publish') {
if (!$this->validate_post_for_markdown($post)) {
return;
}

// Check password protection
if (post_password_required($post)) {
status_header(403);
header('Content-Type: text/plain; charset=UTF-8');
echo 'This content is password protected.';
exit;
}

// Render and serve the markdown content
$renderer = new ContentRenderer();
$markdown = $renderer->render($post);

$this->set_response_headers($post, $markdown);
echo $markdown;
exit;
$this->serve_markdown_response($post);
}

/**
Expand Down Expand Up @@ -429,15 +416,4 @@ private function get_current_canonical_url(): ?string {
return null;
}

/**
* Register rewrite rules statically.
*
* Used by activation hook to ensure rules are registered before flush.
*
* @return void
*/
public static function register_rules(): void {
$handler = new self();
$handler->add_rewrite_rules();
}
}