Loading...
Contentful Algolia Next.js 14
Author Cloudapp
E.G.

Algolia und Next.js 14 - Einfache Integration als On-Site-Suche zum Nulltarif

19. April 2024
Inhaltsverzeichnis

Sie starten einen Blog oder eine Marketing-Website, und mit der Zeit wächst der Inhalt, und das Projekt wird hoffentlich erfolgreich. Wenn Sie Glück haben,geht einer Ihrer Beiträge viral und bringt viel Traffic auf Ihre Seite. Ihre Besucher möchten mehr über Ihr Projekt und wertvollen Inhalt erfahren.

Spätestens dann ist es Zeit für eine Lösung, die einfach zu integrieren ist, sodass jeder finden kann, wonach er sucht.

In meinem vorherigen Beitrag habe ich eine vollständige Anleitung bereitgestellt, die Sie durch den Synchronisationsprozess von Contentful zu Algolia führt. Algolia bietet 10.000 Suchanfragen pro Monat und 1 Million Datensätze im kostenlosen Tarif, was für neue Projekte viel ist bzw. mehr als ausreichend sein sollte.

Wenn diese Limits nicht ausreichen und Sie mehr benötigen, können Sie die Pay-as-you-go-Lösung wählen.

Hier können Sie bereits einen Blick auf die fertige Seite werfen, welche wir bauen werden -> https://nextjs14-algolia-search.vercel.app/

Im vorherherigen Post haben wir bereits die neuen Env-Variablen zur .env.local hinzugefügt.

Benötigte Umgebungsvariablen

NEXT_PUBLIC_ALGOLIA_APP_ID=xxxx -> Algolia ApplicationID 
NEXT_PUBLIC_ALGOLIA_API_KEY=xxxx -> Algolia Search-Only Key used later
ALGOLIA_MASTER_KEY=xxxx -> Algolia Admin Key (Write rights)
NEXT_PUBLIC_ALGOLIA_INDEX_NAME=example_dev
API_KEY=xxxxx -> Random Key, which we use for securing the sync endpoint

Neue NPM Pakete

Nachdem dies bereits erledigt ist, installieren wir nun die neuen NPM Pakete, welche wir für diese Anleitung benötigen.

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

Wir haben auch bei TailwindCss ein wenig aufgeräumt und das NPM Paket “@contentful/f36-tokens” deinstalliert, da die verwendeten Farben, seit der Verison 3 von Tailwind bereits im Basispaket enthalten sind.

Hier eine komplette Übersicht der zur Verfügung stehenden Farben complete overview

Das Paket “@contentful/rich-text-plain-text-renderer” wurde auch bereits installiert, da wir es für den Synchprozess zwischen Contentful und Algolia in der vorherigen Anleitung benötigt haben.

Wir starten mit der Anpassung der layout.tsx file im Verzeichnis src/app/[locale]. Unten sehen Sie den neuen Code. Wir haben einen neuen Import “instantsearch.css/themes/satellite-min.css” hinzugefügt und wir übergeben der Header-Komponente den neuen Wert “showBar={true},”.

Anpassung Layout.tsx

<Header showBar={true} menuItems={headerdata} />
import type { Metadata } from "next";
import { Urbanist } from "next/font/google";
import { draftMode } from "next/headers";
import "instantsearch.css/themes/satellite-min.css";
import "@/app/globals.css";
import Header from "@/components/header/header.component";
import Footer from "@/components/footer/footer.component";
import { Providers } from "@/components/header/providers";
import getAllNavitemsForHome from "@/components/header/navbar.menuitems.component";
import getAllFooteritemsForHome from "@/components/footer/footer.menuitems.component";
import ExitDraftModeLink from "@/components/header/draftmode/ExitDraftModeLink.component";
import { locales } from "@/app/i18n/settings";

const urbanist = Urbanist({ subsets: ["latin"], variable: "--font-urbanist" });

export async function generateStaticParams() {
  return locales.map((lng) => ({ lng }));
}

export const metadata: Metadata = {
  title: "Example Blog",
  description: "Your Example Blog Description",
  icons: {
    icon: [
      { rel: "icon", url: "/favicons/favicon-16x16.png", sizes: "16x16" },
      new URL("/favicons/favicon-16x16.png", process.env.NEXT_PUBLIC_BASE_URL),
      { rel: "icon", url: "/favicons/favicon-32x32.png", sizes: "32x32" },
      new URL("/favicons/favicon-32x32.png", process.env.NEXT_PUBLIC_BASE_URL),
    ],
    shortcut: [{ rel: "shortcut icon", url: "/favicons/favicon.ico" }],
    apple: [
      {
        url: "/favicons/apple-touch-icon.png",
        sizes: "180x180",
        type: "image/png",
      },
    ],
  },
};

type LayoutProps = {
  children: React.ReactNode;
  params: { locale: string };
};

export default async function RootLayout({ children, params }: LayoutProps) {
  const locale = params.locale;
  const htmlLang = locale === "en-US" ? "en" : "de";
  const headerdata = await getAllNavitemsForHome(locale);
  const footerdata = await getAllFooteritemsForHome(locale);

  return (
    <html lang={htmlLang} suppressHydrationWarning>
      <head></head>
      <body>
        <main className={`${urbanist.variable} font-sans dark:bg-gray-900`}>
          <Providers>
            <Header showBar={true} menuItems={headerdata} />
            {draftMode().isEnabled && (
              <p className="bg-emerald-400 py-4 px-[6vw]">
                Draft mode is on! <ExitDraftModeLink className="underline" />
              </p>
            )}
            {children}
            <Footer footerItems={footerdata} />
          </Providers>
        </main>
      </body>
    </html>
  );
}

Neue "Search" Komponente

Bevor wir die Header-Komponente anpassen, müssen wir zuvor die Search-Komponente (Suchleiste) hinzufügen, “search.component.tsx,”. Welche im "components" Unterverzeichnis “header.” erstellt wird.

"use client";

import { useState, useEffect } from "react";
import { useRouter, usePathname, useParams } from "next/navigation";
import type { LocaleTypes } 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 !== "");

  // console.log("path", pathArray[0]);

  // 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; //
    router.push(`/${locale}/search/${search}`); // push to the search page
  }

  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>
  );
}

GitHub Repo

Wie immer finden Sie den kompletten Code im GitHub Repo -> https://github.com/cloudapp-dev/nextjs14-SEO/tree/nextjs14-part7

In der Header-Komponenten fügen wir einen neuen Import und ein neues Interface hinzu.

import SearchBar from "./search.component";
interface HeaderProps {
  showBar: boolean;
  menuItems: any;
}

und natürlich auch die "Suchleiste".

{showBar && (
        <SearchBar
          searchCta="Search"
          searchPlaceholder="Search example.dev..."
        />
      )}

Wir werden diese Komponenten unseren neuen Suchseiten hinzufügen, daher müssen wir diese neuen Komponenten im Verzeichnis „src/components/search“ erstellen.

# Using Components provided by Algolia
algoliasearch.component.tsx
panel.component.tsx
# Used for our custom solution
card.component.tsx
results.component.tsx
tagcloudsimple.component.tsx

Übersetzungen in common.json Dateien hinzufügen

Danch ergänzen wir die fehlenden Übersetzungen in den jeweiligen common.json Dateien, welche wir im Post "Multi-Language" unter src/app/i18n/locales/de-DE/ und src/app/i18n/locales/en-US/ erstellt haben.

Hier die Anpassungen für "de-DE"

"search": {
    "button": "Suche",
    "searchResults": "Suchergebnisse",
    "searchResultsFor": "Suchergebnisse für",
    "noResultsFound": "Keine Ergebnisse gefunden",
    "resultsFoundIn": "Ergebnisse gefunden in",
    "searchPlaceholder": "Suchen auf Example.dev...",
    "dashboardsearchplaceholder": "Suchen nach Benutzern/Rollen auf ...",
    "dashboardsearchhighline": "Benutzer",
    "dashboardsearchdescription": "Suchen Sie nach Benutzern, um deren Profile anzuzeigen und zu verwalten.",
    "searchdescription": "Nachfolgend finden Sie alle verfügbaren Tags und Ihre Suchergebnisse auf ..."
  }

und hier für “en-US”

"search": {
    "button": "Search",
    "searchResults": "Search Results",
    "searchResultsFor": "Search Results for",
    "noResultsFound": "No Results found",
    "resultsFoundIn": "Results found for",
    "searchPlaceholder": "Search Example.dev...",
    "dashboardsearchplaceholder": "Search for User/Roles on ...",
    "dashboardsearchhighline": "Users",
    "dashboardsearchdescription": "Search for Users for Profile edit",
    "searchdescription": "Below you will find all available Tags and your search results on ..."
  }

Wir brauchen auch eine neue API (Route), damit wir die Tags aus dem Algolia-Index abrufen können. Wir benötigen diese Tags für die Tag-Cloud, welche auf der Suchergebnisseite angezeigt werden soll.

Neue API (Route) für Algolia Facets

Wir erstellen also eine neue Datei "route.ts" im Verzeichnis src/app/api/search/facets/route.ts

import { NextResponse } from "next/server";
import algoliasearch from "algoliasearch/lite";

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

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  let slug: string = "*";
  if (searchParams.has("slug")) {
    slug = searchParams.get("slug") || "*";
  }

  const index = client.initIndex(
    `${process.env.NEXT_PUBLIC_ALGOLIA_INDEX_NAME}`
  );
  const datanew: any = [];
  const datanew2: any = [];
  let minSize = 0;
  let maxSize = 0;

  await index
    .search(slug, {
      facets: ["*"],
      hitsPerPage: 50,
    })
    .then(({ facets }) => {
      if (!facets) {
        return NextResponse.json([]);
      }

      const tags = facets["tags"];

      for (let x in tags) {
        datanew.push({
          value: x,
          count: tags[x],
        });
        datanew2.push(tags[x]);
      }
      datanew2.sort();
      minSize = datanew2[0];
      maxSize = datanew2[datanew2.length - 1];
      // console.log("datanew", datanew);
      //   return NextResponse.json(datanew);
    });

  return NextResponse.json({ datanew, minSize, maxSize });
}

Da wir eine neue ArticleLabel-Komponente innerhalb der neuen card.component.tsx auf der Suchergebnisseite benötigen, müssen wir diese Komponente unter src/components/contentful/ArticleLabel.tsx hinzufügen.

import { HTMLProps, ReactNode } from "react";
import { twMerge } from "tailwind-merge";

interface ArticleLabelProps extends HTMLProps<HTMLSpanElement> {
  children: ReactNode;
}

export const ArticleLabel = ({
  children,
  className,
  ...props
}: ArticleLabelProps) => {
  return (
    <span
      className={twMerge(
        "rounded bg-purple-200 px-2 py-1 text-2xs font-semibold uppercase leading-none tracking-widest text-purple-600",
        className
      )}
      {...props}
    >
      {children}
    </span>
  );
};

Screenshot, welcher das Artikellabel bzw. Taglabel des jeweilien Suchtreffers zeigt.

ArticleLabel Card Component
ArticleLabel Card Component

Screenshot, welcher die Tags der Tag-Cloud auf der Suchergebnisseite zeigt.

Tag TagCloud Searchpage
Tag TagCloud Searchpage

Neue Next.js 14 Seiten hinzufügen

Als letzten Schritt fügen wir die neuen Seiten „search“ und „searchalgolia“ unter src/app/[locale]/search/[searchTerm] hinzu.

export const dynamic = "force-dynamic";

import Results from "@/components/search/results.component";
import { Container } from "@/components/contentful/container/Container";
import { createTranslation } from "@/app/i18n/server";
import { LocaleTypes } from "@/app/i18n/settings";
import TagCloudSimple from "@/components/search/tagcloudsimple.component";

interface SearchPageParams {
  searchTerm: string;
  locale: string;
}

interface SearchPageProps {
  params: SearchPageParams;
}

async function SearchPage({ params }: SearchPageProps) {
  const res = await fetch(
    `https://${process.env.NEXT_PUBLIC_ALGOLIA_APP_ID}-dsn.algolia.net/1/indexes/${process.env.NEXT_PUBLIC_ALGOLIA_INDEX_NAME}?query=${params.searchTerm}&attributesToHighlight=[]&attributesToRetrieve=lang.${params.locale},intName,tags,height,width,image,pubdate,slug`,
    {
      headers: new Headers({
        "X-Algolia-API-Key": process.env.NEXT_PUBLIC_ALGOLIA_API_KEY || "",
        "X-Algolia-Application-Id":
          process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "",
      }),
    }
  );

  if (!res.ok) {
    throw new Error("Failed to fetch data");
  }

  const searchFacets = await fetch(
    `${process.env.NEXT_PUBLIC_BASE_URL}/api/search/facets`,
    {
      next: { revalidate: 0 }, // No cache
    }
  ).then((res) => res.json());

  const { datanew, minSize, maxSize } = searchFacets;
  const data = await res.json();
  const results = data.hits;
  const { t } = await createTranslation(params.locale as LocaleTypes, "common");

  return (
    <Container className="my-8 md:mb-10 lg:mb-16">
      <h1 className="flex items-center justify-center mb-4">
        {params.searchTerm}
      </h1>

      {/* Tag Cloud Integration */}
      <TagCloudSimple
        datanew={datanew}
        minSize={minSize * 10}
        maxSize={maxSize * 5}
        locale={params.locale}
      />

      <div className="mt-8"></div>

      {results && results.length === 0 && (
        <h2 className="pt-6 text-center">No results found</h2>
      )}
      {results && <Results results={results} />}
    </Container>
  );
}

export default SearchPage;
search page
search page

und src/app/[locale]/searchalgolia

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

export const dynamic = "force-dynamic";

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

Damit wir die Attribute für die Komponente “Algolia Snippet Component” in der Komponente algoliasearch.component.tsx nutzen können

<Snippet hit={hit} attribute="pubdate" />

müssen wir diese im jeweiligen Aloglia-Index aktivieren bzw. konfigurieren.

Algolia Snippet Config
Algolia Snippet Config

Im letzten Schritt aktivieren wir noch die Facets in der Index-Config, da wir diese für die T

Algolia Tag Facet Config
Algolia Tag Facet Config

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.

Verwandte Artikel