From 0917e032d90b2015e88585053434ef2fec14e996 Mon Sep 17 00:00:00 2001 From: FengYQ <329650437@qq.com> Date: Tue, 10 Nov 2026 19:02:17 +0800 Subject: [PATCH] feat: add OneTag AI "ask this page" plugin Adds an optional plugin that loads the OneTag AI widget: visitors type a question and get an answer drawn from the page they are reading. Nothing is loaded unless a key is set in `$docsify.onetag`. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017g16kS8U6SzeKsAZguBPEn --- docs/plugins.md | 27 +++++++++++++++ src/plugins/onetag.js | 36 ++++++++++++++++++++ test/e2e/onetag.test.js | 73 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 src/plugins/onetag.js create mode 100644 test/e2e/onetag.test.js diff --git a/docs/plugins.md b/docs/plugins.md index bca83191b..91fd1aa95 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -219,6 +219,33 @@ Disqus comments. https://disqus.com/ ``` +## OneTag AI + +[OneTag AI](https://onetagai.com) adds an "ask this page" button. Visitors type a question and get an answer drawn from the page they are reading. + +The key is a public site key with a hard usage cap, so it can be committed with your docs. Get one for free at https://onetagai.com. + +```html + + +``` + +Pass an object to set the [widget options](https://onetagai.com/docs#widget): + +```js +window.$docsify = { + onetag: { + key: 'your-onetag-key', + title: 'Ask the docs', + theme: 'dark', // 'light', 'dark' or 'auto' (default) + }, +}; +``` + ## Pagination Pagination for docsify. By [@imyelo](https://github.com/imyelo) diff --git a/src/plugins/onetag.js b/src/plugins/onetag.js new file mode 100644 index 000000000..3097380e5 --- /dev/null +++ b/src/plugins/onetag.js @@ -0,0 +1,36 @@ +// OneTag AI "ask this page" button — https://onetagai.com +// Visitors type a question and get an answer drawn from the page they are reading. + +const window = /** @type {any} */ (globalThis); + +function install(hook, vm) { + const config = vm.config.onetag; + const options = typeof config === 'string' ? { key: config } : { ...config }; + + if (!options.key) { + // eslint-disable-next-line no-console + console.error('[Docsify] onetag key is required.'); + return; + } + + hook.mounted(_ => { + const script = document.createElement('script'); + + script.async = true; + script.src = 'https://onetagai.com/widget.js'; + + // Answers are drawn from the rendered page, not from the sidebar or navbar. + Object.assign(script.dataset, { + ref: 'docsify', + source: '.markdown-section', + ...options, + }); + + document.body.appendChild(script); + }); +} + +window.$docsify = window.$docsify || {}; +window.$docsify.plugins = [install, ...(window.$docsify.plugins || [])]; + +export {}; diff --git a/test/e2e/onetag.test.js b/test/e2e/onetag.test.js new file mode 100644 index 000000000..0eca03703 --- /dev/null +++ b/test/e2e/onetag.test.js @@ -0,0 +1,73 @@ +import docsifyInit from '../helpers/docsify-init.js'; +import { test, expect } from './fixtures/docsify-init-fixture.js'; + +test.describe('OneTag Plugin Tests', () => { + const widgetURL = 'https://onetagai.com/widget.js'; + const widgetSelector = `script[src="${widgetURL}"]`; + + // The widget script is replaced with a stub so that tests do not depend on the network + async function stubWidget(page) { + await page.route(widgetURL, route => + route.fulfill({ + contentType: 'text/javascript', + body: 'window.onetagDataset = { ...document.currentScript.dataset };', + }), + ); + } + + test('key as a string', async ({ page }) => { + await stubWidget(page); + await docsifyInit({ + config: { + onetag: 'ot_test', + }, + scriptURLs: ['/dist/plugins/onetag.js'], + }); + + await expect + .poll(() => page.evaluate(() => window.onetagDataset)) + .toEqual({ + key: 'ot_test', + ref: 'docsify', + source: '.markdown-section', + }); + await expect(page.locator(widgetSelector)).toHaveCount(1); + }); + + test('options as an object', async ({ page }) => { + await stubWidget(page); + await docsifyInit({ + config: { + onetag: { + key: 'ot_test', + position: 'left', + title: 'Ask the docs', + }, + }, + scriptURLs: ['/dist/plugins/onetag.js'], + }); + + await expect + .poll(() => page.evaluate(() => window.onetagDataset)) + .toEqual({ + key: 'ot_test', + position: 'left', + ref: 'docsify', + source: '.markdown-section', + title: 'Ask the docs', + }); + }); + + test('nothing is loaded without a key', async ({ page }) => { + const consoleMsgs = []; + + page.on('console', msg => consoleMsgs.push(msg.text())); + await stubWidget(page); + await docsifyInit({ + scriptURLs: ['/dist/plugins/onetag.js'], + }); + + await expect(page.locator(widgetSelector)).toHaveCount(0); + expect(consoleMsgs).toContain('[Docsify] onetag key is required.'); + }); +});