Skip to content

feat: resolve relative markdown links to internal docs routes - #31

Merged
loadinglucian merged 3 commits into
mainfrom
feat/resolve-relative-docs-links
Feb 24, 2026
Merged

feat: resolve relative markdown links to internal docs routes#31
loadinglucian merged 3 commits into
mainfrom
feat/resolve-relative-docs-links

Conversation

@loadinglucian

Copy link
Copy Markdown
Owner

Summary

  • Adds resolveRelativeDocsUrl to MarkdownService to convert relative .md links (e.g. installation.md, ../README.md) into internal /docs/{page} routes
  • Threads $sourceFilePath through toHtmltransformLinkstransformLink so the resolver can anchor relative paths against the source file's directory
  • Falls back to GitHub URLs only for links that are external, absolute, or can't be resolved within the docs directory

Test plan

  • New test in DocsSidebarNavigationTest covers: README relative link → home route, sibling .md link → internal doc route, subdirectory link → GitHub URL
  • Run php artisan test --compact tests/Feature/DocsSidebarNavigationTest.php to verify

Convert relative .md links (e.g. installation.md, ../README.md) found
in documentation files to internal /docs/{page} URLs, falling back to
GitHub URLs only for external or unresolvable links.
@coderabbitai

coderabbitai Bot commented Feb 20, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb0b959 and 6e7a7fb.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • .phpstorm.meta.php
  • _ide_helper.php
  • app/Services/MarkdownService.php
  • tests/Unit/MarkdownServiceTest.php

📝 Walkthrough

Walkthrough

DocumentService now supplies the source file path to MarkdownService when rendering. MarkdownService gained source-path-aware link resolution (including resolveRelativeDocsUrl and splitFragment) so relative markdown links can be converted to internal /docs/... routes. Tests and a fixture were added to verify this behavior.

Changes

Cohort / File(s) Summary
Markdown link & rendering logic
app/Services/MarkdownService.php
Added optional ?string $sourceFilePath parameter to toHtml() and threaded it into transformLinks() / transformLink(). Implemented resolveRelativeDocsUrl() to map relative .md links to internal /docs/... URLs and splitFragment() to preserve fragments. Link transformation now prefers resolved docs URLs before falling back to GitHub/internal generation.
Service integration
app/Services/DocumentService.php
Changed loadFromPath() call to MarkdownService::toHtml() to pass the file path and a null title when documentIdentity is readme, enabling source-aware link resolution and title inference.
Tests & fixtures
tests/Feature/DocsSidebarNavigationTest.php, tests/Fixtures/docs/docs/documentation.md
Added feature test asserting relative markdown links convert to internal routes and updated a fixture link from README.md to ../README.md to exercise relative resolution.
Unit tests
tests/Unit/MarkdownServiceTest.php
Added a unit test for rendering [!NOTE] block-style alerts with expected HTML output.
IDE/meta updates & stubs
.phpstorm.meta.php, _ide_helper.php
Updated PhpStorm meta keys related to docs cache and boost executable paths; added an empty CommandIndex Livewire component stub in IDE helper file.

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding functionality to resolve relative markdown links to internal docs routes, which matches the primary objective of the pull request.
Description check ✅ Passed The description is directly related to the changeset, clearly explaining what was added (resolveRelativeDocsUrl method), how it threads through the code, and the fallback behavior for GitHub URLs.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/Services/MarkdownService.php (1)

288-296: The page validation regex rejects subdirectory paths and certain characters.

The regex [a-z0-9-]+ at line 291 rejects:

  • Uppercase characters (e.g., README.md)
  • Underscores (e.g., getting_started.md)
  • Subdirectory paths (e.g., recipes/laravel.md → page would be recipes/laravel)

This causes the operations/runbooks.md link in the test fixture to fall through to GitHub URL, which appears intentional based on the test assertions. However, if the intent is to support nested docs in the future, this would need adjustment.

📝 Consider documenting this intentional constraint
         $page = preg_replace('/\.md$/', '', $relativeTargetPath);
 
+        // Only resolve flat docs pages (no subdirectories, lowercase alphanumeric + hyphens)
+        // Nested paths like operations/runbooks.md fall through to GitHub URLs
         if ($page === null || $page === '' || preg_match('/^[a-z0-9-]+$/', $page) !== 1) {
             return null;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Services/MarkdownService.php` around lines 288 - 296, The current
validation for $page (computed from $resolvedTargetPath and $docsPrefix) uses
preg_match('/^[a-z0-9-]+$/') which rejects uppercase letters, underscores and
subdirectory paths; update the validation in the method that computes
$relativeTargetPath/$page to use a more permissive pattern such as allowing
letters (both cases), digits, underscores, hyphens and forward slashes (e.g.
/^[A-Za-z0-9_\/-]+$/) so filenames like README.md, getting_started.md and nested
paths like recipes/laravel.md pass, and ensure preg_replace that strips '.md'
remains applied to $relativeTargetPath before the preg_match check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Services/DocumentService.php`:
- Line 84: The markdown-to-HTML call in DocumentService (the line calling
$this->markdown->toHtml in DocumentService.php) is passing $titleFallback as the
$currentSection which causes relative links to be resolved as if they live under
a subdirectory; change the call to pass null for $currentSection (same as
README) or pass the actual $sourceFilePath used for GitHub resolution so links
like [Installation](installation.md) resolve to docs/installation.md instead of
docs/<page>/installation.md; update the invocation in the method that constructs
$html and ensure any tests or callers expecting the old behavior are adjusted
accordingly.

---

Nitpick comments:
In `@app/Services/MarkdownService.php`:
- Around line 288-296: The current validation for $page (computed from
$resolvedTargetPath and $docsPrefix) uses preg_match('/^[a-z0-9-]+$/') which
rejects uppercase letters, underscores and subdirectory paths; update the
validation in the method that computes $relativeTargetPath/$page to use a more
permissive pattern such as allowing letters (both cases), digits, underscores,
hyphens and forward slashes (e.g. /^[A-Za-z0-9_\/-]+$/) so filenames like
README.md, getting_started.md and nested paths like recipes/laravel.md pass, and
ensure preg_replace that strips '.md' remains applied to $relativeTargetPath
before the preg_match check.

}

$html = $this->markdown->toHtml($content);
$html = $this->markdown->toHtml($content, $documentIdentity === 'readme' ? null : $titleFallback, $filePath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how currentSection is used in resolveGitHubUrl
echo "=== resolveGitHubUrl implementation ==="
rg -nA 20 'private function resolveGitHubUrl' app/Services/MarkdownService.php

echo -e "\n=== loadFromPath calls and titleFallback usage ==="
rg -nB 2 -A 5 'toHtml' app/Services/DocumentService.php | head -50

echo -e "\n=== MarkdownService toHtml signature ==="
rg -nA 5 'public function toHtml' app/Services/MarkdownService.php

Repository: loadinglucian/deployerphp.com

Length of output: 1633


🏁 Script executed:

#!/bin/bash
# Get complete resolveGitHubUrl function
echo "=== Complete resolveGitHubUrl implementation ==="
rg -nA 40 'private function resolveGitHubUrl' app/Services/MarkdownService.php

echo -e "\n=== How titleFallback is set in DocumentService ==="
rg -nB 10 'titleFallback' app/Services/DocumentService.php | grep -A 10 -B 10 'titleFallback ='

echo -e "\n=== toHtml method body to see how currentSection is used ==="
rg -nA 30 'public function toHtml' app/Services/MarkdownService.php

Repository: loadinglucian/deployerphp.com

Length of output: 2949


🏁 Script executed:

#!/bin/bash
# Check the docs directory structure
echo "=== Docs directory structure ==="
find docs -type f -name "*.md" 2>/dev/null | head -30

echo -e "\n=== How titleFallback is populated in DocumentService ==="
rg -nB 20 'toHtml' app/Services/DocumentService.php | grep -A 15 -B 15 'titleFallback'

echo -e "\n=== Load method to see how titleFallback comes from file path ==="
rg -nA 10 'public function load' app/Services/DocumentService.php | head -30

Repository: loadinglucian/deployerphp.com

Length of output: 1429


🏁 Script executed:

#!/bin/bash
# Check the loadFromPath signature and parameters
echo "=== DocumentService loadFromPath signature ==="
rg -nA 20 'private function loadFromPath' app/Services/DocumentService.php

echo -e "\n=== Check if docs directory exists and structure ==="
ls -la docs/ 2>/dev/null || echo "docs/ not found, checking other locations"
find . -maxdepth 2 -type d -name docs 2>/dev/null

echo -e "\n=== Check if there are test files or fixtures showing docs structure ==="
find . -name "*.md" -type f 2>/dev/null | head -20

echo -e "\n=== Check config for docs.github.dir ==="
rg -n 'docs\.github' config/

Repository: loadinglucian/deployerphp.com

Length of output: 1253


🏁 Script executed:

#!/bin/bash
# Check the test fixture docs structure
echo "=== Test fixture docs structure ==="
tree tests/Fixtures/docs/ 2>/dev/null || find tests/Fixtures/docs/ -type f

echo -e "\n=== Check test files that use these docs ==="
rg -l 'DocumentService' tests/ | head -5

echo -e "\n=== Look at a test file to understand expected behavior ==="
find tests/ -name "*Document*Test*" -type f -exec cat {} \; | head -100

echo -e "\n=== Check what config value is used for docs.github.dir ==="
rg -n "docs\.github\.dir" . 2>/dev/null

Repository: loadinglucian/deployerphp.com

Length of output: 3785


🏁 Script executed:

#!/bin/bash
# Check MarkdownServiceTest for GitHub URL tests
echo "=== MarkdownServiceTest GitHub URL tests ==="
rg -nA 20 'github' tests/Unit/MarkdownServiceTest.php -i

echo -e "\n=== Check if there are tests for resolveGitHubUrl specifically ==="
rg -nA 15 'resolveGitHubUrl\|currentSection' tests/Unit/MarkdownServiceTest.php

echo -e "\n=== Look at the actual test fixtures to see how links are expected to work ==="
cat tests/Fixtures/docs/docs/link-behavior.md

Repository: loadinglucian/deployerphp.com

Length of output: 2701


🏁 Script executed:

#!/bin/bash
# Check if there are tests for actual page loading with links
echo "=== Check DocumentServiceTest for link behavior ==="
cat tests/Unit/DocumentServiceTest.php

echo -e "\n=== Check if docs structure is hierarchical or flat in fixture ==="
rg -n "relative\|link" tests/Fixtures/docs/docs/*.md -i -A 2 -B 2

Repository: loadinglucian/deployerphp.com

Length of output: 3042


🏁 Script executed:

#!/bin/bash
# Read the actual fixture docs
echo "=== installation.md ==="
cat tests/Fixtures/docs/docs/installation.md

echo -e "\n=== documentation.md ==="
cat tests/Fixtures/docs/docs/documentation.md

echo -e "\n=== link-behavior.md ==="
cat tests/Fixtures/docs/docs/link-behavior.md

Repository: loadinglucian/deployerphp.com

Length of output: 752


🏁 Script executed:

#!/bin/bash
# Test what happens when documentation.md (flat structure) contains relative links
echo "=== Test the actual behavior with flat docs ==="
cat > /tmp/test_url.php << 'EOF'
<?php
// Simulate the resolveGitHubUrl logic
function testResolveGitHubUrl(string $href, ?string $currentSection): string {
    $docsDir = 'docs';
    $baseUrl = 'https://github.com/acme/repo/blob/main';
    
    // Handle .md links without slashes
    if (str_ends_with($href, '.md')) {
        if ($currentSection !== null && ! str_contains($href, '/')) {
            return "{$baseUrl}/{$docsDir}/{$currentSection}/{$href}";
        }
        return "{$baseUrl}/{$docsDir}/{$href}";
    }
    
    // Other relative paths
    if ($currentSection !== null && ! str_contains($href, '/')) {
        return "{$baseUrl}/{$docsDir}/{$currentSection}/{$href}.md";
    }
    return "{$baseUrl}/{$docsDir}/{$href}.md";
}

// Test case: documentation.md page with relative links to installation.md
echo "When documentation page contains '[Installation](installation.md)':\n";
echo "Generated URL: " . testResolveGitHubUrl('installation.md', 'documentation') . "\n";
echo "Expected URL:  https://github.com/acme/repo/blob/main/docs/installation.md\n\n";

// Test case: link with slash (should work correctly)
echo "When page contains '[Operations](operations/runbooks.md)':\n";
echo "Generated URL: " . testResolveGitHubUrl('operations/runbooks.md', 'documentation') . "\n";
echo "Expected URL:  https://github.com/acme/repo/blob/main/docs/operations/runbooks.md\n";
EOF

php /tmp/test_url.php

Repository: loadinglucian/deployerphp.com

Length of output: 576


Fix incorrect GitHub URL generation for relative links in flat docs structure.

Passing $titleFallback (the page slug) as $currentSection causes relative links without directory separators to generate incorrect GitHub URLs. For example, when documentation.md contains [Installation](installation.md), it generates https://github.com/repo/blob/main/docs/documentation/installation.md instead of https://github.com/repo/blob/main/docs/installation.md.

In a flat docs structure where pages like installation.md and documentation.md live directly in the docs directory, they should not have a "current section" prepended. The null value passed for README is correct; regular pages should use the same approach or $sourceFilePath should be used instead for GitHub URL resolution.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Services/DocumentService.php` at line 84, The markdown-to-HTML call in
DocumentService (the line calling $this->markdown->toHtml in
DocumentService.php) is passing $titleFallback as the $currentSection which
causes relative links to be resolved as if they live under a subdirectory;
change the call to pass null for $currentSection (same as README) or pass the
actual $sourceFilePath used for GitHub resolution so links like
[Installation](installation.md) resolve to docs/installation.md instead of
docs/<page>/installation.md; update the invocation in the method that constructs
$html and ensure any tests or callers expecting the old behavior are adjusted
accordingly.

@loadinglucian
loadinglucian merged commit bce7561 into main Feb 24, 2026
6 checks passed
@loadinglucian
loadinglucian deleted the feat/resolve-relative-docs-links branch February 24, 2026 08:49
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.

1 participant