Skip to content

Instantly share code, notes, and snippets.

@andrewcourtice
Created February 15, 2023 23:09
Show Gist options
  • Save andrewcourtice/748a6cb52e75b58a63727d712bb3b817 to your computer and use it in GitHub Desktop.
Save andrewcourtice/748a6cb52e75b58a63727d712bb3b817 to your computer and use it in GitHub Desktop.
Vite search index plugin
import fs from 'fs';
import path from 'path';
import getPagesPlugin from 'vite-plugin-pages';
import generateSitemap from 'vite-plugin-pages-sitemap';
import merge from 'lodash/merge.js';
import startCase from 'lodash/startCase.js';
import parseFrontmatter from '@yankeeinlondon/gray-matter';
import markdownIt from 'markdown-it';
import {
parse as parseVue,
} from '@vue/compiler-sfc';
import {
parse as parseHTML,
} from 'node-html-parser';
const markdownParser = markdownIt('commonmark');
// Remove code blocks from search results
markdownParser.use(md => {
md.renderer.rules.fence = () => '';
});
/**
* @typedef FileRoutesPluginOptions
* @property { string? } dir
*/
/**
* @typedef FileRouteMeta
* @property { string? } title
* @property { string? } description
* @property { object } search
*/
const EXTENSIONS = {
vue: 'vue',
markdown: 'md',
};
const ROUTE_INFO_PARSER = {
[EXTENSIONS.vue]: parseVueRouteInfo,
[EXTENSIONS.markdown]: parseMarkdownRouteInfo,
};
const CONTENT_PARSER = {
[EXTENSIONS.vue]: parseVueContent,
[EXTENSIONS.markdown]: parseMarkdownContent,
};
/**
* @param { string } value
*/
function titleCase(value) {
return startCase(value.toLocaleLowerCase());
}
/**
* @param { import('vite-plugin-pages').VueRoute } route
* @param { Record<string, any> } meta
*/
function assignBasicMeta(route, meta) {
return merge({}, route, {
meta,
});
}
/**
* @param { import('vite-plugin-pages').VueRoute } route
* @param { string } source
*
* @returns { import('vite-plugin-pages').VueRoute }
*/
function parseVueRouteInfo(route, source) {
return assignBasicMeta(route, {
isMarkdownComponent: false,
});
}
/**
* @param { import('vite-plugin-pages').VueRoute } route
* @param { string } source
*
* @returns { import('vite-plugin-pages').VueRoute }
*/
function parseMarkdownRouteInfo(route, source) {
const {
data,
} = parseFrontmatter(source);
return assignBasicMeta(data, {
isMarkdownComponent: true,
});
}
/**
* @param { string } source
*/
function parseVueContent(source) {
return parseVue(source).descriptor.template.content;
}
/**
* @param { string } source
*/
function parseMarkdownContent(source) {
const {
content,
} = parseFrontmatter(source);
return markdownParser.render(content);
}
/**
* @param { FileRoutesPluginOptions } options
*
* @returns { import('vite').Plugin }
*/
export default function fileRoutesPlugin(options) {
const searchIndex = [];
let root = process.cwd();
const pagesPlugin = getPagesPlugin({
dirs: ['src/routes'],
resolver: 'vue',
extensions: ['vue', 'md'],
moduleId: 'virtual:file-routes',
/**
* @param { import('vite-plugin-pages').VueRoute } route
*/
extendRoute(route) {
const filePath = path.join(root, route.component);
let {
name: fileName,
ext: fileExtension,
} = path.parse(filePath);
fileExtension = fileExtension.replace('.', '');
const routeInfoParser = ROUTE_INFO_PARSER[fileExtension];
const contentParser = CONTENT_PARSER[fileExtension];
const source = fs.readFileSync(filePath, {
encoding: 'utf-8',
});
const {
meta = {},
...routeInfo
} = routeInfoParser(route, source, filePath) || {};
const output = {
...route,
...routeInfo,
meta: {
title: titleCase(fileName),
description: '',
tags: [],
...meta,
},
};
if (output.meta?.search?.indexed === false) {
return output;
}
try {
const content = parseHTML(contentParser(source))
.structuredText
.trim()
.replace(/\{\{.*\}\}/g, '');
if (content) {
searchIndex.push({
content,
title: output.meta.title,
description: output.meta.description,
tags: output.meta.tags.join(),
route: {
name: route.name,
path: route.path,
},
});
}
} catch (error) {
console.warn(error);
}
return output;
},
onRoutesGenerated(routes) {
return generateSitemap({
routes,
changefreq: 'weekly',
// hostname TODO
});
},
});
return {
...pagesPlugin,
name: 'fathom:pages-plugin',
async configResolved(config) {
root = config.root;
return pagesPlugin.configResolved(config);
},
generateBundle(options, bundle) {
this.emitFile({
type: 'asset',
fileName: 'search-index.json',
source: JSON.stringify(searchIndex),
});
},
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment