kit
kit copied to clipboard
Programmatically get all routes
Describe the problem
It is not a frustration, it is a feature request -
but in a project I would like to progammatically generate menu items using the routes available. This way, whenever I have new routes introduced, I immediately have them in the menu (with a bit of chances to show in a nice way).
Describe the proposed solution
I would like to see, as a suggestion that $app-navigation's api get extended with a routes object that has a list or tree structure the represents the routes as currently present within the app. Then I can use that object to generate my menu or whatever feature I want to build
Alternatives considered
Just create a json file or static js object in the project that has all the menu items.
Importance
nice to have
Additional Information
No response
Not exactly a duplicate, but it feels like this could be folded into #1142.
Seems indeed - except that I don't hope I need to parse xml programmatically then :)
Can you use https://vitejs.dev/guide/features.html#glob-import ? That's how I've seen this done in the past
@Tommertom as a temporary solution, you can use the following as a basis for what you're trying:
src/routes/data/navigation.json.ts
import {Buffer} from 'buffer';
import {fileURLToPath} from 'url';
import {dirname} from 'path';
import rehypeParse from 'rehype-parse';
import {select} from 'unist-util-select';
import {unified} from 'unified';
import {visit} from 'unist-util-visit';
import dree from 'dree';
import type {Literal} from 'unist';
import type {Node} from 'unist-util-visit';
import type {ScanOptions, Dree} from 'dree';
import type {RequestHandler} from '@sveltejs/kit';
import type {Page} from '@sveltejs/kit';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const paths = _dirname.split('/');
const docsRootIndex = paths.indexOf('docs');
const docsPath = paths.slice(0, docsRootIndex + 1).join('/');
interface ElementNode extends Node {
tagName: string;
}
interface Entry {
name: string;
title: string;
order?: Page['stuff']['frontmatter']['navOrder'];
}
export interface NavTree {
children?: NavTree[];
depth: number;
name: string;
order?: number;
pathname: string;
title: string;
}
/**
* Stuff's interface, as referenced by Page['stuff']
interface Stuff {
frontmatter: {
//... other properties
navOrder?: number;
};
navigation: Locals.navTree;
}
*/
interface RawTree extends Omit<NavTree, 'children'> {
type: dree.Type;
children?: RawTree[];
order?: Page['stuff']['frontmatter']['navOrder'];
}
interface ViteModule {
default: {render: () => {html: string}};
metadata?: Page['stuff']['frontmatter'];
}
const treeCache: Record<string, RawTree> = {};
function createNavTree(tree: Dree, depth = 0): RawTree {
const {children, type, path} = tree;
const name = path.replace(docsPath, '').replace('/src/routes', '');
const indexRegex = /index\..+$/;
const pathname =
type === dree.Type.DIRECTORY
? name
: [name]
.map((x) => {
const delimiter = indexRegex.test(x) ? '/' : '.';
return x.split(delimiter).slice(0, -1).join(delimiter);
})
.join('');
const newTree: RawTree = {name, type, pathname, depth, title: name};
if (children) {
newTree.children = children.map((child) => createNavTree(child, depth + 1));
}
return newTree;
}
function sortTree(tree: RawTree | NavTree) {
const indexRegex = /index\..+$/;
const {children} = tree;
const safeChildren = children || [];
const isDir = (tree as RawTree).type === dree.Type.DIRECTORY;
const hasChildAsIndex = safeChildren.some((child) => indexRegex.test(child.name));
// if:
// is a directory, and,
// has a child as index
// set the child's order on the tree
[{useChildOrder: isDir && hasChildAsIndex, tree}]
.filter(({useChildOrder}) => useChildOrder)
.map(({tree}) => tree)
.map((treeItem) => {
(treeItem.children || [])
.filter((child) => indexRegex.test(child.name))
.slice(0, 1)
.map((child) => {
treeItem.order = child.order;
treeItem.title = child.title;
});
return treeItem;
});
// sort the children
[tree]
.filter((treeItem) => Array.isArray(treeItem.children))
.map((treeItem) => {
treeItem.children = (tree.children || [])
.sort((a, b) => {
const [orderA, orderB] = [a.order, b.order].map((order) => {
return typeof order === 'number' ? order : Number.MAX_VALUE;
});
const sortByOrder = orderA - orderB;
const sortByTitle = a.title > b.title ? 1 : -1;
return sortByOrder || sortByTitle;
})
.map((child) => sortTree(child));
return treeItem;
});
return tree;
}
function addEntryToTree(tree: RawTree | NavTree, entry: Entry): NavTree {
const isFile = (tree as RawTree).type === dree.Type.FILE;
const hasSameFilename = tree.name === entry.name;
const isMatch = [isFile, hasSameFilename].every(Boolean);
const config = {isMatch, tree};
// if not a match, pass entry to children of current tree
[config]
.filter(({isMatch}) => !isMatch)
.map(({tree}) => tree.children)
.filter((children): children is [] => Array.isArray(children))
.flatMap((children) => children.map((child) => addEntryToTree(child, entry)));
// if a match, set the tree's title and order using the entry
[config]
.filter(({isMatch}) => isMatch)
.map(({tree}) => tree)
.map((treeItem) => {
treeItem.title = entry.title;
treeItem.order = entry.order;
});
return tree;
}
function createEntry({name, module}: {name: string; module: ViteModule}): Entry {
const rendered = module.default.render();
const markupTree = unified().use(rehypeParse, {fragment: true}).parse(rendered.html);
const frontmatter: Page['stuff']['frontmatter'] = module.metadata || {};
let elements: ElementNode[] = [];
visit(markupTree, 'element', (node: ElementNode) => {
elements = [...elements, node];
});
const headings = elements
.filter((element) => ['h1', 'h2'].indexOf(element.tagName) > -1)
.sort((a, b) => (a.tagName > b.tagName ? 1 : -1));
const heading = headings.find(Boolean);
const headingText: Literal<string> | null | undefined = [heading]
.map((element) => select('text', element))
.find(Boolean) as Literal<string>;
const title = headingText ? (headingText.value as string) : name;
return {
name: name.replace('/src/routes', ''),
order: frontmatter.navOrder,
title,
};
}
const GET: RequestHandler = async function GET() {
const options: ScanOptions = {
excludeEmptyDirectories: true,
extensions: ['md', 'svx'],
followLinks: false,
hash: false,
showHidden: false,
size: false,
skipErrors: false,
stat: false,
};
const tree = dree.scan('./src/routes', options);
const encodedTree = Buffer.from(JSON.stringify(tree), 'base64').toString();
let navTree = treeCache[encodedTree];
if (!navTree) {
navTree = createNavTree(tree);
treeCache[encodedTree] = navTree;
}
const modules = import.meta.glob<ViteModule>('/src/routes/**/*.{md,svx}');
const files = await Promise.all(
Object.entries(modules).map(async ([name, fn]) => {
const module = await fn();
return {name, module};
}),
);
const entries: Entry[] = files.map(createEntry);
const navigation = entries.reduce<NavTree>((acc, entry) => {
return addEntryToTree(acc, entry);
}, navTree);
const sortedNavigation = sortTree(navigation);
return {body: JSON.stringify(sortedNavigation)};
};
export {GET};
I've recently implemented this for a SvelteKit site using MDSvex to allow for writing in markdown.
What it does is:
- traverse the folder structure under
src/routesusing dree, filtering for.mdand.svxfiles - you could filter for.sveltefiles here - get the files via
import.meta.glob - build a tree using dree and the data from the files
- sort the tree
- return the tree in the response
You can then get the tree via the load function in your layout to build it out.
There's additional stuff like using frontmatter to allow for ordering of pages, and using unifiedjs to traverse the markdown / HTML to pull out headings etc. - useful for creating menu titles etc. in the navigation.
It's quite a chunk of code, and my use-case with markdown is likely not going to match yours, but ye... it was non-trivial to implement, so thought I'd give a heads up on what could be a good starting point for others until something official is released.
Hi @larrybotha and @benmccann
Thanks for both pointers.
I will use this which is sufficint for my use case -giving an array of menu items for the app
const modules = import.meta.glob('./components/*.*');
const menuItems = Object.keys(modules).map((item) =>
item.replace('./components/', '').replace('.svelte', '')
);
console.log('MODULES & Menu', modules, menuItems);
It'd be super nice to get all route details from, say, import { routes } from '$app/paths'.
Example of data I'd find useful
[
{ id: '/', pattern: /^\/$/, params: [] },
{ id: '/about', pattern: /^\/about\/?$/, params: [] },
{ id: '/dashboard', pattern: /^\/dashboard\/?$/, params: [] },
{
id: '/dashboard/widgets',
pattern: /^\/dashboard\/widgets\/?$/,
params: []
},
{
id: '/dashboard/widgets/[widgetId]',
pattern: /^\/dashboard\/widgets\/([^/]+?)\/?$/,
params: [
{
name: 'widgetId',
matcher: undefined,
optional: false,
rest: false,
chained: false
}
]
},
{
id: '/dashboard/widgets/[widgetId]/edit/[[what]]',
pattern: /^\/dashboard\/widgets\/([^/]+?)\/edit(?:\/([^/]+))?\/?$/,
params: [
{
name: 'widgetId',
matcher: undefined,
optional: false,
rest: false,
chained: false
},
{
name: 'what',
matcher: undefined,
optional: true,
rest: false,
chained: true
}
]
},
{ id: '/help', pattern: /^\/help\/?$/, params: [] },
{
id: '/help/[...slug]',
pattern: /^\/help(?:\/(.*))?\/?$/,
params: [
{
name: 'slug',
matcher: undefined,
optional: false,
rest: true,
chained: true
}
]
}
]
Some people are already doing this, either with Vite plugins or with import.meta.glob. Examples:
- https://www.kitql.dev/docs/tools/06_vite-plugin-kit-routes,
- Ben's comment above
It's seems better/safer for SvelteKit to provide it, given that it is in charge of the logic (what's considered a "route", optionality and spreadability of params, etc.)
It's seems better/safer for SvelteKit to provide it, given that it is in charge of the logic (what's considered a "route"
+100 especially with the addition of reroute, import.meta.glob can no longer be depended upon.
This is something super-sitemap will have to deal with eventually, to move away from import.meta.glob to something else, due to i18n packages like Paraglide https://github.com/jasongitmail/super-sitemap/issues/24