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.');
+ });
+});