gracile-web / gracile

A thin, full-stack, web framework — Powered by Vite and Lit SSR. Works with Node's HTTP or WHATWG Fetch.
https://gracile.js.org
ISC License
24 stars 1 forks source link

feature request: component to generate a sidebar menu #4

Closed lschierer closed 1 month ago

lschierer commented 2 months ago

one of the harder things to get right when working with a site generator is a sidebar navigation menu. In the past I've worked with fairly proscriptive generators like Hugo and Astro's Starlight overlay, each of which helps with this, but each of which eventually proved overly constrictive. Still this is one feature I definitely miss.

My hope is that since Gracile does have a router, that its internals at build time do in fact have in memory an object that contains all routes that can be iterated. If so, then that object can be used to do one of several things, any of which would meet my needs:

Thanks!

JulianCataldo commented 1 month ago

Hello,

The routes object doesn't contain anything from modules, just surface level infos, not even static analysis (e.g. Astro is doing some of it, with getStaticPaths, prerender, etc.). This is keeping things simpler and snappier, even if it's a bit more inflexible for more content aware routing.

I'm planning to add a bit of code generation for allowing type-safe routes, for links etc. (with a helper, providing auto-completion). You'll get bits like url path, param groups, physical file path etc. but nothing much deeper. From there, you'll get a hook for all routes if you want (not sure about the implementation yet, maybe a virtual module).

For the Gracile docs website I'm iterating through the glob imports. https://gracile.js.org/docs/add-ons/markdown/#doc_with-vites-glob-import But that indeed, kills performance. The whole MD files has to be parsed and transformed for even one page to load. It's a trade-off.

Using import.glob It's a well known pattern when using Vite, that will give you maximum control. I prefer it over MD files based routing, with the frontmatter doing a lot of stuff (loading layouts…), but I understand some want a plug and play approach. It is just that I always encounter limitations, like you.

I'll release the website code once it's clean-up and stable https://github.com/gracile-web/website. For now, its repo only contains the actual Markdown content, not the website that renders it.

But honestly, it's very difficult to please everyone for this. At least, projects like Starlight have a large community working on it and a lot of plugins, so I think It's the best option for most people! Conventions over too much configuration.

JulianCataldo commented 1 month ago

Try fdir, fast-glob or import.meta.glob for populating your sidebar.

This is how the Gracile website is doing, for inspiration:

export default defineRoute({
  staticPaths: () =>
    docsImports.map(({ docsParam, module, href }, index) => ({
      params: {
        path: docsParam,
      },
      props: {
        current: module,
        prev:
          module.path.endsWith('/README.md') ||
          docsImports[index - 1]?.module?.path?.endsWith('/README.md')
            ? null
            : docsImports[index - 1] || null,

        next:
          module.path.endsWith('/README.md') ||
          docsImports[index + 1]?.module?.path?.endsWith('/README.md')
            ? null
            : docsImports[index + 1] || null,

        index: docsImports
          .filter((m) => {
            if (module.path.endsWith('/README.md') === false) return false;

            if (m.module.path === module.path) return false;

            const base = module.path.split('/').slice(0, -1).join('/');
            const siblings = base + '/*.md';
            const subReadmes = base + '/*/README.md';

            const tested = m.module.path;

            const match = picomatch([siblings, subReadmes]);
            return match(tested);
          })
          .sort((p, n) =>
            p.module.path.replace('README.md', '') >
            n.module.path.replace('README.md', '')
              ? 1
              : -1,
          ),

        breadcrumbs: [
          //
          { url: '/docs/', title: 'Docs' },
          ...(docsParam
            ? docsParam
                .split('/')
                .slice(0, -1)
                .map((_d, i) => {
                  const m = docsImports.find((m) => {
                    return (
                      m.docsParam ===
                      docsParam
                        ?.split('/')
                        .slice(0, i + 1)
                        .join('/')
                    );
                  });
                  if (!m) throw Error('Missing breadcrumb item!');

                  return { url: m.href, title: m.module.title };
                })
            : []),
        ] satisfies BreadCrumbs,
      },
    })),
});
export const docsImportsGlob = import.meta.glob<MarkdownModule>(
  ['/src/content/docs/**/*.md', '!/**/_*'],
  { eager: true },
);

export const docsImports = Object.entries(docsImportsGlob).map(
  ([path, module]) =>
    ({
      docsParam: pathsHandlers.filePathToDocsPathParam(path),
      href: pathsHandlers.pathToHref(path),
      module: module as MarkdownModule,
    }) satisfies MarkdownModuleConsumable,
);