Skip to content

Instantly share code, notes, and snippets.

@simonw
Created February 3, 2025 22:43
Show Gist options
  • Save simonw/cd1afb97e595b40fdeedebb48be7f4f1 to your computer and use it in GitHub Desktop.
Save simonw/cd1afb97e595b40fdeedebb48be7f4f1 to your computer and use it in GitHub Desktop.

GitHub Issue to Markdown Converter

human (Feb 3, 2025, 02:08 PM)

Build a tool that turns a link to a GitHub issues thread into markdown I can copy out - the markdown should include the title and body of the issue and the body of every attached comment

Include a copy to clipboard button

Use the GitHub public api and fetch()

assistant (Feb 3, 2025, 02:09 PM)

I'll help create a tool that converts GitHub issue threads to markdown with a copy button.

create GitHub Issue to Markdown Converter

<!DOCTYPE html>
<html>
<head>
  <style>
  * {
    box-sizing: border-box;
  }

  body {
    font-family: Helvetica, Arial, sans-serif;
    max-width: 800px;
    margin: 0 auto;
    padding: 20px;
  }

  h1 {
    margin-bottom: 24px;
  }

  .input-group {
    margin-bottom: 24px;
  }

  label {
    display: block;
    margin-bottom: 8px;
  }

  input {
    width: 100%;
    padding: 8px;
    font-size: 16px;
    border: 1px solid #ccc;
    border-radius: 4px;
  }

  button {
    background: #2ea44f;
    color: white;
    border: none;
    padding: 8px 16px;
    border-radius: 4px;
    cursor: pointer;
  }

  button:hover {
    background: #2c974b;
  }

  button:disabled {
    background: #94d3a2;
    cursor: not-allowed;
  }

  .output {
    margin-top: 24px;
  }

  textarea {
    width: 100%;
    min-height: 300px;
    padding: 12px;
    font-size: 16px;
    font-family: monospace;
    border: 1px solid #ccc;
    border-radius: 4px;
    resize: vertical;
  }

  .error {
    color: #cf222e;
    margin-top: 8px;
  }

  .copy-button {
    margin-top: 12px;
  }
  </style>
</head>
<body>
  <h1>Convert GitHub issue to markdown</h1>

  <div class="input-group">
    <label for="issue-url">GitHub issue URL</label>
    <input 
      type="text" 
      id="issue-url"
      placeholder="https://github.com/owner/repo/issues/123"
    >
  </div>

  <button id="convert">Convert to markdown</button>
  
  <div class="output">
    <textarea id="markdown-output" readonly></textarea>
    <button class="copy-button" id="copy">Copy to clipboard</button>
  </div>

  <p class="error" id="error"></p>

<script type="module">
const urlInput = document.getElementById('issue-url')
const convertButton = document.getElementById('convert')
const markdownOutput = document.getElementById('markdown-output')
const copyButton = document.getElementById('copy')
const errorElement = document.getElementById('error')

function parseGitHubUrl(url) {
  try {
    const urlObj = new URL(url)
    const [, owner, repo, , number] = urlObj.pathname.split('/')
    return { owner, repo, number }
  } catch (e) {
    throw new Error('Invalid GitHub URL')
  }
}

function convertToMarkdown(issue, comments) {
  let md = `# ${issue.title}\n\n`
  md += `*Posted by @${issue.user.login}*\n\n`
  md += issue.body + '\n\n'
  
  if (comments.length > 0) {
    md += '---\n\n'
    comments.forEach(comment => {
      md += `### Comment by @${comment.user.login}\n\n`
      md += comment.body + '\n\n'
      md += '---\n\n'
    })
  }
  
  return md
}

async function fetchIssueAndComments(owner, repo, number) {
  const issueUrl = `https://api.github.com/repos/${owner}/${repo}/issues/${number}`
  const commentsUrl = `${issueUrl}/comments`
  
  const [issueRes, commentsRes] = await Promise.all([
    fetch(issueUrl),
    fetch(commentsUrl)
  ])

  if (!issueRes.ok || !commentsRes.ok) {
    throw new Error('Failed to fetch issue data')
  }

  const issue = await issueRes.json()
  const comments = await commentsRes.json()
  
  return { issue, comments }
}

convertButton.addEventListener('click', async () => {
  errorElement.textContent = ''
  markdownOutput.value = ''
  convertButton.disabled = true
  
  try {
    const { owner, repo, number } = parseGitHubUrl(urlInput.value)
    const { issue, comments } = await fetchIssueAndComments(owner, repo, number)
    const markdown = convertToMarkdown(issue, comments)
    markdownOutput.value = markdown
  } catch (error) {
    errorElement.textContent = error.message
  } finally {
    convertButton.disabled = false
  }
})

copyButton.addEventListener('click', () => {
  markdownOutput.select()
  document.execCommand('copy')
})

</script>
</body>
</html>

I've created a simple web tool that:

  1. Takes a GitHub issue URL as input
  2. Fetches the issue data and comments using the GitHub API
  3. Converts everything to markdown format
  4. Provides a copy button to easily copy the output

To use it:

  1. Paste a GitHub issue URL (e.g. https://github.com/owner/repo/issues/123)
  2. Click "Convert to markdown"
  3. The formatted markdown will appear in the textarea
  4. Click "Copy to clipboard" to copy it

The markdown format includes:

  • Issue title as a heading
  • Original poster's username
  • Issue body
  • All comments with usernames and separators

Let me know if you'd like any adjustments to the formatting or functionality!

human (Feb 3, 2025, 02:26 PM)

Copy button should change text to "Copied" for a few seconds then change back

assistant (Feb 3, 2025, 02:27 PM)

I'll update the copy button behavior.

update github-to-md

Find this:

copyButton.addEventListener('click', () => {
  markdownOutput.select()
  document.execCommand('copy')
})

Replace with this:

copyButton.addEventListener('click', () => {
  markdownOutput.select()
  document.execCommand('copy')
  
  const originalText = copyButton.textContent
  copyButton.textContent = 'Copied'
  copyButton.disabled = true
  
  setTimeout(() => {
    copyButton.textContent = originalText
    copyButton.disabled = false
  }, 2000)
})

I've updated the copy button so that:

  1. When clicked, it changes text to "Copied"
  2. The button is temporarily disabled to prevent multiple clicks
  3. After 2 seconds, it reverts to "Copy to clipboard" and becomes enabled again

The button will now provide better visual feedback when the copy action succeeds. Would you like any other improvements to the interface?

human (Feb 3, 2025, 02:28 PM)

Make sure it handles pagination if there are many comments

assistant (Feb 3, 2025, 02:28 PM)

I'll update the code to handle GitHub's pagination using the Link header.

update github-to-md

Find this:

async function fetchIssueAndComments(owner, repo, number) {
  const issueUrl = `https://api.github.com/repos/${owner}/${repo}/issues/${number}`
  const commentsUrl = `${issueUrl}/comments`
  
  const [issueRes, commentsRes] = await Promise.all([
    fetch(issueUrl),
    fetch(commentsUrl)
  ])

  if (!issueRes.ok || !commentsRes.ok) {
    throw new Error('Failed to fetch issue data')
  }

  const issue = await issueRes.json()
  const comments = await commentsRes.json()
  
  return { issue, comments }

Replace with this:

async function getAllPages(url) {
  let allItems = []
  let nextUrl = url
  
  while (nextUrl) {
    const response = await fetch(nextUrl)
    
    if (!response.ok) {
      throw new Error('Failed to fetch data')
    }
    
    const items = await response.json()
    allItems = allItems.concat(items)
    
    // Check for pagination in Link header
    const link = response.headers.get('Link')
    nextUrl = null
    
    if (link) {
      const nextLink = link.split(',').find(s => s.includes('rel="next"'))
      if (nextLink) {
        nextUrl = nextLink.split(';')[0].trim().slice(1, -1)
      }
    }
  }
  
  return allItems
}

async function fetchIssueAndComments(owner, repo, number) {
  const issueUrl = `https://api.github.com/repos/${owner}/${repo}/issues/${number}`
  const commentsUrl = `${issueUrl}/comments`
  
  const [issue, comments] = await Promise.all([
    fetch(issueUrl).then(res => {
      if (!res.ok) throw new Error('Failed to fetch issue')
      return res.json()
    }),
    getAllPages(commentsUrl)
  ])
  
  return { issue, comments }

I've updated the code to:

  1. Add a new getAllPages function that:

    • Follows the GitHub API pagination using the Link header
    • Recursively fetches all pages of comments
    • Concatenates the results into a single array
  2. Modified fetchIssueAndComments to:

    • Use the new pagination-aware function for comments
    • Keep the single request for the issue itself (since issues aren't paginated)
    • Handle errors more specifically

Now the tool will correctly handle issues with any number of comments. GitHub typically paginates at 30 comments per page, so this ensures we get the complete thread no matter how long it is.

Would you like any other improvements to the functionality?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment