diff --git a/.github/workflows/sync-orama.yml b/.github/workflows/sync-orama.yml
deleted file mode 100644
index c094f8c25971f..0000000000000
--- a/.github/workflows/sync-orama.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-# Security Notes
-# This workflow uses `pull_request_target`, so will run against all PRs automatically (without approval), be careful with allowing any user-provided code to be run here
-# Only selected Actions are allowed within this repository. Please refer to (https://github.com/nodejs/nodejs.org/settings/actions)
-# for the full list of available actions. If you want to add a new one, please reach out a maintainer with Admin permissions.
-# REVIEWERS, please always double-check security practices before merging a PR that contains Workflow changes!!
-# AUTHORS, please only use actions with explicit SHA references, and avoid using `@master` or `@main` references or `@version` tags.
-# MERGE QUEUE NOTE: This Workflow does not run on `merge_group` trigger, as this Workflow is not required for Merge Queue's
-
-name: Sync Orama Cloud
-
-on:
- workflow_dispatch:
- push:
- branches:
- - main
- pull_request_target:
- branches:
- - main
- types:
- - labeled
-
-permissions:
- contents: read
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: ${{ github.event_name != 'push' }}
-
-jobs:
- sync-orama-cloud:
- name: Sync Orama Cloud
- runs-on: ubuntu-latest
-
- # This Job should run either on non-`pull_request_target` events,
- # or `pull_request_target` event with a `labeled` action with a label named `github_actions:pull-request`
- # since we want to run Website Builds on all these occasions. As this allows us to be certain the that builds are passing
- if: github.event_name != 'pull_request_target' || github.event.label.name == 'github_actions:pull-request'
-
- steps:
- - uses: nodejs/web-team/actions/setup-environment@9f3c83af227d721768d9dbb63009a47ed4f4282f
- with:
- pnpm: true
- use-version-file: true
-
- - name: Sync Orama Cloud
- working-directory: apps/site
- run: node --run sync-orama
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- NEW_ORAMA_PROJECT_ID: ${{ github.event_name == 'push' && secrets.NEW_ORAMA_PRODUCTION_PROJECT_ID || secrets.NEW_ORAMA_PROJECT_ID }}
- NEW_ORAMA_PRIVATE_API_KEY: ${{ github.event_name == 'push' && secrets.NEW_ORAMA_PRODUCTION_PRIVATE_API_KEY || secrets.NEW_ORAMA_PRIVATE_API_KEY }}
- NEW_ORAMA_DATASOURCE_ID: ${{ github.event_name == 'push' && secrets.NEW_ORAMA_PRODUCTION_DATASOURCE_ID || secrets.NEW_ORAMA_DATASOURCE_ID }}
diff --git a/apps/site/components/Common/Searchbox/ChatInteractions/index.module.css b/apps/site/components/Common/Searchbox/ChatInteractions/index.module.css
deleted file mode 100644
index 01ed9a85fa432..0000000000000
--- a/apps/site/components/Common/Searchbox/ChatInteractions/index.module.css
+++ /dev/null
@@ -1,65 +0,0 @@
-@reference "../../../../styles/index.css";
-
-.chatInteractionsContainer {
- @apply relative
- mb-6
- flex
- h-full
- w-[95%]
- flex-1
- flex-col
- items-start
- self-center
- overflow-auto
- px-1;
-
- &::-webkit-scrollbar {
- @apply size-1.5;
- }
-
- &::-webkit-scrollbar-track {
- @apply rounded-md
- bg-transparent;
- }
-
- &::-webkit-scrollbar-thumb {
- @apply rounded-md
- bg-neutral-900;
- }
-}
-
-.chatInteractionsWrapper {
- @apply flex
- w-full
- flex-wrap
- gap-6;
-
- > div {
- @apply w-full;
- }
-}
-
-.scrollDownButton {
- @apply absolute
- bottom-36
- left-1/2
- inline-flex
- -translate-x-1/2
- items-center
- justify-center
- rounded-xl
- bg-neutral-200
- p-2
- text-neutral-900
- focus:bg-neutral-300
- focus:outline-none
- motion-safe:transition-colors
- lg:bottom-28
- dark:bg-neutral-900
- dark:text-neutral-200
- focus:dark:bg-neutral-800;
-
- svg {
- @apply size-4;
- }
-}
diff --git a/apps/site/components/Common/Searchbox/ChatInteractions/index.tsx b/apps/site/components/Common/Searchbox/ChatInteractions/index.tsx
deleted file mode 100644
index 50e00fbf643d8..0000000000000
--- a/apps/site/components/Common/Searchbox/ChatInteractions/index.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-'use client';
-
-import { ArrowDownIcon } from '@heroicons/react/24/solid';
-import { ChatInteractions } from '@orama/ui/components';
-import { useScrollableContainer } from '@orama/ui/hooks/useScrollableContainer';
-import { useTranslations } from 'next-intl';
-
-import type { Interaction } from '@orama/core';
-
-import { ChatMessage } from '../ChatMessage';
-
-import styles from './index.module.css';
-
-export const ChatInteractionsContainer = () => {
- const t = useTranslations();
- const {
- containerRef,
- scrollToBottom,
- recalculateGoToBottomButton,
- showGoToBottomButton,
- } = useScrollableContainer();
-
- return (
- <>
-
- scrollToBottom({ animated: true })}
- className={styles.chatInteractionsWrapper}
- >
- {(interaction: Interaction) => (
-
- )}
-
-
- {showGoToBottomButton && (
- scrollToBottom({ animated: true })}
- className={styles.scrollDownButton}
- aria-label={t('components.search.scrollToBottom')}
- >
-
-
- )}
- >
- );
-};
diff --git a/apps/site/components/Common/Searchbox/ChatMessage/index.module.css b/apps/site/components/Common/Searchbox/ChatMessage/index.module.css
deleted file mode 100644
index 792c1857f9b8c..0000000000000
--- a/apps/site/components/Common/Searchbox/ChatMessage/index.module.css
+++ /dev/null
@@ -1,50 +0,0 @@
-@reference "../../../../styles/index.css";
-
-.chatUserPrompt {
- @apply py-3;
-
- p {
- @apply max-w-2xl
- rounded-xl
- text-neutral-900
- dark:text-neutral-200;
- }
-}
-
-.chatAssistantMessageWrapper {
- @apply my-2
- rounded-xl
- bg-neutral-100
- px-4
- py-1
- text-neutral-900
- empty:hidden
- dark:bg-neutral-950
- dark:text-neutral-200;
-}
-
-.typingIndicator {
- @apply flex
- items-center
- gap-1
- rounded-xl
- bg-neutral-200
- p-4
- dark:bg-neutral-950;
-}
-
-.typingDot {
- @apply animate-dot-move
- size-1
- rounded-full
- bg-neutral-500
- dark:bg-neutral-400;
-
- &:nth-child(2) {
- @apply animate-dot-move-delay-200;
- }
-
- &:nth-child(3) {
- @apply animate-dot-move-delay-400;
- }
-}
diff --git a/apps/site/components/Common/Searchbox/ChatMessage/index.tsx b/apps/site/components/Common/Searchbox/ChatMessage/index.tsx
deleted file mode 100644
index 1f468d43cf10c..0000000000000
--- a/apps/site/components/Common/Searchbox/ChatMessage/index.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import ChatActions from '@node-core/ui-components/Common/Search/Chat/Actions';
-import { ChatInteractions } from '@orama/ui/components';
-
-import type { Interaction } from '@orama/core';
-import type { FC } from 'react';
-
-import ChatSources from '../ChatSources';
-
-import styles from './index.module.css';
-
-type ChatMessageProps = {
- interaction: Interaction;
-};
-
-const TypingIndicator: FC = () => (
-
-
-
-
-
-);
-
-export const ChatMessage: FC = ({ interaction }) => {
- if (!interaction) {
- return null;
- }
-
- return (
- <>
-
- {interaction?.query}
-
-
-
-
-
-
-
-
-
-
- {interaction.response && (
-
-
- {interaction.response || ''}
-
-
-
- )}
- >
- );
-};
diff --git a/apps/site/components/Common/Searchbox/ChatSources/index.module.css b/apps/site/components/Common/Searchbox/ChatSources/index.module.css
deleted file mode 100644
index b5e63af20653e..0000000000000
--- a/apps/site/components/Common/Searchbox/ChatSources/index.module.css
+++ /dev/null
@@ -1,53 +0,0 @@
-@reference "../../../../styles/index.css";
-
-.chatSources {
- @apply mb-4
- flex
- flex-nowrap
- items-center
- gap-3
- overflow-x-scroll
- scroll-smooth
- [-ms-overflow-style:none]
- [scrollbar-width:none];
-
- &::-webkit-scrollbar {
- @apply hidden;
- }
-}
-
-.chatSource {
- @apply flex
- max-w-full
- items-center
- gap-2
- text-base;
-}
-
-.chatSourceLink {
- @apply w-3xs
- rounded-xl
- bg-white
- px-4
- py-2
- text-neutral-900
- hover:bg-neutral-200
- focus:bg-neutral-200
- focus:outline-none
- motion-safe:transition-colors
- lg:bg-neutral-100
- dark:bg-neutral-950
- dark:text-neutral-200
- hover:dark:bg-neutral-900
- focus:dark:bg-neutral-900;
-}
-
-.chatSourceTitle {
- @apply max-w-full
- truncate
- overflow-hidden
- text-sm
- font-semibold
- text-ellipsis
- whitespace-nowrap;
-}
diff --git a/apps/site/components/Common/Searchbox/ChatSources/index.tsx b/apps/site/components/Common/Searchbox/ChatSources/index.tsx
deleted file mode 100644
index 11a177a98c35b..0000000000000
--- a/apps/site/components/Common/Searchbox/ChatSources/index.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { ChatInteractions } from '@orama/ui/components';
-
-import type { Document } from '../DocumentLink';
-import type { Interaction, AnyObject } from '@orama/core';
-import type { FC } from 'react';
-
-import { DocumentLink } from '../DocumentLink';
-
-import styles from './index.module.css';
-
-type ChatSourcesProps = {
- interaction: Interaction;
-};
-
-const ChatSources: FC = ({ interaction }) => {
- if (!interaction?.sources) {
- return null;
- }
-
- return (
-
- {(document: AnyObject, index: number) => (
-
- {!!document.pageSectionTitle &&
- typeof document.pageSectionTitle === 'string' && (
-
-
- {document.pageSectionTitle &&
- document.pageSectionTitle.length > 25
- ? `${document.pageSectionTitle.substring(0, 25)}...`
- : document.pageSectionTitle}
-
-
- )}
-
- )}
-
- );
-};
-
-export default ChatSources;
diff --git a/apps/site/components/Common/Searchbox/DocumentLink/index.tsx b/apps/site/components/Common/Searchbox/DocumentLink/index.tsx
deleted file mode 100644
index 1a7c1f8ad5492..0000000000000
--- a/apps/site/components/Common/Searchbox/DocumentLink/index.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-'use client';
-
-import styles from '@node-core/ui-components/Common/Search/Results/Hit/index.module.css';
-import Link from 'next/link';
-import { useLocale } from 'next-intl';
-
-import type { FC } from 'react';
-
-import { getDocumentHref } from '../SearchItem/utils';
-
-export type Document = {
- path: string;
- siteSection: string;
- pageSectionTitle?: string;
-};
-
-type DocumentLinkProps = {
- document: Document;
- className?: string;
- children?: React.ReactNode;
- 'data-focus-on-arrow-nav'?: boolean;
-} & React.AnchorHTMLAttributes;
-
-export const DocumentLink: FC = ({
- document,
- className = styles.link,
- children,
- 'data-focus-on-arrow-nav': dataFocusOnArrowNav,
- ...props
-}) => {
- const locale = useLocale();
-
- return (
-
- {children}
-
- );
-};
diff --git a/apps/site/components/Common/Searchbox/Footer/index.tsx b/apps/site/components/Common/Searchbox/Footer/index.tsx
deleted file mode 100644
index abe61775ac964..0000000000000
--- a/apps/site/components/Common/Searchbox/Footer/index.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-'use client';
-
-import {
- ArrowTurnDownLeftIcon,
- ArrowDownIcon,
- ArrowUpIcon,
-} from '@heroicons/react/24/solid';
-import Image from 'next/image';
-import { useTranslations } from 'next-intl';
-import { useTheme } from 'next-themes';
-
-import styles from './index.module.css';
-
-export const Footer = () => {
- const t = useTranslations();
- const { resolvedTheme } = useTheme();
-
- const oramaLogo = `https://website-assets.oramasearch.com/orama-when-${resolvedTheme}.svg`;
-
- return (
-
-
-
-
-
-
-
- {t('components.search.keyboardShortcuts.select')}
-
-
-
-
-
-
-
-
-
-
- {t('components.search.keyboardShortcuts.navigate')}
-
-
-
- esc
-
- {t('components.search.keyboardShortcuts.close')}
-
-
-
-
-
- );
-};
diff --git a/apps/site/components/Common/Searchbox/SearchItem/index.tsx b/apps/site/components/Common/Searchbox/SearchItem/index.tsx
deleted file mode 100644
index a69775b0a8076..0000000000000
--- a/apps/site/components/Common/Searchbox/SearchItem/index.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import SearchHit from '@node-core/ui-components/Common/Search/Results/Hit';
-import Link from 'next/link';
-import { useLocale } from 'next-intl';
-
-import type { Document } from '../DocumentLink';
-import type { LinkLike } from '@node-core/ui-components/types';
-import type { ComponentProps, FC } from 'react';
-
-import { getDocumentHref, getFormattedPath } from './utils';
-
-type SearchItemProps = Omit<
- ComponentProps,
- 'document' | 'as'
-> & {
- document: Document;
-};
-
-const SearchItem: FC = ({ document, ...props }) => {
- const locale = useLocale();
-
- return (
-
- );
-};
-
-export default SearchItem;
diff --git a/apps/site/components/Common/Searchbox/SearchItem/utils.ts b/apps/site/components/Common/Searchbox/SearchItem/utils.ts
deleted file mode 100644
index ad4a9a4cc63bb..0000000000000
--- a/apps/site/components/Common/Searchbox/SearchItem/utils.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { Document } from '../DocumentLink';
-
-export const uppercaseFirst = (word: string) =>
- word.charAt(0).toUpperCase() + word.slice(1);
-
-export const getFormattedPath = (path: string, title: string) =>
- `${path
- .replace(/#.+$/, '')
- .split('/')
- .map(element => element.replaceAll('-', ' '))
- .map(element => uppercaseFirst(element))
- .filter(Boolean)
- .join(' > ')} — ${title}`;
-
-export const getDocumentHref = (document: Document, locale: string) =>
- document.siteSection?.toLowerCase() === 'docs'
- ? `/${document.path}`
- : `/${locale}/${document.path}`;
diff --git a/apps/site/components/Common/Searchbox/SlidingChatPanel/index.module.css b/apps/site/components/Common/Searchbox/SlidingChatPanel/index.module.css
deleted file mode 100644
index 4e9612bb55082..0000000000000
--- a/apps/site/components/Common/Searchbox/SlidingChatPanel/index.module.css
+++ /dev/null
@@ -1,58 +0,0 @@
-@reference "../../../../styles/index.css";
-
-.slidingPanelCloseButton {
- @apply absolute
- top-2
- right-6
- z-20
- cursor-pointer
- rounded-full
- p-2
- text-neutral-700
- duration-300
- hover:bg-white/20
- focus:bg-white/20
- focus:outline-none
- motion-safe:transition-colors
- dark:text-white;
-
- svg {
- @apply size-5;
- }
-}
-
-.slidingPanelContentWrapper {
- @apply fixed
- bottom-0
- left-0
- box-border
- h-[95vh]
- w-full
- justify-self-center
- overflow-hidden
- rounded-lg
- border
- border-neutral-300
- bg-white
- p-0
- text-white
- duration-300
- motion-safe:ease-in-out
- dark:border-neutral-900
- dark:bg-zinc-950;
-}
-
-.slidingPanelInner {
- @apply relative
- mx-auto
- flex
- h-full
- max-w-4xl
- flex-col
- justify-between
- py-6;
-}
-
-.slidingPanelBottom {
- @apply relative;
-}
diff --git a/apps/site/components/Common/Searchbox/SlidingChatPanel/index.tsx b/apps/site/components/Common/Searchbox/SlidingChatPanel/index.tsx
deleted file mode 100644
index 5e1d89fbe8c3f..0000000000000
--- a/apps/site/components/Common/Searchbox/SlidingChatPanel/index.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-'use client';
-
-import { XMarkIcon } from '@heroicons/react/24/solid';
-import ChatInput from '@node-core/ui-components/Common/Search/Chat/Input';
-import { SlidingPanel } from '@orama/ui/components';
-import { useTranslations } from 'next-intl';
-
-import type { FC, PropsWithChildren } from 'react';
-
-import { ChatInteractionsContainer } from '../ChatInteractions';
-
-import styles from './index.module.css';
-
-type SlidingChatPanelProps = PropsWithChildren<{
- open: boolean;
- onClose: () => void;
-}>;
-
-export const SlidingChatPanel: FC = ({
- open,
- onClose,
-}) => {
- const t = useTranslations();
-
- return (
- <>
-
-
-
-
-
-
-
-
-
- >
- );
-};
diff --git a/apps/site/components/Common/Searchbox/index.module.css b/apps/site/components/Common/Searchbox/index.module.css
deleted file mode 100644
index 207d7ab262ae6..0000000000000
--- a/apps/site/components/Common/Searchbox/index.module.css
+++ /dev/null
@@ -1,8 +0,0 @@
-@reference "../../../styles/index.css";
-
-.searchResultsContainer {
- @apply flex
- grow
- flex-col
- overflow-y-auto;
-}
diff --git a/apps/site/components/Common/Searchbox/index.tsx b/apps/site/components/Common/Searchbox/index.tsx
deleted file mode 100644
index 340a1fec8673c..0000000000000
--- a/apps/site/components/Common/Searchbox/index.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-'use client';
-
-import ChatTrigger from '@node-core/ui-components/Common/Search/Chat/Trigger';
-import SearchModal from '@node-core/ui-components/Common/Search/Modal';
-import SearchResults from '@node-core/ui-components/Common/Search/Results';
-import SearchSuggestions from '@node-core/ui-components/Common/Search/Suggestions';
-import { useTranslations } from 'next-intl';
-import { useState } from 'react';
-
-import { DEFAULT_ORAMA_QUERY_PARAMS } from '#site/next.constants.mjs';
-
-import type { Document } from './DocumentLink';
-import type { FC } from 'react';
-
-import { Footer } from './Footer';
-import { oramaClient } from './orama-client';
-import SearchItem from './SearchItem';
-import { SlidingChatPanel } from './SlidingChatPanel';
-
-import styles from './index.module.css';
-
-const Searchbox: FC = () => {
- const t = useTranslations();
- const [mode, setMode] = useState<'chat' | 'search'>('search');
-
- const sharedProps = {
- searchParams: DEFAULT_ORAMA_QUERY_PARAMS,
- tabIndex: mode === 'search' ? 0 : -1,
- 'aria-hidden': mode === 'chat',
- };
-
- return (
-
-
- setMode('chat')}>
- {t('components.search.chatButtonLabel')}
-
- (
-
- )}
- {...sharedProps}
- >
- setMode('chat')}
- />
-
-
-
- setMode('search')}
- />
-
- );
-};
-
-export default Searchbox;
diff --git a/apps/site/components/Common/Searchbox/orama-client.ts b/apps/site/components/Common/Searchbox/orama-client.ts
deleted file mode 100644
index a7e48869061ff..0000000000000
--- a/apps/site/components/Common/Searchbox/orama-client.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { OramaCloud } from '@orama/core';
-
-import {
- ORAMA_CLOUD_PROJECT_ID,
- ORAMA_CLOUD_READ_API_KEY,
-} from '#site/next.constants.mjs';
-
-export const oramaClient =
- ORAMA_CLOUD_PROJECT_ID && ORAMA_CLOUD_READ_API_KEY
- ? new OramaCloud({
- projectId: ORAMA_CLOUD_PROJECT_ID,
- apiKey: ORAMA_CLOUD_READ_API_KEY,
- })
- : null;
diff --git a/apps/site/components/withNavBar.tsx b/apps/site/components/withNavBar.tsx
index d89dbc3d22c97..275419bbf18a8 100644
--- a/apps/site/components/withNavBar.tsx
+++ b/apps/site/components/withNavBar.tsx
@@ -15,6 +15,7 @@ import { useTheme } from 'next-themes';
import Link from '#site/components/Link';
import WithBanner from '#site/components/withBanner';
import WithNodejsLogo from '#site/components/withNodejsLogo';
+import WithSearch from '#site/components/withSearch';
import useSiteNavigation from '#site/hooks/useSiteNavigation';
import { useRouter, usePathname } from '#site/navigation.mjs';
@@ -65,7 +66,7 @@ const WithNavBar: FC = () => {
'components.containers.navBar.controls.toggle'
)}
>
- {/* */}
+
;
+
+/**
+ * Shape of a serialized Orama database snapshot (from `save()` on the server
+ * side, fetched as JSON on the client). We only type the parts we touch.
+ */
+type SerializedOramaDb = {
+ docs: {
+ docs: Record;
+ };
+};
+
+/**
+ * Each locale/section of the site ships its own prebuilt Orama index, but the
+ * hrefs inside those indexes are relative to that section's root. When we
+ * merge multiple indexes into a single client-side DB, we need to re-scope
+ * those hrefs so clicks route to the correct top-level path.
+ */
+export const addPrefixToDocs = (
+ db: T,
+ prefix: string
+): T => {
+ const prefixedDocs: Record = {};
+
+ // Object.entries + Object.fromEntries would also work, but a single pass
+ // with a plain loop avoids the intermediate array allocations
+ for (const [id, doc] of Object.entries(db.docs.docs)) {
+ prefixedDocs[id] = { ...doc, href: `${prefix}${doc.href}` };
+ }
+
+ return {
+ ...db,
+ docs: { ...db.docs, docs: prefixedDocs },
+ };
+};
+
+const loadOrama = async () => {
+ const db = create({
+ schema: {
+ title: 'string',
+ description: 'string',
+ href: 'string',
+ siteSection: 'string',
+ },
+ });
+
+ const indexes = await Promise.all(
+ Object.entries(ORAMA_DB_URLS).map(async ([key, url]) => {
+ const response = await fetch(url);
+ const fetchedDb = (await response.json()) as SerializedOramaDb;
+ return addPrefixToDocs(fetchedDb, `/${key}`);
+ })
+ );
+
+ for (const index of indexes) {
+ await insertMultiple(db, Object.values(index.docs.docs) as Array);
+ }
+
+ return save(db);
+};
+
+const WithSearch: FC = () => {
+ const t = useTranslations();
+
+ const client = useOrama(loadOrama);
+
+ return (
+
+ );
+};
+
+export default WithSearch;
diff --git a/apps/site/next.constants.mjs b/apps/site/next.constants.mjs
index f621472d3c309..29b72b7407b8f 100644
--- a/apps/site/next.constants.mjs
+++ b/apps/site/next.constants.mjs
@@ -117,63 +117,6 @@ export const EXTERNAL_LINKS_SITEMAP = [
'https://www.linuxfoundation.org/cookies',
];
-/**
- * These are the default Orama Query Parameters that are used by the Website
- * @see https://docs.oramasearch.com/open-source/usage/search/introduction
- */
-export const DEFAULT_ORAMA_QUERY_PARAMS = {
- limit: 25,
- threshold: 0,
- boost: {
- pageSectionTitle: 4,
- pageSectionContent: 2.5,
- pageTitle: 1.5,
- },
-};
-
-/**
- * The initial Orama Cloud chat suggestions visible in the empty state of the search box.
- */
-export const DEFAULT_ORAMA_SUGGESTIONS = [
- 'How to install Node.js?',
- 'How to create an HTTP server?',
- 'Upgrading Node.js version',
-];
-
-/**
- * The default batch size to use when syncing Orama Cloud
- */
-export const ORAMA_SYNC_BATCH_SIZE = 250;
-
-/**
- * The default Orama Cloud endpoint to use when searching with Orama Cloud.
- */
-export const ORAMA_CLOUD_ENDPOINT =
- process.env.NEXT_PUBLIC_ORAMA_ENDPOINT ||
- 'https://cloud.orama.run/v1/indexes/nodejs-org-dev-hhqrzv';
-
-/**
- * The default Orama Cloud API Key to use when searching with Orama Cloud.
- * This is a public API key and can be shared publicly on the frontend.
- */
-export const ORAMA_CLOUD_READ_API_KEY =
- process.env.NEXT_PUBLIC_NEW_ORAMA_API_KEY ||
- 'c1__KPYDQNEFr$nFgrTgFTVLHf8BuNf08COBqBUzk65AYJEmSsJONPsO$_cihl';
-
-/**
- * The default Orama Cloud Datasource ID to use when searching with Orama Cloud.
- */
-export const ORAMA_CLOUD_DATASOURCE_ID =
- process.env.NEXT_PUBLIC_NEW_ORAMA_DATASOURCE_ID ||
- '6044121f-53c3-46af-aaf0-f498e3c548f2';
-
-/**
- * The default Orama Cloud Project ID to use when initializing Orama Cloud.
- */
-export const ORAMA_CLOUD_PROJECT_ID =
- process.env.NEXT_PUBLIC_NEW_ORAMA_PROJECT_ID ||
- '2eac5680-790b-44b7-8640-359608f104bd';
-
/**
* A GitHub Access Token for accessing the GitHub API and not being rate-limited
* The current token is registered on the "nodejs-vercel" GitHub Account.
@@ -226,3 +169,11 @@ export const VULNERABILITIES_URL =
*/
export const OPENCOLLECTIVE_MEMBERS_URL =
'https://opencollective.com/nodejs/members/all.json';
+
+/**
+ * Orama DB URLs for the Learn and API sections of the website
+ */
+export const ORAMA_DB_URLS = {
+ learn: 'https://nodejs.org/learn/orama-db.json',
+ api: 'https://beta.docs.nodejs.org/orama-db.json',
+};
diff --git a/apps/site/package.json b/apps/site/package.json
index 1fd73ab23c9be..0132623f3bcf9 100644
--- a/apps/site/package.json
+++ b/apps/site/package.json
@@ -26,7 +26,6 @@
"scripts:release-post": "cross-env NODE_NO_WARNINGS=1 node scripts/release-post/index.mjs",
"serve": "node --run dev",
"start": "cross-env NODE_NO_WARNINGS=1 next start",
- "sync-orama": "cross-env NODE_NO_WARNINGS=1 node ./scripts/orama-search/sync-orama-cloud.mjs",
"test": "node --run test:unit",
"test:unit": "cross-env NODE_NO_WARNINGS=1 node --experimental-test-coverage --test-coverage-exclude=**/*.test.* --experimental-test-module-mocks --enable-source-maps --import=global-jsdom/register --import=tsx --import=tests/setup.jsx --test **/*.test.*",
"test:unit:watch": "node --run test:unit -- --watch"
@@ -42,8 +41,7 @@
"@opentelemetry/instrumentation": "~0.213.0",
"@opentelemetry/resources": "~1.30.1",
"@opentelemetry/sdk-logs": "~0.213.0",
- "@orama/core": "^1.2.19",
- "@orama/ui": "^1.5.4",
+ "@orama/orama": "^3.1.18",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/postcss": "~4.2.2",
diff --git a/apps/site/scripts/orama-search/__tests__/get-documents.test.mjs b/apps/site/scripts/orama-search/__tests__/get-documents.test.mjs
deleted file mode 100644
index 3aec546acd434..0000000000000
--- a/apps/site/scripts/orama-search/__tests__/get-documents.test.mjs
+++ /dev/null
@@ -1,38 +0,0 @@
-import assert from 'node:assert/strict';
-import { test, mock } from 'node:test';
-
-import nock from 'nock';
-
-mock.module('node:fs/promises', {
- namedExports: {
- glob: () => ['filename'],
- readFile: name => name.endsWith('filename') && 'content',
- },
-});
-
-const { getAPIDocs, getArticles } = await import('../get-documents.mjs');
-
-test('getAPIDocs', async () => {
- nock('https://api.github.com')
- .get('/repos/nodejs/node/contents/doc/api')
- .query(true)
- .reply(200, [
- {
- name: 'fs.md',
- download_url: 'data:text/plain,fs',
- },
- ]);
-
- const result = await getAPIDocs();
-
- assert.equal(result.length, 1);
- assert.equal(result[0].content, 'fs');
- assert.match(result[0].pathname, /^docs\/v[^/]+\/api\/fs\.html$/);
-});
-
-test('getArticles', async () => {
- const result = await getArticles();
- assert.deepStrictEqual(result, [
- { content: 'content', pathname: 'filename' },
- ]);
-});
diff --git a/apps/site/scripts/orama-search/__tests__/process-documents.test.mjs b/apps/site/scripts/orama-search/__tests__/process-documents.test.mjs
deleted file mode 100644
index 83db237682721..0000000000000
--- a/apps/site/scripts/orama-search/__tests__/process-documents.test.mjs
+++ /dev/null
@@ -1,104 +0,0 @@
-import assert from 'node:assert';
-import test from 'node:test';
-
-import dedent from 'dedent';
-
-import { processDocument } from '../process-documents.mjs';
-
-const testCases = [
- {
- name: 'Uses front matter title if available',
- input: {
- pathname: 'blog/my-post.html',
- content: dedent`
- ---
- title: Custom Title
- ---
- # Intro
- Hello world
- `,
- },
- expected: [
- {
- path: 'blog/my-post.html#intro',
- siteSection: 'Blog',
- pageTitle: 'Custom Title',
- pageSectionTitle: 'Intro',
- pageSectionContent: 'Hello world',
- },
- ],
- },
- {
- name: 'Falls back to filename for title',
- input: {
- pathname: 'docs/another-post.html',
- content: dedent`
- # Start
- Content here
- `,
- },
- expected: [
- {
- path: 'docs/another-post.html#start',
- siteSection: 'Docs',
- pageTitle: 'another post',
- pageSectionTitle: 'Start',
- pageSectionContent: 'Content here',
- },
- ],
- },
- {
- name: 'Handles multiple sections',
- input: {
- pathname: 'guides/test.html',
- content: dedent`
- # First
- Paragraph A
-
- # Second
- Paragraph B
- `,
- },
- expected: [
- {
- path: 'guides/test.html#first',
- siteSection: 'Guides',
- pageTitle: 'test',
- pageSectionTitle: 'First',
- pageSectionContent: 'Paragraph A',
- },
- {
- path: 'guides/test.html#second',
- siteSection: 'Guides',
- pageTitle: 'test',
- pageSectionTitle: 'Second',
- pageSectionContent: 'Paragraph B',
- },
- ],
- },
- {
- name: 'Section with no heading',
- input: {
- pathname: 'misc/untitled.html',
- content: dedent`
- Just some text without a heading
- `,
- },
- expected: [
- {
- path: 'misc/untitled.html#',
- siteSection: 'Misc',
- pageTitle: 'untitled',
- pageSectionTitle: '',
- pageSectionContent: 'Just some text without a heading',
- },
- ],
- },
-];
-
-for (const { name, input, expected } of testCases) {
- test(name, () => {
- const result = processDocument(input);
- assert.deepStrictEqual(result, expected);
- });
-}
diff --git a/apps/site/scripts/orama-search/get-documents.mjs b/apps/site/scripts/orama-search/get-documents.mjs
deleted file mode 100644
index bd374421520d5..0000000000000
--- a/apps/site/scripts/orama-search/get-documents.mjs
+++ /dev/null
@@ -1,83 +0,0 @@
-import { readFile, glob } from 'node:fs/promises';
-import { join, basename, posix, win32 } from 'node:path';
-
-import generateReleaseData from '#site/next-data/generators/releaseData.mjs';
-import { getRelativePath } from '#site/next.helpers.mjs';
-
-import { processDocument } from './process-documents.mjs';
-
-// If a GitHub token is available, include it for higher rate limits
-const fetchOptions = process.env.GITHUB_TOKEN
- ? { headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } }
- : undefined;
-
-/**
- * Fetch Node.js API documentation directly from GitHub
- * for the current LTS version.
- */
-export const getAPIDocs = async () => {
- // Find the current Active LTS version
- const releaseData = await generateReleaseData();
- const ltsRelease = releaseData.find(r => r.status === 'LTS');
-
- if (!ltsRelease) {
- throw new Error('No LTS release found');
- }
-
- // Get list of API docs from the Node.js repo
- const fetchResponse = await fetch(
- `https://api.github.com/repos/nodejs/node/contents/doc/api?ref=${ltsRelease.versionWithPrefix}`,
- fetchOptions
- );
- const documents = await fetchResponse.json();
-
- // Download and return content + metadata for each doc
- return Promise.all(
- documents.map(async ({ name, download_url }) => {
- const res = await fetch(download_url, fetchOptions);
-
- return {
- content: await res.text(),
- pathname: `docs/${ltsRelease.versionWithPrefix}/api/${basename(name, '.md')}.html`,
- };
- })
- );
-};
-
-/**
- * Collect all local markdown/mdx articles under /pages/en,
- * excluding blog content.
- */
-export const getArticles = async () => {
- const relativePath = getRelativePath(import.meta.url);
- const root = join(relativePath, '..', '..', 'pages', 'en');
-
- // Find all markdown files (excluding blog)
- const files = await Array.fromAsync(glob('**/*.{md,mdx}', { cwd: root }));
-
- // Read content + metadata
- return Promise.all(
- files
- // Exclude blog posts: they tend to surface in feature searches and
- // direct users to a release announcement rather than the actual docs.
- .filter(path => !path.startsWith('blog'))
- .map(async path => ({
- content: await readFile(join(root, path), 'utf8'),
- pathname: path
- // Strip the extension
- .replace(/\.mdx?$/, '')
- // Normalize to a POSIX path
- .replaceAll(win32.sep, posix.sep),
- }))
- );
-};
-
-/**
- * Aggregate all documents (API docs + local articles).
- */
-export const getDocuments = async () => {
- const documentPromises = await Promise.all([getAPIDocs(), getArticles()]);
- return documentPromises.flatMap(documents =>
- documents.flatMap(processDocument)
- );
-};
diff --git a/apps/site/scripts/orama-search/process-documents.mjs b/apps/site/scripts/orama-search/process-documents.mjs
deleted file mode 100644
index be499a8fd229b..0000000000000
--- a/apps/site/scripts/orama-search/process-documents.mjs
+++ /dev/null
@@ -1,89 +0,0 @@
-import { basename } from 'node:path';
-
-import { slug } from 'github-slugger';
-import matter from 'gray-matter';
-import { fromMarkdown } from 'mdast-util-from-markdown';
-import { toString } from 'mdast-util-to-string';
-
-/**
- * Extracts top-level sections from a Markdown AST.
- * Each section starts with a heading (if present) and includes all subsequent nodes
- * until the next heading.
- */
-const extractSections = tree => {
- const sections = [];
- let current = null;
-
- // Visit each top-level node
- tree.children.forEach(node => {
- if (node.type === 'heading') {
- // Push the previous section if it exists
- if (current) {
- sections.push(current);
- }
-
- // Start a new section with the current heading
- current = {
- heading: node,
- children: [],
- };
- } else {
- // If no heading yet, initialize an empty section
- if (!current) {
- current = { heading: null, children: [] };
- }
-
- // Add the node to the current section's children
- current.children.push(node);
- }
- });
-
- // Push the last section if it exists
- if (current) {
- sections.push(current);
- }
-
- // Convert AST nodes to strings and structure the output
- return sections.map(({ heading, children }) => ({
- pageSectionTitle: toString(heading),
- pageSectionContent: children
- .map(child => toString(child, { includeHtml: false }))
- .join('\n'),
- }));
-};
-
-// Derive page title from path
-const getPageTitle = path => basename(path, '.html').replace(/-/g, ' ');
-
-// Capitalize first character
-const getSiteSection = path => {
- const subpath = path.split('/')[0];
-
- return subpath[0].toUpperCase() + subpath.slice(1);
-};
-
-/**
- * Processes a Markdown document with front matter.
- * Extracts sections and logs them.
- */
-export const processDocument = ({ pathname, content }) => {
- // Parse front matter and separate body
- const { data, content: body } = matter(content);
-
- // Convert Markdown body to AST
- const ast = fromMarkdown(body);
-
- // Extract sections from the AST
- const sections = extractSections(ast);
-
- // Get titles
- const siteSection = getSiteSection(pathname);
- const pageTitle = data.title || getPageTitle(pathname);
-
- return sections.map(section => ({
- path: `${pathname}#${slug(section.pageSectionTitle)}`,
- siteSection,
- pageTitle,
- ...section,
- }));
-};
diff --git a/apps/site/scripts/orama-search/sync-orama-cloud.mjs b/apps/site/scripts/orama-search/sync-orama-cloud.mjs
deleted file mode 100644
index de1f5df9e167c..0000000000000
--- a/apps/site/scripts/orama-search/sync-orama-cloud.mjs
+++ /dev/null
@@ -1,44 +0,0 @@
-import { OramaCloud } from '@orama/core';
-
-import { getDocuments } from './get-documents.mjs';
-import { ORAMA_SYNC_BATCH_SIZE } from '../../next.constants.mjs';
-
-// The following follows the instructions at https://docs.oramasearch.com/docs/cloud/data-sources/rest-APIs/using-rest-apis
-
-const orama = new OramaCloud({
- projectId: process.env.NEW_ORAMA_PROJECT_ID || '',
- apiKey: process.env.NEW_ORAMA_PRIVATE_API_KEY || '',
-});
-
-const datasource = orama.dataSource(process.env.NEW_ORAMA_DATASOURCE_ID || '');
-
-// Create a temporary index to perform the insertions
-const temporary = await datasource.createTemporaryIndex();
-const documents = await getDocuments();
-
-console.log(`Syncing ${documents.length} documents to Orama Cloud index`);
-
-// Orama allows to send several documents at once, so we batch them in groups of 50.
-// This is not strictly necessary, but it makes the process faster.
-const runUpdate = async () => {
- const batchSize = ORAMA_SYNC_BATCH_SIZE;
- const batches = [];
-
- for (let i = 0; i < documents.length; i += batchSize) {
- batches.push(documents.slice(i, i + batchSize));
- }
-
- console.log(`Sending ${batches.length} batches of ${batchSize} documents`);
-
- // Insert documents batch by batch into the temporary index
- for (const batch of batches) {
- await temporary.insertDocuments(batch);
- }
-
- // Once all documents are inserted into the temporary index, we swap it with the live one atomically.
- await temporary.swap();
-};
-
-await runUpdate();
-
-console.log('Orama Cloud sync completed successfully!');
diff --git a/apps/site/turbo.json b/apps/site/turbo.json
index e08e9169e7a11..47a5049c2b701 100644
--- a/apps/site/turbo.json
+++ b/apps/site/turbo.json
@@ -15,8 +15,6 @@
"NEXT_PUBLIC_DIST_URL",
"NEXT_PUBLIC_DOCS_URL",
"NEXT_PUBLIC_BASE_PATH",
- "NEXT_PUBLIC_ORAMA_API_KEY",
- "NEXT_PUBLIC_ORAMA_ENDPOINT",
"NEXT_PUBLIC_DATA_URL",
"TURBO_CACHE",
"TURBO_TELEMETRY_DISABLED",
@@ -42,8 +40,6 @@
"NEXT_PUBLIC_DIST_URL",
"NEXT_PUBLIC_DOCS_URL",
"NEXT_PUBLIC_BASE_PATH",
- "NEXT_PUBLIC_ORAMA_API_KEY",
- "NEXT_PUBLIC_ORAMA_ENDPOINT",
"NEXT_PUBLIC_DATA_URL",
"TURBO_CACHE",
"TURBO_TELEMETRY_DISABLED",
@@ -63,8 +59,6 @@
"NEXT_PUBLIC_DIST_URL",
"NEXT_PUBLIC_DOCS_URL",
"NEXT_PUBLIC_BASE_PATH",
- "NEXT_PUBLIC_ORAMA_API_KEY",
- "NEXT_PUBLIC_ORAMA_ENDPOINT",
"NEXT_PUBLIC_DATA_URL",
"TURBO_CACHE",
"TURBO_TELEMETRY_DISABLED",
@@ -89,8 +83,6 @@
"NEXT_PUBLIC_DIST_URL",
"NEXT_PUBLIC_DOCS_URL",
"NEXT_PUBLIC_BASE_PATH",
- "NEXT_PUBLIC_ORAMA_API_KEY",
- "NEXT_PUBLIC_ORAMA_ENDPOINT",
"NEXT_PUBLIC_DATA_URL",
"TURBO_CACHE",
"TURBO_TELEMETRY_DISABLED",
diff --git a/docs/technologies.md b/docs/technologies.md
index 9c6113ab7dd8c..e54940426bc72 100644
--- a/docs/technologies.md
+++ b/docs/technologies.md
@@ -11,6 +11,7 @@ This document provides an overview of the technologies used in the Node.js websi
- [PostCSS Plugins](#postcss-plugins)
- [Content Management](#content-management)
- [Content Processing Plugins](#content-processing-plugins)
+ - [Search](#search)
- [UI Components](#ui-components)
- [Why Radix UI?](#why-radix-ui)
- [Internationalization](#internationalization)
@@ -91,6 +92,12 @@ We chose Next.js because it is:
- `rehype-autolink-headings`: Automatic anchor links for headings
- `rehype-slug`: Automatic ID generation for headings
+### Search
+
+- **[Orama](https://orama.com/)**: Local full-text search engine used for site search
+- Prebuilt Learn and API documentation indexes are fetched client-side and merged into a single local search database
+- Search remains self-contained in the site deployment without depending on Orama Cloud at runtime
+
### UI Components
- **[Radix UI](https://www.radix-ui.com/)**: Accessible, unstyled component primitives
diff --git a/packages/ui-components/package.json b/packages/ui-components/package.json
index bec5b14bbba85..71af2eaff5289 100644
--- a/packages/ui-components/package.json
+++ b/packages/ui-components/package.json
@@ -1,6 +1,6 @@
{
"name": "@node-core/ui-components",
- "version": "1.6.3",
+ "version": "1.7.0",
"type": "module",
"exports": {
"./*": {
@@ -44,7 +44,7 @@
},
"dependencies": {
"@heroicons/react": "^2.2.0",
- "@orama/core": "^1.2.19",
+ "@orama/orama": "^3.1.18",
"@orama/ui": "^1.5.4",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.15",
@@ -65,15 +65,16 @@
"typescript": "catalog:"
},
"devDependencies": {
+ "@eslint-react/eslint-plugin": "~3.0.0",
+ "@orama/core": "^1.2.19",
"@storybook/addon-styling-webpack": "~3.0.1",
"@storybook/addon-themes": "~10.3.3",
"@storybook/addon-webpack5-compiler-swc": "~4.0.3",
"@storybook/react-webpack5": "~10.3.3",
- "@eslint-react/eslint-plugin": "~3.0.0",
"@testing-library/user-event": "~14.6.1",
"@types/node": "catalog:",
- "cross-env": "catalog:",
"concurrently": "9.2.1",
+ "cross-env": "catalog:",
"css-loader": "7.1.4",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "7.0.1",
diff --git a/packages/ui-components/src/Common/Search/Chat/Actions/index.module.css b/packages/ui-components/src/Common/Search/Chat/Actions/index.module.css
deleted file mode 100644
index ab4fb5ee91e59..0000000000000
--- a/packages/ui-components/src/Common/Search/Chat/Actions/index.module.css
+++ /dev/null
@@ -1,43 +0,0 @@
-@reference "../../../../styles/index.css";
-
-.chatActionsContainer {
- @apply flex
- items-center
- justify-end;
-}
-
-.chatActionsList {
- @apply flex
- list-none
- items-center
- gap-2
- p-0;
-}
-
-.chatAction {
- @apply cursor-pointer
- rounded-full
- p-2
- text-neutral-800
- hover:bg-neutral-300
- focus:bg-neutral-300
- focus:outline-none
- motion-safe:transition-colors
- dark:text-neutral-400
- dark:hover:bg-neutral-900
- dark:focus:bg-neutral-900;
-
- svg {
- @apply size-4;
- }
-}
-
-.chatActionIconSelected {
- @apply text-green-600
- dark:text-green-400;
-}
-
-.chatActionDisaliked {
- @apply text-neutral-900
- dark:text-neutral-800;
-}
diff --git a/packages/ui-components/src/Common/Search/Chat/Actions/index.tsx b/packages/ui-components/src/Common/Search/Chat/Actions/index.tsx
deleted file mode 100644
index 5421ddb2903bc..0000000000000
--- a/packages/ui-components/src/Common/Search/Chat/Actions/index.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import {
- DocumentCheckIcon,
- ClipboardIcon,
- ArrowPathIcon,
- HandThumbDownIcon,
-} from '@heroicons/react/24/solid';
-import { ChatInteractions } from '@orama/ui/components';
-import classNames from 'classnames';
-import { useState } from 'react';
-
-import type { Interaction } from '@orama/core';
-import type { FC } from 'react';
-
-import styles from './index.module.css';
-
-type ChatActionsProps = {
- interaction: Interaction;
-};
-
-const ChatActions: FC = ({ interaction }) => {
- const [isDisliked, setIsDisliked] = useState(false);
-
- const dislikeMessage = () => setIsDisliked(!isDisliked);
-
- if (!interaction.response) {
- return null;
- }
-
- return (
-
-
-
-
-
-
-
-
-
- {(copied: boolean) =>
- copied ? (
-
- ) : (
-
- )
- }
-
-
- {!interaction.loading && (
-
-
-
-
-
- )}
-
-
- );
-};
-
-export default ChatActions;
diff --git a/packages/ui-components/src/Common/Search/Chat/Input/index.module.css b/packages/ui-components/src/Common/Search/Chat/Input/index.module.css
deleted file mode 100644
index 22c7b21aee715..0000000000000
--- a/packages/ui-components/src/Common/Search/Chat/Input/index.module.css
+++ /dev/null
@@ -1,97 +0,0 @@
-@reference "../../../../styles/index.css";
-
-.textareaContainer {
- @apply px-1;
-}
-
-.textareaWrapper {
- @apply flex
- items-center
- rounded-2xl
- border
- border-neutral-300
- bg-neutral-100
- py-2
- pr-1
- pl-3
- dark:border-neutral-900
- dark:bg-neutral-950;
-}
-
-.textareaField {
- @apply flex-1
- border-0
- bg-transparent
- text-neutral-900
- focus:outline-none
- dark:text-neutral-200;
-}
-
-.textareaButton {
- @apply cursor-pointer
- rounded-xl
- bg-green-600
- p-2
- text-white
- focus:bg-green-600/75
- focus:outline-none
- disabled:cursor-not-allowed
- disabled:bg-neutral-200/60
- disabled:text-neutral-800
- motion-safe:transition-colors
- dark:bg-green-400
- dark:text-neutral-400
- focus:dark:bg-green-400/75
- disabled:dark:bg-neutral-900/60;
-
- svg {
- @apply size-4;
- }
-}
-
-.textareaFooter {
- @apply pt-1
- text-center
- text-xs
- text-neutral-800
- sm:text-sm
- dark:text-neutral-500;
-}
-
-.suggestionsWrapper {
- @apply mb-4
- flex
- items-center
- gap-2
- overflow-x-auto
- px-1
- text-sm
- lg:justify-center;
-
- &::-webkit-scrollbar {
- @apply hidden;
- }
-}
-
-.suggestionsItem {
- @apply flex
- size-max
- cursor-pointer
- rounded-full
- border
- border-neutral-300
- bg-neutral-200
- px-3
- py-1
- whitespace-nowrap
- text-neutral-900
- hover:bg-neutral-300
- focus:bg-neutral-300
- focus:outline-none
- motion-safe:transition-colors
- dark:border-neutral-900
- dark:bg-neutral-950
- dark:text-neutral-200
- dark:hover:bg-neutral-900
- dark:focus:bg-neutral-900;
-}
diff --git a/packages/ui-components/src/Common/Search/Chat/Input/index.tsx b/packages/ui-components/src/Common/Search/Chat/Input/index.tsx
deleted file mode 100644
index 9440db9996551..0000000000000
--- a/packages/ui-components/src/Common/Search/Chat/Input/index.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import { PaperAirplaneIcon } from '@heroicons/react/20/solid';
-import { PauseCircleIcon } from '@heroicons/react/24/solid';
-import { PromptTextArea } from '@orama/ui/components';
-import { useChat } from '@orama/ui/hooks';
-import { useEffect, useRef } from 'react';
-
-import SearchSuggestions from '#ui/Common/Search/Suggestions';
-
-import type { FC, PropsWithChildren } from 'react';
-
-import styles from './index.module.css';
-
-type ChatInputProps = {
- suggestions: Array;
- placeholder: string;
- disclaimer: string;
-};
-
-const ChatInput: FC> = ({
- suggestions,
- placeholder,
- disclaimer,
-}) => {
- const textareaRef = useRef(null);
-
- const {
- context: { interactions },
- } = useChat();
-
- useEffect(() => {
- const timeoutId = setTimeout(() => {
- textareaRef.current?.focus();
- }, 100);
-
- return () => {
- clearTimeout(timeoutId);
- };
- }, []);
-
- return (
- <>
- {!interactions?.length && (
-
- )}
-
-
-
- }
- className={styles.textareaButton}
- >
-
-
-
-
- {disclaimer}
-
-
- >
- );
-};
-
-export default ChatInput;
diff --git a/packages/ui-components/src/Common/Search/Chat/Trigger/index.module.css b/packages/ui-components/src/Common/Search/Chat/Trigger/index.module.css
deleted file mode 100644
index ce44a8010645e..0000000000000
--- a/packages/ui-components/src/Common/Search/Chat/Trigger/index.module.css
+++ /dev/null
@@ -1,38 +0,0 @@
-@reference "../../../../styles/index.css";
-
-.chatButtonWrapper {
- @apply block
- border-b
- border-neutral-200
- p-2
- dark:border-neutral-900;
-
- svg {
- @apply size-4;
- }
-}
-
-.chatButton {
- @apply flex
- w-full
- cursor-pointer
- items-center
- gap-2
- rounded-lg
- border
- border-transparent
- bg-transparent
- p-3
- text-sm
- hover:bg-neutral-300
- focus-visible:border-green-600
- focus-visible:outline-none
- motion-safe:transition-colors
- dark:hover:bg-neutral-900
- dark:focus-visible:border-green-400;
-}
-
-.chatButtonWithSearch {
- @apply bg-neutral-300
- dark:bg-neutral-900;
-}
diff --git a/packages/ui-components/src/Common/Search/Chat/Trigger/index.tsx b/packages/ui-components/src/Common/Search/Chat/Trigger/index.tsx
deleted file mode 100644
index 5e7c89a538ebb..0000000000000
--- a/packages/ui-components/src/Common/Search/Chat/Trigger/index.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { SparklesIcon } from '@heroicons/react/24/outline';
-import { SlidingPanel } from '@orama/ui/components/SlidingPanel';
-import { useSearch } from '@orama/ui/hooks/useSearch';
-import classNames from 'classnames';
-
-import type { ComponentProps, FC } from 'react';
-
-import styles from './index.module.css';
-
-const ChatTrigger: FC> = ({
- children,
- ...props
-}) => {
- const {
- context: { searchTerm },
- } = useSearch();
-
- return (
-
-
-
-
- {searchTerm ? `${searchTerm} - ` : ''}
- {children}
-
-
-
- );
-};
-
-export default ChatTrigger;
diff --git a/packages/ui-components/src/Common/Search/README.md b/packages/ui-components/src/Common/Search/README.md
deleted file mode 100644
index 7939460b3f961..0000000000000
--- a/packages/ui-components/src/Common/Search/README.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# Orama Search Components
-
-This directory contains components for creating a Node.js-styled Orama Search Box.
-
-A search modal is constructed using the following format, but additional components can also be used.
-
-```jsx
-
- }
- >
- {/* If you want to include search suggestions, there's a SearchSuggestions
- component */}
-
-
-
-```
-
-(For this example, `myOramaClient` and `MySearchItemComponent` refer to the Orama client, and search item component, respectively. These variables **are not** included in @node-core/ui-components at this time)
diff --git a/packages/ui-components/src/Common/Search/Results/Hit/index.tsx b/packages/ui-components/src/Common/Search/Results/Hit/index.tsx
index 9020c4c4c4f38..0903bb9ad3a2c 100644
--- a/packages/ui-components/src/Common/Search/Results/Hit/index.tsx
+++ b/packages/ui-components/src/Common/Search/Results/Hit/index.tsx
@@ -12,19 +12,12 @@ type HitProps = {
description?: string;
href: string;
};
- mode?: 'search' | 'chat';
as?: LinkLike;
};
-const Hit: FC = ({ document, mode = 'search', as: Link = 'a' }) => (
+const Hit: FC = ({ document, as: Link = 'a' }) => (
-
+
{typeof document?.title === 'string' &&
{document.title} }
diff --git a/packages/ui-components/src/Common/Search/Results/index.tsx b/packages/ui-components/src/Common/Search/Results/index.tsx
index ddc4810697d66..4b758a8ff934e 100644
--- a/packages/ui-components/src/Common/Search/Results/index.tsx
+++ b/packages/ui-components/src/Common/Search/Results/index.tsx
@@ -10,13 +10,11 @@ import type { ComponentProps, FC } from 'react';
import styles from './index.module.css';
type SearchResultsWrapperProps = {
- searchParams: Omit
['searchParams'], 'term'>;
onHit: ComponentProps['children'];
noResultsTitle: string;
} & Omit, 'label'>;
const SearchResultsWrapper: FC = ({
- searchParams,
onHit,
noResultsTitle,
...props
@@ -30,7 +28,16 @@ const SearchResultsWrapper: FC = ({
diff --git a/packages/ui-components/src/Common/Search/Suggestions/index.module.css b/packages/ui-components/src/Common/Search/Suggestions/index.module.css
deleted file mode 100644
index 36070f20a0fe4..0000000000000
--- a/packages/ui-components/src/Common/Search/Suggestions/index.module.css
+++ /dev/null
@@ -1,48 +0,0 @@
-@reference "../../../styles/index.css";
-
-.suggestionsWrapper {
- @apply flex
- min-h-0
- flex-1
- flex-col
- overflow-y-auto
- pt-2
- pb-4
- text-neutral-900
- dark:text-neutral-200;
-}
-
-.suggestionsList {
- @apply mt-1
- space-y-1;
-}
-
-.suggestionsTitle {
- @apply my-3
- text-xs
- font-semibold
- text-neutral-800
- uppercase
- dark:text-neutral-500;
-}
-
-.suggestionItem {
- @apply flex
- cursor-pointer
- items-center
- gap-2
- rounded-lg
- border
- border-transparent
- py-2
- text-sm
- text-green-600
- focus-visible:border-green-600
- focus-visible:outline-none
- dark:text-green-400
- dark:focus-visible:border-green-400;
-
- svg {
- @apply size-5;
- }
-}
diff --git a/packages/ui-components/src/Common/Search/Suggestions/index.tsx b/packages/ui-components/src/Common/Search/Suggestions/index.tsx
deleted file mode 100644
index 915770908070d..0000000000000
--- a/packages/ui-components/src/Common/Search/Suggestions/index.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { SparklesIcon } from '@heroicons/react/24/outline';
-import { Suggestions } from '@orama/ui/components/Suggestions';
-
-import type { ComponentProps, FC } from 'react';
-
-import styles from './index.module.css';
-
-type SearchSuggestionsProps = {
- suggestions: Array;
- label?: string;
- wrapper?: string;
-} & Omit, 'children'>;
-
-const SearchSuggestions: FC = ({
- suggestions,
- label,
- wrapper = styles.suggestionsWrapper,
- className = styles.suggestionItem,
- ...props
-}) => (
-
- {label && {label}
}
- {suggestions.map(suggestion => (
-
-
- {suggestion}
-
- ))}
-
-);
-
-export default SearchSuggestions;
diff --git a/apps/site/components/Common/Searchbox/Footer/index.module.css b/packages/ui-components/src/Common/Search/index.module.css
similarity index 67%
rename from apps/site/components/Common/Searchbox/Footer/index.module.css
rename to packages/ui-components/src/Common/Search/index.module.css
index 9f2342e65c412..43a6653dd660f 100644
--- a/apps/site/components/Common/Searchbox/Footer/index.module.css
+++ b/packages/ui-components/src/Common/Search/index.module.css
@@ -1,28 +1,26 @@
-@reference "../../../../styles/index.css";
+@reference "../../styles/index.css";
+
+.searchResultsContainer {
+ @apply flex
+ grow
+ flex-col
+ overflow-y-auto;
+}
.footer {
@apply flex
+ items-baseline
justify-center
border-t
border-neutral-200
bg-neutral-100
p-4
- align-baseline
lg:justify-between
lg:rounded-b-xl
dark:border-neutral-900
dark:bg-neutral-950;
}
-.poweredByLink {
- @apply flex
- items-center
- gap-2
- text-sm
- text-neutral-800
- dark:text-neutral-600;
-}
-
.shortcutWrapper {
@apply hidden
items-center
@@ -40,10 +38,10 @@
}
.shortcutKey {
- @apply font-ibm-plex-mono
- rounded-md
+ @apply rounded-md
bg-neutral-200
p-1
+ font-mono
text-xs
dark:bg-neutral-900;
@@ -52,10 +50,7 @@
}
}
-.poweredByWrapper {
- @apply ml-0
- flex
- items-center
- justify-end
- lg:ml-8;
+.shortcutLabel {
+ @apply text-neutral-800
+ dark:text-neutral-600;
}
diff --git a/packages/ui-components/src/Common/Search/index.tsx b/packages/ui-components/src/Common/Search/index.tsx
new file mode 100644
index 0000000000000..a9e427c14a3b7
--- /dev/null
+++ b/packages/ui-components/src/Common/Search/index.tsx
@@ -0,0 +1,73 @@
+import {
+ ArrowDownIcon,
+ ArrowTurnDownLeftIcon,
+ ArrowUpIcon,
+} from '@heroicons/react/24/solid';
+
+import SearchModal from '#ui/Common/Search/Modal';
+import SearchResults from '#ui/Common/Search/Results';
+import SearchHit from '#ui/Common/Search/Results/Hit';
+
+import type { OramaCloud } from '@orama/core';
+
+import styles from './index.module.css';
+
+type SearchBoxProps = {
+ client: OramaCloud;
+ closeShortcutLabel?: string;
+ navigateShortcutLabel?: string;
+ noResultsTitle?: string;
+ placeholder?: string;
+ selectShortcutLabel?: string;
+};
+
+const SearchBox: React.FC = ({
+ client,
+ placeholder = 'Start typing...',
+ noResultsTitle = 'No results found for',
+ closeShortcutLabel = 'to close',
+ navigateShortcutLabel = 'to navigate',
+ selectShortcutLabel = 'to select',
+}) => {
+ return (
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+
{selectShortcutLabel}
+
+
+
+
+
+
+
+
+
+
+ {navigateShortcutLabel}
+
+
+
+
+ esc
+ {closeShortcutLabel}
+
+
+
+
+ );
+};
+
+export default SearchBox;
diff --git a/packages/ui-components/src/hooks/useOrama.ts b/packages/ui-components/src/hooks/useOrama.ts
new file mode 100644
index 0000000000000..a23b02f00c0df
--- /dev/null
+++ b/packages/ui-components/src/hooks/useOrama.ts
@@ -0,0 +1,48 @@
+import { create, search, load, type RawData } from '@orama/orama';
+import { useMemo, useRef } from 'react';
+
+import type { OramaCloud } from '@orama/core';
+
+/**
+ * Hook for initializing and managing an Orama search database.
+ * Search data is loaded lazily on the first search call and reused thereafter.
+ *
+ * @param loadData Function returning the serialized Orama database payload.
+ */
+export default function useOrama(loadData: () => Promise): OramaCloud {
+ const loadPromiseRef = useRef | null>(null);
+
+ const client = useMemo(() => {
+ loadPromiseRef.current = null;
+
+ const db = create({
+ schema: {
+ title: 'string',
+ description: 'string',
+ href: 'string',
+ siteSection: 'string',
+ },
+ });
+
+ const ensureLoaded = (): Promise => {
+ if (!loadPromiseRef.current) {
+ loadPromiseRef.current = loadData().then(data => {
+ load(db, data);
+ });
+ }
+
+ return loadPromiseRef.current;
+ };
+
+ // TODO(@avivkeller): Orama might need to be replaced
+ // @ts-expect-error - We are overriding the search method
+ db.search = async options => {
+ await ensureLoaded();
+ return search(db, options);
+ };
+
+ return db;
+ }, [loadData]);
+
+ return client as unknown as OramaCloud;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e50916229bc79..e0cf8288b8245 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -111,12 +111,9 @@ importers:
'@opentelemetry/sdk-logs':
specifier: ~0.213.0
version: 0.213.0(@opentelemetry/api@1.9.1)
- '@orama/core':
- specifier: ^1.2.19
- version: 1.2.19
- '@orama/ui':
- specifier: ^1.5.4
- version: 1.5.4(@orama/core@1.2.19)(@types/react@19.2.14)(react@19.2.4)
+ '@orama/orama':
+ specifier: ^3.1.18
+ version: 3.1.18
'@radix-ui/react-tabs':
specifier: ^1.1.13
version: 1.1.13(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -471,9 +468,9 @@ importers:
'@heroicons/react':
specifier: ^2.2.0
version: 2.2.0(react@19.2.4)
- '@orama/core':
- specifier: ^1.2.19
- version: 1.2.19
+ '@orama/orama':
+ specifier: ^3.1.18
+ version: 3.1.18
'@orama/ui':
specifier: ^1.5.4
version: 1.5.4(@orama/core@1.2.19)(@types/react@19.2.14)(react@19.2.4)
@@ -532,6 +529,9 @@ importers:
'@eslint-react/eslint-plugin':
specifier: ~3.0.0
version: 3.0.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)
+ '@orama/core':
+ specifier: ^1.2.19
+ version: 1.2.19
'@storybook/addon-styling-webpack':
specifier: ~3.0.1
version: 3.0.1(storybook@10.3.3(@testing-library/dom@10.4.0)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(@swc/core@1.15.21))
diff --git a/turbo.json b/turbo.json
index 463db927aff91..e4f62db3943b8 100644
--- a/turbo.json
+++ b/turbo.json
@@ -12,7 +12,7 @@
"dependsOn": ["^topo"]
},
"build": {
- "dependsOn": ["compile", "^topo"]
+ "dependsOn": ["^topo"]
},
"lint": {
"dependsOn": ["^topo"]