diff --git a/.changeset/fuzzy-waves-thank.md b/.changeset/fuzzy-waves-thank.md
new file mode 100644
index 0000000000..6d49cee325
--- /dev/null
+++ b/.changeset/fuzzy-waves-thank.md
@@ -0,0 +1,7 @@
+---
+'nextra-theme-blog': minor
+'nextra-theme-docs': minor
+'nextra': minor
+---
+
+insert `twoslash.css` styles only on pages where it was used
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 1604a2249e..2feac10882 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -17,9 +17,10 @@ jobs:
- name: Checkout Main
uses: actions/checkout@v4
- - uses: pnpm/action-setup@v4
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
- - name: Use Node.js
+ - name: Install Node.js
uses: actions/setup-node@v4
with:
node-version-file: .node-version
diff --git a/.github/workflows/nextjs-bundle-analysis.yml b/.github/workflows/nextjs-bundle-analysis.yml
index fc65fa9006..0ccd00d767 100644
--- a/.github/workflows/nextjs-bundle-analysis.yml
+++ b/.github/workflows/nextjs-bundle-analysis.yml
@@ -27,7 +27,8 @@ jobs:
- name: Checkout Main
uses: actions/checkout@v4
- - uses: pnpm/action-setup@v4
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
- name: Install Node.js
uses: actions/setup-node@v4
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index af09a24f44..d8ad91084c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -14,7 +14,8 @@ jobs:
- name: Checkout Main
uses: actions/checkout@v4
- - uses: pnpm/action-setup@v4
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
- name: Install Node.js
uses: actions/setup-node@v4
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 5fc3264b0f..fcdd5f3e86 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -21,9 +21,10 @@ jobs:
- name: Checkout Main
uses: actions/checkout@v4
- - uses: pnpm/action-setup@v4
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
- - name: Use Node.js
+ - name: Install Node.js
uses: actions/setup-node@v4
with:
node-version-file: .node-version
diff --git a/docs/app/docs/built-ins/head/page.mdx b/docs/app/docs/built-ins/head/page.mdx
index b422201ff0..fc222cb264 100644
--- a/docs/app/docs/built-ins/head/page.mdx
+++ b/docs/app/docs/built-ins/head/page.mdx
@@ -91,7 +91,7 @@ website:
| Hue (H) | Saturation (S) | Lightness (L) | Background Color |
| -------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- | ------------------ |
-| | | | |
+| | | | |
> [!TIP]
>
diff --git a/packages/nextra/package.json b/packages/nextra/package.json
index c7e18a9fa4..7030b3935f 100644
--- a/packages/nextra/package.json
+++ b/packages/nextra/package.json
@@ -10,6 +10,7 @@
},
"exports": {
"./katex.css": "./styles/katex.css",
+ "./twoslash.css": "./dist/twoslash.css",
"./package.json": "./package.json",
".": {
"types": "./dist/server/index.d.ts",
diff --git a/packages/nextra/postcss.config.js b/packages/nextra/postcss.config.js
new file mode 100644
index 0000000000..a814841204
--- /dev/null
+++ b/packages/nextra/postcss.config.js
@@ -0,0 +1 @@
+export { default } from '../nextra-theme-docs/postcss.config.mjs'
diff --git a/packages/nextra/src/client/setup-page.tsx b/packages/nextra/src/client/setup-page.tsx
index 5814b5dd5d..5b6b469b0f 100644
--- a/packages/nextra/src/client/setup-page.tsx
+++ b/packages/nextra/src/client/setup-page.tsx
@@ -3,17 +3,18 @@
* This file should be never used directly, only in loader.ts
*/
-import { useMDXComponents } from 'next-mdx-import-source-file'
+import { useMDXComponents as getMDXComponents } from 'next-mdx-import-source-file'
import type { ComponentProps, FC } from 'react'
import { createElement } from 'react'
import type { Heading, MDXWrapper, PageOpts } from '../types'
+const Wrapper = getMDXComponents().wrapper
+
export function HOC_MDXWrapper(
MDXContent: MDXWrapper,
hocProps: PageOpts & { toc: Heading[] }
): FC> {
return function MDXWrapper(props) {
- const Wrapper = useMDXComponents().wrapper
const children = createElement(MDXContent, props)
return Wrapper ? {children} : children
diff --git a/packages/nextra/src/server/rehype-plugins/rehype-twoslash-popup.ts b/packages/nextra/src/server/rehype-plugins/rehype-twoslash-popup.ts
index dc97260dd2..6d3ee7f80c 100644
--- a/packages/nextra/src/server/rehype-plugins/rehype-twoslash-popup.ts
+++ b/packages/nextra/src/server/rehype-plugins/rehype-twoslash-popup.ts
@@ -25,6 +25,24 @@ const TWOSLASH_POPUP_IMPORT_AST = {
}
} as MdxjsEsm
+/**
+ * To reduce bundle size, we insert css only where twoslash was used
+ */
+const TWOSLASH_CSS = {
+ type: 'mdxjsEsm',
+ data: {
+ estree: {
+ body: [
+ {
+ type: 'ImportDeclaration',
+ source: { type: 'Literal', value: 'nextra/twoslash.css' },
+ specifiers: []
+ } satisfies ImportDeclaration
+ ]
+ }
+ }
+} as any
+
export const rehypeTwoslashPopup: Plugin<[], Root> = () => ast => {
// The tagName is being converted to lowercase when calling the shiki.codeToHtml
// method inside rehypePrettyCode. Convert it back to Uppercase.
@@ -48,7 +66,7 @@ export const rehypeTwoslashPopup: Plugin<[], Root> = () => ast => {
visit(ast, { tagName: 'code' }, node => {
if (node.data?.meta === 'twoslash') {
- ast.children.unshift(TWOSLASH_POPUP_IMPORT_AST)
+ ast.children.unshift(TWOSLASH_CSS, TWOSLASH_POPUP_IMPORT_AST)
return EXIT
}
})
diff --git a/packages/nextra/src/twoslash.css b/packages/nextra/src/twoslash.css
new file mode 100644
index 0000000000..7b8ab5498b
--- /dev/null
+++ b/packages/nextra/src/twoslash.css
@@ -0,0 +1,200 @@
+/* ===== Basic ===== */
+:root {
+ --twoslash-border-color: #8888;
+ --twoslash-underline-color: currentColor;
+ --twoslash-highlighted-border: 195, 125, 13;
+ --twoslash-popup-bg: #f8f8f8;
+ --twoslash-popup-color: inherit;
+ --twoslash-popup-shadow: rgba(0, 0, 0.08) 0px 1px 4px;
+ --twoslash-docs-color: #888;
+ --twoslash-docs-font: sans-serif;
+ --twoslash-matched-color: inherit;
+ --twoslash-unmatched-color: #888;
+ --twoslash-cursor-color: #8888;
+ --twoslash-error-color: 212, 86, 86;
+ --twoslash-error-bg: rgba(var(--twoslash-error-color), 0.13);
+ --twoslash-tag-color: 55, 114, 207;
+ --twoslash-tag-warn-color: 195, 125, 13;
+ --twoslash-tag-annotate-color: 27, 166, 115;
+}
+
+.dark {
+ --twoslash-popup-bg: #000;
+ --twoslash-border-color: #404040;
+}
+
+/* Respect people's wishes to not have animations */
+@media (prefers-reduced-motion: reduce) {
+ .twoslash * {
+ transition: none !important;
+ }
+}
+
+/* ===== Hover Info ===== */
+.twoslash:hover .twoslash-hover {
+ border-color: var(--twoslash-underline-color);
+}
+
+.twoslash-hover {
+ border-bottom: 1px dotted transparent;
+ transition-timing-function: ease;
+ transition: border-color 0.3s;
+ position: relative;
+}
+
+.twoslash-popup-container {
+ @apply _inline-flex _flex-col _absolute _transition-opacity _duration-300 _z-10 _mt-1.5 _rounded;
+ transform: translateY(1.1em);
+ background: var(--twoslash-popup-bg) !important;
+ color: var(--twoslash-popup-color);
+ border: 1px solid var(--twoslash-border-color);
+ text-align: left;
+ /*box-shadow: var(--twoslash-popup-shadow);*/
+}
+
+.twoslash-query-presisted .twoslash-popup-container {
+ z-index: 9;
+ transform: translateY(1.5em);
+}
+
+.twoslash-popup-arrow {
+ @apply _absolute _-top-1 _border-t _border-r _size-1.5 _-rotate-45;
+ @apply _border-[--twoslash-border-color] _bg-[--twoslash-popup-bg];
+ left: 1em;
+ pointer-events: none;
+}
+
+.twoslash-popup-code,
+.twoslash-popup-docs {
+ padding: 6px 8px;
+}
+
+.twoslash-popup-docs {
+ @apply _text-sm;
+ color: var(--twoslash-docs-color);
+ font-family: var(--twoslash-docs-font);
+ border-top: 1px solid var(--twoslash-border-color);
+}
+
+.twoslash-popup-docs-tags {
+ display: flex;
+ flex-direction: column;
+ font-family: var(--twoslash-docs-font);
+}
+
+.twoslash-popup-docs-tags,
+.twoslash-popup-docs-tag-name {
+ margin-right: 0.5em;
+}
+
+/* ===== Error Line ===== */
+.twoslash-error-line {
+ position: relative;
+ background-color: var(--twoslash-error-bg);
+ border-left: 3px solid currentColor;
+ color: rgb(var(--twoslash-error-color));
+ padding: 6px 12px;
+ margin: 0.2em 0;
+}
+
+.twoslash-error {
+ background: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23c94824'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")
+ repeat-x bottom left;
+ padding-bottom: 2px;
+}
+
+/* ===== Completeions ===== */
+.twoslash-completion-cursor {
+ position: relative;
+}
+
+.twoslash-completion-cursor .twoslash-completion-list {
+ @apply _absolute _left-0 _border _top-1 _rounded;
+ transform: translate(0, 1.2em);
+ background: var(--twoslash-popup-bg);
+ border-color: var(--twoslash-border-color);
+}
+
+.twoslash-completion-list {
+ @apply _py-1 _px-2 _w-60;
+}
+
+.twoslash-completion-list::before {
+ background-color: var(--twoslash-cursor-color);
+ width: 2px;
+ position: absolute;
+ top: -1.6em;
+ height: 1.4em;
+ left: -1px;
+ content: ' ';
+}
+
+.twoslash-completion-list li {
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ gap: 0.25em;
+ line-height: 1em;
+}
+
+.twoslash-completion-list li span.twoslash-completions-unmatched {
+ color: var(--twoslash-unmatched-color);
+}
+
+.twoslash-completion-list .deprecated {
+ text-decoration: line-through;
+ opacity: 0.5;
+}
+
+.twoslash-completion-list li span.twoslash-completions-matched {
+ color: var(--twoslash-matched-color);
+}
+
+/* Highlights */
+.twoslash-highlighted {
+ background-color: rgba(var(--twoslash-highlighted-border), 0.13);
+ border: 1px solid rgba(var(--twoslash-highlighted-border), 0.31);
+ padding: 1px 2px;
+ margin: -1px -3px;
+ border-radius: 4px;
+}
+
+/* Icons */
+.twoslash-completion-list .twoslash-completions-icon {
+ color: var(--twoslash-unmatched-color);
+ width: 1em;
+ flex: none;
+}
+
+/* Custom Tags */
+.twoslash-tag-line {
+ position: relative;
+ background-color: rgba(var(--twoslash-tag-color), 0.13);
+ border-left: 3px solid currentColor;
+ color: rgb(var(--twoslash-tag-color));
+ padding: 6px 10px;
+ margin: 0.2em 0;
+ display: flex;
+ align-items: center;
+ gap: 0.3em;
+}
+
+.twoslash-tag-line .twoslash-tag-icon {
+ width: 1.1em;
+ color: inherit;
+}
+
+.twoslash-tag-line.twoslash-tag-error-line {
+ background-color: var(--twoslash-error-bg);
+ color: rgb(var(--twoslash-error-color));
+}
+
+.twoslash-tag-line.twoslash-tag-warn-line {
+ background-color: rgba(var(--twoslash-tag-warn-color), 0.13);
+ color: rgb(var(--twoslash-tag-warn-color));
+}
+
+.twoslash-tag-line.twoslash-tag-annotate-line {
+ background-color: rgba(var(--twoslash-tag-annotate-color), 0.13);
+ color: rgb(var(--twoslash-tag-annotate-color));
+}
diff --git a/packages/nextra/styles/code-block.css b/packages/nextra/styles/code-block.css
index 37259db486..7febc1925e 100644
--- a/packages/nextra/styles/code-block.css
+++ b/packages/nextra/styles/code-block.css
@@ -60,204 +60,3 @@ pre code.nextra-code:not([class*='twoslash-']) {
opacity: 1;
}
}
-
-/* ===== Basic ===== */
-:root {
- --twoslash-border-color: #8888;
- --twoslash-underline-color: currentColor;
- --twoslash-highlighted-border: 195, 125, 13;
- --twoslash-popup-bg: #f8f8f8;
- --twoslash-popup-color: inherit;
- --twoslash-popup-shadow: rgba(0, 0, 0.08) 0px 1px 4px;
- --twoslash-docs-color: #888;
- --twoslash-docs-font: sans-serif;
- --twoslash-matched-color: inherit;
- --twoslash-unmatched-color: #888;
- --twoslash-cursor-color: #8888;
- --twoslash-error-color: 212, 86, 86;
- --twoslash-error-bg: rgba(var(--twoslash-error-color), 0.13);
- --twoslash-tag-color: 55, 114, 207;
- --twoslash-tag-warn-color: 195, 125, 13;
- --twoslash-tag-annotate-color: 27, 166, 115;
-}
-
-.dark {
- --twoslash-popup-bg: #000;
- --twoslash-border-color: #404040;
-}
-
-/* Respect people's wishes to not have animations */
-@media (prefers-reduced-motion: reduce) {
- .twoslash * {
- transition: none !important;
- }
-}
-
-/* ===== Hover Info ===== */
-.twoslash:hover .twoslash-hover {
- border-color: var(--twoslash-underline-color);
-}
-
-.twoslash-hover {
- border-bottom: 1px dotted transparent;
- transition-timing-function: ease;
- transition: border-color 0.3s;
- position: relative;
-}
-
-.twoslash-popup-container {
- @apply _inline-flex _flex-col _absolute _transition-opacity _duration-300 _z-10 _mt-1.5 _rounded;
- transform: translateY(1.1em);
- background: var(--twoslash-popup-bg) !important;
- color: var(--twoslash-popup-color);
- border: 1px solid var(--twoslash-border-color);
- text-align: left;
- /*box-shadow: var(--twoslash-popup-shadow);*/
-}
-
-.twoslash-query-presisted .twoslash-popup-container {
- z-index: 9;
- transform: translateY(1.5em);
-}
-
-.twoslash-popup-arrow {
- @apply _absolute _-top-1 _border-t _border-r _size-1.5 _-rotate-45;
- @apply _border-[--twoslash-border-color] _bg-[--twoslash-popup-bg];
- left: 1em;
- pointer-events: none;
-}
-
-.twoslash-popup-code,
-.twoslash-popup-docs {
- padding: 6px 8px;
-}
-
-.twoslash-popup-docs {
- @apply _text-sm;
- color: var(--twoslash-docs-color);
- font-family: var(--twoslash-docs-font);
- border-top: 1px solid var(--twoslash-border-color);
-}
-
-.twoslash-popup-docs-tags {
- display: flex;
- flex-direction: column;
- font-family: var(--twoslash-docs-font);
-}
-
-.twoslash-popup-docs-tags,
-.twoslash-popup-docs-tag-name {
- margin-right: 0.5em;
-}
-
-/* ===== Error Line ===== */
-.twoslash-error-line {
- position: relative;
- background-color: var(--twoslash-error-bg);
- border-left: 3px solid currentColor;
- color: rgb(var(--twoslash-error-color));
- padding: 6px 12px;
- margin: 0.2em 0;
-}
-
-.twoslash-error {
- background: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23c94824'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")
- repeat-x bottom left;
- padding-bottom: 2px;
-}
-
-/* ===== Completeions ===== */
-.twoslash-completion-cursor {
- position: relative;
-}
-
-.twoslash-completion-cursor .twoslash-completion-list {
- @apply _absolute _left-0 _border _top-1 _rounded;
- transform: translate(0, 1.2em);
- background: var(--twoslash-popup-bg);
- border-color: var(--twoslash-border-color);
-}
-
-.twoslash-completion-list {
- @apply _py-1 _px-2 _w-60;
-}
-
-.twoslash-completion-list::before {
- background-color: var(--twoslash-cursor-color);
- width: 2px;
- position: absolute;
- top: -1.6em;
- height: 1.4em;
- left: -1px;
- content: ' ';
-}
-
-.twoslash-completion-list li {
- overflow: hidden;
- display: flex;
- align-items: center;
- gap: 0.25em;
- line-height: 1em;
-}
-
-.twoslash-completion-list li span.twoslash-completions-unmatched {
- color: var(--twoslash-unmatched-color);
-}
-
-.twoslash-completion-list .deprecated {
- text-decoration: line-through;
- opacity: 0.5;
-}
-
-.twoslash-completion-list li span.twoslash-completions-matched {
- color: var(--twoslash-matched-color);
-}
-
-/* Highlights */
-.twoslash-highlighted {
- background-color: rgba(var(--twoslash-highlighted-border), 0.13);
- border: 1px solid rgba(var(--twoslash-highlighted-border), 0.31);
- padding: 1px 2px;
- margin: -1px -3px;
- border-radius: 4px;
-}
-
-/* Icons */
-.twoslash-completion-list .twoslash-completions-icon {
- color: var(--twoslash-unmatched-color);
- width: 1em;
- flex: none;
-}
-
-/* Custom Tags */
-.twoslash-tag-line {
- position: relative;
- background-color: rgba(var(--twoslash-tag-color), 0.13);
- border-left: 3px solid currentColor;
- color: rgb(var(--twoslash-tag-color));
- padding: 6px 10px;
- margin: 0.2em 0;
- display: flex;
- align-items: center;
- gap: 0.3em;
-}
-
-.twoslash-tag-line .twoslash-tag-icon {
- width: 1.1em;
- color: inherit;
-}
-
-.twoslash-tag-line.twoslash-tag-error-line {
- background-color: var(--twoslash-error-bg);
- color: rgb(var(--twoslash-error-color));
-}
-
-.twoslash-tag-line.twoslash-tag-warn-line {
- background-color: rgba(var(--twoslash-tag-warn-color), 0.13);
- color: rgb(var(--twoslash-tag-warn-color));
-}
-
-.twoslash-tag-line.twoslash-tag-annotate-line {
- background-color: rgba(var(--twoslash-tag-annotate-color), 0.13);
- color: rgb(var(--twoslash-tag-annotate-color));
-}
diff --git a/packages/nextra/tailwind.config.ts b/packages/nextra/tailwind.config.ts
new file mode 100644
index 0000000000..c4d7f505d1
--- /dev/null
+++ b/packages/nextra/tailwind.config.ts
@@ -0,0 +1 @@
+export { default } from '../nextra-theme-docs/tailwind.config.js'
diff --git a/packages/nextra/tsup.config.ts b/packages/nextra/tsup.config.ts
index 844ccf4dea..45a0f94983 100644
--- a/packages/nextra/tsup.config.ts
+++ b/packages/nextra/tsup.config.ts
@@ -8,7 +8,12 @@ import { CWD, IS_PRODUCTION } from './src/server/constants.js'
export default defineConfig([
{
name: 'nextra',
- entry: [...defaultEntry, '!src/types.ts', '!src/client/icons'],
+ entry: [
+ ...defaultEntry,
+ '!src/types.ts',
+ '!src/client/icons',
+ 'src/twoslash.css'
+ ],
target: 'es2020',
format: 'esm',
dts: true,