diff --git a/.env.example b/.env.example index 889769b4..e5bad031 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,14 @@ GITHUB_TOKEN=your_github_token_here # This must be set for the discussions section to fetch live data from GitHub # Create a Classic PAT with read:discussion scope at https://github.com/settings/tokens DOCUSAURUS_GIT_TOKEN=your_github_token_here +# Algolia SiteSearch Configuration +# Request these from the maintainers after the Algolia crawler is configured. +# The navbar search is enabled only when all three values are set. +# ALGOLIA_INDEX_NAME must be an actual Algolia index name, for example: +# www_recodehive_com_8oew5oqz0y_pages +ALGOLIA_APP_ID=your_algolia_app_id +ALGOLIA_SEARCH_API_KEY=your_algolia_search_api_key +ALGOLIA_INDEX_NAME=your_algolia_index_name # Shopify Configuration (for Merch Store) # Get these from: Shopify Admin > Settings > Apps and sales channels > Develop apps diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 9a5ba1a2..2ad3d2b6 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -7,6 +7,24 @@ dotenv.config(); // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) +const algoliaAppId = process.env.ALGOLIA_APP_ID?.trim(); +const algoliaSearchApiKey = process.env.ALGOLIA_SEARCH_API_KEY?.trim(); +const algoliaIndexName = process.env.ALGOLIA_INDEX_NAME?.trim(); + +const hasAlgoliaSiteSearch = Boolean( + algoliaAppId && algoliaSearchApiKey && algoliaIndexName, +); + +const hasPartialAlgoliaSiteSearchConfig = Boolean( + algoliaAppId || algoliaSearchApiKey || algoliaIndexName, +); + +if (hasPartialAlgoliaSiteSearchConfig && !hasAlgoliaSiteSearch) { + console.warn( + "Algolia SiteSearch is partially configured. Set ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY, and the actual Algolia index name in ALGOLIA_INDEX_NAME to enable navbar search.", + ); +} + const config: Config = { title: "recode hive", tagline: "Learn, Build & Grow with Open Source", @@ -206,11 +224,6 @@ const config: Config = { }, ], }, - // Search disabled until Algolia is properly configured - // { - // type: "search", - // position: "right", - // }, { type: "html", position: "right", @@ -245,21 +258,6 @@ const config: Config = { theme: prismThemes.github, darkTheme: prismThemes.dracula, }, - // Disable Algolia search until properly configured - // algolia: { - // appId: "YOUR_APP_ID", - // apiKey: "YOUR_SEARCH_API_KEY", - // indexName: "YOUR_INDEX_NAME", - // contextualSearch: true, - // externalUrlRegex: "external\\.com|domain\\.com", - // replaceSearchResultPathname: { - // from: "/docs/", - // to: "/", - // }, - // searchParameters: {}, - // searchPagePath: "search", - // insights: false, - // }, } satisfies Preset.ThemeConfig, markdown: { @@ -296,6 +294,13 @@ const config: Config = { EMAILJS_PUBLIC_KEY: process.env.EMAILJS_PUBLIC_KEY || "", EMAILJS_SERVICE_ID: process.env.EMAILJS_SERVICE_ID || "", EMAILJS_TEMPLATE_ID: process.env.EMAILJS_TEMPLATE_ID || "", + algoliaSiteSearch: hasAlgoliaSiteSearch + ? { + applicationId: algoliaAppId, + apiKey: algoliaSearchApiKey, + indexName: algoliaIndexName, + } + : null, hooks: { onBrokenMarkdownLinks: "warn", }, diff --git a/package-lock.json b/package-lock.json index a21cc615..22b50008 100644 --- a/package-lock.json +++ b/package-lock.json @@ -383,7 +383,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -9818,7 +9818,7 @@ "version": "19.2.1", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -25657,7 +25657,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -27159,7 +27159,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/src/components/AlgoliaSiteSearch/index.tsx b/src/components/AlgoliaSiteSearch/index.tsx new file mode 100644 index 00000000..2bec1a04 --- /dev/null +++ b/src/components/AlgoliaSiteSearch/index.tsx @@ -0,0 +1,146 @@ +import React, { useEffect } from "react"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; + +const containerId = "algolia-sitesearch-navbar"; +const siteSearchScriptId = "algolia-sitesearch-script"; +const siteSearchStylesheetId = "algolia-sitesearch-stylesheet"; +const siteSearchScriptUrl = + "https://unpkg.com/@algolia/sitesearch@latest/dist/search.min.js"; +const siteSearchStylesheetUrl = + "https://unpkg.com/@algolia/sitesearch@latest/dist/search.min.css"; + +type SiteSearchConfig = { + applicationId?: unknown; + apiKey?: unknown; + indexName?: unknown; +}; + +declare global { + interface Window { + SiteSearch?: { + init: (config: { + container: string; + applicationId: string; + apiKey: string; + indexName: string; + attributes: { + primaryText: string; + secondaryText: string; + tertiaryText: string; + url: string; + image: string; + }; + darkMode: boolean; + }) => void; + }; + } +} + +function toConfigString(value: unknown): string { + return typeof value === "string" ? value.trim() : ""; +} + +function ensureSiteSearchStylesheet() { + if (document.getElementById(siteSearchStylesheetId)) { + return; + } + + const stylesheet = document.createElement("link"); + stylesheet.id = siteSearchStylesheetId; + stylesheet.rel = "stylesheet"; + stylesheet.href = siteSearchStylesheetUrl; + document.head.appendChild(stylesheet); +} + +function loadSiteSearchScript(): Promise { + if (window.SiteSearch?.init) { + return Promise.resolve(); + } + + const existingScript = document.getElementById( + siteSearchScriptId, + ) as HTMLScriptElement | null; + + if (existingScript) { + return new Promise((resolve, reject) => { + existingScript.addEventListener("load", () => resolve(), { once: true }); + existingScript.addEventListener("error", reject, { once: true }); + }); + } + + return new Promise((resolve, reject) => { + const script = document.createElement("script"); + script.id = siteSearchScriptId; + script.src = siteSearchScriptUrl; + script.async = true; + script.addEventListener("load", () => resolve(), { once: true }); + script.addEventListener("error", reject, { once: true }); + document.head.appendChild(script); + }); +} + +export default function AlgoliaSiteSearch(): React.ReactElement | null { + const { siteConfig } = useDocusaurusContext(); + const config = siteConfig.customFields.algoliaSiteSearch as + | SiteSearchConfig + | undefined; + + const applicationId = toConfigString(config?.applicationId); + const apiKey = toConfigString(config?.apiKey); + const indexName = toConfigString(config?.indexName); + const isConfigured = Boolean(applicationId && apiKey && indexName); + + useEffect(() => { + if (!isConfigured) { + return; + } + + let cancelled = false; + + ensureSiteSearchStylesheet(); + loadSiteSearchScript() + .then(() => { + if (cancelled || !window.SiteSearch?.init) { + return; + } + + const container = document.getElementById(containerId); + if (!container) { + return; + } + + container.innerHTML = ""; + window.SiteSearch.init({ + container: `#${containerId}`, + applicationId, + apiKey, + indexName, + attributes: { + primaryText: "title", + secondaryText: "description", + tertiaryText: "headers", + url: "url", + image: "image", + }, + darkMode: document.documentElement.dataset.theme === "dark", + }); + }) + .catch((error) => { + console.error("Failed to initialize Algolia SiteSearch", error); + }); + + return () => { + cancelled = true; + }; + }, [apiKey, applicationId, indexName, isConfigured]); + + if (!isConfigured) { + return null; + } + + return ( +
+
+
+ ); +} diff --git a/src/theme/Navbar/Content/index.tsx b/src/theme/Navbar/Content/index.tsx index d0acaf98..8816967f 100644 --- a/src/theme/Navbar/Content/index.tsx +++ b/src/theme/Navbar/Content/index.tsx @@ -8,6 +8,7 @@ import { useThemeConfig, ErrorCauseBoundary } from "@docusaurus/theme-common"; import { splitNavbarItems } from "@docusaurus/theme-common/internal"; import NavbarItem, { type Props as NavbarItemConfig } from "@theme/NavbarItem"; import NavbarColorModeToggle from "@theme/Navbar/ColorModeToggle"; +import AlgoliaSiteSearch from "@site/src/components/AlgoliaSiteSearch"; // import SearchBar from '@theme/SearchBar'; import NavbarMobileSidebarToggle from "@theme/Navbar/MobileSidebar/Toggle"; import NavbarLogo from "@theme/Navbar/Logo"; @@ -80,10 +81,6 @@ export default function NavbarContent(): ReactNode { () => splitNavbarItems(items), [items], ); - const searchBarItem = useMemo( - () => items.find((item) => item.type === "search"), - [items], - ); return ( more flexible <> + {/* Search component disabled */} {/* {!searchBarItem && (