How to Build a Dynamic Sticky Table of Contents for Wix Blogs in Wix Studio

Have you ever scrolled through a long, insightful blog post and wished you could quickly jump to a specific section? While the Wix Blog is powerful, it doesn’t currently offer a native sticky Table of Contents (TOC).

In this topic, we’ll build a dynamic, auto-generating, and sticky TOC that reads your blog headings and indents them based on their hierarchy.


Step 1: Layout Setup in Wix Studio

To make the Table of Contents stay with the reader as they scroll, we need to utilize a multi-column layout.

  1. Split the Section: In the Wix Studio Editor, select the section containing your Blog Post widget and split it into two columns. A great starting point for proportions is:
    1. Column 1 (TOC): Min/max 200px, 1fr
    2. Column 2 (Blog): 3fr
  2. The Blog Column: Place your Post Widget into the larger column. Set it to Stretch so it fills the available space.
  3. The TOC Column: In the smaller column, add a Container and configure it as follows:
    1. Set width to 100%.
    2. Dock it to the Top, Left, and Right with consistent margins (e.g., 16px)
    3. Set a Max Height (e.g., 90vh) and set Overflow Content to Scroll. This ensures that if your TOC is longer than the screen, users can still scroll through the links.
  4. Make it Sticky: With the container selected, go to the Inspector, and change the Position to Sticky. Set a Top Offset (e.g., 16px) so it sits comfortably below the top of the browser.
  5. Add a Repeater: Drop a Repeater inside that container.
    1. Add a button inside the repeater item to serve as the clickable link
    2. Important: Change the IDs in the code panel to #repeater and the button to #button.

Your layers will look something like this:


Step 2: Add the code

Now that the structure is ready, we need to “read” the blog post data and populate our repeater.

Open your Page Code panel and paste the following:

import * as wixSiteLocation from '@wix/site-location';

$w.onReady(async () => {
    // Set the repeater with blank data before further steps to prevent "flashing" of temp data
    $w("#repeater").data = []

    // 1. Fetch the actual content data from the blog post widget
    const postData = await $w("#postWidget").getPost();
    const currentUrl = await wixSiteLocation.location.url();
    
    // 2. Set up how each item in our TOC repeater should behave
    $w("#repeater").onItemReady(async ($item, itemData) => {
        // Set the button label to the heading text found in the post
        $item("#button").label = itemData.heading;
        $item("#button").target = "_self";
        
        // Construct the anchor link using the unique ID of the heading block
        $item("#button").link = `${currentUrl}#viewer-${itemData._id}`;

        // Add a CSS class based on heading level (H1, H2, etc.) for indentation
        $item("#repeaterItem").customClassList.add(`table-of-contents__level-${itemData.headingLevel}`);
    });

    // 3. Generate the data array and assign it to the repeater
    $w("#repeater").data = await generateTOCData(postData.richContent);
});

/**
 * Converts Wix Rich Content into an array for a Table of Contents repeater.
 */
function generateTOCData(richContent) {
    // Safety check: if there's no content, return an empty list
    if (!richContent || !richContent.nodes) return [];

    return richContent.nodes
        .filter(node => node.type === 'HEADING') // Only grab heading nodes
        .map(node => {
            // Extract text from child nodes (this handles bold/italic formatting within headings)
            const headingText = node.nodes
                .filter(child => child.type === 'TEXT')
                .map(child => child.textData?.text || "")
                .join("");

            return {
                _id: node.id,
                heading: headingText,
                headingLevel: node.headingData?.level || 1 // Defaults to 1 if level is missing
            };
        })
        .filter(item => item.heading.trim().length > 0); // Ignore empty headings
}


How the Code Works

  • postData.richContent: This is a JSON object containing every element of your post (paragraphs, images, headings, etc.).
  • generateTOCData: This function filters that JSON to find only headings, then maps them into a format the Repeater understands.
  • #viewer-${itemData._id}: Wix Blog automatically assigns unique IDs to headings. By creating anchor links to these IDs, we enable the “jump-to” functionality.

Step 3: Global CSS for Hierarchy

To make the TOC look organized, we want deeper headings (like H3s) to be indented further than H2s. Add this to your Global CSS file:

/* Indentation logic based on Heading Levels */
.table-of-contents__level-1 { padding-left: 0; }
.table-of-contents__level-2 { padding-left: 12px; }
.table-of-contents__level-3 { padding-left: 24px; }
.table-of-contents__level-4 { padding-left: 36px; }
.table-of-contents__level-5 { padding-left: 48px; }
.table-of-contents__level-6 { padding-left: 60px; }

Note: Ensure the ID of the repeater item in the code (#repeaterItem) matches the ID you set in the editor for the CSS classes to apply correctly via customClassList.

Summary

That’s it! You now have a fully functional, automated Table of Contents.

The Benefits:

  • User Experience: Readers can navigate long-form content effortlessly.
  • Dynamic: You don’t need to manually update links; if you change a heading in the Blog Manager, the TOC updates itself.
  • Visual Hierarchy: The CSS ensures your subheadings look like subheadings, keeping the sidebar organized.
2 Likes