├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── [keyword] │ ├── keywordConfig.ts │ ├── keywords.ts │ └── page.tsx ├── blog │ ├── [articleId] │ │ └── page.tsx │ ├── _assets │ │ ├── components │ │ │ ├── Avatar.tsx │ │ │ ├── BadgeCategory.tsx │ │ │ ├── CardArticle.tsx │ │ │ └── CardCategory.tsx │ │ └── content.tsx │ ├── category │ │ └── [categoryId] │ │ │ └── page.tsx │ ├── layout.tsx │ └── page.tsx ├── components │ ├── accordion-features.tsx │ ├── before-after.tsx │ ├── button.tsx │ ├── container.tsx │ ├── cta.tsx │ ├── faq.tsx │ ├── footer.tsx │ ├── header.tsx │ ├── hero.tsx │ ├── logo-clouds.tsx │ ├── nav-links.tsx │ ├── pricing.tsx │ ├── svg-logo.tsx │ ├── testimonial-single.tsx │ └── testimonials-avatars.tsx ├── favicon.ico ├── fonts │ ├── GeistMonoVF.woff │ └── GeistVF.woff ├── globals.css ├── layout.tsx ├── lib │ └── seo.tsx └── page.tsx ├── config.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── artem.png ├── cover.png ├── feature_1.gif ├── feature_2.png └── feature_3.gif ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learning Next.js Series 2 | 3 | This repository contains the code for a YouTube video series dedicated to learning Next.js. Follow along as we learn together. 4 | 5 | ## 🎥 Video Series 6 | 7 | 1. [Building Beautiful Landing Pages with Next.js](https://www.youtube.com/watch?v=u8itgg8216k) 8 | 2. [SEO Best Practices in Next.js. SEO Part 1](https://www.youtube.com/watch?v=_w5Jn2sAJXw) 9 | 3. [Creating Unique Landing Pages for Different Keywords. SEO Part 2](https://www.youtube.com/watch?v=IMUGTOiyhWk) 10 | 11 | ## 🚀 Getting Started 12 | 13 | First, run the development server: 14 | 15 | ```bash 16 | npm run dev 17 | # or 18 | yarn dev 19 | # or 20 | pnpm dev 21 | # or 22 | bun dev 23 | ``` 24 | 25 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 26 | 27 | ## 🛠 Tech Stack 28 | 29 | - [Next.js](https://nextjs.org/) 30 | - [React](https://reactjs.org/) 31 | - [Vercel](https://vercel.com/) for deployment 32 | 33 | ## 📚 Learn More 34 | 35 | To dive deeper into Next.js, check out the following resources: 36 | 37 | - [Next.js Documentation](https://nextjs.org/docs) 38 | - [Learn Next.js](https://nextjs.org/learn) 39 | 40 | ## 📄 License 41 | 42 | This project is [MIT](https://choosealicense.com/licenses/mit/) licensed. 43 | -------------------------------------------------------------------------------- /app/[keyword]/keywordConfig.ts: -------------------------------------------------------------------------------- 1 | export type ComponentConfig = { 2 | [key: string]: any; 3 | }; 4 | 5 | export type KeywordConfig = { 6 | [component: string]: ComponentConfig; 7 | }; 8 | 9 | const keywordConfigs: { [keyword: string]: KeywordConfig } = { 10 | "ai-quiz-creation": { 11 | Hero: { 12 | title: "AI-Powered Quiz Creation", 13 | subtitle: "Create engaging quizzes in minutes with AI assistance", 14 | }, 15 | AccordionFeatures: { 16 | features: [ 17 | { title: "Automatic Question Generation", description: "..." }, 18 | { title: "Customizable Difficulty Levels", description: "..." }, 19 | ], 20 | }, 21 | BeforeAfter: { 22 | before: "Hours spent creating quizzes manually", 23 | after: "Minutes to generate AI-powered quizzes", 24 | }, 25 | FAQ: { 26 | questions: [ 27 | { question: "How does AI quiz creation work?", answer: "..." }, 28 | { question: "Can I customize the generated quizzes?", answer: "..." }, 29 | ], 30 | }, 31 | CTA: { 32 | title: "Start Creating AI-Powered Quizzes Today", 33 | buttonText: "Try AI Quiz Creator", 34 | }, 35 | }, 36 | "ai-lesson-planning": { 37 | Hero: { 38 | title: "AI-Powered Lesson Planning", 39 | subtitle: "Create engaging lessons in minutes with AI assistance", 40 | }, 41 | }, 42 | }; 43 | 44 | export default keywordConfigs; 45 | -------------------------------------------------------------------------------- /app/[keyword]/keywords.ts: -------------------------------------------------------------------------------- 1 | export const keywords = [ 2 | "ai lesson planning", 3 | "ai quiz creation", 4 | "ai flashcards creation", 5 | "ai assignment creation", 6 | "lesson-plan-generator", 7 | "quiz-generator", 8 | "flashcards", 9 | "assignment-creator", 10 | ]; 11 | -------------------------------------------------------------------------------- /app/[keyword]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Container } from "@/app/components/container"; 2 | import Hero from "@/app/components/hero"; 3 | import LogoClouds from "@/app/components/logo-clouds"; 4 | import { Header } from "@/app/components/header"; 5 | import BeforeAfter from "@/app/components/before-after"; 6 | import AccordionFeatures from "@/app/components/accordion-features"; 7 | import Pricing from "@/app/components/pricing"; 8 | import FAQ from "@/app/components/faq"; 9 | import Footer from "@/app/components/footer"; 10 | import CTA from "@/app/components/cta"; 11 | import { getSEOTags } from "@/app/lib/seo"; 12 | import { keywords } from "./keywords"; 13 | import { redirect } from "next/navigation"; 14 | import keywordConfigs from "./keywordConfig"; 15 | 16 | export const generateMetadata = ({ 17 | params, 18 | }: { 19 | params: { keyword: string }; 20 | }) => { 21 | return getSEOTags({ 22 | title: `Quillminds for ${params.keyword}`, 23 | description: `Quillminds helps with ${params.keyword}. Create lesson plans, quizzes, and more.`, 24 | canonicalUrlRelative: `/${params.keyword}`, 25 | }); 26 | }; 27 | 28 | export async function generateStaticParams() { 29 | return keywords.map((keyword) => ({ 30 | keyword: keyword.replace(/\s+/g, "-").toLowerCase(), 31 | })); 32 | } 33 | function isValidKeyword(keyword: string): boolean { 34 | return keywords 35 | .map((k) => k.replace(/\s+/g, "-").toLowerCase()) 36 | .includes(keyword.toLowerCase()); 37 | } 38 | 39 | export default function KeywordPage({ 40 | params, 41 | }: { 42 | params: { keyword: string }; 43 | }) { 44 | if (!isValidKeyword(params.keyword)) { 45 | return redirect("/"); 46 | } 47 | //Use the decoded keyword when needed 48 | const decodedKeyword = decodeURIComponent(params.keyword).replace(/-/g, " "); 49 | const config = keywordConfigs[params.keyword] || {}; 50 | 51 | return ( 52 | 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |