πŸŽ‰ Let's Build an AI LinkedIn Post Generator project using Gemini AI, Next.js, and TailwindCSS πŸš€

Photo by Abid Shah on Unsplash

πŸŽ‰ Let's Build an AI LinkedIn Post Generator project using Gemini AI, Next.js, and TailwindCSS πŸš€

Β·

4 min read

2025 is here, and what a time to be alive, what better way to kick off the year than by building an awesome LinkedIn Post project? 🎯 In this blog, I’ll show you how to integrate the Gemini API with Next.js and style it using TailwindCSS, to make it more interesting lets create our UI using V0.dev. Plus, we'll use the Gemini API Key to fetch posts results and display them.

Now,Let’s dive in! πŸ”₯

Prerequisites πŸ“‹

Before we get started, make sure you have:

  • Node.js installed

  • A Gemini API key (set up at Gemini for key)

  • Familiarity with Next.js/React.js and TailwindCSS

Implementation

1. Create a Next.js Project πŸ–₯️

Start by creating a new Next.js project:

npx create-next-app linkedin-wizard
cd linkedin-wizard

2. Install Gemini API Package πŸ“¦

npm i @google/generative-ai

Create a .env file in the root directory and add your Gemini API key:

GEMINI_API_KEY=your_api_key_here

3. Fetch Twitter Posts with Gemini API πŸ”₯

Create app/api/generatepost/route.tspath in project,In route.ts we will fetch the Twitter-like posts using the Gemini API and display them.

import { GoogleGenerativeAI } from "@google/generative-ai";
import { NextResponse } from "next/server";

const API_KEY = process.env.GEMINI_API_KEY || "";

export async function POST(req: Request) {
  const { description, wordlimit = 100 } = await req.json();

  if (!description) {
    return NextResponse.json(
      { error: "Description is required." },
      { status: 400 }
    );
  }

  try {
    const genAI = new GoogleGenerativeAI(API_KEY);
    const model = await genAI.getGenerativeModel({ model: "gemini-1.5-pro" });
    const prompt = `Generate a linkedpost post under ${wordlimit} words on the basis of this description: ${description}`;
    const result = await model.generateContent([prompt]);

    if (result && result.response) {
      const generatedText = await result.response.text();
      return NextResponse.json({ post: generatedText });
    } else {
      throw new Error("No response received from model.");
    }
  } catch (error) {
    console.error("Error generating post:", error);
    return NextResponse.json(
      { error: "Failed to generate post" },
      { status: 500 }
    );
  }
}

Above code's functionality description is:

  • Generates Post: Takes a description and an optional wordlimit, uses Google's AI to create a tweet based on it.

  • Error Handling: Returns errors if no description is provided or if AI fails.

  • AI Model Used: Uses gemini-1.5-pro for content generation.

4. Main front-end logic of handling : generate post, copy post. regenerate post is :

Lets open V0.dev to generate a UI for us and copy the generated UI to our code

"use client";

import { useState } from "react";
import { Loader2, Copy, RefreshCw, Linkedin } from "lucide-react";

export default function PostGenerator() {
  const [description, setDescription] = useState("");
  const [wordLimit, setWordLimit] = useState("");
  const [generatedPost, setGeneratedPost] = useState("");
  const [isLoading, setIsLoading] = useState(false);

  const generatePost = async () => {
    setIsLoading(true);
    try {
      const response = await fetch("/api/generatePost", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          description,
          wordLimit: Number.parseInt(wordLimit) || undefined,
        }),
      });
      const data = await response.json();
      setGeneratedPost(data.post);
    } catch (error) {
      console.error("Error generating post:", error);
    }
    setIsLoading(false);
  };

  const copyToClipboard = () => {
    navigator.clipboard.writeText(generatedPost);
    alert("copied post to clipboard");
  };

  return (
    <div className="max-w-2xl mx-auto p-4 space-y-6">
      <h1 className="text-2xl font-bold flex justify-center items-center">
        <Linkedin />
        <div className="ml-2 pt-1">Post Generator</div>
      </h1>
      <div className="space-y-2">
        <label
          htmlFor="description"
          className="block text-sm font-medium text-gray-700"
        >
          Description
        </label>
        <textarea
          id="description"
          placeholder="Enter post description"
          value={description}
          onChange={(e) => setDescription(e.target.value)}
          className="w-full px-3 py-2 text-gray-700 border rounded-lg focus:outline-none focus:border-blue-500 min-h-[100px]"
        />
      </div>
      <div className="space-y-2">
        <label
          htmlFor="wordLimit"
          className="block text-sm font-medium text-gray-700"
        >
          Word Limit (optional)
        </label>
        <input
          id="wordLimit"
          type="number"
          placeholder="Enter word limit"
          value={wordLimit}
          onChange={(e) => setWordLimit(e.target.value)}
          className="w-full px-3 py-2 text-gray-700 border rounded-lg focus:outline-none focus:border-blue-500"
        />
      </div>
      <div className="flex space-x-2">
        <button
          onClick={generatePost}
          disabled={isLoading || !description}
          className={`flex items-center justify-center px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 ${
            isLoading || !description ? "opacity-50 cursor-not-allowed" : ""
          }`}
        >
          {isLoading ? (
            <>
              <Loader2 className="mr-2 h-4 w-4 animate-spin" />
              Generating...
            </>
          ) : (
            "Generate Post"
          )}
        </button>
        <button
          onClick={generatePost}
          disabled={isLoading || !description}
          className={`flex items-center justify-center px-4 py-2 text-blue-600 bg-white border border-blue-600 rounded-lg hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 ${
            isLoading || !description ? "opacity-50 cursor-not-allowed" : ""
          }`}
        >
          <RefreshCw className="mr-2 h-4 w-4" />
          Regenerate
        </button>
      </div>
      {generatedPost && (
        <div className="space-y-2">
          <label
            htmlFor="generatedPost"
            className="block text-sm font-medium text-gray-700"
          >
            Generated Post
          </label>
          <div className="relative">
            <textarea
              id="generatedPost"
              value={generatedPost}
              readOnly
              className="w-full px-3 py-2 text-gray-700 border rounded-lg focus:outline-none focus:border-blue-500 min-h-[200px]"
            />
            <button
              onClick={copyToClipboard}
              className="absolute top-2 right-2 p-2 text-gray-500 hover:text-gray-700 focus:outline-none"
            >
              <Copy className="h-4 w-4" />
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

You can easily change colors, spacing, and other design elements using Tailwind classes.

5. Run the Project πŸš€

Now, it’s time to run your project:

npm run dev

Open http://localhost:3000 in your browser, and you’ll see your linkedIn-like post feed in action! πŸŽ‰

Output:

Conclusion

To get access to the code that I have used above, click here. For any query, you can get in touch with me via LinkedIn and twitter.Leave all your comments and suggestions in the comment section. Let's connect and share more ideas.

Happy coding!

Β