Inhaltsverzeichnis
- Verwendeter Stack
- Benötigte NPM Pakete für InstantSearch
- Neue Seite “SearchAlgolia”
- Neue Komponente Algoliasearch
- Neues und erweiterte Card-Komponente für die Suchergebnisse
- Restying des "Hits Algolia UI Widgets"
- Neues Styling des "RefinementList Algolia UI Widgets"
- Global.css mit benutzerdefinierten Klassen
- Anpassung der Datei Middleware.ts für das richtige Verarbeiten der SearchParams
- Anpassungen in der Datei search.component.tsx
- Neue Algolia Suchseite
- Cloudapp-dev und bevor Sie uns verlassen
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.
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-nextjsNeue 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.

Cloudapp-dev und bevor Sie uns verlassen
Danke, dass Sie bis zum Ende gelesen haben. Noch eine Bitte bevor Sie gehen:


