PR preview comments

A GitHub Action that comments on content pull requests with instant /tree/ preview links for every changed page.

Every branch of your docs is already live at /tree/:branch β€” nothing to build, nothing to deploy. This optional GitHub Action closes the loop: when a pull request touches content/, it posts (and keeps updated) a comment linking each changed page to its live preview.

The workflow

Create .github/workflows/docs-preview-comment.yml in your docs repository. Replace https://docs.example.com with your production URL, and adjust content/ if your content directory differs:

.github/workflows/docs-preview-comment.yml
name: docs preview comment

on:
  pull_request:
    branches:
      - main
    paths:
      - 'content/**'
    types:
      - opened
      - reopened
      - synchronize

permissions:
  contents: read
  pull-requests: write

jobs:
  comment:
    if: github.event.pull_request.head.repo.full_name == github.repository
    runs-on: ubuntu-latest

    steps:
      - name: Create or update preview comment
        uses: actions/github-script@v8
        env:
          HEAD_BRANCH: ${{ github.head_ref }}
        with:
          script: |
            const marker = '<!-- comark-docs-preview -->'
            const previewRoot = `https://docs.example.com/tree/${encodeURIComponent(process.env.HEAD_BRANCH)}`

            const { owner, repo } = context.repo
            const pull_number = context.issue.number
            const files = await github.paginate(github.rest.pulls.listFiles, {
              owner,
              repo,
              pull_number,
              per_page: 100,
            })

            const pages = files
              .filter(file =>
                file.status !== 'removed'
                && file.filename.startsWith('content/')
                && file.filename.endsWith('.md'),
              )
              .map(file => {
                const relativePath = file.filename
                  .slice('content/'.length)
                  .replace(/\.md$/, '')
                const routeSegments = relativePath
                  .split('/')
                  .map(segment => segment.replace(/^\d+\./, ''))
                  .map(encodeURIComponent)
                if (routeSegments.at(-1) === 'index') {
                  routeSegments.pop()
                }
                const route = routeSegments.join('/')

                return {
                  filename: file.filename,
                  route,
                  url: route ? `${previewRoot}/${route}` : `${previewRoot}/`,
                }
              })
              .sort((a, b) => a.filename.localeCompare(b.filename))

            const body = [
              marker,
              '## Documentation previews',
              '',
              `πŸ“š [Preview all documentation changes](${previewRoot})`,
              ...(pages.length
                ? [
                    '',
                    ...pages.map(page => `- [/${page.route}](${page.url})`),
                  ]
                : []),
            ].join('\n')

            const issue_number = context.issue.number
            const comments = await github.paginate(github.rest.issues.listComments, {
              owner,
              repo,
              issue_number,
              per_page: 100,
            })
            const existingComment = comments.find(comment =>
              comment.user?.type === 'Bot' && comment.body?.includes(marker),
            )

            if (existingComment) {
              await github.rest.issues.updateComment({
                owner,
                repo,
                comment_id: existingComment.id,
                body,
              })
            } else {
              await github.rest.issues.createComment({
                owner,
                repo,
                issue_number,
                body,
              })
            }

How it works

  • The paths filter means the workflow only runs when the PR touches content/.
  • Each changed Markdown file is mapped to its route the same way the site does it: numeric prefixes stripped from every segment, trailing index removed. content/1.getting-started/2.installation.md becomes /tree/<branch>/getting-started/installation.
  • The comment carries a hidden HTML marker, so subsequent pushes update the existing comment instead of stacking new ones.
  • The if guard skips forks: the preview URL renders branches of your repository, so fork branches have nothing to link to. Removed files are excluded since their preview would 404.

No token setup is needed β€” the workflow only uses the built-in GITHUB_TOKEN with pull-requests: write permission.

Trying it out

Open a pull request that edits a page under content/, and the comment appears within seconds:

Documentation previews

πŸ“š Preview all documentation changes

Reviewers can read the rendered pages β€” navigation, search, and all β€” before the PR merges, and the links stay current as the branch moves.