Loading...
Nextjs14-Algolia-Native
Author Cloudapp
E.G.

Next.js 14 -Erweiterte Suchintegration mit Algolia UI Bibliotheken (Widgets)

7. August 2024
Inhaltsverzeichnis

In diesem Folgebeitrag zeige ich Ihnen, wie Sie die Algolia Instant-Suche ganz einfach in Ihr bestehendes Next.js 14-Projekt integrieren können. Wir werden auch benutzerdefiniertes Styling mit TailwindCss verwenden.

Hier ist das GitHub Repo mit dem gesamten Code und darunter der Link zur Beispielwebsite.

Beispielseite für mit integrierter Datenerfassung -> https://nextjs14-advanced-algoliasearch.vercel.app/

In der folgenden Geschichte habe ich Ihnen gezeigt, wie Sie Algolia in Ihr Nextjs 14 Projekt integrieren können. Hier habe ich die REST-API von Algolia verwendet, aber jetzt verwenden wir die native UI-Bibliothek (Widgets), um den blitzschnellen Datenservice direkt zu integrieren.

Einfache Integration von Algolia als interne Suchmaschine

Bitte beachten Sie auch diesen Artikel, in dem ich erkläre, wie Sie Ihre Daten in Contentful mit Algolia synchronisieren können.

Datensynchronisation zwischen Contentful und Algolia

Verwendeter Stack

Ich werde mit meinem Standard-Stack beginnen:

  • Next.js 14 als Web-Framework, und ich werde die mitgelieferte Middleware Edge-Funktion verwenden

  • TailwindCss for Styling

  • Contentful CMS (Kostenloses Abo)

  • Algolia als Such-Engine

  • Vercel für das Hosting

Benötigte NPM Pakete für InstantSearch

If you read the first story, you have already installed the needed NPM packages

npm i algoliasearch instantsearch.css react-instantsearch react-instantsearch-nextjs react-instantsearch-router-nextjs

Neue Seite “SearchAlgolia”

Unter src/app/[locale]/ haben wir bereits die neue Seite "searchalgolia" für die Integration der UI Widgets hinzugefügt.Der Aufbau dieser Seite ist recht einfach, da wir nur die Komponente algoliasearch importieren und anzeigen, die wir im nächsten Schritt erstellen werden.

export const dynamic = "force-dynamic";

import Search from "@/components/search/algoliasearch.component";

export default function Page() {
  return <Search />;
}

Neue Komponente Algoliasearch

Jetzt kommt die Magie. Wir importieren alle notwendigen Komponenten, um die Suchanfrage zu bearbeiten und Algolia Features wie das Suchfeld, die RefinementList und die DynamicWidgets anzuzeigen. Snippets und Highlights werden in einer der nächsten Geschichten gezeigt. Ich möchte auf die Komponente "CardAlgolia" hinweisen, die wir verwenden werden, um die Suchergebnisse unterhalb des Suchfeldes anzuzeigen.

"use client";

import algoliasearch from "algoliasearch/lite";
import { Hit as AlgoliaHit } from "instantsearch.js";
import {
  Hits,
  Highlight,
  SearchBox,
  RefinementList,
  DynamicWidgets,
  Snippet,
} from "react-instantsearch";
import { PageBlogPostFieldsFragment } from "@/lib/__generated/sdk";
import { InstantSearchNext } from "react-instantsearch-nextjs";
import { Panel } from "@/components/search/panel.component";
import { useParams, useSearchParams } from "next/navigation";
import type { LocaleTypes } from "@/app/i18n/settings";
import CardAlgolia from "./cardalgolia.component";

interface ArticleAuthorProps {
  article: PageBlogPostFieldsFragment;
}

const client = algoliasearch(
  process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "",
  process.env.NEXT_PUBLIC_ALGOLIA_API_KEY || ""
);

type CardProps = {
  hit: AlgoliaHit<{
    intName: string;
    image: string;
    pubdate: Date;
    slug: string;
    width: number;
    height: number;
    tags: string[];
    lang: {
      "de-DE": { content: string; shortDescription: string; title: string };
      "en-US": { content: string; shortDescription: string; title: string };
    };
  }>;
};

let locale: LocaleTypes;

function Hit({ hit }: CardProps) {
  const blurURL = new URL(hit.image);
  blurURL.searchParams.set("w", "10");

  return (
    <div>
      <CardAlgolia key={hit.objectID} result={hit} />
    </div>
  );
}

export default function Search() {
  const urlSearchParams = useSearchParams();
  const params = Object.fromEntries(urlSearchParams.entries());

  console.log("params_searchompo", params);

  locale = useParams()?.locale as LocaleTypes;
  return (
    <InstantSearchNext
      searchClient={client}
      indexName={process.env.NEXT_PUBLIC_ALGOLIA_INDEX_NAME}
      routing={{
        router: {
          cleanUrlOnDispose: false,
          windowTitle(routeState) {
            const indexState = routeState.indexName || {};
            return indexState.query
              ? `MyWebsite - Results for: ${indexState.query}`
              : "MyWebsite - Results page";
          },
        },
      }}
      future={{
        preserveSharedStateOnUnmount: true,
      }}
    >
      <div className="Container mx-4">
        <div>
          <DynamicWidgets fallbackComponent={FallbackComponent} />
        </div>
        <div>
          <SearchBox className="p-3 shadow-sm" />
          <Hits
            hitComponent={Hit}
            classNames={{
              root: "MyCustomHits",
            }}
          />
        </div>
      </div>
    </InstantSearchNext>
  );
}

function FallbackComponent({ attribute }: { attribute: string }) {
  return (
    <Panel header={attribute}>
      <RefinementList
        classNames={{
          root: "MyCustomRefinementList",
        }}
        attribute={attribute}
        limit={8}
        showMore={true}
        showMoreLimit={20}
      />
    </Panel>
  );
}

Neues und erweiterte Card-Komponente für die Suchergebnisse

"use client";

import Image from "next/image";
import Link from "next/link";
import { FormatDate } from "@/components/contentful/format-date/FormatDate";
import { useParams } from "next/navigation";
import type { LocaleTypes } from "@/app/i18n/settings";
import { twMerge } from "tailwind-merge";
import { ArticleLabel } from "@/components/contentful/ArticleLabel";

interface CardProps {
  result: {
    intName: string;
    image: string;
    pubdate: Date;
    slug: string;
    width: number;
    height: number;
    tags: string[];
    lang: {
      "de-DE": { content: string; shortDescription: string; title: string };
      "en-US": { content: string; shortDescription: string; title: string };
    };
  };
}

interface LangProps {
  title: string;
  content: string;
  shortDescription: string;
}

export default function CardAlgolia({ result }: CardProps) { 
  const locale = useParams()?.locale as LocaleTypes;
  const langNr = locale === "de-DE" ? 0 : 1;
  const langresult = JSON.parse(
    JSON.stringify(Object.entries(result.lang)[langNr][1])
  ) as LangProps;

  const className = "md:grid-cols-2 lg:grid-cols-3";
  const classNameImage = "object-cover aspect-[16/10] w-full";

  const blurURL = new URL(result.image);
  blurURL.searchParams.set("w", "10");

  return (
    // {/* group - wird benötigt damit man unten im Classname darauf verweisen kann mit group-hover:.... */}
    <div className="flex flex-col">
      <div
        className={twMerge(
          "flex flex-1 flex-col overflow-hidden dark:shadow-white shadow-lg dark:shadow-sm-light",
          className
        )}
      >
        <Link href={`/${locale}/${result.slug}`}>
          <Image
            src={result.image}
            width={result.width || 722}
            height={result.height || 590}
            sizes="(max-width: 1200px) 100vw, 50vw"
            placeholder="blur"
            blurDataURL={blurURL.toString()}
            alt={langresult.title || ""}
            className={twMerge(classNameImage, "transition-all")}
          ></Image>
        </Link>
        <div className="flex flex-col flex-1 px-4 py-3 dark:bg-gray-800 md:px-5 md:py-4 lg:px-7 lg:py-5">          
          {langresult.title && (
            <Link href={`/${locale}/${result.slug}`}>
              <p className="mb-2 h3 line-clamp-2 text-gray-800 dark:text-[#AEC1CC] md:mb-3">                
                {langresult.title}
              </p>
            </Link>
          )}
          {langresult.shortDescription && (
            <p className="mt-2 text-base line-clamp-2">
              {langresult.shortDescription}
            </p>
          )}
          <div className="flex flex-wrap max-w-2xl gap-2 mr-auto">          
            {result.tags.map((tag: string, index) => (
              <Link href={`/${locale}/search/${tag}`} key={index}>
                <ArticleLabel className="flex items-center ml-1">
                  {tag}
                </ArticleLabel>
              </Link>
            ))}
            <div className={twMerge("ml-auto pl-2 text-xs text-gray-600")}>
              <FormatDate date={result.pubdate} />
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

Restying des "Hits Algolia UI Widgets"

mit classnames={{root: "MyCustomHits",}} fügen wir dem Widget eine benutzerdefinierte Klasse hinzu, die wir in unserer global.css-Datei ansprechen können.

         <Hits
            hitComponent={Hit}
            classNames={{
              root: "MyCustomHits",
            }}
          />

Neues Styling des "RefinementList Algolia UI Widgets"

Gleicher Ansatz hier mit classNames={{root: "MyCustomRefinementList",}}. Mit dem Parameter-Attribut definieren wir das Attribut, das wir für die Verfeinerung haben möchten. Ich habe mich für das Attribut "Tag" in der Algolia-Backend-Konfiguration entschieden.

<RefinementList
        classNames={{
          root: "MyCustomRefinementList",
        }}
        attribute={attribute}
        limit={8}
        showMore={true}
        showMoreLimit={20}
      />

Global.css mit benutzerdefinierten Klassen

Hier sprechen wir die neu hinzugefügten Klassen in der global.css an

.MyCustomHits ol {
    @apply grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-y-4 gap-x-5 lg:gap-x-12 lg:gap-y-12;
  }

  .MyCustomHits li {
    @apply dark:bg-gray-900 bg-white p-3 text-sm dark:text-[#FAFAFA] text-gray-600 md:text-sm;
  }

  .MyCustomRefinementList li {
    @apply dark:bg-gray-900 bg-white ml-3 py-1 text-base dark:text-[#FAFAFA] text-gray-600;
  }

Anpassung der Datei Middleware.ts für das richtige Verarbeiten der SearchParams

Hier die komplette Middleware.ts Datei

import { NextResponse } from "next/server";
import type { NextFetchEvent, NextRequest } from "next/server";
import { fallbackLng, locales } from "@/app/i18n/settings";

export async function middleware(request: NextRequest, event: NextFetchEvent) {
  const { pathname, search } = request.nextUrl;

  //Entfernt den FallbackLng aus dem Pathname, sofern dieser vorhanden ist
  if (
    pathname.startsWith(`/${fallbackLng}/`) ||
    pathname === `/${fallbackLng}`
  ) {
    return NextResponse.redirect(
      new URL(
        pathname.replace(
          `/${fallbackLng}`,
          pathname === `/${fallbackLng}` ? "/" : ""
        ),
        request.url
      ),
      301
    );
  }

  // Check if the pathname is missing any locale
  const pathnameIsMissingLocale = locales.every(
    (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
  );

  // Füght den FallbackLng vor den Pathname ein, sofern keine Sprache im Pathname vorhanden ist
  if (pathnameIsMissingLocale) {
    const RewriteUrl = request.nextUrl;
    RewriteUrl.pathname = `/${fallbackLng}${pathname}`;

    return NextResponse.rewrite(new URL(RewriteUrl, request.url));
  }
}

export const config = {
  /*
   * Match all request paths except for the ones starting with:
   * - api (API routes)
   * - _next/static (static files)
   * - _next/image (image optimization files)
   * - favicon.ico (favicon file)
   */
  matcher: [
    "/((?!api|sitemap.xml|robots.txt|_next/static|_next/image|favicons|images|favicon.ico).*)",
  ],
};

Anpassungen in der Datei search.component.tsx

Wir sind fast fertig; jetzt müssen wir noch die search.component.tsx modifizieren, die wir in die Header-Komponente integriert haben und die den Suchbegriff an die richtige Algoliapage weiterleitet, um die Suche durchzuführen.

"use client";

import { useState, useEffect } from "react";
import { useRouter, usePathname, useParams } from "next/navigation";
import type { LocaleTypes } from "@/app/i18n/settings";
import { fallbackLng } from "@/app/i18n/settings";
import { useTranslation } from "@/app/i18n/client";

export default function SearchBar({
  searchCta,
  searchPlaceholder,
}: {
  searchCta: string;
  searchPlaceholder: string;
}) {
  const [search, setSearch] = useState("");
  const router = useRouter();
  const path = usePathname();
  const locale = useParams()?.locale as LocaleTypes;
  const { t } = useTranslation(locale, "common");

  const pathWithoutQuery = path.split("?")[0];
  let pathArray = pathWithoutQuery.split("/");
  pathArray.shift();
  pathArray = pathArray.filter((path) => path !== "");

  // if the path is searchalgolia, don't show the search bar
  if (pathArray[0] === "searchalgolia") {
    return null;
  }

  function handleSubmit(e: any) {
    e.preventDefault(); // prevent page refresh
    if (!search) return; // if there is no search, return
    const searchParams = `example_dev[query]`;
    if (locale === fallbackLng) {
      router.push(
        `/searchalgolia?${encodeURIComponent(searchParams)}=${search}`
      );
    } else {
      router.push(
        `/${locale}/searchalgolia?${encodeURIComponent(searchParams)}=${search}`
      );
    }
  }

  return (
    <div className="mb-1 bg-gray-100 rounded-sm shadow-md dark:bg-gray-800">
      <form onSubmit={handleSubmit}>
        <label
          htmlFor="default-search"
          className="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white"
        >
          {t("search.button")}
        </label>
        <div className="relative">
          <div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
            <svg
              className="w-4 h-4 text-gray-500 dark:text-gray-400"
              aria-hidden="true"
              xmlns="http://www.w3.org/2000/svg"
              fill="none"
              viewBox="0 0 20 20"
            >
              <path
                stroke="currentColor"
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth="2"
                d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"
              />
            </svg>
          </div>
          <input
            value={search}
            onChange={(e) => setSearch(e.target.value)}
            //   type="text"
            type="search"
            id="default-search"
            className="block w-full p-4 pl-10 text-base text-gray-900 border border-gray-300 bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
            placeholder={t("search.searchPlaceholder")}
            // {searchPlaceholder ? searchPlaceholder : "Search keywords..."}
            required
          />
          <button
            disabled={!search} // disable the button if there is no search
            type="submit"
            className="text-white absolute right-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-base px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
          >
            {t("search.button")}
            {/* {searchCta ? searchCta : Search} */}
          </button>
        </div>
      </form>
    </div>
  );
}

Neue Algolia Suchseite

Und voilá, jetzt haben wir eine neue, individuell gestaltete Suchseite, die von der Algolia UI Library unterstützt wird. In der nächsten Geschichte werden wir neue Widgets wie Snippets, Highlights, etc. hinzufügen.

MyWebsite-Results-page
MyWebsite-Results-page

Cloudapp-dev und bevor Sie uns verlassen

Danke, dass Sie bis zum Ende gelesen haben. Noch eine Bitte bevor Sie gehen:

Wenn Ihnen gefallen hat was Sie gelesen haben oder wenn es Ihnen sogar geholfen hat, dann würden wir uns über einen "Clap" 👏 oder einen neuen Follower auf unseren Medium Account sehr freuen.

Oder folgen Sie uns auf Twitter -> Cloudapp.dev

Verwandte Artikel