Builder Book logo

Book: Builder Book

  1. Introduction
  2. Set up Node.js project. VS code editor and lint. Set up Next.js project. Material-UI integration. Server-side rendering. Custom styles.
  3. HTTP. Express server. Next-Express server, nodemon. Index.getInitialProps. User data model and mongoose. MongoDB database and dotenv. Testing server-database connection. Retrieving document. Session and cookie. MenuWithAvatar and Header components.
  4. Authentication HOC. getInitialProps method. Login page and NProgress. Asynchronous execution. Promise.then. async/await. Google Oauth API infrastructure. setupGoogle, verify, passport, strategy. Express routes /auth/google, /oauth2callback, /logout. generateSlug. this. Set up at Google Cloud Platform.
  5. Testing method with Jest. Transactional email API with AWS SES service. Set up AWS SES service, security credentials. sendEmail method. Export and import syntax for server code. EmailTemplate data model. Update User.signInOrSignUp. Informational success/error messages. Notifier component. notify method.
  6. Book data model. Chapter data model. MongoDB index. API infrastructure and user roles. Read chapter API.
  7. Set up Github API infrastructure. Sync content API infrastructure. Missing UI infrastructure for Admin user. Two improvements. Testing.
  8. Table of Contents. Sections. Sidebar. Toggle TOC. Highlight for section. Active section. Hide Header. Mobile browser.
  9. BuyButton component. Buy book API infrastructure. Setup at Stripe dashboard and environmental variables. isPurchased and ReadChapter page. Redirect. My books API and MyBooks page. Mailchimp API.
  10. Prepare project for deployment. Environmental variables, production/development. Logger. SEO, robots.txt, sitemap.xml. Compression and security. Deploy project. Heroku. Testing deployed project. AWS Elastic Beanstalk.

Chapter 7: Table of Contents. Sections. Sidebar. Toggle TOC. Highlight for section. Active section. Hide Header. Mobile browser.

We keep our book up to date with recent libraries and packages. Latest update Nov 2023.


The section below is a preview of Builder Book. To read the full text, you will need to purchase the book.


In Chapter 7, you'll start with the codebase in the 7-begin folder of our builderbook repo and end up with the codebase in the 7-end folder. We'll cover the following topics in this chapter:

  • Table of Contents
    - Sections
    - Sidebar
    - Toggle TOC

  • Highlight for section
    - Active section

  • Hide Header

  • Mobile browser


In Chapter 5, you built the ReadChapter page, which displays the content of one chapter. In this chapter, we will make multiple improvements to this page.

For example, a user should be able to navigate between chapters and between sections within one chapter. To achieve that, we need to introduce a Table of Contents (TOC). The TOC should contain:

  • hyperlinked sections within each chapter
  • hyperlinked titles of all chapters

Here's an example of what the TOC would look like once we implement it:: Builder Book

Table of Contents link

On a high level, we will add the TOC to our ReadChapter page in two main steps. We will discuss and write:

  • renderSections method that returns a list of hyperlinked sections within one chapter
  • renderSidebar method that returns a list of hyperlinked titles for all chapters and includes renderSections under each chapter's title

We will add the renderSidebar method inside the ReadChapter.render method and then test out the TOC.


Sections link

In this subsection, we define the renderSections method. This function returns a list of hyperlinked sections for one chapter.

Recall how we defined the sections parameter in our Chapter model. Open server/models/Chapter.js and look at the schema:

sections: [
    {
        text: String,
        level: Number,
        escapedText: String,
    },
],

Sections is an array of objects, and each object has text, level, and escapedText. Where do these three parameters come from? In the same server/models/Chapter.js file, find how we generate the sections array:

const sections = getSections(content);

Definition of getSections method:

function getSections(content) {
    const renderer = new marked.Renderer();

    const sections = [];

    renderer.heading = (text, level) => {
        if (level !== 2) {
            return;
        }

        const escapedText = text
            .trim()
            .toLowerCase()
            .replace(/[^\w]+/g, '-');

        sections.push({ text, level, escapedText });
    };

    marked.setOptions({
        renderer,
    });

    marked(he.decode(content));

    return sections;
}

marked parses markdown content and finds headings with level equal to 2 (any heading that has ##). For every heading, we push an object to the sections array:

sections.push({ text, level, escapedText })

In other words, if your markdown content has:

## Why this book?

Then getSections(content) will return this array:

[
    {
        "text": "Why this book?",
        "level": 2,
        "escapedText": "why-this-book-"
    },
]

We will use text as text inside <a>{text}</a> and use escapedText for href. When a user clicks on a hyperlinked section inside the TOC, we want the page to scroll to the beginning of that section. In fact, when we wrote our markdownToHtml method in Chapter 6, we defined the <h2> heading as follows:

if (level === 2) {
    return `<h${level} class="chapter-section" style="color: #222; font-weight: 400;">
        <a
            class="section-anchor"
            name="${escapedText}"
            href="#${escapedText}"
            style="color: #222;"
        > 
            <i class="material-icons" style="vertical-align: middle; opacity: 0.5; cursor: pointer;">link</i>
        </a>
        ${text}
    </h${level}>`;
}

Let's say we have the heading ## Why this book? in our markdown content. When user a clicks on the link icon next to that heading, two things will happen:
- the URL in the browser address bar gets #why-this-book- appended to it
- the page scrolls to the beginning of that section because <a> has the name attribute

We want to get the exact same behavior when a user clicks on the hyperlinked section text inside our TOC. Thus we get:

<a href={`#${s.escapedText}`}>
    {s.text}
</a>

Keep in mind that sections is an array. Thus, you should use JavaScript's method Array.map:

<ul>
    {sections.map((s) => (
        <li key={s.escapedText} style={{ paddingTop: '10px' }}>
            <a href={`#${s.escapedText}`}>
                {s.text}
            </a>
        </li>
    ))}
</ul>

You already used this method earlier in this book. Check out books.map inside pages/admin/index.jsx.

At this point, the main part of our renderSection method is actually done.

Open your pages/public/read-chapter.jsx file. Remember that we defined the chapter object isnide the page with:

const chapter = await getChapterDetailApiMethod({ bookSlug, chapterSlug }, { headers });

And then we set the initial state object of our ReadChapter page component with the following properties:

this.state = {
    chapter,
    htmlContent,
};

Now let's use ES6 object destructuring to define sections as this.state.chapter.sections:

const { sections } = this.state.chapter;

Also, let's return null if the section array does not exist or the array has zero members:

if (!sections || !sections.length === 0) {
    return null;
}

Put together these two code snippets, plus return a list of hyperlinked sections, and you get:

renderSections() {
    const { sections } = this.state.chapter;

    if (!sections || !sections.length === 0) {
        return null;
    }

    return (
        <ul>
            {sections.map((s) => (
                <li key={s.escapedText} style={{ paddingTop: '10px' }}>
                    <a href={`#${s.escapedText}`}>
                        {s.text}
                    </a>
                </li>
            ))}
        </ul>
    );
}

Good job if you got the same result. We'll add this to our ReadChapter page at the end of the next subsection.


Sidebar link

In this subsection, we discuss the renderSidebar method that returns a list of hyperlinked titles for all chapters. We will then add our renderSection method from above to add a list of all sections under each chapter list item. Together, chapters and their sections, make up the Table of Contents (TOC).

Similar to how we defined the renderSections method, we will use list, list item, and anchor elements together with the JavaScript method Array.map for our new renderSidebar method:

renderSidebar() {
    return (
        <div>
            <p>{book.name}</p>
            <ol>
                {chapters.map((ch, i) => (
                    <li key={ch._id} role="presentation">
                        <Link
                            as={`/books/${book.slug}/${ch.slug}`}
                            href={`/public/read-chapter?bookSlug=${book.slug}&chapterSlug=${ch.slug}`}
                        >
                            <a>{ch.title}</a>
                        </Link>
                    </li>
                ))}
            </ol>
        </div>
    );
}

The only differences with renderSections are:
- Instead of an unordered list <ul>, we use an ordered list <ol>.
- We wrap the anchor <a> with Next.js's <Link>, so we can, later, take advantage of the default prefetch feature in production.
- We also display the book name on the top of the TOC with <p>{book.name}</p>.


You've reached the end of the Chapter 7 preview. To continue reading, you will need to purchase the book.

We keep our book up to date with recent libraries and packages. Latest update Nov 2023.