Skip to content

Commit

Permalink
Merge pull request #928 from prezly/feature/meilisearch-integration
Browse files Browse the repository at this point in the history
[DEV-13018] Feature - MeiliSearch integration
  • Loading branch information
yuriyyakym authored Jul 23, 2024
2 parents e7c7034 + d3df1f2 commit 60600c8
Show file tree
Hide file tree
Showing 18 changed files with 662 additions and 84 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ PREZLY_THEME_UUID=9648bb4f-19df-44a3-a1b4-18bce22c76fd

# This key is unique for every newsroom
ALGOLIA_API_KEY=
MEILISEARCH_API_KEY=

# These variables have default values in the code, but you can override them if you need to
# ALGOLIA_APP_ID=
# ALGOLIA_INDEX=
# MEILISEARCH_HOST=
# MEILISEARCH_INDEX=

# API_BASE_URL_OVERRIDE=

Expand Down
4 changes: 2 additions & 2 deletions components/CategoriesList/CategoriesList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Category } from '@prezly/sdk';
import type { AlgoliaCategoryRef } from '@prezly/theme-kit-core';
import type { IndexedCategoryRef } from '@prezly/theme-kit-core';
import { getLocalizedCategoryData } from '@prezly/theme-kit-core';
import { useCurrentLocale } from '@prezly/theme-kit-nextjs';
import { useEffect, useMemo, useState } from 'react';
Expand All @@ -9,7 +9,7 @@ import CategoryLink from '../CategoryLink';
import styles from './CategoriesList.module.scss';

type Props = {
categories: Category[] | AlgoliaCategoryRef[];
categories: Category[] | IndexedCategoryRef[];
showAllCategories?: boolean;
isStatic?: boolean;
linkClassName?: string;
Expand Down
4 changes: 2 additions & 2 deletions components/CategoryLink/CategoryLink.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Category } from '@prezly/sdk';
import type { AlgoliaCategoryRef } from '@prezly/theme-kit-core';
import type { IndexedCategoryRef } from '@prezly/theme-kit-core';
import { getCategoryUrl, getLocalizedCategoryData } from '@prezly/theme-kit-core';
import { useCurrentLocale, useGetLinkLocaleSlug } from '@prezly/theme-kit-nextjs';
import classNames from 'classnames';
Expand All @@ -8,7 +8,7 @@ import Link from 'next/link';
import styles from './CategoryLink.module.scss';

type Props = {
category: Category | AlgoliaCategoryRef;
category: Category | IndexedCategoryRef;
className?: string;
onClick?: () => void;
};
Expand Down
4 changes: 2 additions & 2 deletions components/StoryImage/StoryImage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AlgoliaStory } from '@prezly/theme-kit-core';
import type { IndexedStory } from '@prezly/theme-kit-core';
import { useNewsroom } from '@prezly/theme-kit-nextjs';
import UploadcareImage from '@uploadcare/nextjs-loader';
import classNames from 'classnames';
Expand All @@ -11,7 +11,7 @@ import { getStoryThumbnail } from './lib';
import styles from './StoryImage.module.scss';

type Props = {
story: StoryWithImage | AlgoliaStory;
story: StoryWithImage | IndexedStory;
size: CardSize;
className?: string;
placeholderClassName?: string;
Expand Down
4 changes: 2 additions & 2 deletions components/StoryImage/lib/getStoryThumbnail.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { AlgoliaStory } from '@prezly/theme-kit-core';
import type { IndexedStory } from '@prezly/theme-kit-core';
import type { UploadcareImageDetails } from '@prezly/uploadcare-image/build/types';

import type { StoryWithImage } from 'types';

export function getStoryThumbnail(
story: StoryWithImage | AlgoliaStory,
story: StoryWithImage | IndexedStory,
): UploadcareImageDetails | null {
const { thumbnail_image } = story;

Expand Down
4 changes: 2 additions & 2 deletions components/StoryPublicationDate/StoryPublicationDate.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Story } from '@prezly/sdk';
import type { AlgoliaStory } from '@prezly/theme-kit-core';
import type { IndexedStory } from '@prezly/theme-kit-core';
import { getStoryPublicationDate } from '@prezly/theme-kit-core';
import { FormattedDate } from 'react-intl';

interface Props {
story: Story | AlgoliaStory;
story: Story | IndexedStory;
}

export function StoryPublicationDate({ story }: Props) {
Expand Down
11 changes: 5 additions & 6 deletions modules/Layout/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { translations } from '@prezly/theme-kit-intl';
import {
useAlgoliaSettings,
useCategories,
useCompanyInformation,
useGetLinkLocaleSlug,
useNewsroom,
useSearchSettings,
} from '@prezly/theme-kit-nextjs';
import classNames from 'classnames';
import dynamic from 'next/dynamic';
Expand Down Expand Up @@ -36,15 +36,13 @@ function Header({ hasError }: Props) {
const { name } = useCompanyInformation();
const getLinkLocaleSlug = useGetLinkLocaleSlug();
const { formatMessage } = useIntl();
const { ALGOLIA_API_KEY } = useAlgoliaSettings();
const searchSettings = useSearchSettings();
const { isMobile } = useDevice();

const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isSearchWidgetShown, setIsSearchWidgetShown] = useState(false);
const headerRef = useRef<HTMLElement>(null);

const IS_SEARCH_ENABLED = Boolean(ALGOLIA_API_KEY);

function alignMobileHeader() {
if (!isMobile) {
return;
Expand Down Expand Up @@ -132,7 +130,7 @@ function Header({ hasError }: Props) {
aria-label={formatMessage(translations.misc.toggleMobileNavigation)}
/>

{IS_SEARCH_ENABLED && (
{searchSettings && (
<ButtonLink
href="/search"
localeCode={getLinkLocaleSlug()}
Expand Down Expand Up @@ -187,10 +185,11 @@ function Header({ hasError }: Props) {
</ul>
</div>

{IS_SEARCH_ENABLED && (
{searchSettings && (
<SearchWidget
dialogClassName={styles.mobileSearchWrapper}
isOpen={isSearchWidgetShown}
settings={searchSettings}
onClose={closeSearchWidget}
/>
)}
Expand Down
25 changes: 11 additions & 14 deletions modules/Layout/Header/SearchWidget/SearchWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useAlgoliaSettings, useCurrentLocale } from '@prezly/theme-kit-nextjs';
import algoliasearch from 'algoliasearch/lite';
import type { SearchSettings } from '@prezly/theme-kit-core/server';
import { useCurrentLocale, useSearchClient } from '@prezly/theme-kit-nextjs';
import classNames from 'classnames';
import { useMemo } from 'react';
import { Configure, InstantSearch } from 'react-instantsearch-dom';

import { Modal } from '@/ui';
Expand All @@ -14,17 +13,18 @@ interface Props {
isOpen: boolean;
className?: string;
dialogClassName?: string;
settings: SearchSettings;
onClose: () => void;
}

function SearchWidget({ isOpen, className, dialogClassName, onClose }: Props) {
function SearchWidget({ isOpen, className, dialogClassName, settings, onClose }: Props) {
const currentLocale = useCurrentLocale();
const { ALGOLIA_APP_ID, ALGOLIA_API_KEY, ALGOLIA_INDEX } = useAlgoliaSettings();
const searchClient = useSearchClient(settings);

const searchClient = useMemo(
() => algoliasearch(ALGOLIA_APP_ID, ALGOLIA_API_KEY),
[ALGOLIA_API_KEY, ALGOLIA_APP_ID],
);
const filters =
settings.searchBackend === 'algolia'
? `attributes.culture.code:${currentLocale.toUnderscoreCode()}`
: `attributes.culture.code=${currentLocale.toUnderscoreCode()}`;

return (
<Modal
Expand All @@ -35,11 +35,8 @@ function SearchWidget({ isOpen, className, dialogClassName, onClose }: Props) {
dialogClassName={dialogClassName}
wrapperClassName={styles.wrapper}
>
<InstantSearch searchClient={searchClient} indexName={ALGOLIA_INDEX}>
<Configure
hitsPerPage={3}
filters={`attributes.culture.code:${currentLocale.toUnderscoreCode()}`}
/>
<InstantSearch searchClient={searchClient} indexName={settings.index}>
<Configure hitsPerPage={3} filters={filters} />
<SearchBar />
<MainPanel onClose={onClose} />
</InstantSearch>
Expand Down
4 changes: 2 additions & 2 deletions modules/Layout/Header/SearchWidget/components/Hit.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AlgoliaStory } from '@prezly/theme-kit-core';
import type { IndexedStory } from '@prezly/theme-kit-core';
import Link from 'next/link';
import type { Hit } from 'react-instantsearch-core';
import { Highlight } from 'react-instantsearch-dom';
Expand All @@ -8,7 +8,7 @@ import { StoryImage } from '@/components';
import styles from './Hit.module.scss';

interface Props {
hit: Hit<{ attributes: AlgoliaStory }>;
hit: Hit<{ attributes: IndexedStory }>;
}

function HitComponent({ hit }: Props) {
Expand Down
4 changes: 2 additions & 2 deletions modules/Layout/Header/SearchWidget/components/MainPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AlgoliaStory } from '@prezly/theme-kit-core';
import type { IndexedStory } from '@prezly/theme-kit-core';
import { getCategoryHasTranslation } from '@prezly/theme-kit-core';
import { useCategories, useCurrentLocale } from '@prezly/theme-kit-nextjs';
import type { StateResultsProvided } from 'react-instantsearch-core';
Expand All @@ -17,7 +17,7 @@ function MainPanel({
searchState,
searchResults,
onClose,
}: StateResultsProvided<AlgoliaStory> & Props) {
}: StateResultsProvided<IndexedStory> & Props) {
const isQuerySet = Boolean(searchState.query?.length);
const categories = useCategories();
const currentLocale = useCurrentLocale();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AlgoliaStory } from '@prezly/theme-kit-core';
import type { IndexedStory } from '@prezly/theme-kit-core';
import { translations } from '@prezly/theme-kit-intl';
import classNames from 'classnames';
import { useRouter } from 'next/router';
Expand All @@ -12,7 +12,7 @@ import Hit from './Hit';

import styles from './MainPanel.module.scss';

type Props = Pick<StateResultsProvided<AlgoliaStory>, 'searchResults'> & {
type Props = Pick<StateResultsProvided<IndexedStory>, 'searchResults'> & {
query?: string;
};

Expand Down
30 changes: 15 additions & 15 deletions modules/Search/SearchPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useAlgoliaSettings, useCurrentLocale } from '@prezly/theme-kit-nextjs';
import algoliasearch from 'algoliasearch';
import type { SearchSettings } from '@prezly/theme-kit-core/server';
import { useCurrentLocale, useSearchClient } from '@prezly/theme-kit-nextjs';
import { useRouter } from 'next/router';
import { useMemo, useRef, useState } from 'react';
import { useRef, useState } from 'react';
import { Configure, InstantSearch } from 'react-instantsearch-dom';

import Layout from '../Layout';
Expand All @@ -14,21 +14,24 @@ import { createUrl, queryToSearchState, searchStateToQuery } from './utils';

import styles from './SearchPage.module.scss';

interface Props {
settings: SearchSettings;
}

const DEBOUNCE_TIME_MS = 300;

function SearchPage() {
function SearchPage({ settings }: Props) {
const currentLocale = useCurrentLocale();
const searchClient = useSearchClient(settings);

const { query, push } = useRouter();
const [searchState, setSearchState] = useState<SearchState>(queryToSearchState(query));
const debouncedSetStateRef = useRef<number>();

const { ALGOLIA_APP_ID, ALGOLIA_API_KEY, ALGOLIA_INDEX } = useAlgoliaSettings();

const searchClient = useMemo(
() => algoliasearch(ALGOLIA_APP_ID, ALGOLIA_API_KEY),
[ALGOLIA_API_KEY, ALGOLIA_APP_ID],
);
const filters =
settings.searchBackend === 'algolia'
? `attributes.culture.code:${currentLocale.toUnderscoreCode()}`
: `attributes.culture.code=${currentLocale.toUnderscoreCode()}`;

function onSearchStateChange(updatedSearchState: SearchState) {
if (typeof window === 'undefined') {
Expand All @@ -50,15 +53,12 @@ function SearchPage() {
<Layout>
<InstantSearch
searchClient={searchClient}
indexName={ALGOLIA_INDEX}
indexName={settings.index}
searchState={searchState}
onSearchStateChange={onSearchStateChange}
createURL={createUrl}
>
<Configure
hitsPerPage={6}
filters={`attributes.culture.code:${currentLocale.toUnderscoreCode()}`}
/>
<Configure hitsPerPage={6} filters={filters} />
<AlgoliaStateContextProvider>
<Title />
<div className={styles.container}>
Expand Down
6 changes: 3 additions & 3 deletions modules/Search/components/AlgoliaStateContext.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import type { AlgoliaStory } from '@prezly/theme-kit-core';
import type { IndexedStory } from '@prezly/theme-kit-core';
import type { PropsWithChildren } from 'react';
import { createContext, useContext } from 'react';
import type { StateResultsProvided } from 'react-instantsearch-core';
import { connectStateResults } from 'react-instantsearch-dom';

const AlgoliaStateContext = createContext<StateResultsProvided<AlgoliaStory> | undefined>(
const AlgoliaStateContext = createContext<StateResultsProvided<IndexedStory> | undefined>(
undefined,
);

// Algolia connect API is pretty broken, so this is a workaround to provide some search state to components lower in the tree
function AlgoliaStateContextProvider({
children,
...contextValue
}: PropsWithChildren<StateResultsProvided<AlgoliaStory>>) {
}: PropsWithChildren<StateResultsProvided<IndexedStory>>) {
return (
<AlgoliaStateContext.Provider value={contextValue}>{children}</AlgoliaStateContext.Provider>
);
Expand Down
4 changes: 2 additions & 2 deletions modules/Search/components/Hit.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AlgoliaStory } from '@prezly/theme-kit-core';
import type { IndexedStory } from '@prezly/theme-kit-core';
import classNames from 'classnames';
import Link from 'next/link';
import type { Hit } from 'react-instantsearch-core';
Expand All @@ -11,7 +11,7 @@ import styles from './Hit.module.scss';
import cardStyles from '@/components/StoryCards/StoryCard.module.scss';

interface Props {
hit: Hit<{ attributes: AlgoliaStory }>;
hit: Hit<{ attributes: IndexedStory }>;
}

// This is mostly a copy of `StoryCard` component, but since the data structure is a bit different,
Expand Down
4 changes: 2 additions & 2 deletions modules/Search/components/Results.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AlgoliaStory } from '@prezly/theme-kit-core';
import type { IndexedStory } from '@prezly/theme-kit-core';
import { translations } from '@prezly/theme-kit-intl';
import classNames from 'classnames';
import type { Hit as HitType, InfiniteHitsProvided } from 'react-instantsearch-core';
Expand All @@ -14,7 +14,7 @@ import styles from './Results.module.scss';
import containerStyles from '@/modules/InfiniteStories/InfiniteStories.module.scss';
import listStyles from '@/modules/InfiniteStories/StoriesList.module.scss';

type SearchHit = HitType<{ attributes: AlgoliaStory }>;
type SearchHit = HitType<{ attributes: IndexedStory }>;

function Results({ hits, hasMore, refineNext }: InfiniteHitsProvided<SearchHit>) {
const { formatMessage } = useIntl();
Expand Down
Loading

0 comments on commit 60600c8

Please sign in to comment.