Loading...
Slugify and Word - Character Count
Author Cloudapp
E.G.

Next.js 14 - Erweitern Sie Ihre SEO-Tools mit Slugify/Wort & Zeichenzähler

23. Juni 2024
Inhaltsverzeichnis

Eine Slugify-Funktion ist wertvoll für die Erstellung sauberer, lesbarer und SEO-freundlicher URLs, die die Benutzerfreundlichkeit und die Verwaltung der Website verbessern. Um das Serviceangebot abzurunden, fügen wir auch eine Wort-/Zeichenzählfunktion hinzu.

Hier ist das GitHub repo mit dem vollständigen Code, in dem Sie die beiden neuen Komponenten sehen können.

Im gleichen GitHub-Repository ist auch die Logik für die

Kontrolle auf kaputte Links basierend auf der Sitemap.xml

Erstellung der HTML-Sitemap

Zähler für Sitemap-Einträge

Beispielseite gehostet auf Vercel -> https://nextjs14-azureb2c-prisma.vercel.app/

Eine Slugify-Funktion ist aus mehreren Gründen nützlich, insbesondere in der Webentwicklung und im Content-Management:

  1. URL-Optimierung: Slugs sind benutzerfreundliche, lesbare und SEO-freundliche Versionen von URLs. Sie ersetzen Leerzeichen und Sonderzeichen durch Bindestriche oder andere geeignete Zeichen, wodurch URLs sauber und leicht lesbar werden. Dies verbessert die Suchmaschinenoptimierung (SEO), indem Schlüsselwörter in der URL enthalten sind.

  2. Konsistenz: Slugify-Funktionen sorgen für einheitliche Formatierung von URLs, Titeln und Identifikatoren auf einer Website. Diese Konsistenz verbessert die Benutzererfahrung und erleichtert das Verlinken und Teilen von Inhalten.

  3. Lesbarkeit: Slugs sind für Benutzer lesbarer im Vergleich zu rohen Zeichenfolgen oder IDs. Zum Beispiel ist eine URL mit einem Slug wie /how-to-train-your-dragon viel einfacher zu verstehen und zu merken als eine mit einer Abfragezeichenfolge wie /?id=12345.

  4. Vermeidung von Fehlern: Durch die Umwandlung komplexer Zeichenfolgen in Slugs reduziert die Funktion die Wahrscheinlichkeit von Fehlern in URLs, wie z. B. Leerzeichen, Sonderzeichen oder Groß-/Kleinschreibungsprobleme, die zu fehlerhaften Links oder falschem Routing führen könnten.

  5. Lokalisierung: Slugify-Funktionen können angepasst werden, um mehrere Sprachen zu unterstützen und Zeichen entsprechend für verschiedene Regionen zu konvertieren. Dies stellt sicher, dass URLs für ein globales Publikum lesbar und relevant bleiben.

  6. Dynamische Inhalte: Für dynamisch generierte Inhalte, wie Blogbeiträge oder Produktseiten, bieten Slugs einen Mechanismus, um automatisch bedeutungsvolle und beschreibende URLs basierend auf Titeln oder anderen Attributen zu erstellen.

Neue Komponenten für Slugify und Wort-/Zeichenzählung

Wir erstellen zwei neue Komponenten und beginnen mit der Logik der Wort-/Zeichenzählung.

Wort-/Zeichenzählung Komponente

// components/tools/wordcount.component.tsx
"use client";
import React, { useState } from "react";

const WordCount: React.FC = () => {
  const [text, setText] = useState("");
  const [wordCount, setWordCount] = useState(0);
  const [charCount, setCharCount] = useState(0);

  const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
    const inputText = event.target.value;
    setText(inputText);
    const count = inputText.trim().split(/\s+/).filter(Boolean).length;
    const charCount = inputText.length;
    const digits = inputText.trim().match(/[^ ]/g); // Exclude spaces
    const digitCount = digits?.length || 0;

    setWordCount(count);
    setCharCount(digitCount);
  };

  return (
    <div className="max-w-md mt-4">
      <h2 className="text-2xl font-bold mb-4">Word Count Service</h2>
      <textarea
        className="w-full p-2 border text-base rounded mb-4"
        rows={10}
        value={text}
        onChange={handleChange}
        placeholder="Type or paste your text here..."
      ></textarea>
      <div className="text-lg">
        Word Count: <span className="font-semibold">{wordCount}</span> - Char
        Count without Spaces: <span className="font-semibold">{charCount}</span>
      </div>
    </div>
  );
};

export default WordCount;

Slugify Komponente

Jetzt erstellen wir die slugify-Komponente

// components/slugify/slugify.component.tsx
"use client";
import React, { useState } from "react";

// Utility function to slugify a string
const slugify = (text: string): string => {
  return text
    .toString()
    .toLowerCase()
    .replace(/\s+/g, "-") // Replace spaces with -
    .replace(/[^\w-]+/g, "") // Remove all non-word chars
    .replace(/--+/g, "-") // Replace multiple - with single -
    .replace(/^-+/, "") // Trim - from start of text
    .replace(/-+$/, ""); // Trim - from end of text
};

const Slugify: React.FC = () => {
  const [inputText, setInputText] = useState<string>("");
  const [slugifiedText, setSlugifiedText] = useState<string>("");
  const [copySuccess, setCopySuccess] = useState<string>("");

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const text = e.target.value;
    setInputText(text);
    setSlugifiedText(slugify(text));
    setCopySuccess(""); // Reset copy success message on input change
  };

  const handleCopy = async () => {
    try {
      await navigator.clipboard.writeText(slugifiedText);
      setCopySuccess("Copied!");
    } catch (err) {
      setCopySuccess("Failed to copy!");
    }
  };

  return (
    <div>
      <h2 className="text-3xl font-bold mb-4">Slugify String</h2>
      <input
        type="text"
        value={inputText}
        onChange={handleInputChange}
        className="block w-full  max-w-md h-10 border border-gray-200 rounded-md pl-2 focus:border-indigo-500 focus:ring-indigo-500 sm:text-base"
        placeholder="Enter text to slugify"
      />
      <div className="p-4 border border-gray-300 rounded w-full max-w-md mb-6">
        <p className="text-lg font-semibold">Slugified Text:</p>
        <p className="text-black">{slugifiedText}</p>
        <button
          onClick={handleCopy}
          className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
        >
          Copy
        </button>
        {copySuccess && <p className="text-green-500 mt-2">{copySuccess}</p>}
      </div>
    </div>
  );
};

export default Slugify;

Eine API-Route oder neue NPM-Pakete sind nicht erforderlich, es ist also ganz einfach.

Hinzufügen neuer Komponenten zu page.tsx

Als letzten Schritt fügen wir die neuen Komponenten zur page.tsx hinzu

import { client } from "@/lib/client";
import { notFound } from "next/navigation";
import { ArticleHero } from "@/components/contentful/ArticleHero";
import { ArticleTileGrid } from "@/components/contentful/ArticleTileGrid";
import { Container } from "@/components/contentful/container/Container";
import { draftMode } from "next/headers";
// Internationalization
import { locales, LocaleTypes } from "@/app/i18n/settings";
import { createTranslation } from "@/app/i18n/server";
//SEO - JSON-LD
import { Article, WithContext } from "schema-dts";
import path from "path";
import Script from "next/script";
import { Metadata, ResolvingMetadata } from "next";
// New Fields Part 9 of tutorial
import { PageBlogPostOrder } from "@/lib/__generated/sdk";
import { TextHighLight } from "@/components/contentful/TextHighLight";
import { revalidateDuration } from "@/utils/constants";
import { TagCloudSimpleHome } from "@/components/search/tagcloudsimpleHome.component";
import Link from "next/link";
import { LandingContent } from "@/components/contentful/ArticleContentLanding";

import Sitemapcounter from "@/components/tools/sitemapcounter/counter.component";
import WordCount from "@/components/tools/wordcount/wordcount.component";
import SitemapChecker from "@/components/tools/sitemapchecker/sitemapchecker.component";
import Slugify from "@/components/tools/slugify/slugify.component";

export const revalidate = revalidateDuration; // revalidate at most every hour

interface PageParams {
  slug: string;
  locale: string;
}

interface PageProps {
  params: PageParams;
}

const generateUrl = (locale: string, slug: string) => {
  if (locale === "en-US") {
    return new URL(slug, process.env.NEXT_PUBLIC_BASE_URL!).toString();
  } else {
    return new URL(locale, process.env.NEXT_PUBLIC_BASE_URL!).toString();
  }
};

const WebUrl = process.env.NEXT_PUBLIC_BASE_URL as string;

export async function generateMetadata(
  { params }: PageProps,
  parent: ResolvingMetadata
): Promise<Metadata> {
  const [PagedataSeo] = await Promise.all([
    client.pageLanding({
      slug: "/",
      locale: params.locale.toString(),
      preview: draftMode().isEnabled,
    }),
  ]);

  const landingPage = PagedataSeo.pageLandingCollection?.items[0];

  if (!landingPage) {
    return notFound();
  }

  const url = generateUrl(params.locale || "", "");

  return {
    title: landingPage.seoFields?.pageTitle,
    description: landingPage.seoFields?.pageDescription,
    metadataBase: new URL(WebUrl),
    alternates: {
      canonical: url,
      languages: {
        "en-US": "/",
        "de-DE": "/de-DE",
        "x-default": "/",
      },
    },
    openGraph: {
      type: "website",
      siteName: "Example.dev - Free Tutorials and Resources for Developers",
      locale: params.locale,
      url: url || "",

      title: landingPage.seoFields?.pageTitle || undefined,
      description: landingPage.seoFields?.pageDescription || undefined,
      images: landingPage.seoFields?.shareImagesCollection?.items.map(
        (item) => ({
          url: item?.url || "",
          width: item?.width || 0,
          height: item?.height || 0,
          alt: item?.description || "",
          type: item?.contentType || "",
        })
      ),
    },
    robots: {
      follow: landingPage.seoFields?.follow || false,
      index: landingPage.seoFields?.index || false,
      googleBot: {
        index: true,
        follow: true,
        "max-image-preview": "large",
      },
    },
  };
}

async function Home({ params }: PageProps) {
  const { isEnabled } = draftMode();
  //declare JSON-LD schema
  let jsonLd: WithContext<Article> = {} as WithContext<Article>;
  const [landingPageData] = await Promise.all([
    client.pageLanding({
      slug: "/",
      locale: params.locale.toString(),
      preview: isEnabled,
    }),
  ]);

  const page = landingPageData.pageLandingCollection?.items[0];

  if (!page) {
    // If a blog post can't be found,
    // tell Next.js to render a 404 page.
    return notFound();
  }

  // TagCloud
  const showTagCloud = page.showTagCloud === "Yes";

  let { datanew, minSize, maxSize } = {
    datanew: [],
    minSize: 0,
    maxSize: 0,
  };

  if (showTagCloud) {
    const searchFacets = await fetch(
      `${process.env.NEXT_PUBLIC_BASE_URL}/api/search/facets`,
      {}
    )
      .then((res) => res.json())
      .catch((error) => {
        console.log("No data found");
      });

    if (searchFacets) {
      maxSize = searchFacets.maxSize;
      minSize = searchFacets.minSize;
      datanew = searchFacets.datanew;
    }
  }

  // Getting BlogPosts
  const blogPostsData = await client.pageBlogPostCollection({
    limit: 10,
    locale: params.locale.toString(),
    skip: 0,
    preview: isEnabled,
    order: PageBlogPostOrder.PublishedDateDesc,
    where: {
      slug_not: page?.featuredBlogPost?.slug,
    },
  });
  const posts = blogPostsData.pageBlogPostCollection?.items;

  // Create JSON-LD schema only if blogPost is available
  if (page) {
    jsonLd = {
      "@context": "https://schema.org",
      "@type": "Article",
      headline: page?.seoFields?.pageTitle || undefined,
      author: {
        "@type": "Person",
        name: page?.featuredBlogPost?.author?.name || undefined,
        // The full URL must be provided, including the website's domain.
        url: new URL(
          path.join(
            params.locale.toString() || "",
            params.slug?.toString() || ""
          ),
          process.env.NEXT_PUBLIC_BASE_URL!
        ).toString(),
      },
      publisher: {
        "@type": "Organization",
        name: "Example.dev - Free Tutorials and Resources for Developers",
        logo: {
          "@type": "ImageObject",
          url: "https://www.example.dev/favicons/icon-192x192.png",
        },
      },
      image: page?.featuredBlogPost?.featuredImage?.url || undefined,
      datePublished: page.sys.firstPublishedAt,
      dateModified: page.sys.publishedAt,
    };
  }

  // Internationalization, get the translation function
  const { t } = await createTranslation(params.locale as LocaleTypes, "common");

  const highLightHeadings: any = page.textHighlightCollection?.items[0];

  if (!page?.featuredBlogPost || !posts) return;

  return (
    <>
      {page && (
        <Script
          id="article-schema"
          type="application/ld+json"
          dangerouslySetInnerHTML={{
            __html: JSON.stringify(jsonLd),
          }}
        />
      )}
      <Container className="mt-5">
        {highLightHeadings && <TextHighLight headings={highLightHeadings} />}

        {showTagCloud && datanew.length > 0 && (
          <TagCloudSimpleHome
            datanew={datanew}
            minSize={minSize * 10}
            maxSize={maxSize * 5}
            locale={params.locale.toString()}
            source={"homepage"}
          />
        )}

        <Link
          href={`/${params.locale.toString()}/${page.featuredBlogPost.slug}`}
        >
          <ArticleHero article={page.featuredBlogPost} isHomePage={true} />
        </Link>
        <div className="md:mx-24 md:my-24 sm:mx-16 sm:my-16">
          <LandingContent landing={page} />
        </div>
      </Container>

      <Container className="my-8 md:mb-10 lg:mb-16">
        {posts.length > 0 && (
          <h2 className="mb-4 md:mb-6">{t("landingPage.latestArticles")}</h2>
        )}
        <ArticleTileGrid
          className="grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
          articles={posts}
          slug={page.featuredBlogPost.slug}
          source="loadmore"
          locale={params.locale.toString()}
        />
        {/* SitemapCounter */}
        <Sitemapcounter />
        {/* SitemapChecker */}
        <SitemapChecker />
        {/* WordCount */}
        <WordCount />
        {/* Slugify */}
        <Slugify />
      </Container>
    </>
  );
}

export default Home;

Hier ist das Endergebnis auf der Homepage

Slugify-Component-on-home
Slugify-Component-on-home

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