Quick Use
- Get
contentandcitations[]from Sonar response - Regex
[\d+]markers in content, replace with<sup><a>linking to citations[i-1] - 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+