Build 02: Footnote Component
On This Page
Problem
I wanted to share a report from Gemini's Deep Research. Those reports are hard to export - they're clearly generated in some kind of Markdown flavour, but you can't just export Markdown. You have to do this convoluted pathway of Gemini => Docs => Download As => Markdown.
That markdown is kind of specialized - there is unique Footnote-Link kind of markup that takes the shape .N where N is a number.
Additionally, the actual works cited likes do not, obviously, conform to the previously created reference link component.
Exploration
Here's the shape of the exported works cited:
1. With AI on the Rise, Toronto Takes No. 3 Spot in CBRE's Tech Talent Ranking, accessed January 29, 2026, [https://www.cbre.ca/press-releases/toronto-takes-number-3-spot-in-cbres-tech-talent-ranking](https://www.cbre.ca/press-releases/toronto-takes-number-3-spot-in-cbres-tech-talent-ranking)
2. Vancouver's Tech Talent: Insights from CBRE's 2025 Report, accessed January 29, 2026, [https://vantechjournal.com/p/vancouver-s-tech-talent-insights-from-cbre-s-2025-report](https://vantechjournal.com/p/vancouver-s-tech-talent-insights-from-cbre-s-2025-report)
3. Vancouver Climbs Tech Talent Rankings into Top Ten as AI 'Reshapes Landscape', accessed January 29, 2026, [https://techcouver.com/2025/09/11/vancouver-tech-talent-rankings-ai-reshapes-landscape/](https://techcouver.com/2025/09/11/vancouver-tech-talent-rankings-ai-reshapes-landscape/)And a representative sample of text interspersed with those .N ideas:
Data synthesized from 2025 CBRE Scoring Tech Talent reports and regional economic development data.1
The divergence in wages is the most striking metric. While Toronto and Vancouver have reached elite status in terms of talent volume and concentration, they remain "low-cost" hubs from the perspective of global firms. A typical 500-person tech company in Vancouver faces an estimated annual operating cost of US $41.7 million, compared to US $87 million in San Francisco.2 This cost efficiency is a double-edged sword: it attracts multinationals looking to arbitrage talent but hinders the creation of a high-liquidity founder class capable of fueling a self-sustaining venture ecosystem.1. Fetching / Splitting
The Works Cited is a clean heading at the end of the document, so I can extract them by a clean split:
const report_location =
"/app/content/ai-reports/building-canadas-tech-hub/page.mdx";
const works_cited_heading = "#### **Works cited**";
async function main() {
// pull the markdown
const fileLocation = join(cwd(), report_location);
const content = await Bun.file(fileLocation).text();
// split before and after the clean heading break
const contentParts = content.split(works_cited_heading);
// pull the second half, clean it up
const works = contentParts
.slice(1)
.join(" ")
.split("\n")
.filter((item) => item !== "")
.map((item) => item.trim());
}2. Converting the citations
const createWorksCitedPayload = (works: string[]) => {
const parts = works
.map((work) => {
const regex =
/^(?<index>\d+)\.\s+(?<label>.+?)\s+\[(?<text>.+?)\]\((?<href>.+?)\)$/;
const match = work.match(regex);
if (match && match.groups) {
return {
index: parseInt(match.groups.index),
label: match.groups.label,
href: match.groups.href,
};
}
})
.filter((part) => part !== undefined);
let referencePayload = "";
referencePayload += works_cited_heading;
referencePayload += `\n\n<References references={[\n`;
parts.map((part) => {
referencePayload += `\t{\n`;
referencePayload += `\t\thref: "${part.href}",\n`;
referencePayload += `\t\tlabel: "${part.label}",\n`;
referencePayload += `\t},\n`;
});
referencePayload += `]} />\n`;
return referencePayload;
};Using match groups a bit inefficiently in the current code - marking the index, label, link text, and actual href.
We don't wind up using the index and link text, and yet I refuse to refactor at this time. There's yet more work to do.
Then we abuse map (rather than forEach which would be more appropriate) to write the markup for the previous component.
3. Replacing .N
const replaceFootnotes = (content: string) => {
const regex = /(?<!\d)\.(?<index>\d+)(?=\s|$)/g;
return content.replaceAll(regex, (match, index) => {
const indexInt = parseInt(index);
return `.<FootnoteAnchor index={${indexInt - 1}} label="[${index}]" />`;
});
};Here, we use a negative look behind (/(?<!\d)/) to make sure that we're not accidentally selecting the second half of a decimal number, for example, 7.0.
Then, we reference a new, simple component:
export default function FootnoteAnchor(args: { index: number; label: string }) {
return (
<a
href={`#ref-${args.index}`}
className="align-super text-sm text-blue-500 dark:text-blue-300 hover:text-blue-600 dark:hover:text-blue-400"
>
{args.label}
</a>
);
}Just using tailwind to create a basic super script link idea - using the id target of hrefs to link to the appropriate citation/reference.
4. All Together
async function main() {
const fileLocation = join(cwd(), report_location);
const content = await Bun.file(fileLocation).text();
const contentParts = content.split(works_cited_heading);
const works = contentParts
.slice(1)
.join(" ")
.split("\n")
.filter((item) => item !== "")
.map((item) => item.trim());
const original = contentParts.slice(0, 1)[0];
const referencePayload = createWorksCitedPayload(works);
const withFootnotes = replaceFootnotes(original);
console.log({ withFootnotes });
const newPayload = withFootnotes + "\n" + referencePayload;
await Bun.write(join(fileLocation), newPayload);
}
main();Result
Now, we have an extensible, easy way of:
- Adding "footnotes" to our mdx arbitrarily,
- Automatically converting Gemini Deep Research back into a usefully sourced HTML document.
As before...
This isn't exactly ground-breaking research, but I hope it shows how I personally approach problems.
Next Steps...?
This code needs to be cleaned up a bit - variable names aren't great. There's room for improvement, for sure.
For now, this can sit as a tool to make it easy to import and style Deep Research Reports, so job done!