PR preview comments
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:
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
pathsfilter means the workflow only runs when the PR touchescontent/. - Each changed Markdown file is mapped to its route the same way the site does it: numeric prefixes stripped from every segment, trailing
indexremoved.content/1.getting-started/2.installation.mdbecomes/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
ifguard 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
Reviewers can read the rendered pages β navigation, search, and all β before the PR merges, and the links stay current as the branch moves.