How to Export Metadata from MDX for Next.js SEO
MDX metadata is how each article tells Next.js — and search engines — its title, description, and canonical URL.
TechnicalJan 15, 20264 min read
Exporting metadata from MDX means declaring a metadata object in the file so Next.js can drive SEO tags from the content itself. One source of truth for humans reading the note and machines indexing it.
The method
Add an exported metadata object at the top of your MDX file:
<span class="sh__line"><span class="sh__token--keyword" style="color:var(--sh-keyword)">export</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--keyword" style="color:var(--sh-keyword)">const</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--identifier" style="color:var(--sh-identifier)">metadata</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--sign" style="color:var(--sh-sign)">=</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--sign" style="color:var(--sh-sign)">{</span></span>
<span class="sh__line"><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--identifier" style="color:var(--sh-identifier)">title</span><span class="sh__token--sign" style="color:var(--sh-sign)">:</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--string" style="color:var(--sh-string)">'</span><span class="sh__token--string" style="color:var(--sh-string)">My Article Title</span><span class="sh__token--string" style="color:var(--sh-string)">'</span><span class="sh__token--sign" style="color:var(--sh-sign)">,</span></span>
<span class="sh__line"><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--identifier" style="color:var(--sh-identifier)">description</span><span class="sh__token--sign" style="color:var(--sh-sign)">:</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--string" style="color:var(--sh-string)">'</span><span class="sh__token--string" style="color:var(--sh-string)">A brief description of the article</span><span class="sh__token--string" style="color:var(--sh-string)">'</span><span class="sh__token--sign" style="color:var(--sh-sign)">,</span></span>
<span class="sh__line"><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--identifier" style="color:var(--sh-identifier)">authors</span><span class="sh__token--sign" style="color:var(--sh-sign)">:</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--sign" style="color:var(--sh-sign)">[</span><span class="sh__token--sign" style="color:var(--sh-sign)">{</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--identifier" style="color:var(--sh-identifier)">name</span><span class="sh__token--sign" style="color:var(--sh-sign)">:</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--string" style="color:var(--sh-string)">'</span><span class="sh__token--string" style="color:var(--sh-string)">Author</span><span class="sh__token--string" style="color:var(--sh-string)">'</span><span class="sh__token--sign" style="color:var(--sh-sign)">,</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--identifier" style="color:var(--sh-identifier)">url</span><span class="sh__token--sign" style="color:var(--sh-sign)">:</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--string" style="color:var(--sh-string)">'</span><span class="sh__token--string" style="color:var(--sh-string)">https://example.com</span><span class="sh__token--string" style="color:var(--sh-string)">'</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--sign" style="color:var(--sh-sign)">}</span><span class="sh__token--sign" style="color:var(--sh-sign)">]</span><span class="sh__token--sign" style="color:var(--sh-sign)">,</span></span>
<span class="sh__line"><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--identifier" style="color:var(--sh-identifier)">alternates</span><span class="sh__token--sign" style="color:var(--sh-sign)">:</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--sign" style="color:var(--sh-sign)">{</span></span>
<span class="sh__line"><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--identifier" style="color:var(--sh-identifier)">canonical</span><span class="sh__token--sign" style="color:var(--sh-sign)">:</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--string" style="color:var(--sh-string)">'</span><span class="sh__token--string" style="color:var(--sh-string)">/blog/my-article</span><span class="sh__token--string" style="color:var(--sh-string)">'</span><span class="sh__token--sign" style="color:var(--sh-sign)">,</span></span>
<span class="sh__line"><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--sign" style="color:var(--sh-sign)">}</span><span class="sh__token--sign" style="color:var(--sh-sign)">,</span></span>
<span class="sh__line"><span class="sh__token--sign" style="color:var(--sh-sign)">}</span></span>
<span class="sh__line"></span>What Next.js does
When the MDX page renders, that export feeds the App Router metadata pipeline. You get dynamic <title>, description, and related head tags without a separate database row for every field.
Practical pattern
Keep content and SEO fields together. Use generateMetadata or the file export so layout and page stay aligned:
<span class="sh__line"><span class="sh__token--keyword" style="color:var(--sh-keyword)">export</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--keyword" style="color:var(--sh-keyword)">async</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--keyword" style="color:var(--sh-keyword)">function</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--identifier" style="color:var(--sh-identifier)">generateMetadata</span><span class="sh__token--sign" style="color:var(--sh-sign)">(</span><span class="sh__token--sign" style="color:var(--sh-sign)">{</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--identifier" style="color:var(--sh-identifier)">params</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--sign" style="color:var(--sh-sign)">}</span><span class="sh__token--sign" style="color:var(--sh-sign)">:</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--class" style="color:var(--sh-class)">Props</span><span class="sh__token--sign" style="color:var(--sh-sign)">)</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--sign" style="color:var(--sh-sign)">{</span></span>
<span class="sh__line"><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--keyword" style="color:var(--sh-keyword)">const</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--identifier" style="color:var(--sh-identifier)">post</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--sign" style="color:var(--sh-sign)">=</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--keyword" style="color:var(--sh-keyword)">await</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--identifier" style="color:var(--sh-identifier)">getPost</span><span class="sh__token--sign" style="color:var(--sh-sign)">(</span><span class="sh__token--identifier" style="color:var(--sh-identifier)">params</span><span class="sh__token--sign" style="color:var(--sh-sign)">)</span></span>
<span class="sh__line"><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--keyword" style="color:var(--sh-keyword)">return</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--sign" style="color:var(--sh-sign)">{</span></span>
<span class="sh__line"><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--identifier" style="color:var(--sh-identifier)">title</span><span class="sh__token--sign" style="color:var(--sh-sign)">:</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--identifier" style="color:var(--sh-identifier)">post</span><span class="sh__token--sign" style="color:var(--sh-sign)">.</span><span class="sh__token--property" style="color:var(--sh-property)">metadata</span><span class="sh__token--sign" style="color:var(--sh-sign)">.</span><span class="sh__token--property" style="color:var(--sh-property)">title</span><span class="sh__token--sign" style="color:var(--sh-sign)">,</span></span>
<span class="sh__line"><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--identifier" style="color:var(--sh-identifier)">description</span><span class="sh__token--sign" style="color:var(--sh-sign)">:</span><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--identifier" style="color:var(--sh-identifier)">post</span><span class="sh__token--sign" style="color:var(--sh-sign)">.</span><span class="sh__token--property" style="color:var(--sh-property)">metadata</span><span class="sh__token--sign" style="color:var(--sh-sign)">.</span><span class="sh__token--property" style="color:var(--sh-property)">description</span><span class="sh__token--sign" style="color:var(--sh-sign)">,</span></span>
<span class="sh__line"><span class="sh__token--space" style="color:var(--sh-space)"> </span><span class="sh__token--sign" style="color:var(--sh-sign)">}</span></span>
<span class="sh__line"><span class="sh__token--sign" style="color:var(--sh-sign)">}</span></span>
<span class="sh__line"></span>| Field | Why it matters |
|---|---|
title | SERP and tab label |
description | Snippet / social context |
canonical | Duplicate URL control |
publishedTime | Freshness signal for articles |
| JSON-LD | Richer machine understanding |
Metadata is table stakes. Clear writing and real usefulness are the strategy.
Checklist
- Unique
descriptionper post - Canonical URL for every article
- Structured data (Article, FAQ when you have FAQs)
- Open Graph / Twitter fields for shares
publishedTimeon article-type pages
Exporting metadata from MDX is simple and worth doing on every note. It keeps SEO next to the words — which is where it belongs.
Frequently asked questions
An exported metadata object in an MDX file that Next.js can use for title, description, canonical URL, authors, and Open Graph fields — the same shape as the App Router Metadata API.
About the author
mosh
mosh is a product designer and design engineer working with design systems, LLM-powered prototypes, agent-safe interfaces, production UI, and automated workflows.
Keep reading
- The last 20% is the whole point
AI nails the first 80% of a product in minutes. The last 20% — hierarchy, edge cases, copy, point of view — is where taste lives, and it is still your job.
- Somebody still has to care
AI can write your code, but it cannot care. The human filter — nitpicking, obsessing, fixing — is what makes an app memorable.
- AI Design Engineering: How Product Teams Ship Faster Without Losing Quality
A practical guide to LLM workflows, agent-safe design systems, production UI, and human quality gates.