@stackql/docusaurus-plugin-aeo 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +17 -2
- package/package.json +11 -1
- package/src/theme/AskAiButton/index.jsx +80 -85
- package/src/theme/AskAiButton/styles.module.css +8 -84
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
Visual refresh of the Ask AI button (feature 3) to match the look-and-feel of MUI-based dropdown components used elsewhere on consumer sites. No changes to features 1, 2, or 4.
|
|
6
|
+
|
|
7
|
+
### Changed (breaking)
|
|
8
|
+
|
|
9
|
+
- `@mui/material`, `@mui/icons-material`, `@emotion/react`, `@emotion/styled` are now required peer dependencies when `askAi.enabled` is `true`. Consumers that already use MUI (common in Docusaurus sites) get a single deduped copy at install time; consumers without MUI will get a clean npm peer-dependency install error pointing at exactly what to install. Consumers who disable the button (`askAi.enabled: false`) can skip these - the theme components are not registered and MUI is never imported.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Ask AI button reimplemented with MUI primitives: outlined `Button` (small, with `KeyboardArrowDownIcon` caret) for the trigger; `Menu` with `MenuItem`s containing `ListItemIcon` (the simple-icons brand SVGs from v0.3.0) and `ListItemText`. Menu is anchored bottom-right of the trigger and opens with transform-origin top-right.
|
|
14
|
+
- Button is hidden on viewports under 997px (matches the Docusaurus mobile breakpoint).
|
|
15
|
+
|
|
16
|
+
### Removed
|
|
17
|
+
|
|
18
|
+
- Custom click-outside and Esc-to-close handlers. MUI `Menu` provides both natively. Net reduction in `src/theme/AskAiButton/index.jsx`: ~25 lines.
|
|
19
|
+
- All custom trigger / menu CSS classes. The CSS module is now a 2-rule wrapper that controls only the responsive show/hide; all other styling lives on the MUI `sx` prop.
|
|
20
|
+
|
|
3
21
|
## 0.3.0
|
|
4
22
|
|
|
5
23
|
Focused UX upgrade to the Ask AI button (feature 3). No changes to features 1, 2, or 4.
|
package/README.md
CHANGED
|
@@ -170,7 +170,7 @@ Behavior:
|
|
|
170
170
|
|
|
171
171
|
## Feature 3: "Ask AI" button
|
|
172
172
|
|
|
173
|
-
|
|
173
|
+
An outlined pill button with a caret reading "Ask AI about this page" is injected at the top of every doc and blog-post content area, right-aligned in the breadcrumb row. Each item in the dropdown opens the corresponding AI surface in a new tab with a prefilled prompt that references the current page's `.md` companion. The button is hidden on viewports under 997px to keep the breadcrumb row uncluttered on mobile.
|
|
174
174
|
|
|
175
175
|
Placement specifics:
|
|
176
176
|
|
|
@@ -207,7 +207,22 @@ Options:
|
|
|
207
207
|
| `askAi.promptTemplate` | `'Read {pageUrl}.md and help me with the following question about it: '` | Prompt sent to each provider. `{pageUrl}` is the page's canonical URL (no trailing slash). If feature 1 is disabled, the consumer should remove the `.md` from the template. |
|
|
208
208
|
| `askAi.placement` | `'breadcrumb-row'` | `'breadcrumb-row'` puts the button at the top of every doc/blog page (docs breadcrumb row, or above the blog title). `'none'` does not register any theme components - swizzle the button into your preferred location manually. |
|
|
209
209
|
|
|
210
|
-
|
|
210
|
+
The button is built from [MUI](https://mui.com) primitives - outlined `Button` with a `KeyboardArrowDownIcon` caret as the trigger, and a `Menu` of `MenuItem` rows for the providers. Theming reads `--ifm-color-primary` and `--ifm-font-family-base` via MUI's `sx` prop, so dark/light mode work automatically. The MUI `Menu` handles click-outside-to-close and Esc-to-close natively.
|
|
211
|
+
|
|
212
|
+
### Peer dependencies
|
|
213
|
+
|
|
214
|
+
When `askAi.enabled` is `true` (the default), the following peer dependencies must be installed by the consumer site:
|
|
215
|
+
|
|
216
|
+
| Package | Range |
|
|
217
|
+
| --- | --- |
|
|
218
|
+
| `@mui/material` | `^5.0.0 \|\| ^6.0.0 \|\| ^7.0.0` |
|
|
219
|
+
| `@mui/icons-material` | `^5.0.0 \|\| ^6.0.0 \|\| ^7.0.0` |
|
|
220
|
+
| `@emotion/react` | `^11.0.0` |
|
|
221
|
+
| `@emotion/styled` | `^11.0.0` |
|
|
222
|
+
|
|
223
|
+
These are declared as peer dependencies (not direct dependencies) so consumers that already use MUI - common in Docusaurus sites - get a single deduped copy at install time. Consumers without MUI will get a clean npm peer-dependency error pointing at exactly what to install.
|
|
224
|
+
|
|
225
|
+
Consumers who disable the Ask AI button (`askAi.enabled: false`) can ignore these peers; the theme components are not registered in that case and MUI is never imported.
|
|
211
226
|
|
|
212
227
|
### Customizing placement
|
|
213
228
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackql/docusaurus-plugin-aeo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "AEO (Answer Engine Optimization) helpers for Docusaurus: .md companion files, llms.txt / llms-full.txt, an Ask AI dropdown, and /ai/* route conventions.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -21,9 +21,19 @@
|
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"@docusaurus/core": "^3.0.0",
|
|
24
|
+
"@mui/material": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
25
|
+
"@mui/icons-material": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
26
|
+
"@emotion/react": "^11.0.0",
|
|
27
|
+
"@emotion/styled": "^11.0.0",
|
|
24
28
|
"react": "^18.0.0 || ^19.0.0",
|
|
25
29
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
26
30
|
},
|
|
31
|
+
"peerDependenciesMeta": {
|
|
32
|
+
"@mui/material": { "optional": false },
|
|
33
|
+
"@mui/icons-material": { "optional": false },
|
|
34
|
+
"@emotion/react": { "optional": false },
|
|
35
|
+
"@emotion/styled": { "optional": false }
|
|
36
|
+
},
|
|
27
37
|
"dependencies": {
|
|
28
38
|
"gray-matter": "^4.0.3",
|
|
29
39
|
"simple-icons": "^16.22.0"
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import Button from '@mui/material/Button';
|
|
3
|
+
import Menu from '@mui/material/Menu';
|
|
4
|
+
import MenuItem from '@mui/material/MenuItem';
|
|
5
|
+
import ListItemIcon from '@mui/material/ListItemIcon';
|
|
6
|
+
import ListItemText from '@mui/material/ListItemText';
|
|
7
|
+
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|
2
8
|
import { useLocation } from '@docusaurus/router';
|
|
3
9
|
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
4
10
|
import { usePluginData } from '@docusaurus/useGlobalData';
|
|
@@ -19,51 +25,24 @@ const PROVIDER_URLS = {
|
|
|
19
25
|
gemini: 'https://gemini.google.com/app?q=',
|
|
20
26
|
};
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
28
|
+
const DEFAULT_PROMPT_TEMPLATE =
|
|
29
|
+
'Read {pageUrl}.md and help me with the following question about it: ';
|
|
25
30
|
|
|
26
|
-
export default function AskAiButton(
|
|
31
|
+
export default function AskAiButton() {
|
|
27
32
|
const { siteConfig } = useDocusaurusContext();
|
|
28
33
|
const data = usePluginData('@stackql/docusaurus-plugin-aeo') || {};
|
|
29
34
|
const cfg = data.askAi || {};
|
|
30
35
|
const location = useLocation();
|
|
31
36
|
|
|
32
|
-
const [
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
function onDocClick(e) {
|
|
37
|
-
if (wrapperRef.current && !wrapperRef.current.contains(e.target)) {
|
|
38
|
-
setOpen(false);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function onEsc(e) {
|
|
42
|
-
if (e.key === 'Escape') setOpen(false);
|
|
43
|
-
}
|
|
44
|
-
if (open) {
|
|
45
|
-
document.addEventListener('mousedown', onDocClick);
|
|
46
|
-
document.addEventListener('keydown', onEsc);
|
|
47
|
-
}
|
|
48
|
-
return () => {
|
|
49
|
-
document.removeEventListener('mousedown', onDocClick);
|
|
50
|
-
document.removeEventListener('keydown', onEsc);
|
|
51
|
-
};
|
|
52
|
-
}, [open]);
|
|
53
|
-
|
|
54
|
-
const toggle = useCallback(() => setOpen((v) => !v), []);
|
|
37
|
+
const [anchorEl, setAnchorEl] = useState(null);
|
|
38
|
+
const open = Boolean(anchorEl);
|
|
55
39
|
|
|
56
40
|
if (cfg.enabled === false) return null;
|
|
57
41
|
|
|
58
42
|
const baseUrl = (siteConfig.url || '').replace(/\/$/, '');
|
|
59
43
|
const pageUrl = `${baseUrl}${location.pathname.replace(/\/$/, '') || ''}`;
|
|
60
|
-
const promptTemplate =
|
|
61
|
-
|
|
62
|
-
'Read {pageUrl}.md and help me with the following question about it: ';
|
|
63
|
-
const targetUrl = cfg.companionsEnabled === false
|
|
64
|
-
? pageUrl
|
|
65
|
-
: pageUrl; // template controls .md suffix; pageUrl is the HTML route
|
|
66
|
-
const prompt = buildPrompt(promptTemplate, targetUrl);
|
|
44
|
+
const promptTemplate = cfg.promptTemplate || DEFAULT_PROMPT_TEMPLATE;
|
|
45
|
+
const prompt = promptTemplate.replace('{pageUrl}', pageUrl);
|
|
67
46
|
const encoded = encodeURIComponent(prompt);
|
|
68
47
|
|
|
69
48
|
const order =
|
|
@@ -71,60 +50,76 @@ export default function AskAiButton(props) {
|
|
|
71
50
|
? cfg.providerOrder
|
|
72
51
|
: ['claude', 'chatgpt', 'perplexity', 'gemini'];
|
|
73
52
|
|
|
53
|
+
const handleOpen = (e) => setAnchorEl(e.currentTarget);
|
|
54
|
+
const handleClose = () => setAnchorEl(null);
|
|
55
|
+
|
|
74
56
|
return (
|
|
75
|
-
<div className={styles.
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
57
|
+
<div className={styles.dropdownWrapper}>
|
|
58
|
+
<Button
|
|
59
|
+
variant="outlined"
|
|
60
|
+
size="small"
|
|
61
|
+
endIcon={<KeyboardArrowDownIcon />}
|
|
62
|
+
onClick={handleOpen}
|
|
79
63
|
aria-haspopup="menu"
|
|
80
64
|
aria-expanded={open}
|
|
81
|
-
|
|
65
|
+
sx={{
|
|
66
|
+
textTransform: 'none',
|
|
67
|
+
fontFamily: 'var(--ifm-font-family-base)',
|
|
68
|
+
fontWeight: 600,
|
|
69
|
+
fontSize: '0.75rem',
|
|
70
|
+
borderColor: 'var(--ifm-color-primary)',
|
|
71
|
+
color: 'var(--ifm-color-primary)',
|
|
72
|
+
'&:hover': {
|
|
73
|
+
borderColor: 'var(--ifm-color-primary)',
|
|
74
|
+
backgroundColor: 'rgba(0, 65, 101, 0.04)',
|
|
75
|
+
},
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
Ask AI about this page
|
|
79
|
+
</Button>
|
|
80
|
+
<Menu
|
|
81
|
+
anchorEl={anchorEl}
|
|
82
|
+
open={open}
|
|
83
|
+
onClose={handleClose}
|
|
84
|
+
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
|
85
|
+
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
|
86
|
+
sx={{
|
|
87
|
+
'& .MuiPaper-root': {
|
|
88
|
+
fontFamily: 'var(--ifm-font-family-base)',
|
|
89
|
+
minWidth: 200,
|
|
90
|
+
},
|
|
91
|
+
}}
|
|
82
92
|
>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
href={href}
|
|
114
|
-
target="_blank"
|
|
115
|
-
rel="noopener noreferrer"
|
|
116
|
-
onClick={() => setOpen(false)}
|
|
117
|
-
>
|
|
118
|
-
<span className={styles.icon}>
|
|
119
|
-
<Icon />
|
|
120
|
-
</span>
|
|
121
|
-
<span>{label}</span>
|
|
122
|
-
</a>
|
|
123
|
-
</li>
|
|
124
|
-
);
|
|
125
|
-
})}
|
|
126
|
-
</ul>
|
|
127
|
-
)}
|
|
93
|
+
{order.map((key) => {
|
|
94
|
+
const Icon = icons[key];
|
|
95
|
+
const label = PROVIDER_LABELS[key];
|
|
96
|
+
const base = PROVIDER_URLS[key];
|
|
97
|
+
if (!base || !Icon) return null;
|
|
98
|
+
const href = `${base}${encoded}`;
|
|
99
|
+
return (
|
|
100
|
+
<MenuItem
|
|
101
|
+
key={key}
|
|
102
|
+
component="a"
|
|
103
|
+
href={href}
|
|
104
|
+
target="_blank"
|
|
105
|
+
rel="noopener noreferrer"
|
|
106
|
+
onClick={handleClose}
|
|
107
|
+
>
|
|
108
|
+
<ListItemIcon>
|
|
109
|
+
<Icon />
|
|
110
|
+
</ListItemIcon>
|
|
111
|
+
<ListItemText
|
|
112
|
+
primaryTypographyProps={{
|
|
113
|
+
fontSize: '0.85rem',
|
|
114
|
+
fontFamily: 'var(--ifm-font-family-base)',
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
{label}
|
|
118
|
+
</ListItemText>
|
|
119
|
+
</MenuItem>
|
|
120
|
+
);
|
|
121
|
+
})}
|
|
122
|
+
</Menu>
|
|
128
123
|
</div>
|
|
129
124
|
);
|
|
130
125
|
}
|
|
@@ -1,87 +1,11 @@
|
|
|
1
|
-
.
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
.dropdownWrapper {
|
|
2
|
+
display: none;
|
|
3
|
+
flex-shrink: 0;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
background: var(--ifm-color-primary);
|
|
12
|
-
color: var(--ifm-color-primary-contrast-foreground, #fff);
|
|
13
|
-
border: none;
|
|
14
|
-
border-radius: 999px;
|
|
15
|
-
font-size: 0.875rem;
|
|
16
|
-
font-weight: 500;
|
|
17
|
-
line-height: 1.2;
|
|
18
|
-
cursor: pointer;
|
|
19
|
-
white-space: nowrap;
|
|
20
|
-
transition: background-color 0.15s ease;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
.trigger:hover {
|
|
24
|
-
background: var(--ifm-color-primary-darker, var(--ifm-color-primary-dark));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.trigger:focus-visible {
|
|
28
|
-
outline: 2px solid var(--ifm-color-primary-light, currentColor);
|
|
29
|
-
outline-offset: 2px;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
.caret {
|
|
33
|
-
display: inline-block;
|
|
34
|
-
font-size: 0.7em;
|
|
35
|
-
opacity: 0.9;
|
|
36
|
-
transition: transform 0.15s ease;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
.caretOpen {
|
|
40
|
-
transform: rotate(180deg);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
.menu {
|
|
44
|
-
position: absolute;
|
|
45
|
-
top: calc(100% + 0.4rem);
|
|
46
|
-
right: 0;
|
|
47
|
-
z-index: 100;
|
|
48
|
-
min-width: 200px;
|
|
49
|
-
margin: 0;
|
|
50
|
-
padding: 0.25rem 0;
|
|
51
|
-
list-style: none;
|
|
52
|
-
background: var(--ifm-background-surface-color, var(--ifm-background-color));
|
|
53
|
-
border: 1px solid var(--ifm-color-emphasis-300);
|
|
54
|
-
border-radius: 0.5rem;
|
|
55
|
-
box-shadow: 0 6px 24px -8px rgba(0, 0, 0, 0.18);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
.item {
|
|
59
|
-
margin: 0;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.itemLink {
|
|
63
|
-
display: flex;
|
|
64
|
-
align-items: center;
|
|
65
|
-
gap: 0.65rem;
|
|
66
|
-
padding: 0.5rem 0.85rem;
|
|
67
|
-
color: var(--ifm-color-content, var(--ifm-font-color-base));
|
|
68
|
-
text-decoration: none;
|
|
69
|
-
font-size: 0.875rem;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.itemLink:hover {
|
|
73
|
-
background: var(--ifm-color-emphasis-100);
|
|
74
|
-
color: var(--ifm-color-content, var(--ifm-font-color-base));
|
|
75
|
-
text-decoration: none;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.icon {
|
|
79
|
-
display: inline-flex;
|
|
80
|
-
align-items: center;
|
|
81
|
-
justify-content: center;
|
|
82
|
-
width: 18px;
|
|
83
|
-
height: 18px;
|
|
84
|
-
font-size: 18px;
|
|
85
|
-
color: var(--ifm-color-content, var(--ifm-font-color-base));
|
|
86
|
-
flex: 0 0 auto;
|
|
6
|
+
@media screen and (min-width: 997px) {
|
|
7
|
+
.dropdownWrapper {
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
}
|
|
87
11
|
}
|