Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to generate a menu based on the files in the pages directory in Next.js

I am trying to create a menu component that reads the contents of the pages folder at build time. However I haven't had any success. Here is what I have tried:

import path from "path";
import * as ChangeCase from "change-case";

export default class Nav extends React.Component {
    render() {
        return (
            <nav>
                {this.props.pages.map((page) => (
                    <a href={page.link}>{page.name}</a>
                ))}
            </nav>
        );
    }

    async getStaticProps() {
        let files = fs.readdirSync("../pages");
        files = files.filter((file) => {
            if (file == "_app.js") return false;
            const stat = fs.lstatSync(file);
            return stat.isFile();
        });

        const pages = files.map((file) => {
            if (file == "index.js") {
                const name = "home";
                const link = "/";
            } else {
                const link = path.parse(file).name;
                const name = ChangeCase.camelCase(link);
            }
            console.log(link, name);
            return {
                name: name,
                link: link,
            };
        });

        return {
            props: {
                pages: pages,
            },
        };
    }
}

This does not work, the component does not receive the pages prop. I have tried switching to a functional component, returning a promise from getStaticProps(), switching to getServerSideProps(), and including the directory reading code into the render method.

The first two don't work because getStaticProps() and getServerSideProps() never get called unless the component is a page, and including the code in the render method fails because fs is not defined or importable since the code might run on the front end which wouldn't have fs access.

I've also tried adding the code to a getStaticProps() function inside _app.js, with the hopes of pushing the pages to the component via context, but it seems getStaticProps() doesn't get called there either.

I could run the code in the getStaticProps function of the pages that include the menu, but I would have to repeat that for every page. Even if I extract the logic into a module that gets called from the getStaticProps, so something like:

// ...

export async function getStaticProps() {
    return {
        props: {
            pages: MenuMaker.getPages(),
            // ...
        }
    }
}

and then pass the pages to the navigation component inside the page via the Layout component:

export default function Page(props) {
    return (
        <Layout pages={props.pages}></Layout>
    )
}

then that's still a lot of boilerplate to add to each page on the site.

Surely there is a better way... It can't be that there is no way to add static data to the global state at build time, can it? How do I generate a dynamic menu at build time?

like image 337
kaan_a Avatar asked Sep 01 '20 17:09

kaan_a


People also ask

How do I route a page in NextJS?

js enables you to define dynamic routes in your app using the brackets ( [param] ). Instead of setting a static name on your pages, you can use a dynamic one. Next. js will get the route parameters passed in and then use it as a name for the route.

How do I create a nested route in NextJS?

Create a new page named article. js inside the pages folder. Users can implement nested routes by simply nesting folders inside the pages folder just make sure that the file and folder name are the same. Now if you navigate to the URL localhost:300/article/competitive-programming you will see the desired results.

How do I use SSR in NextJS?

To use SSR for a page, we need to export an async function called “getServerSideProps“. This async function is called each time a request is made for the page. Note: In place of data you can take any other name of the variable. Also, you can pass multiple props by separating them with commas “,“.

What is getLayout in NextJS?

Per-Page Layouts If you need multiple layouts, you can add a property getLayout to your page, allowing you to return a React component for the layout. This allows you to define the layout on a per-page basis.


Video Answer


1 Answers

I managed to get this working by exporting a function from next.config.js and setting an environment variable that contains the menu structure. I abstracted the menu loading code into it's own file. After seeing the result, I understand better why I was not able to find an example of anyone doing something similar:

The menu is not ordered the way I would like. I could sort it alphabetically, or by the modification date but realistically it almost always needs to be manually sorted in relation to the subject of the pages. I could use an integer, either tacked on to the filename or somewhere in the file (perhaps in a comment line). But in retrospect I think that just hard coding the links in a component is probably the best way after all since it offers much more flexibility and probably isn't going to be much more work even in the very long run.

That being said I am sharing my solution as it is a way to initialize an app wide static state. It's not ideal, you will have to restart the dev server if you wish to recalculate the variables here, which is why I'm still interested in other possible solutions, but it does work. So here it is:

next.config.js

const menu = require("./libraries/menu.js");

module.exports = (phase, { defaultConfig }) => {
    return {
        // ...
        env: {
            // ...
            menu: menu.get('pages'),
            // ...
        },
        // ...
    };
};

libraries/menu.js

const fs = require("fs");
const path = require("path");
const ccase = require("change-case");

module.exports = {
    get: (pagePath) => {
        if (pagePath.slice(-1) != "/") pagePath += "/";
        let files = fs.readdirSync(pagePath);
        files = files.filter((file) => {
            if (file == "_app.js") return false;
            const stat = fs.lstatSync(pagePath + file);
            return stat.isFile();
        });

        return files.map((file) => {
            if (file == "index.js") {
                return {
                    name: "Home";
                    link: "/";
                };
            } else {
                link = path.parse(file).name;
                return {
                    link: link;
                    name: ccase.capitalCase(link);
                };
            }
        });
    },
};

Then the actual menu is generated from the environment variable in a component that can be included in the layout:

components/nav.js

import Link from "next/link";

export default class Nav extends React.Component {
    render() {
        return (
            <nav>
                {process.env.menu.map((item) => (
                    <Link key={item.link} href={item.link}>
                        <a href={item.link}>
                            {item.name}
                        </a>
                    </Link>
                ))}
            </nav>
        );
    }
}
like image 55
kaan_a Avatar answered Nov 11 '22 19:11

kaan_a