diff --git a/.gitignore b/.gitignore
index 6a8b69a74b034..b5101c2deb404 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,4 +60,5 @@ store
todos.md
examples/*/*.lock
examples/*/*-lock.yaml
-.store
\ No newline at end of file
+.store
+.nextra
\ No newline at end of file
diff --git a/docs/next.config.js b/docs/next.config.js
index 0d943d360ade0..abb74c78b26de 100644
--- a/docs/next.config.js
+++ b/docs/next.config.js
@@ -1,12 +1,12 @@
const withNextra = require("nextra")({
theme: "./nextra-theme-docs",
themeConfig: "./theme.config.js",
- unstable_stork: false,
+ unstable_contentDump: true,
unstable_staticImage: true,
});
module.exports = withNextra({
- reactStrictMode: true,
+ // reactStrictMode: true,
experiments: {
swcLoader: true,
swcMinify: true,
diff --git a/docs/nextra-theme-docs/flexsearch.js b/docs/nextra-theme-docs/flexsearch.js
new file mode 100644
index 0000000000000..858d7ee3d28e5
--- /dev/null
+++ b/docs/nextra-theme-docs/flexsearch.js
@@ -0,0 +1,370 @@
+import React, {
+ memo,
+ useCallback,
+ useRef,
+ useState,
+ useEffect,
+ Fragment,
+} from "react";
+import Router, { useRouter } from "next/router";
+import cn from "classnames";
+import Link from "next/link";
+import FlexSearch from "flexsearch";
+import { Transition } from "@headlessui/react";
+
+import { useConfig } from "./config";
+import renderComponent from "./utils/render-component";
+
+const Item = ({ page, first, title, active, href, onHover, excerpt }) => {
+ return (
+ <>
+ {first ? (
+
+ {page}
+
+ ) : null}
+
+
+
+
+ {title}
+
+ {excerpt ? (
+
+ {excerpt}
+
+ ) : null}
+
+
+
+ >
+ );
+};
+
+const MemoedStringWithMatchHighlights = memo(
+ function StringWithMatchHighlights({ content, search }) {
+ const splittedText = content.split("");
+ const escappedSearch = search.trim().replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
+ const regexp = RegExp(
+ "(" + escappedSearch.split(" ").join("|") + ")",
+ "ig"
+ );
+ let match;
+ let id = 0;
+ let index = 0;
+ const res = [];
+
+ while ((match = regexp.exec(content)) !== null) {
+ res.push(
+
+ {splittedText.splice(0, match.index - index).join("")}
+
+ );
+ res.push(
+
+ {splittedText.splice(0, regexp.lastIndex - match.index).join("")}
+
+ );
+ index = regexp.lastIndex;
+ }
+
+ res.push({splittedText.join("")});
+
+ return res;
+ }
+);
+
+// This can be global for better caching.
+const indexes = {};
+
+export default function Search() {
+ const config = useConfig();
+ const router = useRouter();
+ const [loading, setLoading] = useState(false);
+ const [show, setShow] = useState(false);
+ const [search, setSearch] = useState("");
+ const [active, setActive] = useState(0);
+ const [results, setResults] = useState([]);
+ const input = useRef(null);
+
+ const doSearch = () => {
+ if (!search) return;
+
+ const localeCode = Router.locale || "default";
+ const index = indexes[localeCode];
+
+ if (!index) return;
+
+ const pages = {};
+ const results = []
+ .concat(
+ ...index
+ .search(search, { enrich: true, limit: 10, suggest: true })
+ .map((r) => r.result)
+ )
+ .map((r, i) => ({
+ ...r,
+ index: i,
+ matchTitle:
+ r.doc.content.indexOf(search) > r.doc.content.indexOf(" _NEXTRA_ "),
+ }))
+ .sort((a, b) => {
+ if (a.matchTitle !== b.matchTitle) return a.matchTitle ? -1 : 1;
+ if (a.doc.page !== b.doc.page) return a.doc.page > b.doc.page ? 1 : -1;
+ return a.index - b.index;
+ })
+ .map((item) => {
+ const firstItemOfPage = !pages[item.doc.page];
+ pages[item.doc.page] = true;
+
+ return {
+ first: firstItemOfPage,
+ route: item.doc.url,
+ page: item.doc.page,
+ title: (
+
+ ),
+ excerpt:
+ item.doc.title !== item.doc.content ? (
+
+ ) : null,
+ };
+ });
+
+ setResults(results);
+ };
+ useEffect(doSearch, [search]);
+
+ const handleKeyDown = useCallback(
+ (e) => {
+ switch (e.key) {
+ case "ArrowDown": {
+ e.preventDefault();
+ if (active + 1 < results.length) {
+ setActive(active + 1);
+ const activeElement = document.querySelector(
+ `.nextra-flexsearch ul > a:nth-of-type(${active + 2})`
+ );
+ if (activeElement && activeElement.scrollIntoView) {
+ activeElement.scrollIntoView({
+ behavior: "smooth",
+ block: "nearest",
+ });
+ }
+ }
+ break;
+ }
+ case "ArrowUp": {
+ e.preventDefault();
+ if (active - 1 >= 0) {
+ setActive(active - 1);
+ const activeElement = document.querySelector(
+ `.nextra-flexsearch ul > a:nth-of-type(${active})`
+ );
+ if (activeElement && activeElement.scrollIntoView) {
+ activeElement.scrollIntoView({
+ behavior: "smooth",
+ block: "nearest",
+ });
+ }
+ }
+ break;
+ }
+ case "Enter": {
+ router.push(results[active].route);
+ break;
+ }
+ }
+ },
+ [active, results, router]
+ );
+
+ const load = async () => {
+ const localeCode = Router.locale || "default";
+ if (!indexes[localeCode] && !loading) {
+ setLoading(true);
+ const data = await (
+ await fetch(`/.nextra/data-${localeCode}.json`)
+ ).json();
+
+ const index = new FlexSearch.Document({
+ cache: 100,
+ tokenize: "full",
+ document: {
+ id: "id",
+ index: "content",
+ store: ["title", "content", "url", "page"],
+ },
+ context: {
+ resolution: 9,
+ depth: 1,
+ bidirectional: true,
+ },
+ filter: ["_NEXTRA_"],
+ });
+
+ for (let route in data) {
+ for (let heading in data[route].data) {
+ const [hash, text] = heading.split("#");
+ const title = text || data[route].title;
+ const url = route + (hash ? "#" + hash : "");
+
+ const paragraphs = (data[route].data[heading] || "")
+ .split("\n")
+ .filter(Boolean);
+
+ if (!paragraphs.length) {
+ index.add({
+ id: url,
+ url: url,
+ title,
+ content: title,
+ page: data[route].title,
+ });
+ }
+
+ for (let i = 0; i < paragraphs.length; i++) {
+ index.add({
+ id: url + "_" + i,
+ url: url,
+ title: title,
+ content: paragraphs[i] + (i === 0 ? " _NEXTRA_ " + title : ""),
+ page: data[route].title,
+ });
+ }
+ }
+ }
+
+ indexes[localeCode] = index;
+ setLoading(false);
+ setSearch((s) => s + " "); // Trigger the effect
+ }
+ };
+
+ useEffect(() => {
+ setActive(0);
+ }, [search]);
+
+ useEffect(() => {
+ const inputs = ["input", "select", "button", "textarea"];
+
+ const down = (e) => {
+ if (
+ document.activeElement &&
+ inputs.indexOf(document.activeElement.tagName.toLowerCase()) === -1
+ ) {
+ if (e.key === "/") {
+ e.preventDefault();
+ input.current.focus();
+ } else if (e.key === "Escape") {
+ setShow(false);
+ }
+ }
+ };
+
+ window.addEventListener("keydown", down);
+ return () => window.removeEventListener("keydown", down);
+ }, []);
+
+ const renderList = show && !!search;
+
+ return (
+
+ {renderList && (
+
setShow(false)} />
+ )}
+
+
{
+ setSearch(e.target.value);
+ setShow(true);
+ }}
+ className="block w-full px-3 py-2 leading-tight rounded-lg appearance-none focus:outline-none focus:ring-1 focus:ring-gray-200 focus:bg-white hover:bg-opacity-5 transition-colors dark:focus:bg-dark dark:focus:ring-gray-100 dark:focus:ring-opacity-20"
+ type="search"
+ placeholder={renderComponent(
+ config.searchPlaceholder,
+ {
+ locale: router.locale,
+ },
+ true
+ )}
+ onKeyDown={handleKeyDown}
+ onFocus={() => {
+ load();
+ setShow(true);
+ }}
+ onBlur={() => setShow(false)}
+ ref={input}
+ spellCheck={false}
+ />
+ {renderList ? null : (
+
+
+ /
+
+
+ )}
+
+
+
+ {loading ? (
+
+
+ Loading...
+
+ ) : results.length === 0 ? (
+ renderComponent(config.unstable_searchResultEmpty, {
+ locale: router.locale,
+ })
+ ) : (
+ results.map((res, i) => {
+ return (
+ - setActive(i)}
+ />
+ );
+ })
+ )}
+
+
+
+ );
+}
diff --git a/docs/nextra-theme-docs/index.js b/docs/nextra-theme-docs/index.js
index ac8b2d7c700d5..6789fc7410547 100644
--- a/docs/nextra-theme-docs/index.js
+++ b/docs/nextra-theme-docs/index.js
@@ -7,7 +7,7 @@ import cn from "classnames";
import Head from "./head";
import Navbar from "./navbar";
import Footer, { NavLinks } from "./footer";
-import Theme from "./misc/theme";
+import { MDXTheme } from "./misc/theme";
import Sidebar from "./sidebar";
import ToC from "./toc";
import { ThemeConfigContext, useConfig } from "./config";
@@ -16,8 +16,6 @@ import defaultConfig from "./misc/default.config";
import { getFSRoute } from "./utils/get-fs-route";
import { MenuContext } from "./utils/menu-context";
import normalizePages from "./utils/normalize-pages";
-import { getHeadings } from "./utils/get-headings";
-import { getTitle } from "./utils/get-title";
import traverse from "./utils/traverse";
import sortDate from "./utils/sort-date";
import Link from "next/link";
@@ -37,7 +35,7 @@ function useDirectoryInfo(pageMap) {
}, [pageMap, locale, defaultLocale, asPath]);
}
-function Body({ meta, toc, filepathWithName, navLinks, children, postList }) {
+function Body({ meta, toc, filepathWithName, navLinks, MDXContent, postList }) {
const config = useConfig();
return (
@@ -47,13 +45,15 @@ function Body({ meta, toc, filepathWithName, navLinks, children, postList }) {
) : (
- {children}
+
+
+
)
) : postList ? (
@@ -65,7 +65,7 @@ function Body({ meta, toc, filepathWithName, navLinks, children, postList }) {
) : meta.full ? (
- {children}
+
) : meta.type === "post" ? (
@@ -83,7 +83,7 @@ function Body({ meta, toc, filepathWithName, navLinks, children, postList }) {
- {children}
+
@@ -91,7 +91,7 @@ function Body({ meta, toc, filepathWithName, navLinks, children, postList }) {
) : (
- {children}
+
@@ -103,7 +103,15 @@ function Body({ meta, toc, filepathWithName, navLinks, children, postList }) {
);
}
-const Layout = ({ filename, pageMap, meta, route: _route, children }) => {
+const Layout = ({
+ filename,
+ pageMap,
+ meta,
+ route: _route,
+ MDXContent,
+ headings,
+ titleText,
+}) => {
const { route, locale } = useRouter();
const config = useConfig();
@@ -118,11 +126,10 @@ const Layout = ({ filename, pageMap, meta, route: _route, children }) => {
directories,
} = useDirectoryInfo(pageMap);
- const content = children.type();
const filepath = route.slice(0, route.lastIndexOf("/") + 1);
const filepathWithName = filepath + filename;
- const headings = getHeadings(content.props.children);
- const title = meta.title || getTitle(headings) || "Untitled";
+ const title = meta.title || titleText || "Untitled";
+
// gather info for tag/posts pages
let posts = null;
let navPages = [];
@@ -284,9 +291,8 @@ const Layout = ({ filename, pageMap, meta, route: _route, children }) => {
) : null
}
postList={postList}
- >
- {children}
-