Skip to content

Commit e8d8767

Browse files
iitzIrFaniitzIrFangioboa
authored
docs: add useContent headings guide (#8073)
--------- Co-authored-by: iitzIrFan <[email protected]> Co-authored-by: gioboa <[email protected]>
1 parent a748c30 commit e8d8767

File tree

1 file changed

+166
-0
lines changed
  • packages/docs/src/routes/docs/(qwikcity)/guides/mdx

1 file changed

+166
-0
lines changed

packages/docs/src/routes/docs/(qwikcity)/guides/mdx/index.mdx

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,169 @@ The `headings` array includes data about a markdown file's `<h1>` to `<h6>` [htm
210210

211211
Menus are contextual data declared with `menu.md` files. See [menus file definition](/docs/(qwikcity)/advanced/menu/index.mdx) for more information on the file format and location.
212212

213+
# Dynamic Page Navigation with MDX
214+
215+
When working with documentation or content-heavy pages in Qwik City, you often need to generate a table of contents or sidebar navigation based on the page's content. Qwik City provides a built-in solution for this through the `useContent()` hook, which can automatically extract headings from your MDX files.
216+
217+
## Using `useContent()` for Page Navigation
218+
219+
The `useContent()` hook allows you to access metadata about your current MDX page, including all its headings. This is particularly useful for:
220+
221+
- Creating a table of contents for long articles
222+
- Building dynamic sidebar navigation
223+
- Implementing "jump to section" functionality
224+
- Generating progress indicators for article sections
225+
226+
Here's a complete example of how to create a dynamic table of contents:
227+
228+
```tsx
229+
import { component$, useContent } from '@builder.io/qwik';
230+
231+
export const TableOfContents = component$(() => {
232+
const content = useContent();
233+
234+
return (
235+
<nav class="toc">
236+
<h4>On this page</h4>
237+
<ul>
238+
{content.headings?.map((heading) => (
239+
<li
240+
key={heading.id}
241+
style={{
242+
// Indent based on heading level
243+
marginLeft: `${(heading.level - 1) * 12}px`
244+
}}
245+
>
246+
<a href={`#${heading.id}`}>{heading.text}</a>
247+
</li>
248+
))}
249+
</ul>
250+
</nav>
251+
);
252+
});
253+
```
254+
255+
## Understanding the Headings Data
256+
257+
The `headings` property from `useContent()` provides an array of heading objects with the following information:
258+
259+
- `id`: The auto-generated ID for the heading (used for anchor links)
260+
- `text`: The actual text content of the heading
261+
- `level`: The heading level (1 for h1, 2 for h2, etc.)
262+
263+
This only works with `.mdx` files - headings in `.tsx` files are not detected.
264+
265+
## Common Use Cases
266+
267+
### Progressive Disclosure Navigation
268+
269+
You can create a collapsible navigation that shows the current section and its sub-sections:
270+
271+
```tsx
272+
export const ProgressiveNav = component$(() => {
273+
const content = useContent();
274+
const currentSection = useSignal<string | null>(null);
275+
276+
return (
277+
<nav>
278+
{content.headings?.map((heading) => {
279+
if (heading.level === 2) { // Only show h2 as main sections
280+
const subHeadings = content.headings.filter(h =>
281+
h.level === 3 &&
282+
h.id.startsWith(heading.id.split('-')[0])
283+
);
284+
285+
return (
286+
<div key={heading.id}>
287+
<a
288+
href={`#${heading.id}`}
289+
onClick$={() => currentSection.value = heading.id}
290+
>
291+
{heading.text}
292+
</a>
293+
{currentSection.value === heading.id && (
294+
<ul>
295+
{subHeadings.map(sub => (
296+
<li key={sub.id}>
297+
<a href={`#${sub.id}`}>{sub.text}</a>
298+
</li>
299+
))}
300+
</ul>
301+
)}
302+
</div>
303+
);
304+
}
305+
})}
306+
</nav>
307+
);
308+
});
309+
```
310+
311+
### Reading Progress Indicator
312+
313+
You can combine heading information with scroll position to create a reading progress indicator:
314+
315+
```tsx
316+
export const ReadingProgress = component$(() => {
317+
const content = useContent();
318+
const activeSection = useSignal('');
319+
320+
useOnWindow('scroll', $(() => {
321+
const headingElements = content.headings?.map(h =>
322+
document.getElementById(h.id)
323+
).filter(Boolean) || [];
324+
325+
const currentHeading = headingElements.find(el => {
326+
const rect = el!.getBoundingClientRect();
327+
return rect.top > 0 && rect.top < window.innerHeight / 2;
328+
});
329+
330+
if (currentHeading) {
331+
activeSection.value = currentHeading.id;
332+
}
333+
}));
334+
335+
return (
336+
<nav>
337+
{content.headings?.map(heading => (
338+
<a
339+
key={heading.id}
340+
href={`#${heading.id}`}
341+
class={{
342+
active: activeSection.value === heading.id
343+
}}
344+
>
345+
{heading.text}
346+
</a>
347+
))}
348+
</nav>
349+
);
350+
});
351+
```
352+
353+
## Tips and Best Practices
354+
355+
1. **Consistent Heading Structure**: Maintain a logical heading hierarchy in your MDX files to ensure the navigation makes sense.
356+
357+
2. **Performance**: The `useContent()` hook is optimized and won't cause unnecessary re-renders, so you can safely use it in navigation components.
358+
359+
3. **Styling**: Consider using the heading level information to create visual hierarchy in your navigation:
360+
```css
361+
.toc a {
362+
/* Base styles */
363+
}
364+
365+
/* Style based on heading level */
366+
[data-level="1"] { font-size: 1.2em; font-weight: bold; }
367+
[data-level="2"] { font-size: 1.1em; }
368+
[data-level="3"] { font-size: 1em; }
369+
```
370+
371+
4. **Accessibility**: Always ensure your dynamic navigation includes proper ARIA labels and keyboard navigation support.
372+
373+
## Notes and Limitations
374+
375+
- This functionality only works with `.mdx` files, not with `.tsx` or other file types
376+
- Headings must have unique content to generate unique IDs
377+
- The heading data is available only on the client-side after hydration
378+
- Consider using `useVisibleTask$` if you need to interact with the heading elements in the DOM

0 commit comments

Comments
 (0)