Aggregated TOC of Sub Pages

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.

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)

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();


3 Likes

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 :slight_smile:

1 Like

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:

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);
    }
}

1 Like

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.

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

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

2 Likes