Home > Software design >  Make site map generated from flat array of pages recursive
Make site map generated from flat array of pages recursive

Time:06-22

I have a flat array of pages and I'm trying to create a hierarchical site map from them. What I have so far is very messy so I would like a better way of doing it.

This is the array of pages I have

const pages = [
  {
    "name": "Page 1",
    "url": "/page-1/",
  },
  {
    "name": "Page 2",
    "url": "/page-2/",
  },
  {
    "name": "Child 1",
    "url": "/page-1/child-1/",
  },
  {
    "name": "Child 2",
    "url": "/page-1/child-1/child-2/",
  },
  {
    "name": "Child 3",
    "url": "/page-1/child-1/child-2/child-3/",
  }
]

and this is the result I want outputting

<ul>
  <li>
    <a href="/page-1/">Page 1</a>
    <ul>
      <li>
        <a href="/page-1/child-1/">Child 1</a>
        <ul>
          <li>
            <a href="/page-1/child-1/child-2/">Child 2</a>
            <ul>
              <li>
                <a href="/page-1/child-1/child-2/child-3/">Child 3</a>
              </li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li><a href="/page-2/">Page 2</a></li>
</ul>

This is what I have currently, which works but would like to find a better way to do it

const generateSitemap = function(pages) => {
  let sitemap = "";
  let organisedPages = [];

  const sortPages = function (runs) {
    if (pages.length === 0) return;
    pages.forEach((page) => {
      const title = page.name;
      const url = page.url;

      // Get homepage and content pages only
      let pageObj = {
        title: title,
        url: url,
        children: [],
      };

      // Handle top level pages first then build up children as you go deeper
      if (pageLevel(url) === 1) {
        organisedPages.push(pageObj);
        pages = pages.filter((page1) => page !== page1);
      } else if (runs === 2) {
        organisedPages.forEach((oPage, i) => {
          // Check to see if url is in segments and matches
          let parseUrl = url.substring(1).slice(0, -1);
          const urlParts = parseUrl.split("/");
          const parentUrl = `/${urlParts.slice(0, -1).join("/")}/`;

          if (oPage.url === parentUrl) {
            organisedPages[i].children.push(pageObj);
            pages = pages.filter(
              (page1) => pageObj.url !== page1.systemProperties.url
            );
            return;
          }
        });
      } else if (runs === 3) {
        organisedPages.forEach((oPage, i) => {
          // Check to see if url is in segments and matches
          let parseUrl = url.substring(1).slice(0, -1);
          const urlParts = parseUrl.split("/");
          const parentUrl = urlParts.slice(0, -1);
          const parentUrlComp = `/${parentUrl.join("/")}/`;
          const parentUrl2 = parentUrl.slice(0, -1);
          const parentUrl2Comp = `/${parentUrl2.join("/")}/`;

          if (oPage.url === parentUrl2Comp) {
            organisedPages[i].children.forEach((child, j) => {
              if (child.url === parentUrlComp) {
                organisedPages[i].children[j].children.push(pageObj);
                pages = pages.filter(
                  (page1) => pageObj.url !== page1.systemProperties.url
                );
                return;
              }
            });
          }
        });
      } else if (runs === 4) {
        organisedPages.forEach((oPage, i) => {
          // Check to see if url is in segments and matches
          let parseUrl = url.substring(1).slice(0, -1);
          const urlParts = parseUrl.split("/");
          const parentUrl = urlParts.slice(0, -1);
          const parentUrlComp = `/${parentUrl.join("/")}/`;
          const parentUrl2 = parentUrl.slice(0, -1);
          const parentUrl2Comp = `/${parentUrl2.join("/")}/`;
          const parentUrl3 = parentUrl2.slice(0, -1);
          const parentUrl3Comp = `/${parentUrl3.join("/")}/`;

          if (oPage.url === parentUrl3Comp) {
            organisedPages[i].children.forEach((child, j) => {
              if (child.url === parentUrl2Comp) {
                organisedPages[i].children[j].children.forEach((child1, k) => {
                  if (child1.url === parentUrlComp) {
                    organisedPages[i].children[j].children[k].children.push(
                      pageObj
                    );
                    pages = pages.filter(
                      (page1) => pageObj.url !== page1.systemProperties.url
                    );
                    return;
                  }
                });
              }
            });
          }
        });
      }
    });

    runs  ;

    if (runs < 5) {
      sortPages(runs);
    }
  };

  sortPages(1);

  /**
   * Check if page is a parent
   *
   * @param {string} url page url
   * @returns {number} length of segments
   */
  function pageLevel(url) {
    // Remove first and last forward slash that is provided
    let parseUrl = url.substring(1).slice(0, -1);

    // Split parsed url by forward slash
    const urlParts = parseUrl.split("/");

    // Check segment length
    return urlParts.length;
  }

  /**
   * Loop through organised pages and set listing.
   */
  organisedPages.forEach((page) => {
    sitemap  = `<li>`;
    sitemap  = `<a href="${page.url}">${page.title}</a>`;

    // Check if we need children loop for each parent page
    if (page.children.length) {
      sitemap  = `<ul>`;
      page.children.forEach((page) => {
        sitemap  = `<li>`;
        sitemap  = `<a href="${page.url}">${page.title}</a>`;

        // Check if we need children loop for each sub-child page
        if (page.children.length) {
          sitemap  = `<ul>`;
          page.children.forEach((page) => {
            sitemap  = `<li>`;
            sitemap  = `<a href="${page.url}">${page.title}</a>`;

            if (page.children.length) {
              sitemap  = `<ul>`;
              page.children.forEach((page) => {
                sitemap  = `<li>`;
                sitemap  = `<a href="${page.url}">${page.title}</a>`;
                sitemap  = `</li>`;
              });
              sitemap  = `</ul>`;
            }

            sitemap  = `</li>`;
          });
          sitemap  = `</ul>`;
        }

        sitemap  = `</li>`;
      });
      sitemap  = `</ul>`;
    }

    sitemap  = `</li>`;
  });

  return sitemap;
};

generateSitemap(pages)

CodePudding user response:

I would choose to break the logical nesting of objects from the HTML formatting, as I think it makes for simpler functions all around. To comfortably do the nesting, I will also add a helper function to get the parent id from a url, so that, for instance,

getParentId ("/page-1/") //=> ""
getParentId ("page-1/child-1/") //=> "page-1"
getParentId ("page1/child-1/child-2/") //=> "page1/child-1"
// ... etc

We could easily inline this function in nest, but I think it's cleaner as a helper. There is a little bit of odd complexity with this, because there is an extra slash somewhere, beginning or end. We choose to slice off the final / when searching through our list to find children.

The code looks like this:

const getParentId = (url) => url .slice (0, url .slice (0, -1) .lastIndexOf ('/'))

const nest = (pages, parentId = "", id = parentId .slice (0, -1)) => pages 
  .filter (({url}) => getParentId (url) == id) 
  .map (({url, ...rest}) => ({...rest, url, children: nest (pages, url)}))

const format = (nodes) => `<ul>${nodes .map (({name, url, children}) => 
  `<li><a href="${url}">${name}</a>${children .length ? format (children) : ''}</li>`
).join('')}</ul>`

const pages2html = (pages) => format (nest (pages))

const pages = [{name: "Page 1", url: "/page-1/"}, {name: "Page 2", url: "/page-2/"}, {name: "Child 1", url: "/page-1/child-1/"}, {name: "Child 2", url: "/page-1/child-1/child-2/"}, {name: "Child 3", url: "/page-1/child-1/child-2/child-3/"}]

console .log (pages2html (pages))
.as-console-wrapper {max-height: 100% !important; top: 0}

Here nest turns your nodes into a format like this:

[
  {
    name: "Page 1",
    url: "/page-1/",
    children: [
      {
        name: "Child 1",
        url: "/page-1/child-1/",
        children: [
          {
            name: "Child 2",
            url: "/page-1/child-1/child-2/",
            children: [
              {
                name: "Child 3",
                url: "/page-1/child-1/child-2/child-3/",
                children: []
              }
            ]
          }
        ]
      }
    ]
  },
  {
    name: "Page 2",
    url: "/page-2/",
    children: []
  }
] 

and then format turns that into your HTML. We wrap them together in pages2html to have a single function to call, but the work is done in those two at-least-possibly reusable functions.

Note that we don't try to preserve the white-space in your requested format. We could do so, at the expense of making format quite a bit uglier. Here's a quick attempt, which I think is correct. But I wouldn't swear to it:

const format = (nodes, depth = 0) => `${depth > 0 ? '\n' : ''
}${'  '.repeat (2 * depth) }<ul>${nodes .map (({name, url, children}) => `
${'  ' .repeat (2 * depth   1) }<li>
${'  ' .repeat (2 * depth   2) }<a href="${url}">${name}</a>${
  children .length ? ``   format (children, depth   1) : ''
}
${'  ' .repeat (2 * depth   1) }</li>`
).join('')}
${'  '.repeat (2 * depth)}</ul>`

We simply use a depth parameter to note the level of nesting, and use that to figure out how many spaces to use at the beginning of lines.


In general, I find this style, working as a sequence of transformations, much simpler to work with. Yes, we could do all this in a single function. It might even have fewer lines of code than my four separate functions. But each of these is simpler to understand, and simpler to change when the need arises.

And recursion will be more flexible and much simpler than your multi-level branching, as you can see from the fairly simple recursive code in next.

  • Related