diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 000000000..429e43e56
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,126 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Repository Overview
+This is the Optimism Documentation website repository that powers docs.optimism.io - the official technical documentation for the Optimism Collective, covering the OP Stack, Superchain, and interoperability features.
+
+## Tech Stack
+- **Framework**: Next.js 14.2.21 with React 18.2.0
+- **Documentation Engine**: Nextra 2.13.2 (docs theme)
+- **Language**: TypeScript
+- **Package Manager**: pnpm (required - do not use npm or yarn)
+- **Content Format**: MDX (Markdown with React components)
+- **Deployment**: Netlify
+
+## Essential Commands
+
+### Development
+```bash
+pnpm dev # Start development server at localhost:3001
+pnpm build # Create production build
+```
+
+### Quality Checks (Run before committing)
+```bash
+pnpm lint # Run all linting (ESLint + spellcheck + breadcrumbs + redirects + metadata)
+pnpm fix # Auto-fix all fixable issues (runs on pre-push automatically)
+```
+
+### Individual Linting Commands
+```bash
+pnpm spellcheck:lint # Check spelling
+pnpm spellcheck:fix # Add words to dictionary (words.txt)
+pnpm lint:eslint # Run ESLint
+pnpm lint:breadcrumbs # Validate breadcrumb structure
+pnpm lint:redirects # Check redirect configuration
+pnpm lint:metadata # Validate page metadata
+```
+
+## Architecture & Structure
+
+### Content Organization
+```
+pages/ # All documentation content (MDX files)
+├── app-developers/ # Application developer guides
+├── operators/ # Node & chain operator documentation
+├── stack/ # OP Stack protocol documentation
+├── superchain/ # Superchain network documentation
+├── interop/ # Interoperability documentation
+└── connect/ # Contributing guides and resources
+```
+
+### Key Directories
+- `components/`: Reusable React components for documentation
+- `public/`: Static assets, images, and tutorial files
+- `utils/`: Utility scripts for linting, validation, and build processes
+- `providers/`: React context providers for global state
+
+## Important Patterns
+
+### MDX Page Structure
+All documentation pages use MDX format with frontmatter metadata:
+```mdx
+---
+title: Page Title
+lang: en-US
+description: Page description for SEO
+---
+
+import { ComponentName } from '@/components/ComponentName'
+
+# Content here...
+```
+
+### Component Imports
+Use the configured path alias for component imports:
+```typescript
+import { ComponentName } from '@/components/ComponentName'
+```
+
+### Adding New Documentation
+1. Create MDX file in appropriate `pages/` subdirectory
+2. Include required frontmatter (title, lang, description)
+3. Run `pnpm lint` to validate metadata and content
+4. Use existing components from `components/` directory when possible
+
+### Spell Checking
+- Custom dictionary maintained in `words.txt`
+- Add technical terms using `pnpm spellcheck:fix`
+- Spell checking runs automatically in the lint pipeline
+
+## Git Workflow
+- **Pre-push hook**: Automatically runs `pnpm fix` via Husky
+- **Auto-commit**: Fixes are automatically committed if changes are made
+- **No pre-commit hooks**: Only validation on push
+
+## Special Features
+- **Kapa.ai Widget**: AI assistant integrated for documentation queries
+- **Algolia Search**: Full-text search across documentation
+- **Feelback**: User feedback collection system
+- **Growth Book**: A/B testing framework for feature experiments
+
+## Common Tasks
+
+### Adding a New Page
+1. Create `.mdx` file in appropriate `pages/` directory
+2. Add frontmatter with title, lang, and description
+3. Write content using Markdown and import React components as needed
+4. Run `pnpm dev` to preview
+5. Run `pnpm lint` before committing
+
+### Updating Components
+- Components are in `components/` directory
+- Follow existing patterns and TypeScript types
+- Test component changes across multiple pages that use them
+
+### Working with Images
+- Place images in `public/img/` directory
+- Reference using `/img/filename.ext` in MDX files
+- Optimize images before adding to repository
+
+## Notes
+- The repository uses automated quality checks - always run `pnpm lint` before pushing
+- Netlify handles deployment automatically on merge to main
+- TypeScript is configured with relaxed strict mode - follow existing patterns
+- MDX allows mixing Markdown with React components - leverage this for interactive content
\ No newline at end of file
diff --git a/components/AskAIButton.tsx b/components/AskAIButton.tsx
index 344e20d17..1a6b13c0d 100644
--- a/components/AskAIButton.tsx
+++ b/components/AskAIButton.tsx
@@ -2,7 +2,13 @@ import { RiSparkling2Fill } from '@remixicon/react';
import { useFeature } from '@growthbook/growthbook-react';
import { useEffect, useState } from 'react';
-const AskAIButton = () => {
+interface AskAIButtonProps {
+ fullWidth?: boolean;
+ large?: boolean;
+ id?: string;
+}
+
+const AskAIButton = ({ fullWidth = false, large = false, id = 'custom-ask-ai-button' }: AskAIButtonProps) => {
const [mounted, setMounted] = useState(false);
const enableDocsAIWidget = useFeature('enable_docs_ai_widget').on;
@@ -19,10 +25,17 @@ const AskAIButton = () => {
return null;
}
+ const baseClasses = 'nx-flex nx-gap-2 nx-items-center nx-rounded-lg nx-font-semibold nx-justify-center';
+ const sizeClasses = large
+ ? 'nx-py-3 nx-px-6 nx-text-base'
+ : 'nx-py-1.5 nx-px-3 nx-text-sm';
+ const widthClasses = fullWidth ? 'nx-w-full' : '';
+ const iconSize = large ? 16 : 14;
+
return (
{
}}
>
Ask AI
-
+
);
};
diff --git a/components/CustomHeader/index.tsx b/components/CustomHeader/index.tsx
new file mode 100644
index 000000000..997853ba0
--- /dev/null
+++ b/components/CustomHeader/index.tsx
@@ -0,0 +1,273 @@
+/** @format */
+
+import React, { useState, useEffect } from 'react';
+import Link from 'next/link';
+import { useRouter } from 'next/router';
+import { Search } from '../Search';
+import { AskAIButton } from '../AskAIButton';
+import { useTheme } from 'nextra-theme-docs';
+import { MoonIcon, SunIcon, Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline';
+
+const CustomHeader = () => {
+ const router = useRouter();
+ const { theme, setTheme } = useTheme();
+ const [mounted, setMounted] = useState(false);
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
+ const [isDesktop, setIsDesktop] = useState(false);
+ const [isNavigating, setIsNavigating] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+
+ const checkScreenSize = () => {
+ setIsDesktop(window.innerWidth >= 768);
+ };
+
+ checkScreenSize();
+ window.addEventListener('resize', checkScreenSize);
+
+ return () => window.removeEventListener('resize', checkScreenSize);
+ }, []);
+
+ useEffect(() => {
+ const handleStart = () => setIsNavigating(true);
+ const handleComplete = () => setIsNavigating(false);
+
+ router.events.on('routeChangeStart', handleStart);
+ router.events.on('routeChangeComplete', handleComplete);
+ router.events.on('routeChangeError', handleComplete);
+
+ return () => {
+ router.events.off('routeChangeStart', handleStart);
+ router.events.off('routeChangeComplete', handleComplete);
+ router.events.off('routeChangeError', handleComplete);
+ };
+ }, [router]);
+
+ useEffect(() => {
+ const handleEscape = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ setIsMobileMenuOpen(false);
+ }
+ };
+
+ if (isMobileMenuOpen) {
+ document.addEventListener('keydown', handleEscape);
+ document.body.style.overflow = 'hidden';
+ return () => {
+ document.removeEventListener('keydown', handleEscape);
+ document.body.style.overflow = '';
+ };
+ } else {
+ document.body.style.overflow = '';
+ }
+ }, [isMobileMenuOpen]);
+
+ const navItems = [
+ { title: 'Get started', href: '/get-started/superchain' },
+ { title: 'Superchain', href: '/superchain/superchain-explainer' },
+ { title: 'Interoperability', href: '/interop/get-started' },
+ { title: 'App Devs', href: '/app-developers/get-started' },
+ { title: 'Operators', href: '/operators/chain-operators/architecture' },
+ { title: 'OP Stack', href: '/stack/getting-started' }
+ ];
+
+ const isActive = (href: string) => {
+ return router.pathname.startsWith(href);
+ };
+
+ const handleMouseEnter = (href: string) => {
+ router.prefetch(href);
+ };
+
+ if (!mounted) return null;
+
+ return (
+
+ {/* Top Section: Logo, Search, Actions */}
+
+ {/* Logo */}
+
+
+
+
+
+
+
+ {/* Search Bar - hidden on mobile, shown on larger screens */}
+ {isDesktop && (
+
+
+
+ )}
+
+ {/* Right side: Ask AI + Theme Toggle + Mobile Menu Button */}
+
+
+
+ setTheme(theme === 'dark' ? 'light' : 'dark')}
+ >
+ {theme === 'dark' ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* Mobile menu button - only visible on mobile */}
+ {!isDesktop && (
+
setIsMobileMenuOpen(!isMobileMenuOpen)}
+ >
+ {isMobileMenuOpen ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+
+ {/* Mobile search bar - only visible on mobile */}
+ {!isDesktop && (
+
+
+
+ )}
+
+ {/* Desktop Navigation */}
+ {isDesktop && (
+
+
+
+ {navItems.map((item) => (
+ handleMouseEnter(item.href)}
+ className={`nx-text-sm nx-font-semibold nx-px-4 nx-py-2 nx-transition-all nx-duration-200 ${
+ isActive(item.href)
+ ? 'nx-text-primary-600 dark:nx-text-primary-400'
+ : 'nx-text-gray-600 hover:nx-text-gray-900 dark:nx-text-gray-400 dark:hover:nx-text-gray-100'
+ } ${isNavigating ? 'nx-opacity-70 nx-pointer-events-none' : ''}`}
+ >
+ {item.title}
+
+ ))}
+
+
+
+ )}
+
+ {/* Mobile Full-Screen Menu Overlay */}
+ {!isDesktop && (
+
+ {/* Mobile Menu Header */}
+
+
setIsMobileMenuOpen(false)}
+ className='nx-flex nx-items-center'
+ >
+
+
+
+
+
+
setIsMobileMenuOpen(false)}
+ >
+
+
+
+
+ {/* Mobile Menu Content - Scrollable */}
+
+ {/* Navigation Links */}
+
+
+ {navItems.map((item) => (
+ handleMouseEnter(item.href)}
+ onClick={() => setIsMobileMenuOpen(false)}
+ className={`nx-text-lg nx-font-semibold nx-py-2 nx-transition-all nx-duration-200 nx-block nx-text-gray-700 hover:nx-text-gray-900 dark:nx-text-gray-300 dark:hover:nx-text-gray-100 ${isNavigating ? 'nx-opacity-70 nx-pointer-events-none' : ''}`}
+ >
+ {item.title}
+
+ ))}
+
+
+
+ {/* Mobile Actions - Bottom of menu */}
+
+
+ setTheme(theme === 'dark' ? 'light' : 'dark')}
+ >
+ {theme === 'dark' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default CustomHeader;
diff --git a/components/LoadingBar/index.tsx b/components/LoadingBar/index.tsx
new file mode 100644
index 000000000..33de15ac7
--- /dev/null
+++ b/components/LoadingBar/index.tsx
@@ -0,0 +1,55 @@
+import React, { useState, useEffect } from 'react';
+import { useRouter } from 'next/router';
+
+const LoadingBar = () => {
+ const [loading, setLoading] = useState(false);
+ const [progress, setProgress] = useState(0);
+ const router = useRouter();
+
+ useEffect(() => {
+ let progressTimer: NodeJS.Timeout;
+ let finishTimer: NodeJS.Timeout;
+
+ const handleStart = () => {
+ setLoading(true);
+ setProgress(0);
+
+ progressTimer = setTimeout(() => {
+ setProgress(70);
+ }, 200);
+ };
+
+ const handleComplete = () => {
+ setProgress(100);
+ finishTimer = setTimeout(() => {
+ setLoading(false);
+ setProgress(0);
+ }, 300);
+ };
+
+ router.events.on('routeChangeStart', handleStart);
+ router.events.on('routeChangeComplete', handleComplete);
+ router.events.on('routeChangeError', handleComplete);
+
+ return () => {
+ router.events.off('routeChangeStart', handleStart);
+ router.events.off('routeChangeComplete', handleComplete);
+ router.events.off('routeChangeError', handleComplete);
+ clearTimeout(progressTimer);
+ clearTimeout(finishTimer);
+ };
+ }, [router]);
+
+ if (!loading) return null;
+
+ return (
+
+ );
+};
+
+export default LoadingBar;
\ No newline at end of file
diff --git a/components/Search/docsearch.tsx b/components/Search/docsearch.tsx
index 902a5c34c..1687b6808 100644
--- a/components/Search/docsearch.tsx
+++ b/components/Search/docsearch.tsx
@@ -1,26 +1,16 @@
-import { Transition } from "@headlessui/react";
-import cn from "clsx";
-import { useRouter } from "next/router";
-import { useMounted } from "nextra/hooks";
-import { InformationCircleIcon, SpinnerIcon } from "nextra/icons";
-import type {
- ReactNode,
- CompositionEvent,
- KeyboardEvent,
- ReactElement,
-} from "react";
-import {
- Fragment,
- useCallback,
- useEffect,
- useRef,
- useState,
- useContext,
-} from "react";
-import { Input } from "./input";
-import Link from "next/link";
-import * as aa from "search-insights";
-import AlgoliaContext from "@/utils/contexts/AlgoliaContext";
+/** @format */
+
+import { Transition } from '@headlessui/react';
+import cn from 'clsx';
+import { useRouter } from 'next/router';
+import { useMounted } from 'nextra/hooks';
+import { InformationCircleIcon, SpinnerIcon } from 'nextra/icons';
+import type { ReactNode, CompositionEvent, KeyboardEvent, ReactElement } from 'react';
+import { Fragment, useCallback, useEffect, useRef, useState, useContext } from 'react';
+import { Input } from './input';
+import Link from 'next/link';
+import * as aa from 'search-insights';
+import AlgoliaContext from '@/utils/contexts/AlgoliaContext';
type SearchResult = {
children: ReactNode;
@@ -40,7 +30,7 @@ type SearchProps = {
results: SearchResult[];
};
-const INPUTS = ["input", "select", "button", "textarea"];
+const INPUTS = ['input', 'select', 'button', 'textarea'];
export function DocSearch({
className,
@@ -50,7 +40,7 @@ export function DocSearch({
onActive,
loading,
error,
- results,
+ results
}: SearchProps): ReactElement {
const [show, setShow] = useState(false);
const [active, setActive] = useState(0);
@@ -78,22 +68,21 @@ export function DocSearch({
)
return;
if (
- e.key === "/" ||
- (e.key === "k" &&
- (e.metaKey /* for Mac */ || /* for non-Mac */ e.ctrlKey))
+ e.key === '/' ||
+ (e.key === 'k' && (e.metaKey /* for Mac */ || /* for non-Mac */ e.ctrlKey))
) {
e.preventDefault();
// prevent scrolling to the top
input.current.focus({ preventScroll: true });
- } else if (e.key === "Escape") {
+ } else if (e.key === 'Escape') {
setShow(false);
input.current.blur();
}
};
- window.addEventListener("keydown", down);
+ window.addEventListener('keydown', down);
return () => {
- window.removeEventListener("keydown", down);
+ window.removeEventListener('keydown', down);
};
}, []);
@@ -102,31 +91,28 @@ export function DocSearch({
const result = results[i];
- aa.default("clickedObjectIDsAfterSearch", {
- index: "docs",
- eventName: "Search Option Clicked",
+ aa.default('clickedObjectIDsAfterSearch', {
+ index: 'docs',
+ eventName: 'Search Option Clicked',
queryID: queryID,
objectIDs: [result.id],
- positions: [results.indexOf(result) + 1],
+ positions: [results.indexOf(result) + 1]
});
setObjectID(result.id);
- onChange("");
+ onChange('');
setShow(false);
};
- const handleActive = useCallback(
- (e: { currentTarget: { dataset: DOMStringMap } }) => {
- const { index } = e.currentTarget.dataset;
- setActive(Number(index));
- },
- []
- );
+ const handleActive = useCallback((e: { currentTarget: { dataset: DOMStringMap } }) => {
+ const { index } = e.currentTarget.dataset;
+ setActive(Number(index));
+ }, []);
const handleKeyDown = useCallback(
function (e: KeyboardEvent) {
switch (e.key) {
- case "ArrowDown": {
+ case 'ArrowDown': {
if (active + 1 < results.length) {
const el = ulRef.current?.querySelector(
`li:nth-of-type(${active + 2}) > a`
@@ -139,7 +125,7 @@ export function DocSearch({
}
break;
}
- case "ArrowUp": {
+ case 'ArrowUp': {
if (active - 1 >= 0) {
const el = ulRef.current?.querySelector(
`li:nth-of-type(${active}) > a`
@@ -152,7 +138,7 @@ export function DocSearch({
}
break;
}
- case "Enter": {
+ case 'Enter': {
const result = results[active];
if (result && composition) {
void router.push(result.route);
@@ -160,7 +146,7 @@ export function DocSearch({
}
break;
}
- case "Escape": {
+ case 'Escape': {
setShow(false);
input.current?.blur();
break;
@@ -177,57 +163,49 @@ export function DocSearch({
{
- onChange("");
+ onChange('');
}}
>
{value && focused
- ? "ESC"
+ ? 'ESC'
: mounted &&
- (navigator.userAgent.includes("Macintosh") ? (
+ (navigator.userAgent.includes('Macintosh') ? (
<>
- ⌘ K
+ ⌘ K
>
) : (
- "CTRL K"
+ 'CTRL K'
))}
);
- const handleComposition = useCallback(
- (e: CompositionEvent) => {
- setComposition(e.type === "compositionend");
- },
- []
- );
+ const handleComposition = useCallback((e: CompositionEvent) => {
+ setComposition(e.type === 'compositionend');
+ }, []);
return (
-
- {renderList && (
-
setShow(false)}
- />
- )}
+
+ {renderList &&
setShow(false)} />}
@@ -256,41 +234,47 @@ export function DocSearch({
show={renderList}
// Transition.Child is required here, otherwise popup will be still present in DOM after focus out
as={Transition.Child}
- leave="nx-transition-opacity nx-duration-100"
- leaveFrom="nx-opacity-100"
- leaveTo="nx-opacity-0"
+ leave='nx-transition-opacity nx-duration-100'
+ leaveFrom='nx-opacity-100'
+ leaveTo='nx-opacity-0'
>
{error ? (
-
-
- {"Error"}
+
+
+ {'Error'}
) : loading ? (
-
-
- {"Loading"}
+
+
+ {'Loading'}
) : results.length > 0 ? (
results.map(({ route, prefix, children, id }, i) => (
@@ -298,15 +282,15 @@ export function DocSearch({
{prefix}
))
) : (
-
+
No results found.
)}
-
+
diff --git a/components/Search/index.tsx b/components/Search/index.tsx
index f1442cf1a..230e1a1cb 100644
--- a/components/Search/index.tsx
+++ b/components/Search/index.tsx
@@ -1,18 +1,20 @@
-import type { Item as NormalItem } from "nextra/normalize-pages";
-import type { ReactElement } from "react";
-import { useContext, useEffect, useState } from "react";
-import { HighlightMatches } from "./highlight-matches";
-import { DocSearch } from "./docsearch";
-import algoliasearch from "algoliasearch";
-import AlgoliaContext from "@/utils/contexts/AlgoliaContext";
+/** @format */
+
+import type { Item as NormalItem } from 'nextra/normalize-pages';
+import type { ReactElement } from 'react';
+import { useContext, useEffect, useState } from 'react';
+import { HighlightMatches } from './highlight-matches';
+import { DocSearch } from './docsearch';
+import algoliasearch from 'algoliasearch';
+import AlgoliaContext from '@/utils/contexts/AlgoliaContext';
// Using environment variables for Algolia configuration
const client = algoliasearch(
- process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID || "",
- process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY || ""
+ process.env.NEXT_PUBLIC_ALGOLIA_APPLICATION_ID || '',
+ process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY || ''
);
-const index = client.initIndex(process.env.NEXT_PUBLIC_ALGOLIA_INDEX_NAME || "docs");
+const index = client.initIndex(process.env.NEXT_PUBLIC_ALGOLIA_INDEX_NAME || 'docs');
type AlgoliaHits = {
hits: AlgoliaHit[];
@@ -28,13 +30,13 @@ export type AlgoliaHit = {
export function Search({
className,
- directories,
+ directories
}: Readonly<{
className?: string;
directories: NormalItem[];
}>): ReactElement {
// Rest of your code remains the same
- const [search, setSearch] = useState("");
+ const [search, setSearch] = useState('');
const [results, setResults] = useState([]);
const { queryID, setQueryID } = useContext(AlgoliaContext);
@@ -42,7 +44,7 @@ export function Search({
useEffect(() => {
async function fetchData() {
const hits: AlgoliaHits = await index.search(search, {
- clickAnalytics: true,
+ clickAnalytics: true
});
setQueryID(hits.queryID);
@@ -53,11 +55,11 @@ export function Search({
children: (
<>
-
+
>
- ),
+ )
}));
setResults(mappedHits);
}
@@ -70,8 +72,8 @@ export function Search({
value={search}
onChange={setSearch}
className={className}
- overlayClassName="nx-w-screen nx-min-h-[100px] nx-max-w-[min(calc(100vw-2rem),calc(100%+20rem))]"
+ overlayClassName='nx-min-h-[100px] nx-w-[600px] nx-max-w-[calc(100vw-2rem)] nx-left-1/2 -nx-translate-x-1/2'
results={results}
/>
);
-}
\ No newline at end of file
+}
diff --git a/package.json b/package.json
index 97163bd87..9ad08ac64 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"@feelback/react": "^0.3.4",
"@growthbook/growthbook-react": "^1.3.1",
"@headlessui/react": "^2.1.8",
+ "@heroicons/react": "^2.2.0",
"@remixicon/react": "^4.6.0",
"algoliasearch": "^4.23.3",
"clsx": "^2.1.1",
diff --git a/pages/_app.tsx b/pages/_app.tsx
index b32bfeab1..8ee602e60 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -6,6 +6,7 @@ import * as gtag from '../utils/gtag';
import * as aa from 'search-insights';
import AlgoliaContext from '@/utils/contexts/AlgoliaContext';
import ScrollDispatcher from '@/components/ScrollDispatcher';
+import LoadingBar from '@/components/LoadingBar';
import { CustomGrowthBookProvider } from '../providers/GrowthbookProvider';
export default function App({ Component, pageProps }) {
@@ -31,6 +32,7 @@ export default function App({ Component, pageProps }) {
return (
+
= 16 || ^19.0.0-rc'
+
'@humanwhocodes/config-array@0.11.13':
resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==}
engines: {node: '>=10.10.0'}
@@ -4718,6 +4726,10 @@ snapshots:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
+ '@heroicons/react@2.2.0(react@18.2.0)':
+ dependencies:
+ react: 18.2.0
+
'@humanwhocodes/config-array@0.11.13':
dependencies:
'@humanwhocodes/object-schema': 2.0.1
@@ -7617,7 +7629,7 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
- nextra-theme-docs@2.13.2(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nextra@2.13.2(patch_hash=a4rp2hgojklggjmthmkiyqaek4)(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
+ nextra-theme-docs@2.13.2(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nextra@2.13.2(patch_hash=81936321c37741ec218dc19817c4a4939f4655b8371e793561fc236bebccc249)(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@headlessui/react': 1.7.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@popperjs/core': 2.11.8
@@ -7631,13 +7643,13 @@ snapshots:
next: 14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-seo: 6.4.0(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-themes: 0.2.1(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
- nextra: 2.13.2(patch_hash=a4rp2hgojklggjmthmkiyqaek4)(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ nextra: 2.13.2(patch_hash=81936321c37741ec218dc19817c4a4939f4655b8371e793561fc236bebccc249)(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
scroll-into-view-if-needed: 3.1.0
zod: 3.22.4
- nextra@2.13.2(patch_hash=a4rp2hgojklggjmthmkiyqaek4)(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
+ nextra@2.13.2(patch_hash=81936321c37741ec218dc19817c4a4939f4655b8371e793561fc236bebccc249)(next@14.2.21(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@headlessui/react': 1.7.17(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@mdx-js/mdx': 2.3.0
@@ -7959,7 +7971,7 @@ snapshots:
hast-util-raw: 9.0.1
vfile: 6.0.1
- remark-code-import@1.2.0(patch_hash=heylvfasxh3ubj2edns2svea2m):
+ remark-code-import@1.2.0(patch_hash=f6b78667b2fd0da0247b6e898a35f71bfde2a83adfa62fa6017a02dc7fb5d436):
dependencies:
strip-indent: 4.0.0
to-gatsby-remark-plugin: 0.1.0
@@ -8032,7 +8044,7 @@ snapshots:
unified: 10.1.2
unified-lint-rule: 2.1.2
- remark-lint-frontmatter-schema@3.15.4(patch_hash=jaxvkozlhcbn7zjsiti5ocoubi):
+ remark-lint-frontmatter-schema@3.15.4(patch_hash=32c1574b8fd989888047ea0226d42029f451eb7c875349c28b6259a73cc7e59f):
dependencies:
'@apidevtools/json-schema-ref-parser': 11.1.0
ajv: 8.12.0
diff --git a/theme.config.tsx b/theme.config.tsx
index 143030bac..a3798144e 100644
--- a/theme.config.tsx
+++ b/theme.config.tsx
@@ -9,29 +9,13 @@ import '@feelback/react/styles/feelback.css';
import { Search } from './components/Search';
import { Footer } from './components/Footer';
import { AskAIButton } from './components/AskAIButton';
+import CustomHeader from './components/CustomHeader';
import { useFeature } from '@growthbook/growthbook-react';
const config: DocsThemeConfig = {
- logo: (
- <>
-
-
-
-
- >
- ),
+ navbar: {
+ component: CustomHeader
+ },
darkMode: true,
// banner: {
// key: 'viem/op-stack',
@@ -42,12 +26,6 @@ const config: DocsThemeConfig = {
//
// )
// },
- search: {
- component: Search
- },
- navbar: {
- extraContent: AskAIButton
- },
docsRepositoryBase: 'https://github.com/ethereum-optimism/docs/blob/main/',
footer: {
text: