Merge pull request #6 from PR0M3TH3AN/codex/implement-plugin-api-and-documentation

Add plugin API and docs
This commit is contained in:
thePR0M3TH3AN
2025-07-10 10:45:02 -04:00
committed by GitHub
5 changed files with 97 additions and 1 deletions

View File

@@ -126,6 +126,7 @@ features:
provider: "plausible"
id: "my-site.io"
pluginsDir: "plugins" # Folder for custom plugins
plugins:
- "analytics" # Load from plugins/
```
@@ -223,4 +224,43 @@ To make DocForge truly user-friendly, include these instructions in the starter
- **Future**: PDF export plugin, AI-assisted search suggestions.
- **Community**: MIT license; GitHub for issues.
## Creating Custom Plugins
Plugins are plain Node.js modules placed in the folder defined by `pluginsDir`.
Each module can export hook functions which DocForge calls during the build:
```js
module.exports = {
// Modify markdown before frontmatter is parsed
async onParseMarkdown({ file, content }) {
return { content };
},
// Modify the final HTML of each page
async onPageRendered({ file, html }) {
return { html };
}
};
```
Example `plugins/analytics.js`:
```js
module.exports = {
onPageRendered: async ({ html, file }) => {
const snippet = `\n<script>console.log('Page viewed: ${file}')</script>`;
return { html: html.replace('</body>', `${snippet}</body>`) };
}
};
```
Enable it in `config.yaml`:
```yaml
pluginsDir: "plugins"
plugins:
- "analytics"
```
Running `npm run build` will load the plugin and execute its hooks.
This spec is complete and ready for implementation. Prototype core parsing first, then add features iteratively. Total est. dev time: 2-4 weeks for MVP.

7
plugins/analytics.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
onPageRendered: async ({ html, file }) => {
// Example: inject analytics script into each page
const snippet = '\n<script>console.log("Page viewed: ' + file + '")</script>';
return html.replace('</body>', `${snippet}</body>`);
}
};

View File

@@ -44,6 +44,7 @@ function loadConfig(configPath = path.join(process.cwd(), 'config.yaml')) {
darkMode: false
},
features: {},
pluginsDir: 'plugins',
plugins: []
};

24
src/config/loadPlugins.js Normal file
View File

@@ -0,0 +1,24 @@
const path = require('path');
const fs = require('fs');
function loadPlugins(config) {
const dir = path.resolve(process.cwd(), config.pluginsDir || 'plugins');
const names = Array.isArray(config.plugins) ? config.plugins : [];
const plugins = [];
for (const name of names) {
const file = path.join(dir, name.endsWith('.js') ? name : `${name}.js`);
if (fs.existsSync(file)) {
try {
const mod = require(file);
plugins.push(mod);
} catch (e) {
console.error(`Failed to load plugin ${name}:`, e);
}
} else {
console.warn(`Plugin not found: ${file}`);
}
}
return plugins;
}
module.exports = loadPlugins;

View File

@@ -6,6 +6,7 @@ const Eleventy = require('@11ty/eleventy');
const lunr = require('lunr');
const { lexer } = require('marked');
const loadConfig = require('../config/loadConfig');
const loadPlugins = require('../config/loadPlugins');
async function readDirRecursive(dir) {
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
@@ -55,6 +56,17 @@ function buildNav(pages) {
async function generate({ contentDir = 'content', outputDir = '_site', configPath } = {}) {
const config = loadConfig(configPath);
const plugins = loadPlugins(config);
async function runHook(name, data) {
for (const plugin of plugins) {
if (typeof plugin[name] === 'function') {
const res = await plugin[name](data);
if (res !== undefined) data = res;
}
}
return data;
}
if (!fs.existsSync(contentDir)) {
console.error(`Content directory not found: ${contentDir}`);
return;
@@ -76,7 +88,9 @@ async function generate({ contentDir = 'content', outputDir = '_site', configPat
continue; // skip unchanged
}
}
const raw = await fs.promises.readFile(file, 'utf8');
let raw = await fs.promises.readFile(file, 'utf8');
const mdObj = await runHook('onParseMarkdown', { file: rel, content: raw });
if (mdObj && mdObj.content) raw = mdObj.content;
const parsed = matter(raw);
const title = parsed.data.title || path.basename(rel, '.md');
const tokens = lexer(parsed.content || '');
@@ -124,6 +138,16 @@ async function generate({ contentDir = 'content', outputDir = '_site', configPat
};
await elev.write();
for (const page of pages) {
const outPath = path.join(outputDir, page.file.replace(/\.md$/, '.html'));
if (fs.existsSync(outPath)) {
let html = await fs.promises.readFile(outPath, 'utf8');
const result = await runHook('onPageRendered', { file: page.file, html });
if (result && result.html) html = result.html;
await fs.promises.writeFile(outPath, html);
}
}
for (const asset of assets) {
const srcPath = path.join(contentDir, asset);
const destPath = path.join(outputDir, asset);