SkillsMay 11, 2026·4 min read

Perplexity Citations — Render Source Footnotes in Your UI

Parse Perplexity inline citation markers ([1][2][3]) + the citations URL array into clickable footnote UI. Markdown render, hover preview.

Agent ready

This asset can be read and installed directly by agents

TokRepo exposes a universal CLI command, install contract, metadata JSON, adapter-aware plan, and raw content links so agents can judge fit, risk, and next actions.

Native · 98/100Policy: allow
Agent surface
Any MCP/CLI agent
Kind
Skill
Install
Single
Trust
Trust: New
Entrypoint
Asset
Universal CLI install command
npx tokrepo install dd02260f-0044-4e5a-8e2d-5849c95fd587
Intro

Perplexity Sonar returns inline citation markers like [1], [2] embedded in the markdown answer, plus a separate citations URL array. To turn this into a polished UI you need to: parse the markers, attach them to URLs by index, render as clickable superscript footnotes, and ideally show a preview tooltip. This skill is the React + Markdown pattern that does all four. Best for: any product that surfaces Sonar answers in a UI. Works with: react-markdown, remark/rehype, any Markdown renderer. Setup time: 15 minutes.


React component

import ReactMarkdown from "react-markdown";

interface Props { content: string; citations: string[] }

export function SonarAnswer({ content, citations }: Props) {
  // Replace [1] [2] etc with HTML superscript anchors
  const withFootnotes = content.replace(/\[(\d+)\]/g, (_, n) => {
    const idx = parseInt(n) - 1;
    const url = citations[idx];
    if (!url) return `[${n}]`;
    return `<sup><a href="${url}" target="_blank" rel="noopener noreferrer" title="${url}">[${n}]</a></sup>`;
  });

  return (
    <div className="prose">
      <ReactMarkdown
        components={{ a: ({ node, ...props }) => <a {...props} className="text-blue-600 hover:underline" /> }}
        rehypePlugins={[]}
        skipHtml={false}
      >
        {withFootnotes}
      </ReactMarkdown>

      <h3 className="mt-6 text-sm font-semibold">Sources</h3>
      <ol className="text-sm">
        {citations.map((url, i) => (
          <li key={i}>
            <span className="text-gray-500">[{i + 1}]</span>
            <a href={url} target="_blank" rel="noopener noreferrer" className="ml-2">{new URL(url).hostname}</a>
          </li>
        ))}
      </ol>
    </div>
  );
}

Hover preview tooltip (optional)

import { Tooltip } from "@radix-ui/react-tooltip";

function CitationSup({ n, url }: { n: number; url: string }) {
  return (
    <Tooltip>
      <Tooltip.Trigger asChild>
        <sup>
          <a href={url} target="_blank" rel="noopener noreferrer">[{n}]</a>
        </sup>
      </Tooltip.Trigger>
      <Tooltip.Content className="bg-black text-white px-2 py-1 rounded text-xs">
        {new URL(url).hostname} — click to open
      </Tooltip.Content>
    </Tooltip>
  );
}

Link safety checklist

  • target="_blank" + rel="noopener noreferrer" — prevents reverse tabnabbing
  • Sanitize URLs — reject javascript:, data:, anything not http(s)
  • Don't auto-fetch the source server-side without consent — privacy
  • Log click-throughs separately to attribute traffic to Sonar in analytics

Edge cases to handle

Case Handling
Marker [42] but only 5 citations Render as plain [42] (no link)
Same citation cited 3× Same number; deduplicate in Sources list
Citation URL with tracking params Strip utm_* before display
Non-HTTP URL Drop or display as plain text

FAQ

Q: What if a marker has no matching citation? A: Render the raw marker without a link. Don't error out — sometimes Sonar refers to inline context the user knows. Logging the mismatch is useful for future debugging.

Q: Should I prefetch source pages? A: No — privacy + cost issues. Let users click through. If you want richer previews, use the source URL's og:image and og:description via a separate metadata fetch with cache.

Q: Can I get structured citations server-side? A: Sonar Pro returns search_results array alongside citations with title + URL + last_updated. That's the structured form for richer rendering.


Quick Use

  1. Get content and citations[] from Sonar response
  2. Regex [\d+] markers in content, replace with <sup><a> linking to citations[i-1]
  3. Render with react-markdown allowing inline HTML, append a Sources list

Intro

Perplexity Sonar returns inline citation markers like [1], [2] embedded in the markdown answer, plus a separate citations URL array. To turn this into a polished UI you need to: parse the markers, attach them to URLs by index, render as clickable superscript footnotes, and ideally show a preview tooltip. This skill is the React + Markdown pattern that does all four. Best for: any product that surfaces Sonar answers in a UI. Works with: react-markdown, remark/rehype, any Markdown renderer. Setup time: 15 minutes.


React component

import ReactMarkdown from "react-markdown";

interface Props { content: string; citations: string[] }

export function SonarAnswer({ content, citations }: Props) {
  // Replace [1] [2] etc with HTML superscript anchors
  const withFootnotes = content.replace(/\[(\d+)\]/g, (_, n) => {
    const idx = parseInt(n) - 1;
    const url = citations[idx];
    if (!url) return `[${n}]`;
    return `<sup><a href="${url}" target="_blank" rel="noopener noreferrer" title="${url}">[${n}]</a></sup>`;
  });

  return (
    <div className="prose">
      <ReactMarkdown
        components={{ a: ({ node, ...props }) => <a {...props} className="text-blue-600 hover:underline" /> }}
        rehypePlugins={[]}
        skipHtml={false}
      >
        {withFootnotes}
      </ReactMarkdown>

      <h3 className="mt-6 text-sm font-semibold">Sources</h3>
      <ol className="text-sm">
        {citations.map((url, i) => (
          <li key={i}>
            <span className="text-gray-500">[{i + 1}]</span>
            <a href={url} target="_blank" rel="noopener noreferrer" className="ml-2">{new URL(url).hostname}</a>
          </li>
        ))}
      </ol>
    </div>
  );
}

Hover preview tooltip (optional)

import { Tooltip } from "@radix-ui/react-tooltip";

function CitationSup({ n, url }: { n: number; url: string }) {
  return (
    <Tooltip>
      <Tooltip.Trigger asChild>
        <sup>
          <a href={url} target="_blank" rel="noopener noreferrer">[{n}]</a>
        </sup>
      </Tooltip.Trigger>
      <Tooltip.Content className="bg-black text-white px-2 py-1 rounded text-xs">
        {new URL(url).hostname} — click to open
      </Tooltip.Content>
    </Tooltip>
  );
}

Link safety checklist

  • target="_blank" + rel="noopener noreferrer" — prevents reverse tabnabbing
  • Sanitize URLs — reject javascript:, data:, anything not http(s)
  • Don't auto-fetch the source server-side without consent — privacy
  • Log click-throughs separately to attribute traffic to Sonar in analytics

Edge cases to handle

Case Handling
Marker [42] but only 5 citations Render as plain [42] (no link)
Same citation cited 3× Same number; deduplicate in Sources list
Citation URL with tracking params Strip utm_* before display
Non-HTTP URL Drop or display as plain text

FAQ

Q: What if a marker has no matching citation? A: Render the raw marker without a link. Don't error out — sometimes Sonar refers to inline context the user knows. Logging the mismatch is useful for future debugging.

Q: Should I prefetch source pages? A: No — privacy + cost issues. Let users click through. If you want richer previews, use the source URL's og:image and og:description via a separate metadata fetch with cache.

Q: Can I get structured citations server-side? A: Sonar Pro returns search_results array alongside citations with title + URL + last_updated. That's the structured form for richer rendering.


Source & Thanks

Pattern compiled from Perplexity API docs + react-markdown.

remarkjs/react-markdown — ⭐ 13,000+

🙏

Source & Thanks

Pattern compiled from Perplexity API docs + react-markdown.

remarkjs/react-markdown — ⭐ 13,000+

Discussion

Sign in to join the discussion.
No comments yet. Be the first to share your thoughts.

Related Assets