diff --git a/seo-tool/.gitignore b/seo-tool/.gitignore new file mode 100644 index 000000000000..5ef6a5207802 --- /dev/null +++ b/seo-tool/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/seo-tool/README.md b/seo-tool/README.md new file mode 100644 index 000000000000..e215bc4ccf13 --- /dev/null +++ b/seo-tool/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/seo-tool/app/api/content-analysis/route.ts b/seo-tool/app/api/content-analysis/route.ts new file mode 100644 index 000000000000..a0d0081cbb08 --- /dev/null +++ b/seo-tool/app/api/content-analysis/route.ts @@ -0,0 +1,191 @@ +import { NextRequest, NextResponse } from 'next/server'; +import axios from 'axios'; +import * as cheerio from 'cheerio'; +import { removeStopwords } from 'stopword'; + +export async function POST(request: NextRequest) { + try { + const { url } = await request.json(); + + if (!url) { + return NextResponse.json( + { error: 'URL is required' }, + { status: 400 } + ); + } + + // Validate URL format + try { + new URL(url); + } catch { + return NextResponse.json( + { error: 'Invalid URL format' }, + { status: 400 } + ); + } + + // Fetch the webpage + const response = await axios.get(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + }, + timeout: 10000, + }); + + const html = response.data; + const $ = cheerio.load(html); + + // Analyze heading structure + const headingStructure = [] as Array<{ level: string; text: string; order: number }>; + let order = 0; + + $('h1, h2, h3, h4, h5, h6').each((_, el) => { + const tagName = el.tagName.toLowerCase(); + const text = $(el).text().trim(); + if (text) { + headingStructure.push({ + level: tagName, + text, + order: order++, + }); + } + }); + + // Count headings by level + const headingCounts = { + h1: $('h1').length, + h2: $('h2').length, + h3: $('h3').length, + h4: $('h4').length, + h5: $('h5').length, + h6: $('h6').length, + }; + + // Extract and analyze body text + $('script, style, nav, footer, header, aside').remove(); + const bodyText = $('body').text().replace(/\s+/g, ' ').trim(); + + // Word count + const words = bodyText.split(/\s+/).filter(word => word.length > 0); + const wordCount = words.length; + + // Character count + const characterCount = bodyText.length; + const characterCountNoSpaces = bodyText.replace(/\s/g, '').length; + + // Sentence count (approximate) + const sentences = bodyText.split(/[.!?]+/).filter(s => s.trim().length > 0); + const sentenceCount = sentences.length; + + // Average words per sentence + const avgWordsPerSentence = sentenceCount > 0 ? Math.round(wordCount / sentenceCount) : 0; + + // Extract keywords (most common words, excluding stopwords) + const cleanWords = words + .map(word => word.toLowerCase().replace(/[^a-z0-9]/g, '')) + .filter(word => word.length > 2); + + const wordsWithoutStopwords = removeStopwords(cleanWords); + + // Count word frequency + const wordFrequency: Record = {}; + wordsWithoutStopwords.forEach(word => { + wordFrequency[word] = (wordFrequency[word] || 0) + 1; + }); + + // Get top keywords + const topKeywords = Object.entries(wordFrequency) + .sort((a, b) => b[1] - a[1]) + .slice(0, 20) + .map(([word, count]) => ({ + word, + count, + density: ((count / wordCount) * 100).toFixed(2) + '%', + })); + + // Analyze paragraphs + const paragraphs = $('p').length; + const avgWordsPerParagraph = paragraphs > 0 ? Math.round(wordCount / paragraphs) : 0; + + // Analyze images + const totalImages = $('img').length; + const imagesWithAlt = $('img[alt]').filter((_, el) => $(el).attr('alt')?.trim()).length; + const imagesWithoutAlt = totalImages - imagesWithAlt; + + // Analyze links + const totalLinks = $('a[href]').length; + const baseUrl = new URL(url); + let internalLinks = 0; + let externalLinks = 0; + + $('a[href]').each((_, el) => { + const href = $(el).attr('href'); + if (href) { + try { + const linkUrl = new URL(href, url); + if (linkUrl.hostname === baseUrl.hostname) { + internalLinks++; + } else { + externalLinks++; + } + } catch { + // Invalid URL + } + } + }); + + // Reading time estimate (average 200 words per minute) + const readingTimeMinutes = Math.ceil(wordCount / 200); + + // SEO recommendations + const recommendations = []; + if (headingCounts.h1 === 0) { + recommendations.push('Add an H1 heading to your page'); + } else if (headingCounts.h1 > 1) { + recommendations.push('Consider using only one H1 heading per page'); + } + if (wordCount < 300) { + recommendations.push('Content is quite short. Consider adding more content (aim for 300+ words)'); + } + if (imagesWithoutAlt > 0) { + recommendations.push(`${imagesWithoutAlt} image(s) missing alt text. Add alt text for better SEO and accessibility`); + } + if (topKeywords.length === 0) { + recommendations.push('No significant keywords found. Add more relevant content'); + } + + return NextResponse.json({ + url, + headingStructure, + headingCounts, + content: { + wordCount, + characterCount, + characterCountNoSpaces, + sentenceCount, + paragraphCount: paragraphs, + avgWordsPerSentence, + avgWordsPerParagraph, + readingTimeMinutes, + }, + keywords: topKeywords, + images: { + total: totalImages, + withAlt: imagesWithAlt, + withoutAlt: imagesWithoutAlt, + }, + links: { + total: totalLinks, + internal: internalLinks, + external: externalLinks, + }, + recommendations, + }); + } catch (error: any) { + console.error('Content analysis error:', error); + return NextResponse.json( + { error: error.message || 'Failed to analyze content' }, + { status: 500 } + ); + } +} diff --git a/seo-tool/app/api/content-audit/route.ts b/seo-tool/app/api/content-audit/route.ts new file mode 100644 index 000000000000..d53527e65422 --- /dev/null +++ b/seo-tool/app/api/content-audit/route.ts @@ -0,0 +1,153 @@ +import { NextRequest, NextResponse } from 'next/server'; +import axios from 'axios'; +import * as cheerio from 'cheerio'; + +export async function POST(request: NextRequest) { + try { + const { url } = await request.json(); + + if (!url) { + return NextResponse.json( + { error: 'URL is required' }, + { status: 400 } + ); + } + + // Validate URL format + try { + new URL(url); + } catch { + return NextResponse.json( + { error: 'Invalid URL format' }, + { status: 400 } + ); + } + + // Fetch the webpage + const response = await axios.get(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + }, + timeout: 10000, + }); + + const html = response.data; + const $ = cheerio.load(html); + + // Extract meta tags + const metaTags = { + title: $('title').text() || '', + description: $('meta[name="description"]').attr('content') || '', + keywords: $('meta[name="keywords"]').attr('content') || '', + ogTitle: $('meta[property="og:title"]').attr('content') || '', + ogDescription: $('meta[property="og:description"]').attr('content') || '', + ogImage: $('meta[property="og:image"]').attr('content') || '', + twitterCard: $('meta[name="twitter:card"]').attr('content') || '', + twitterTitle: $('meta[name="twitter:title"]').attr('content') || '', + twitterDescription: $('meta[name="twitter:description"]').attr('content') || '', + canonical: $('link[rel="canonical"]').attr('href') || '', + robots: $('meta[name="robots"]').attr('content') || '', + }; + + // Extract all headings + const headings = { + h1: [] as string[], + h2: [] as string[], + h3: [] as string[], + h4: [] as string[], + h5: [] as string[], + h6: [] as string[], + }; + + $('h1').each((_, el) => headings.h1.push($(el).text().trim())); + $('h2').each((_, el) => headings.h2.push($(el).text().trim())); + $('h3').each((_, el) => headings.h3.push($(el).text().trim())); + $('h4').each((_, el) => headings.h4.push($(el).text().trim())); + $('h5').each((_, el) => headings.h5.push($(el).text().trim())); + $('h6').each((_, el) => headings.h6.push($(el).text().trim())); + + // Extract all links + const links = { + internal: [] as string[], + external: [] as string[], + }; + + const baseUrl = new URL(url); + $('a[href]').each((_, el) => { + const href = $(el).attr('href'); + if (href) { + try { + const linkUrl = new URL(href, url); + if (linkUrl.hostname === baseUrl.hostname) { + links.internal.push(linkUrl.href); + } else { + links.external.push(linkUrl.href); + } + } catch { + // Invalid URL, skip + } + } + }); + + // Extract images + const images = [] as Array<{ src: string; alt: string }>; + $('img').each((_, el) => { + const src = $(el).attr('src'); + const alt = $(el).attr('alt') || ''; + if (src) { + try { + const imgUrl = new URL(src, url); + images.push({ src: imgUrl.href, alt }); + } catch { + // Invalid URL, skip + } + } + }); + + // Extract body text + $('script, style, nav, footer, header').remove(); + const bodyText = $('body').text().replace(/\s+/g, ' ').trim(); + const wordCount = bodyText.split(/\s+/).filter(word => word.length > 0).length; + + // Extract structured data + const structuredData = [] as any[]; + $('script[type="application/ld+json"]').each((_, el) => { + try { + const data = JSON.parse($(el).html() || ''); + structuredData.push(data); + } catch { + // Invalid JSON, skip + } + }); + + return NextResponse.json({ + url, + metaTags, + headings, + links: { + internal: links.internal.length, + external: links.external.length, + internalLinks: links.internal.slice(0, 50), // Limit to first 50 + externalLinks: links.external.slice(0, 50), + }, + images: { + total: images.length, + withAlt: images.filter(img => img.alt).length, + withoutAlt: images.filter(img => !img.alt).length, + list: images.slice(0, 20), // Limit to first 20 + }, + content: { + wordCount, + textPreview: bodyText.substring(0, 500), + }, + structuredData, + statusCode: response.status, + }); + } catch (error: any) { + console.error('Content audit error:', error); + return NextResponse.json( + { error: error.message || 'Failed to audit content' }, + { status: 500 } + ); + } +} diff --git a/seo-tool/app/api/keyword-generator/route.ts b/seo-tool/app/api/keyword-generator/route.ts new file mode 100644 index 000000000000..e156c7aabe5d --- /dev/null +++ b/seo-tool/app/api/keyword-generator/route.ts @@ -0,0 +1,146 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest) { + try { + const { keyword } = await request.json(); + + if (!keyword) { + return NextResponse.json( + { error: 'Keyword is required' }, + { status: 400 } + ); + } + + const cleanKeyword = keyword.trim().toLowerCase(); + + // Generate related keywords using various strategies + const relatedKeywords = new Set(); + + // 1. Question-based keywords + const questionWords = ['what', 'how', 'why', 'when', 'where', 'who', 'which']; + questionWords.forEach(q => { + relatedKeywords.add(`${q} is ${cleanKeyword}`); + relatedKeywords.add(`${q} ${cleanKeyword}`); + }); + + // 2. Modifier-based keywords + const modifiers = [ + 'best', 'top', 'free', 'online', 'guide', 'tutorial', 'tips', + 'examples', 'tools', 'software', 'services', 'solutions', + 'benefits', 'advantages', 'features', 'review', 'comparison', + 'vs', 'alternative', 'cheap', 'affordable', 'professional' + ]; + modifiers.forEach(mod => { + relatedKeywords.add(`${mod} ${cleanKeyword}`); + relatedKeywords.add(`${cleanKeyword} ${mod}`); + }); + + // 3. Action-based keywords + const actions = [ + 'learn', 'buy', 'get', 'find', 'use', 'create', 'make', + 'build', 'develop', 'improve', 'optimize', 'increase' + ]; + actions.forEach(action => { + relatedKeywords.add(`${action} ${cleanKeyword}`); + relatedKeywords.add(`how to ${action} ${cleanKeyword}`); + }); + + // 4. Time-based keywords + const timeModifiers = ['2024', '2025', 'latest', 'new', 'updated', 'modern']; + timeModifiers.forEach(time => { + relatedKeywords.add(`${cleanKeyword} ${time}`); + }); + + // 5. Location-based keywords (generic) + const locations = ['near me', 'online', 'local', 'remote']; + locations.forEach(loc => { + relatedKeywords.add(`${cleanKeyword} ${loc}`); + }); + + // 6. Comparison keywords + relatedKeywords.add(`${cleanKeyword} vs`); + relatedKeywords.add(`${cleanKeyword} comparison`); + relatedKeywords.add(`${cleanKeyword} alternatives`); + relatedKeywords.add(`${cleanKeyword} competitors`); + + // 7. Problem-solving keywords + relatedKeywords.add(`${cleanKeyword} problems`); + relatedKeywords.add(`${cleanKeyword} solutions`); + relatedKeywords.add(`${cleanKeyword} troubleshooting`); + relatedKeywords.add(`${cleanKeyword} help`); + + // 8. Educational keywords + relatedKeywords.add(`${cleanKeyword} course`); + relatedKeywords.add(`${cleanKeyword} training`); + relatedKeywords.add(`${cleanKeyword} certification`); + relatedKeywords.add(`${cleanKeyword} for beginners`); + + // 9. Commercial keywords + relatedKeywords.add(`${cleanKeyword} price`); + relatedKeywords.add(`${cleanKeyword} cost`); + relatedKeywords.add(`${cleanKeyword} pricing`); + relatedKeywords.add(`${cleanKeyword} discount`); + relatedKeywords.add(`${cleanKeyword} coupon`); + + // 10. Long-tail variations + relatedKeywords.add(`best ${cleanKeyword} for beginners`); + relatedKeywords.add(`how to choose ${cleanKeyword}`); + relatedKeywords.add(`${cleanKeyword} step by step guide`); + relatedKeywords.add(`complete guide to ${cleanKeyword}`); + + // Convert to array and categorize + const keywordArray = Array.from(relatedKeywords); + + // Categorize keywords + const categorized = { + questions: keywordArray.filter(k => + questionWords.some(q => k.startsWith(q)) + ), + howTo: keywordArray.filter(k => k.includes('how to')), + commercial: keywordArray.filter(k => + ['buy', 'price', 'cost', 'pricing', 'discount', 'coupon', 'cheap', 'affordable'].some(w => k.includes(w)) + ), + informational: keywordArray.filter(k => + ['guide', 'tutorial', 'tips', 'examples', 'learn', 'what', 'why'].some(w => k.includes(w)) + ), + comparison: keywordArray.filter(k => + ['vs', 'comparison', 'alternative', 'review'].some(w => k.includes(w)) + ), + local: keywordArray.filter(k => + ['near me', 'local'].some(w => k.includes(w)) + ), + }; + + // Calculate estimated metrics (simulated) + const keywordsWithMetrics = keywordArray.slice(0, 50).map(kw => ({ + keyword: kw, + searchVolume: Math.floor(Math.random() * 10000) + 100, + difficulty: Math.floor(Math.random() * 100), + cpc: (Math.random() * 5).toFixed(2), + })).sort((a, b) => b.searchVolume - a.searchVolume); + + return NextResponse.json({ + seedKeyword: cleanKeyword, + totalGenerated: keywordArray.length, + keywords: keywordsWithMetrics, + categorized: { + questions: categorized.questions.slice(0, 10), + howTo: categorized.howTo.slice(0, 10), + commercial: categorized.commercial.slice(0, 10), + informational: categorized.informational.slice(0, 10), + comparison: categorized.comparison.slice(0, 10), + local: categorized.local.slice(0, 10), + }, + suggestions: { + longTail: keywordArray.filter(k => k.split(' ').length >= 4).slice(0, 10), + shortTail: keywordArray.filter(k => k.split(' ').length <= 2).slice(0, 10), + }, + }); + } catch (error: any) { + console.error('Keyword generation error:', error); + return NextResponse.json( + { error: error.message || 'Failed to generate keywords' }, + { status: 500 } + ); + } +} diff --git a/seo-tool/app/favicon.ico b/seo-tool/app/favicon.ico new file mode 100644 index 000000000000..718d6fea4835 Binary files /dev/null and b/seo-tool/app/favicon.ico differ diff --git a/seo-tool/app/globals.css b/seo-tool/app/globals.css new file mode 100644 index 000000000000..a2dc41ecee5e --- /dev/null +++ b/seo-tool/app/globals.css @@ -0,0 +1,26 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/seo-tool/app/layout.tsx b/seo-tool/app/layout.tsx new file mode 100644 index 000000000000..f7fa87eb8752 --- /dev/null +++ b/seo-tool/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/seo-tool/app/page.tsx b/seo-tool/app/page.tsx new file mode 100644 index 000000000000..295f8fdf14fc --- /dev/null +++ b/seo-tool/app/page.tsx @@ -0,0 +1,65 @@ +import Image from "next/image"; + +export default function Home() { + return ( +
+
+ Next.js logo +
+

+ To get started, edit the page.tsx file. +

+

+ Looking for a starting point or more instructions? Head over to{" "} + + Templates + {" "} + or the{" "} + + Learning + {" "} + center. +

+
+
+ + Vercel logomark + Deploy Now + + + Documentation + +
+
+
+ ); +} diff --git a/seo-tool/eslint.config.mjs b/seo-tool/eslint.config.mjs new file mode 100644 index 000000000000..05e726d1b420 --- /dev/null +++ b/seo-tool/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/seo-tool/next.config.ts b/seo-tool/next.config.ts new file mode 100644 index 000000000000..e9ffa3083ad2 --- /dev/null +++ b/seo-tool/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/seo-tool/package.json b/seo-tool/package.json new file mode 100644 index 000000000000..8ced41874c77 --- /dev/null +++ b/seo-tool/package.json @@ -0,0 +1,30 @@ +{ + "name": "seo-tool", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "axios": "^1.13.2", + "cheerio": "^1.1.2", + "natural": "^8.1.0", + "next": "16.0.1", + "react": "19.2.0", + "react-dom": "19.2.0", + "stopword": "^3.1.5" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.0.1", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/seo-tool/postcss.config.mjs b/seo-tool/postcss.config.mjs new file mode 100644 index 000000000000..61e36849cf7c --- /dev/null +++ b/seo-tool/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/seo-tool/public/file.svg b/seo-tool/public/file.svg new file mode 100644 index 000000000000..004145cddf3f --- /dev/null +++ b/seo-tool/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/seo-tool/public/globe.svg b/seo-tool/public/globe.svg new file mode 100644 index 000000000000..567f17b0d7c7 --- /dev/null +++ b/seo-tool/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/seo-tool/public/next.svg b/seo-tool/public/next.svg new file mode 100644 index 000000000000..5174b28c565c --- /dev/null +++ b/seo-tool/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/seo-tool/public/vercel.svg b/seo-tool/public/vercel.svg new file mode 100644 index 000000000000..77053960334e --- /dev/null +++ b/seo-tool/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/seo-tool/public/window.svg b/seo-tool/public/window.svg new file mode 100644 index 000000000000..b2b2a44f6ebc --- /dev/null +++ b/seo-tool/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/seo-tool/tsconfig.json b/seo-tool/tsconfig.json new file mode 100644 index 000000000000..3a13f90a773b --- /dev/null +++ b/seo-tool/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +}