# Aggregated TOC of Sub Pages

**URL:** <https://community.fibery.io/t/aggregated-toc-of-sub-pages/6058>\
**Category:** Fibery Showcase\
**Created:** [April 5, 2024, 10:47pm UTC](https://community.fibery.io/t/aggregated-toc-of-sub-pages/6058 "2024-04-05T22:47:38Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![Yuri\_BC](https://sea2.discourse-cdn.com/flex020/user_avatar/community.fibery.io/yuri_bc/32/8803_2.png) [@Yuri\_BC](https://community.fibery.io/u/Yuri_BC)\
**Post date:** [April 5, 2024, 10:47pm UTC](https://community.fibery.io/t/aggregated-toc-of-sub-pages/6058/1 "2024-04-05T22:47:38Z")

</div>

This script automates the creation and updating of a hierarchical Table of Contents (TOC) for a Fibery entity (in this case of database ‘Page’) and its sub entities (sub Pages), facilitating easier navigation and organization of content.

[https://i.imgur.com/oluSxzc.mp4](https://i.imgur.com/oluSxzc.mp4)

## Use cases

1. **Documentation Management** : Automates TOC generation for large knowledge bases or documentation, improving content navigation and accessibility.
2. **Project Overview** : Creates a unified TOC for project sub-pages, simplifying progress tracking and task overview in project management.
3. **Educational Resources** : Facilitates structured access to educational modules or courses by automatically generating a navigable TOC for each section.

## Script Explanation

1. **Initialize and Validate** : It starts by attempting to retrieve the current page entity from `args.currentEntities`.
2. **Fetch Page Entity** : The script then fetches the page entity using its ID to obtain information about its subpages, existing TOC, and name.
3. **Handle TOC Entity** : If the page already has a TOC entity associated with it, the script fetches this entity; otherwise, it creates a new TOC entity with a default name and associates it with the page.
4. **Determine TOC Depth** : It calculates the maximum depth of headings to be included in the TOC based on the Level field of the TOC entity, with a default depth of 3 if unspecified.
5. **Generate TOC Content** : For each subpage of the main page, the script:

- Fetches the subpage entity to get its description, public ID, and name.
- The script generates a hierarchical TOC JSON structure that includes the subpage’s title and headings up to the specified maximum depth, using a specific indentation and formatting style.

1. **Update TOC Document** : If the TOC entity has a description and the generated TOC content is non-empty, the script updates the TOC entity’s description document with the new TOC content formatted in JSON.
2. **Error Handling** : Throughout the script, errors are caught and logged to the console, particularly for operations like fetching entities, creating/updating entities, and setting document content.
3. **Utility Functions** : It includes utility functions like `createHierarchicalTOCJson` for constructing the TOC JSON structure for a subpage and `getMaxDepthFromLevelEntity` for determining the TOC depth based on the TOC entity’s Level field.

## Script (v. 2024-04-05)

```auto
const fibery = context.getService('fibery');
const pageType = 'YourSpaceName/Page'; // Replace YourSpaceName
const tocType = 'YourSpaceName/TOC'; // Replace YourSpaceName
const baseUrl = 'https://YourAccountName.fibery.io/YourSpaceName/Page/'; // Replace YourAccountName and YourSpaceName
const databaseId = '8ff09230-0883-11ee-a2e3-dd72e97a05a2'; // Replace with the UUID of your Page database
async function generateTOCAndAppendForSubPages() {
    const currentPageEntity = args.currentEntities[0];
    let pageEntity = await fibery.getEntityById(pageType, currentPageEntity.id, ['Sub Pages', 'TOC', 'Name']);
    if (!pageEntity) return;
    let parentTocEntity;
    if (pageEntity.TOC && pageEntity.TOC.id) {
        parentTocEntity = await fibery.getEntityById(tocType, pageEntity.TOC.id, ['Description', 'Level']);
    } else {
        parentTocEntity = await fibery.createEntity(tocType, { 'Name': `Aggregated Table Of Contents of Subpages`, 'Page': pageEntity.id });
    }
    if (!parentTocEntity.Level) {
        await fibery.updateEntity(tocType, parentTocEntity.id, { 'Level': 'bbed5bd0-f38c-11ee-8ba1-7bd35f7e50e0' });
        parentTocEntity = await fibery.getEntityById(tocType, parentTocEntity.id, ['Description', 'Level']);
    }
    const maxDepth = getMaxDepthFromLevelEntity(parentTocEntity.Level ? parentTocEntity.Level.Name : '3');
    let tocContent = [];
    for (const subPage of pageEntity['Sub Pages']) {
        const subPageEntity = await fibery.getEntityById(pageType, subPage.id, ['Description', 'Public Id', 'Name']);
        if (subPageEntity && subPageEntity.Description && subPageEntity.Description.Secret) {
            let descriptionContentJson = await fibery.getDocumentContent(subPageEntity.Description.Secret, 'json');
            tocContent = tocContent.concat(createHierarchicalTOCJson(descriptionContentJson, subPageEntity['Public Id'], subPageEntity.Name, subPage.id, maxDepth));
        }
    }
    if (parentTocEntity.Description && parentTocEntity.Description.Secret && tocContent.length > 0) {
        await fibery.setDocumentContent(parentTocEntity.Description.Secret, JSON.stringify({ doc: { type: "doc", content: tocContent } }), 'json');
    }
}
function createHierarchicalTOCJson(descriptionContentJson, publicId, pageTitle, pageId, maxDepth) {
    const tocEntry = { type: "paragraph", content: [{ type: "entity", attrs: { id: pageId, text: pageTitle, typeId: databaseId, }, marks: [{ type: "strong" }] }] };
    const baseIndent = " ";
    const additionalIndent = " ";
    let isFirstHeading = true;
    descriptionContentJson.doc.content.forEach(block => {
        if (block.type === 'heading' && block.attrs.level <= maxDepth) {
            if (isFirstHeading) {
                tocEntry.content.push({ type: "hard_break" });
                isFirstHeading = false;
            } else {
                tocEntry.content.push({ type: "hard_break" });
            }
            const indent = baseIndent + additionalIndent.repeat(block.attrs.level - 1);
            const bulletPoint = "• ";
            const textContent = block.content.map(element => element.text || '').join('');
            tocEntry.content.push({ type: "text", text: indent + bulletPoint }, { type: "text", text: textContent, marks: [{ type: "link", attrs: { href: `${baseUrl}${publicId}/anchor=${block.attrs.guid}` } }] });
        }
    });
    return [tocEntry];
}
function getMaxDepthFromLevelEntity(levelText) {
    if (!levelText) return 3;
    const match = levelText.match(/\d+/);
    return match ? parseInt(match[0], 10) : 3;
}
await generateTOCAndAppendForSubPages();

```

---

<div class="post-metadata">

**Author:** ![grex22](https://sea2.discourse-cdn.com/flex020/user_avatar/community.fibery.io/grex22/32/10249_2.png) [@grex22](https://community.fibery.io/u/grex22)\
**Post date:** [April 7, 2024, 5:51am UTC](https://community.fibery.io/t/aggregated-toc-of-sub-pages/6058/2 "2024-04-07T05:51:26Z")

</div>

Wow, this is incredible, Yuri! Amazingly helpful for many use cases.

The one thing I’m curious about – is there any rhyme or reason to the order the pages show in the TOC? In your example, “Etiam…” is the ‘first’ subpage, but in the TOC, “Donec…” shows up first. I thought it may have had something to do with timestamp of creation date, but don’t think that’s the case.

I ask, because the use case I have in mind represents phases through a large project, and obviously it’d be ideal if those phases were shown in the TOC in the order reflected in the subpage order (phase 1 and its tasks, followed by phase 2 and its tasks, and so on)

Thanks for any insights you might have! I’ll give this script a try too and see if I can suss anything out. And like all your recent scripts, thanks for sharing your awesome findings with the community 🙂

---

<div class="post-metadata">

**Author:** ![Yuri\_BC](https://sea2.discourse-cdn.com/flex020/user_avatar/community.fibery.io/yuri_bc/32/8803_2.png) [@Yuri\_BC](https://community.fibery.io/u/Yuri_BC)\
**Post date:** [April 7, 2024, 12:40pm UTC](https://community.fibery.io/t/aggregated-toc-of-sub-pages/6058/3 "2024-04-07T12:40:40Z")

</div>

> [@grex22](#):
>
> The one thing I’m curious about – is there any rhyme or reason to the order the pages show in the TOC? In your example, “Etiam…” is the ‘first’ subpage, but in the TOC, “Donec…” shows up first. I thought it may have had something to do with timestamp of creation date, but don’t think that’s the case.

Indeed it was missing the sort order, thank you.  
The following adapted function takes care of the sort order according to the numeric ‘Weight’ field:

```auto
async function generateTOCAndAppendForSubPages() {
    try {
        const currentPageEntity = args.currentEntities[0];

        let pageEntity = await fibery.getEntityById(pageType, currentPageEntity.id, ['Sub Pages', 'TOC', 'Name']); // No 'Sub Pages.Weight' here

        // Fetch full subpage details before sorting
        const subPagePromises = pageEntity['Sub Pages'].map(subPageRef => 
            fibery.getEntityById(pageType, subPageRef.id, ['Weight']) 
        );
        const subPages = await Promise.all(subPagePromises); 

        // Sort subPages with the fetched Weight
        subPages.sort((a, b) => (a.Weight || 0) - (b.Weight || 0)); 

        let parentTocEntity;
        if (pageEntity.TOC && pageEntity.TOC.id) {
            parentTocEntity = await fibery.getEntityById(tocType, pageEntity.TOC.id, ['Description', 'Level']);
        } else {
            parentTocEntity = await fibery.createEntity(tocType, {
                'Name': `Aggregated Table Of Contents of Subpages`,
                'Page': pageEntity.id,
            });
        }

        if (!parentTocEntity.Level) {
            await fibery.updateEntity(tocType, parentTocEntity.id, {
                'Level': 'bbed5bd0-f38c-11ee-8ba1-7bd35f7e50e0'
            });
            parentTocEntity = await fibery.getEntityById(tocType, parentTocEntity.id, ['Description', 'Level']);
        }

        const maxDepth = getMaxDepthFromLevelEntity(parentTocEntity.Level ? parentTocEntity.Level.Name : '3');

        let tocContent = [];
        for (const subPage of subPages) { // Using sorted subPages now
            const subPageEntity = await fibery.getEntityById(pageType, subPage.id, ['Description', 'Public Id', 'Name']);
            if (subPageEntity && subPageEntity.Description && subPageEntity.Description.Secret) {
                let descriptionContentJson = await fibery.getDocumentContent(subPageEntity.Description.Secret, 'json');
                tocContent = tocContent.concat(createHierarchicalTOCJson(descriptionContentJson, subPageEntity['Public Id'], subPageEntity.Name, subPage.id, maxDepth));
            }
        }

        if (parentTocEntity.Description && parentTocEntity.Description.Secret && tocContent.length > 0) {
            await fibery.setDocumentContent(parentTocEntity.Description.Secret, JSON.stringify({
                doc: {
                    type: "doc",
                    content: tocContent
                }
            }), 'json');
        }
    } catch (error) {
        console.error('Error in script:', error);
    }
}

```

---

<div class="post-metadata">

**Author:** ![Yuri\_BC](https://sea2.discourse-cdn.com/flex020/user_avatar/community.fibery.io/yuri_bc/32/8803_2.png) [@Yuri\_BC](https://community.fibery.io/u/Yuri_BC)\
**Post date:** [April 7, 2024, 12:46pm UTC](https://community.fibery.io/t/aggregated-toc-of-sub-pages/6058/4 "2024-04-07T12:46:09Z")

</div>

> [@grex22](#):
>
> I ask, because the use case I have in mind represents phases through a large project, and obviously it’d be ideal if those phases were shown in the TOC in the order reflected in the subpage order (phase 1 and its tasks, followed by phase 2 and its tasks, and so on)

Your use case is interesting. If you give a more detailed outline of the structure of your space with the databases, the relations and their relation type and any other details, I can try to make a version for that use case.

---

<div class="post-metadata">

**Author:** ![grex22](https://sea2.discourse-cdn.com/flex020/user_avatar/community.fibery.io/grex22/32/10249_2.png) [@grex22](https://community.fibery.io/u/grex22)\
**Post date:** [April 7, 2024, 7:16pm UTC](https://community.fibery.io/t/aggregated-toc-of-sub-pages/6058/5 "2024-04-07T19:16:56Z")

</div>

I appreciate that! Actually , now that you’ve updated the ordering parameter, your code works perfectly for my use case 🙂

I love this… you’re really showing off what’s possible with automations in ways I never would’ve thought of.
