vuejs / vuepress

📝 Minimalistic Vue-powered static site generator
https://vuepress.vuejs.org
MIT License
22.43k stars 4.78k forks source link

Option to automatically list sub-directory in the sidebar #613

Closed mrleblanc101 closed 6 years ago

mrleblanc101 commented 6 years ago

Feature request

Hi,

I would like to suggest a new feature for the sidebar component. Here's a brief description of my issue. I would like to have an index page for a directory (called 'Projects') which contains sub-directory ('Project A', 'Project B', 'Project C', etc...) which each can contain multiple page of documentation ('index.md', 'deploy.md', 'contribute.md', 'wiki.md', 'faq.md' ,etc...). The index of 'Projects' (README.md) has a description of how to use this tool and how to contribute and other important information my company would like to give them and it also has a sidebar which in theory would list all the active projects in from my company like in the screenshot bellow.

capture d ecran 2018-06-25 a 10 33 40 pm

What would make this even better is if it would still respect the sidebarDepth: 2 and collapsable: true option already available in vuepress. For example, I could take my first example further and group all my project in a client repository. If the sidebar depth is specified, I could even see sub-sub-directory and they would be visible depending on the value of collapsable.

capture d ecran 2018-06-25 a 9 54 46 pm

It would be great if we could list those directory in the sidebar automatically similarly to the sidebar: 'auto' feature, but instead of displaying the headers of the current page, it would display the sub-directory of the current directory.

The first thing I tried is using a custom layout but after extracting the default theme using vuepress eject to check how the default layout works, I realized it wouldn't work as the sidebar is outside the layout component. I also initially considered making a custom theme for this but the way the sidebar is handled with a helper would need me to rework a lot of the code and it would also prevent me from getting any vuepress default theme update in the future and as the project is very young, I don't wan to lose this ability as it already is evolving quite quickly which would probably require me to restart everything in a few months if a I want to update.

Finaly, I think I've found a way to do something similar using the multiple-sidebar option https://vuepress.vuejs.org/default-theme-config/#multiple-sidebars, but it require me to hardcode all the value which is cumbersome because we add projects/clients on a regular basis.

The way I suggest it would works is that it would automatically list the sub-directory if you add one. No need to go and edit you .vuepress/config.js every time. Clicking the directory name would take you to the README.md or index.html depending on your structure.

Something similar could be added to the nav-bar at the top of the page. Initially I wanted to list the sub-directory in a dropdown in the nav-bar but decided it would be better to have an index for my 'Project' directory with some instruction/explanation but the problem is the same! We would need to hardcode the value for the dropdown and it wouldn't be as flexible as there is no way of adding a second level inside the dropdown.

capture d ecran 2018-06-25 a 10 17 35 pm

How should this be implemented in your opinion?

I suggest using 'directory, 'sub-directory' or 'folder' String as value of sidebar in .vuepress/config.js or in the page front-matter just like 'auto' String already works for headings in the sidebar.

Are you willing to work on this yourself?**

I can help, but I'm not sure my experience with Vue and VuePress is big enough to make this myself from scratch.

mrleblanc101 commented 6 years ago

Might have found something. In config.js add:

const dirTree = require('directory-tree');
const path = require('path');

const projets = dirTree(path.join(__dirname, '../projets'), {extensions:/\.md/});

module.exports = {
    [...] // Shortened for lisibility
    themeConfig: {
        sidebar: {
          '/projets/': projets.children.map(children => path.parse(children.name).name !== 'README' ? path.parse(children.name).name : '' ),
        },
    }
};
mrleblanc101 commented 6 years ago

Still need a lot of work and some cleanup but here's what i have right now for recursive directory:

var projets = [];
dirTree(path.join(__dirname, '../projets'), {extensions:/\.md/}, (item, PATH) => projets.push(item));
projets = projets.map(children => {
    return path.parse(children.name).name  !== 'README' ? path.join.apply(null, children.path.split(path.sep).slice(7)) : path.join.apply(null, children.path.split(path.sep).slice(7)).replace('README.md', '');
});

The two major hurdle i have right now are: 1) Sidebar can't resolve current directory if path = 'README.md' or 'index.html', it need to be empty String which seems weird, maybe we could change only that and forget about the rest of the issue ? 2) I'm not super familiar with Node.js path so my code to transform the path from /Users/mrleblanc/GitHub/alexandrie/docs/projets/client-b/test.md to /projets/client-b/test.md is probably extremely ugly 3) I might have to change how I'm doing this if i want to nest 'Projects' under their appropriate 'Client' 4) Using my multiple sidebar method force me to have a 'directory' sidebar even in sub-page because sidebar: auto in the file does not overwrite it

ulivz commented 6 years ago

Thank you for your interest in vuepress. and I also read your requirement carefully but I still cannot clearly get what you really want.

In my opinion, I think that you want something that helps you to generate the config automatically according to the file structure, but this is not a common need and different people may have different generation needs, in addition, it also can be done in user-land.

BTW, If you are not familiar to Node.js, sorry but I cannot help you.

MartinMuzatko commented 5 years ago

@ulivz I think the goal is an automated sidebar. The above script is a workaround to generate this.

hello-sunbaixin commented 5 years ago

I have the same requirement, but the above answer needs to be run in the node environment

HerrBertling commented 5 years ago

I like this idea, too. To be honest, that's what I expected to happen.

I've got a docs/components folder for some components. I automatically create a subfolder for each component and put a README.md file into this folder since I thought that would be the way to go to generate the page tree automatically. Currently, I need to update the sidebar pages manually each time I add a new component.

With the folder structure already in place, an option to automatically render the navigation as described here would be awesome.

andykongchi commented 5 years ago

For future reference for anyone who stumbles upon this for a similar use case.

I used the following code to generate a list of all markdown files in reverse order. I use my VuePress instance as my notes viewer app.

Aside: My files start with a YYYYMMDD-*.md pattern so I personally added a .reverse() in the markdownFiles line for easier reference.

Note: 1 new NPM dependency (glob)

  1. npm install glob
  2. insert the following code in your /.vuepress/config.js
    
    const glob = require('glob');

let markdownFiles = glob.sync('docs/*/.md').map(f => '/' + f); // update the docs/*/.md pattern with your folder structure

module.exports = { ...... themeConfig: { sidebar: markdownFiles }, };



Hope this helps someone!
ladislavsulc commented 5 years ago

Why is this closed and why do we have to manually create the tree in the config? It is not very handy...

benjivm commented 5 years ago

I took what Prefect does and added functionality for reading the order value of the YAML metadata if it exists, otherwise it sorts by filename:

const _ = require('lodash');
const fs = require('fs');
const glob = require('glob');
const markdownIt = require('markdown-it');
const meta = require('markdown-it-meta');

// Load all MD files in a specified directory and order by metadata 'order' value
const getChildren = function(parent_path, dir) {
    files = glob
        .sync(parent_path + (dir ? `/${dir}` : '') + '/**/*.md')
        .map(path => {
            // Instantiate MarkdownIt
            md = new markdownIt();
            // Add markdown-it-meta
            md.use(meta);
            // Get the order value
            file = fs.readFileSync(path, 'utf8');
            md.render(file);
            order = md.meta.order;
            // Remove "parent_path" and ".md"
            path = path.slice(parent_path.length + 1, -3);
            // Remove "README", making it the de facto index page
            if (path.endsWith('README')) {
                path = path.slice(0, -6);
            }

            return {
                path,
                order
            };
        });

    // Return the ordered list of files, sort by 'order' then 'path'
    return _.sortBy(files, ['order', 'path'])
        .map(file => file.path);
};

module.exports = {
    getChildren,
};

Usage:

sidebar: {
        '/get-children/': [{
            title: 'Example 1',
            collapsable: true,
            children: getChildren('docs/example-1'),
        }, {
            title: 'Example 2',
            collapsable: true,
            children: getChildren('docs/example-2'),
        }],
       // if more than three 2 dirs deep add a second parameter for target subdirectory
       '/example3/': getChildren('docs/another/location', 'subdirectory')
}

I'm sure this can be simplified, please let me know if you clean it up or spot problems. Make sure there are no trailing or leading slashes in your directory calls.

MartinMuzatko commented 5 years ago

@a-teammate I think we can perfectly use this for our website :)

ozum commented 5 years ago

Give it a try: https://www.npmjs.com/package/vuepress-bar

I know this is a closed case. I came here to find a solution and liked @benjivm solution. Then I developed the idea of @benjivm a little further to create both navbar and sidebar for directory structure and add sort feature to directories too.

In hope helping others, I shared it by publishing above node module.

Thanks for the idea.

wsdo commented 4 years ago

const getFileName = name => { let arr = [] fs.readdirSync(${RDOCS}${name}) .filter(function(file) { return /.(js|md)$/i.test(file) }) .map(function(file) { s1 = file.substring(0, file.indexOf('.')) let res = '' if (s1 === 'readme' || s1 === 'README') { res = ADOCS + name + '/' } else { res = ADOCS + name + '/' + s1 } arr.push(res) }) return arr }

Mister-Hope commented 4 years ago

I do not think it is a personal need, many people dose need this! A situation where someone wants a specific folder auto generated is very common. As you receive 28 disagree and none agree , I really think you should consider open it. @ulivz

JessicaSachs commented 4 years ago

Just adding my voice to those that think this is a valuable feature... Downloading the third party package to do it (thank you @ozum)

@ulivz, OP gave you an amazing write-up, attempted his own solutions, said he didn't understand node PATHs, and then you chided him about not being able to write javascript and closed his issue. Not cool.

I'm happy to submit a PR if you'd like. I much prefer supporting features to using third-party plugins.

disklosr commented 4 years ago

The goal of vuepress (correct me if I'm wrong) is to easily create documentation websites or static websites in general, by using convention over configuration. If I had to write my own config generator that defeats the purpose of this tool and violates the "convention over configuration" principle.

MichaelJCole commented 4 years ago

@ulivz just because you don't understand, doesn't mean its stupid. You should have asked for help instead of shutting this down.

Mister-Hope commented 4 years ago

@ulivz just because you don't understand, doesn't mean its stupid. You should have asked for help instead of shutting this down.

He is not in charge anymore.

Mister-Hope commented 4 years ago

Besides, the core team would be happy if someone can create a PR.

zhqu1148980644 commented 3 years ago

@MichaelJCole why not reopen this?

cnichte commented 1 year ago

Is there any news on the subject? :-) This has to go in, in Vuepress. I vote for it.

hishamdalal commented 1 year ago

This works:

List subfolders in sidebar navigation

ronny1020 commented 1 year ago

This should work. At least it works in my project.

const sidebar: SidebarConfig = [];

glob
  .sync('docs/**/*.md')
  .map((path) => path.replace('docs/', ''))
  .sort()
  .forEach((path) =>
    path.split('/').forEach((name, index, array) => {
      let children = sidebar

      for (let i = 0; i < index; i++) {
        children = (
          children.find(
            (child) => typeof child === 'object' && child.text === array[i]
          ) as SidebarGroup
        ).children
      }

      if (name === 'index.md' || name === 'README.md') {
        children.push(
          `/${path
            .replace('.md', '')
            .replace('index', '')
            .replace('README', '')}`
        )
        return
      }

      if (name.endsWith('.md')) {
        children.push(`/${path.replace('.md', '')}`)
        return
      }

      const child = children.find(
        (child) => typeof child === 'object' && child.text === name
      ) as SidebarGroup

      if (!child) {
        children.push({ text: name, children: [], collapsible: true })
      }
    })
  )

module.exports = {
  title: '.....',
  description:'....',
  plugins: [searchPlugin()],
  theme: defaultTheme({
    repo: '.....',
    docsDir: 'docs',
    sidebar,
  }),
};
Sieboldianus commented 3 months ago

Is there some solution vailable for Vuepress V2? I desperately need automatic sidebar population, otherwise my config.js would explode. I don't want to hack this together.

Mister-Hope commented 3 months ago

vuepress2 repo are at https://github.com/vuepress/core

talking any thing related to vuepress2 here is useless. vuepress-theme-hope support this feature out of box (would be @vuepress/theme-pro in the next future)

Korak-997 commented 3 weeks ago

If yet anyone have the problem. After a lot of research and playing with the stuff. I finally have a solution which works perfectly.

//config.js
import path from "path";
import fs from "fs";

const docsDir = path.resolve(__dirname, "../");

const getSidebarItems = (dir) => {
  const items = [];
  const files = fs.readdirSync(dir, { withFileTypes: true });

  files.forEach((file) => {
    if (file.isDirectory() && file.name !== ".vuepress") {
      const folderName = file.name;
      const folderPath = path.join(dir, folderName);
      const children = fs
        .readdirSync(folderPath, { withFileTypes: true })
        .filter((child) => child.isFile() && child.name.endsWith(".md"))
        .map((child) => child.name.replace(".md", ""));

      if (children.length === 1 && children[0] === "README") {
        // Folder with just README.md
        items.push({
          text: folderName.replace(/-/g, " ").toUpperCase(),
          link: `/${folderName}/`,
        });
      } else {
        // Folder with other markdown files
        items.push({
          text: folderName.replace(/-/g, " ").toUpperCase(),
          link: `/${folderName}/`,
          prefix: `/${folderName}`,
          children: children.filter((child) => child !== "README"),
        });
      }
    }
  });

  return items;
};

const generateSidebar = () => {
  return getSidebarItems(docsDir);
};

//Then just replace the sidebar 
export default defineUserConfig({
  //....
  theme: defaultTheme({
    //....
    sidebar: generateSidebar(),
  }),
  //....
});