DEV Community

MATKARIM MATKARIMOV
MATKARIM MATKARIMOV

Posted on

How I Optimized My Portfolio for Google Search: A Complete Next.js SEO Guide


As a Full Stack Developer, I recently rebuilt my portfolio website using Next.js 15 and wanted to ensure it ranks well on Google. In this article, I'll share the complete SEO strategy I implemented,including metadata management, structured data, dynamic OG images, and multilingual support

The Problem

When I searched for my name on Google, my portfolio was nowhere to be found. Instead, other websites with similar names appeared first. I needed a comprehensive SEO strategy to fix this.

## The Solution: A Multi-Layer SEO Approach

### 1. Centralized SEO Configuration

First, I created a centralized configuration file to manage all SEO-related data:


typescript
  // src/lib/seo/config.ts
  export const BASE_URL = "https://www.eduzen.uz";

  export const seoConfig = {
    siteName: "Matkarim Matkarimov",
    author: {
      name: "Matkarim Matkarimov",
      alternateName: "Matkarimov Matkarim",
      jobTitle: "Full Stack Developer",
      email: "matkarimov1099@gmail.com",
    },
    social: {
      github: "https://github.com/matkarimov099",
      linkedin: "https://www.linkedin.com/in/matkarim-matkarimov/",
      telegram: "https://t.me/m_matkarimov",
    },
    keywords: [
      "Matkarimov Matkarim",
      "Full Stack Developer",
      "React Developer",
      "Next.js Developer",
      // ... more keywords
    ],
  } as const;

  2. Reusable Metadata Generator

  Instead of writing metadata manually for each page, I created a reusable function:

  // src/lib/seo/metadata.ts
  import type { Metadata } from "next";

  interface GenerateMetadataOptions {
    title: string;
    description: string;
    path: string;
    locale: Locale;
    type?: "website" | "article" | "profile";
  }

  export function generatePageMetadata({
    title,
    description,
    path,
    locale,
    type = "website",
  }: GenerateMetadataOptions): Metadata {
    const url = `${BASE_URL}/${locale}${path}`;

    // Generate hreflang alternates for all locales
    const languages: Record<string, string> = {};
    for (const loc of locales) {
      languages[loc] = `${BASE_URL}/${loc}${path}`;
    }
    languages["x-default"] = `${BASE_URL}/${defaultLocale}${path}`;

    return {
      title,
      description,
      alternates: {
        canonical: url,
        languages,
      },
      openGraph: {
        title,
        description,
        url,
        siteName: seoConfig.siteName,
        locale: ogLocaleMap[locale],
        type,
      },
      twitter: {
        card: "summary_large_image",
        title,
        description,
      },
      robots: {
        index: true,
        follow: true,
      },
    };
  }

  Now each page only needs a few lines:

  // src/app/[locale]/about/page.tsx
  export async function generateMetadata({ params }: Props): Promise<Metadata> {
    const { locale } = await params;
    const data = seoData[locale as Locale];

    return generatePageMetadata({
      title: data.title,
      description: data.description,
      path: "/about",
      locale: locale as Locale,
      type: "profile",
    });
  }

  3. Dynamic OG Images with next/og

  Static OG images are boring. I created dynamic images that are generated on-the-fly:

  // src/app/opengraph-image.tsx
  import { ImageResponse } from "next/og";

  export const runtime = "edge";
  export const alt = "Matkarimov Matkarim - Full Stack Developer";
  export const size = { width: 1200, height: 630 };
  export const contentType = "image/png";

  export default async function Image() {
    return new ImageResponse(
      (
        <div
          style={{
            height: "100%",
            width: "100%",
            display: "flex",
            flexDirection: "column",
            alignItems: "center",
            justifyContent: "center",
            backgroundColor: "#09090b",
          }}
        >
          <img
            src="https://avatars.githubusercontent.com/matkarimov099"
            alt="Avatar"
            width={150}
            height={150}
            style={{ borderRadius: "50%" }}
          />
          <h1 style={{ color: "white", fontSize: 64 }}>
            Matkarimov Matkarim
          </h1>
          <p style={{ color: "#10b981", fontSize: 32 }}>
            Full Stack Developer
          </p>
        </div>
      ),
      { ...size }
    );
  }

  4. Schema.org Structured Data

  Structured data helps Google understand your content better:

  // src/lib/seo/structured-data.ts
  export function generatePersonSchema() {
    return {
      "@context": "https://schema.org",
      "@type": "Person",
      "@id": `${BASE_URL}/#person`,
      name: "Matkarim Matkarimov",
      alternateName: "Matkarimov Matkarim",
      jobTitle: "Full Stack Developer",
      url: BASE_URL,
      sameAs: [
        "https://github.com/matkarimov099",
        "https://linkedin.com/in/matkarim-matkarimov",
      ],
      knowsAbout: [
        "React", "Next.js", "TypeScript", "Node.js", "PostgreSQL"
      ],
      worksFor: {
        "@type": "Organization",
        name: "UNICON-SOFT",
      },
    };
  }

  Inject it into your layout:

  // src/app/[locale]/layout.tsx
  import { JsonLd } from "@/components/seo";
  import { generatePersonSchema, generateWebSiteSchema } from "@/lib/seo";

  export default function LocaleLayout({ children }) {
    return (
      <>
        <JsonLd data={generatePersonSchema()} />
        <JsonLd data={generateWebSiteSchema()} />
        {children}
      </>
    );
  }

  5. Sitemap and Robots.txt

  Next.js makes this easy with special files:

  // src/app/sitemap.ts
  import type { MetadataRoute } from "next";

  export default function sitemap(): MetadataRoute.Sitemap {
    const pages = ["", "/about", "/projects", "/contact"];
    const locales = ["en", "ru", "uz"];

    return pages.flatMap((page) =>
      locales.map((locale) => ({
        url: `${BASE_URL}/${locale}${page}`,
        lastModified: new Date(),
        changeFrequency: "weekly",
        priority: page === "" ? 1.0 : 0.8,
      }))
    );
  }

  // src/app/robots.ts
  import type { MetadataRoute } from "next";

  export default function robots(): MetadataRoute.Robots {
    return {
      rules: [
        {
          userAgent: "*",
          allow: "/",
          disallow: ["/api/", "/_next/"],
        },
      ],
      sitemap: `${BASE_URL}/sitemap.xml`,
    };
  }

## Results

  After implementing these changes:

  1. ✅ All pages are properly indexed
  2. ✅ Rich snippets appear in search results
  3. ✅ OG images display correctly on social media
  4. ✅ Multilingual support with proper hreflang tags

  Key Takeaways

  1. Centralize your SEO config - Makes maintenance easier
  2. Use dynamic OG images - They're more engaging and always up-to-date
  3. Implement structured data - Helps Google understand your content
  4. Don't forget hreflang - Essential for multilingual sites
  5. Submit to Google Search Console - Speed up indexing

  Check It Out

  You can see all of this in action on my portfolio: https://www.eduzen.uz

  Feel free to connect with me:
  - GitHub: https://github.com/matkarimov099
  - LinkedIn: https://www.linkedin.com/in/matkarim-matkarimov/
  - Telegram: https://t.me/m_matkarimov

  ---
  What SEO strategies do you use for your portfolio? Let me know in the comments!

  ---
Enter fullscreen mode Exit fullscreen mode

Top comments (0)