@rankcli/agent-runtime 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -946,7 +946,6 @@ __export(index_exports, {
946
946
  generateAllFixes: () => generateAllFixes,
947
947
  generateAngularSEOService: () => generateAngularSEOService,
948
948
  generateAstroBaseHead: () => generateAstroBaseHead,
949
- generateAstroLayout: () => generateAstroLayout,
950
949
  generateAstroMeta: () => generateAstroMeta,
951
950
  generateBlogPost: () => generateBlogPost,
952
951
  generateBranchName: () => generateBranchName,
@@ -972,17 +971,11 @@ __export(index_exports, {
972
971
  generateMarkdownReport: () => generateMarkdownReport,
973
972
  generateNextAppMetadata: () => generateNextAppMetadata,
974
973
  generateNextJsAppRouterMetadata: () => generateNextJsAppRouterMetadata,
975
- generateNextJsDynamicMetadata: () => generateNextJsDynamicMetadata,
976
- generateNextJsPageMetadata: () => generateNextJsPageMetadata,
977
974
  generateNextJsPagesRouterHead: () => generateNextJsPagesRouterHead,
978
- generateNextJsRobots: () => generateNextJsRobots,
979
- generateNextJsSitemap: () => generateNextJsSitemap,
980
975
  generateNextPagesHead: () => generateNextPagesHead,
981
- generateNuxtPageExample: () => generateNuxtPageExample,
982
976
  generateNuxtSEOHead: () => generateNuxtSEOHead,
983
977
  generatePDFReport: () => generatePDFReport,
984
978
  generatePRDescription: () => generatePRDescription,
985
- generateReactAppWrapper: () => generateReactAppWrapper,
986
979
  generateReactHelmetSocialMeta: () => generateReactHelmetSocialMeta,
987
980
  generateReactSEOHead: () => generateReactSEOHead,
988
981
  generateRecommendationQueries: () => generateRecommendationQueries,
@@ -26741,61 +26734,332 @@ var import_path4 = require("path");
26741
26734
 
26742
26735
  // src/fixer/framework-fixes.ts
26743
26736
  function generateReactSEOHead(options) {
26744
- const { siteName, siteUrl, title, description, image } = options;
26737
+ const { siteName, siteUrl, title, description, image, twitterHandle, locale } = options;
26745
26738
  return {
26746
26739
  file: "src/components/SEOHead.tsx",
26747
26740
  code: `import { Helmet } from 'react-helmet-async';
26748
26741
 
26742
+ /**
26743
+ * SEO Head Component
26744
+ *
26745
+ * Comprehensive SEO meta tags following best practices:
26746
+ * - Primary meta tags (title, description)
26747
+ * - Open Graph for Facebook/LinkedIn
26748
+ * - Twitter Card for X/Twitter
26749
+ * - JSON-LD structured data
26750
+ * - Canonical URLs
26751
+ *
26752
+ * @example
26753
+ * <SEOHead
26754
+ * title="Product Name"
26755
+ * description="Product description"
26756
+ * type="product"
26757
+ * schema={{
26758
+ * "@type": "Product",
26759
+ * name: "Product Name",
26760
+ * price: "99.00"
26761
+ * }}
26762
+ * />
26763
+ */
26764
+
26749
26765
  interface SEOHeadProps {
26766
+ // Required
26750
26767
  title?: string;
26751
26768
  description?: string;
26752
- image?: string;
26769
+
26770
+ // URLs
26753
26771
  url?: string;
26754
- type?: 'website' | 'article';
26772
+ canonical?: string;
26773
+ image?: string;
26774
+
26775
+ // Page type
26776
+ type?: 'website' | 'article' | 'product' | 'profile';
26777
+
26778
+ // Article-specific
26779
+ publishedTime?: string;
26780
+ modifiedTime?: string;
26781
+ author?: string;
26782
+ section?: string;
26783
+ tags?: string[];
26784
+
26785
+ // Twitter
26786
+ twitterCard?: 'summary' | 'summary_large_image' | 'player';
26787
+
26788
+ // Structured data
26789
+ schema?: Record<string, unknown> | Record<string, unknown>[];
26790
+
26791
+ // Robots
26792
+ noindex?: boolean;
26793
+ nofollow?: boolean;
26794
+
26795
+ // Alternate languages
26796
+ alternates?: { hrefLang: string; href: string }[];
26755
26797
  }
26756
26798
 
26799
+ const SITE_NAME = '${siteName}';
26800
+ const SITE_URL = '${siteUrl}';
26801
+ const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
26802
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
26803
+ const DEFAULT_LOCALE = '${locale || "en_US"}';
26804
+
26757
26805
  export function SEOHead({
26758
- title = '${title || `${siteName} - Your tagline here`}',
26806
+ title,
26759
26807
  description = '${description || `${siteName} - A compelling description of your product or service.`}',
26760
- image = '${image || `${siteUrl}/og-image.png`}',
26761
- url = typeof window !== 'undefined' ? window.location.href : '${siteUrl}',
26808
+ url,
26809
+ canonical,
26810
+ image = DEFAULT_IMAGE,
26762
26811
  type = 'website',
26812
+ publishedTime,
26813
+ modifiedTime,
26814
+ author,
26815
+ section,
26816
+ tags,
26817
+ twitterCard = 'summary_large_image',
26818
+ schema,
26819
+ noindex = false,
26820
+ nofollow = false,
26821
+ alternates,
26763
26822
  }: SEOHeadProps) {
26764
- const fullTitle = title.includes('${siteName}') ? title : \`\${title} | ${siteName}\`;
26823
+ const pageUrl = url || (typeof window !== 'undefined' ? window.location.href : SITE_URL);
26824
+ const canonicalUrl = canonical || pageUrl;
26825
+ const fullTitle = title
26826
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
26827
+ : SITE_NAME;
26828
+
26829
+ // Ensure image is absolute URL
26830
+ const imageUrl = image.startsWith('http') ? image : \`\${SITE_URL}\${image}\`;
26831
+
26832
+ // Build robots directive
26833
+ const robotsContent = [
26834
+ noindex ? 'noindex' : 'index',
26835
+ nofollow ? 'nofollow' : 'follow',
26836
+ ].join(', ');
26837
+
26838
+ // Default Organization schema
26839
+ const defaultSchema = {
26840
+ '@context': 'https://schema.org',
26841
+ '@type': 'WebSite',
26842
+ name: SITE_NAME,
26843
+ url: SITE_URL,
26844
+ };
26845
+
26846
+ // Merge with provided schema
26847
+ const jsonLd = schema
26848
+ ? Array.isArray(schema)
26849
+ ? [defaultSchema, ...schema]
26850
+ : [defaultSchema, schema]
26851
+ : [defaultSchema];
26765
26852
 
26766
26853
  return (
26767
26854
  <Helmet>
26768
26855
  {/* Primary Meta Tags */}
26769
26856
  <title>{fullTitle}</title>
26857
+ <meta name="title" content={fullTitle} />
26770
26858
  <meta name="description" content={description} />
26771
- <link rel="canonical" href={url} />
26859
+ <meta name="robots" content={robotsContent} />
26860
+ <link rel="canonical" href={canonicalUrl} />
26772
26861
 
26773
26862
  {/* Open Graph / Facebook */}
26774
26863
  <meta property="og:type" content={type} />
26775
- <meta property="og:url" content={url} />
26864
+ <meta property="og:url" content={pageUrl} />
26776
26865
  <meta property="og:title" content={fullTitle} />
26777
26866
  <meta property="og:description" content={description} />
26778
- <meta property="og:image" content={image} />
26779
- <meta property="og:site_name" content="${siteName}" />
26867
+ <meta property="og:image" content={imageUrl} />
26868
+ <meta property="og:image:width" content="1200" />
26869
+ <meta property="og:image:height" content="630" />
26870
+ <meta property="og:image:alt" content={fullTitle} />
26871
+ <meta property="og:site_name" content={SITE_NAME} />
26872
+ <meta property="og:locale" content={DEFAULT_LOCALE} />
26873
+
26874
+ {/* Article-specific Open Graph */}
26875
+ {type === 'article' && publishedTime && (
26876
+ <meta property="article:published_time" content={publishedTime} />
26877
+ )}
26878
+ {type === 'article' && modifiedTime && (
26879
+ <meta property="article:modified_time" content={modifiedTime} />
26880
+ )}
26881
+ {type === 'article' && author && (
26882
+ <meta property="article:author" content={author} />
26883
+ )}
26884
+ {type === 'article' && section && (
26885
+ <meta property="article:section" content={section} />
26886
+ )}
26887
+ {type === 'article' && tags?.map((tag, i) => (
26888
+ <meta key={i} property="article:tag" content={tag} />
26889
+ ))}
26780
26890
 
26781
26891
  {/* Twitter */}
26782
- <meta name="twitter:card" content="summary_large_image" />
26783
- <meta name="twitter:url" content={url} />
26892
+ <meta name="twitter:card" content={twitterCard} />
26893
+ <meta name="twitter:url" content={pageUrl} />
26784
26894
  <meta name="twitter:title" content={fullTitle} />
26785
26895
  <meta name="twitter:description" content={description} />
26786
- <meta name="twitter:image" content={image} />
26896
+ <meta name="twitter:image" content={imageUrl} />
26897
+ <meta name="twitter:image:alt" content={fullTitle} />
26898
+ {TWITTER_HANDLE && <meta name="twitter:site" content={TWITTER_HANDLE} />}
26899
+ {TWITTER_HANDLE && <meta name="twitter:creator" content={TWITTER_HANDLE} />}
26900
+
26901
+ {/* Alternate Languages */}
26902
+ {alternates?.map((alt, i) => (
26903
+ <link key={i} rel="alternate" hrefLang={alt.hrefLang} href={alt.href} />
26904
+ ))}
26905
+
26906
+ {/* JSON-LD Structured Data */}
26907
+ <script type="application/ld+json">
26908
+ {JSON.stringify(jsonLd)}
26909
+ </script>
26787
26910
  </Helmet>
26788
26911
  );
26789
- }`,
26790
- explanation: "React SEO component using react-helmet-async. Wrap your app in <HelmetProvider> and use <SEOHead /> on each page.",
26791
- installCommands: ["npm install react-helmet-async"],
26792
- imports: ["import { HelmetProvider } from 'react-helmet-async';"]
26793
- };
26794
26912
  }
26795
- function generateReactAppWrapper() {
26796
- return {
26797
- file: "src/main.tsx",
26798
- code: `import React from 'react';
26913
+
26914
+ /**
26915
+ * Pre-built schema generators for common page types
26916
+ */
26917
+ export const SchemaGenerators = {
26918
+ organization: (data: {
26919
+ name: string;
26920
+ url: string;
26921
+ logo?: string;
26922
+ sameAs?: string[];
26923
+ }) => ({
26924
+ '@context': 'https://schema.org',
26925
+ '@type': 'Organization',
26926
+ name: data.name,
26927
+ url: data.url,
26928
+ logo: data.logo,
26929
+ sameAs: data.sameAs,
26930
+ }),
26931
+
26932
+ article: (data: {
26933
+ headline: string;
26934
+ description: string;
26935
+ image: string;
26936
+ datePublished: string;
26937
+ dateModified?: string;
26938
+ author: { name: string; url?: string };
26939
+ }) => ({
26940
+ '@context': 'https://schema.org',
26941
+ '@type': 'Article',
26942
+ headline: data.headline,
26943
+ description: data.description,
26944
+ image: data.image,
26945
+ datePublished: data.datePublished,
26946
+ dateModified: data.dateModified || data.datePublished,
26947
+ author: {
26948
+ '@type': 'Person',
26949
+ name: data.author.name,
26950
+ url: data.author.url,
26951
+ },
26952
+ }),
26953
+
26954
+ product: (data: {
26955
+ name: string;
26956
+ description: string;
26957
+ image: string;
26958
+ price: string;
26959
+ currency?: string;
26960
+ availability?: 'InStock' | 'OutOfStock' | 'PreOrder';
26961
+ brand?: string;
26962
+ sku?: string;
26963
+ rating?: { value: number; count: number };
26964
+ }) => ({
26965
+ '@context': 'https://schema.org',
26966
+ '@type': 'Product',
26967
+ name: data.name,
26968
+ description: data.description,
26969
+ image: data.image,
26970
+ brand: data.brand ? { '@type': 'Brand', name: data.brand } : undefined,
26971
+ sku: data.sku,
26972
+ offers: {
26973
+ '@type': 'Offer',
26974
+ price: data.price,
26975
+ priceCurrency: data.currency || 'USD',
26976
+ availability: \`https://schema.org/\${data.availability || 'InStock'}\`,
26977
+ },
26978
+ aggregateRating: data.rating ? {
26979
+ '@type': 'AggregateRating',
26980
+ ratingValue: data.rating.value,
26981
+ reviewCount: data.rating.count,
26982
+ } : undefined,
26983
+ }),
26984
+
26985
+ faq: (items: { question: string; answer: string }[]) => ({
26986
+ '@context': 'https://schema.org',
26987
+ '@type': 'FAQPage',
26988
+ mainEntity: items.map(item => ({
26989
+ '@type': 'Question',
26990
+ name: item.question,
26991
+ acceptedAnswer: {
26992
+ '@type': 'Answer',
26993
+ text: item.answer,
26994
+ },
26995
+ })),
26996
+ }),
26997
+
26998
+ breadcrumb: (items: { name: string; url: string }[]) => ({
26999
+ '@context': 'https://schema.org',
27000
+ '@type': 'BreadcrumbList',
27001
+ itemListElement: items.map((item, index) => ({
27002
+ '@type': 'ListItem',
27003
+ position: index + 1,
27004
+ name: item.name,
27005
+ item: item.url,
27006
+ })),
27007
+ }),
27008
+
27009
+ localBusiness: (data: {
27010
+ name: string;
27011
+ description: string;
27012
+ url: string;
27013
+ phone: string;
27014
+ address: {
27015
+ street: string;
27016
+ city: string;
27017
+ state: string;
27018
+ zip: string;
27019
+ country: string;
27020
+ };
27021
+ geo?: { lat: number; lng: number };
27022
+ hours?: string[];
27023
+ priceRange?: string;
27024
+ }) => ({
27025
+ '@context': 'https://schema.org',
27026
+ '@type': 'LocalBusiness',
27027
+ name: data.name,
27028
+ description: data.description,
27029
+ url: data.url,
27030
+ telephone: data.phone,
27031
+ address: {
27032
+ '@type': 'PostalAddress',
27033
+ streetAddress: data.address.street,
27034
+ addressLocality: data.address.city,
27035
+ addressRegion: data.address.state,
27036
+ postalCode: data.address.zip,
27037
+ addressCountry: data.address.country,
27038
+ },
27039
+ geo: data.geo ? {
27040
+ '@type': 'GeoCoordinates',
27041
+ latitude: data.geo.lat,
27042
+ longitude: data.geo.lng,
27043
+ } : undefined,
27044
+ openingHours: data.hours,
27045
+ priceRange: data.priceRange,
27046
+ }),
27047
+ };`,
27048
+ explanation: `Comprehensive React SEO component with:
27049
+ \u2022 Full Open Graph support (including article metadata)
27050
+ \u2022 Twitter Cards with all variants
27051
+ \u2022 JSON-LD structured data with pre-built schema generators
27052
+ \u2022 Robots directives (noindex/nofollow)
27053
+ \u2022 Hreflang for internationalization
27054
+ \u2022 Canonical URL handling
27055
+
27056
+ Install: npm install react-helmet-async
27057
+ Wrap app: <HelmetProvider><App /></HelmetProvider>`,
27058
+ installCommands: ["npm install react-helmet-async"],
27059
+ additionalFiles: [
27060
+ {
27061
+ file: "src/main.tsx",
27062
+ code: `import React from 'react';
26799
27063
  import ReactDOM from 'react-dom/client';
26800
27064
  import { HelmetProvider } from 'react-helmet-async';
26801
27065
  import App from './App';
@@ -26808,29 +27072,63 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
26808
27072
  </HelmetProvider>
26809
27073
  </React.StrictMode>,
26810
27074
  );`,
26811
- explanation: "Updated main.tsx with HelmetProvider wrapper for react-helmet-async."
27075
+ explanation: "Updated main.tsx with HelmetProvider wrapper."
27076
+ }
27077
+ ]
26812
27078
  };
26813
27079
  }
26814
27080
  function generateNextJsAppRouterMetadata(options) {
26815
- const { siteName, siteUrl, title, description, image } = options;
27081
+ const { siteName, siteUrl, title, description, image, twitterHandle, locale } = options;
26816
27082
  return {
26817
27083
  file: "app/layout.tsx",
26818
- code: `import type { Metadata } from 'next';
27084
+ code: `import type { Metadata, Viewport } from 'next';
26819
27085
  import { Inter } from 'next/font/google';
26820
27086
  import './globals.css';
26821
27087
 
26822
- const inter = Inter({ subsets: ['latin'] });
27088
+ const inter = Inter({ subsets: ['latin'], display: 'swap' });
26823
27089
 
27090
+ /**
27091
+ * Default metadata for all pages
27092
+ * Individual pages can override with their own metadata export
27093
+ */
26824
27094
  export const metadata: Metadata = {
26825
27095
  metadataBase: new URL('${siteUrl}'),
27096
+
27097
+ // Default title with template
26826
27098
  title: {
26827
27099
  default: '${title || siteName}',
26828
27100
  template: \`%s | ${siteName}\`,
26829
27101
  },
27102
+
26830
27103
  description: '${description || `${siteName} - A compelling description of your product or service.`}',
27104
+
27105
+ // Indexing
27106
+ robots: {
27107
+ index: true,
27108
+ follow: true,
27109
+ googleBot: {
27110
+ index: true,
27111
+ follow: true,
27112
+ 'max-video-preview': -1,
27113
+ 'max-image-preview': 'large',
27114
+ 'max-snippet': -1,
27115
+ },
27116
+ },
27117
+
27118
+ // Icons
27119
+ icons: {
27120
+ icon: '/favicon.ico',
27121
+ shortcut: '/favicon-16x16.png',
27122
+ apple: '/apple-touch-icon.png',
27123
+ },
27124
+
27125
+ // Manifest
27126
+ manifest: '/site.webmanifest',
27127
+
27128
+ // Open Graph
26831
27129
  openGraph: {
26832
27130
  type: 'website',
26833
- locale: 'en_US',
27131
+ locale: '${locale || "en_US"}',
26834
27132
  url: '${siteUrl}',
26835
27133
  siteName: '${siteName}',
26836
27134
  title: '${title || siteName}',
@@ -26844,16 +27142,49 @@ export const metadata: Metadata = {
26844
27142
  },
26845
27143
  ],
26846
27144
  },
27145
+
27146
+ // Twitter
26847
27147
  twitter: {
26848
27148
  card: 'summary_large_image',
26849
27149
  title: '${title || siteName}',
26850
27150
  description: '${description || `${siteName} - A compelling description.`}',
26851
27151
  images: ['${image || "/og-image.png"}'],
27152
+ ${twitterHandle ? `site: '${twitterHandle}',
27153
+ creator: '${twitterHandle}',` : ""}
26852
27154
  },
26853
- robots: {
26854
- index: true,
26855
- follow: true,
27155
+
27156
+ // Verification (add your IDs)
27157
+ verification: {
27158
+ // google: 'your-google-verification-code',
27159
+ // yandex: 'your-yandex-verification-code',
27160
+ // bing: 'your-bing-verification-code',
26856
27161
  },
27162
+
27163
+ // Alternate languages (uncomment and customize)
27164
+ // alternates: {
27165
+ // canonical: '${siteUrl}',
27166
+ // languages: {
27167
+ // 'en-US': '${siteUrl}/en',
27168
+ // 'es-ES': '${siteUrl}/es',
27169
+ // },
27170
+ // },
27171
+
27172
+ // Category
27173
+ category: 'technology',
27174
+ };
27175
+
27176
+ /**
27177
+ * Viewport configuration
27178
+ * Separated from metadata in Next.js 14+
27179
+ */
27180
+ export const viewport: Viewport = {
27181
+ themeColor: [
27182
+ { media: '(prefers-color-scheme: light)', color: '#ffffff' },
27183
+ { media: '(prefers-color-scheme: dark)', color: '#000000' },
27184
+ ],
27185
+ width: 'device-width',
27186
+ initialScale: 1,
27187
+ maximumScale: 5,
26857
27188
  };
26858
27189
 
26859
27190
  export default function RootLayout({
@@ -26862,143 +27193,104 @@ export default function RootLayout({
26862
27193
  children: React.ReactNode;
26863
27194
  }) {
26864
27195
  return (
26865
- <html lang="en">
26866
- <body className={inter.className}>{children}</body>
27196
+ <html lang="${(locale || "en_US").split("_")[0]}" className={inter.className}>
27197
+ <body>
27198
+ {children}
27199
+
27200
+ {/* JSON-LD Organization Schema */}
27201
+ <script
27202
+ type="application/ld+json"
27203
+ dangerouslySetInnerHTML={{
27204
+ __html: JSON.stringify({
27205
+ '@context': 'https://schema.org',
27206
+ '@type': 'Organization',
27207
+ name: '${siteName}',
27208
+ url: '${siteUrl}',
27209
+ logo: '${siteUrl}/logo.png',
27210
+ sameAs: [
27211
+ // Add your social profiles
27212
+ // 'https://twitter.com/yourhandle',
27213
+ // 'https://linkedin.com/company/yourcompany',
27214
+ ],
27215
+ }),
27216
+ }}
27217
+ />
27218
+ </body>
26867
27219
  </html>
26868
27220
  );
26869
27221
  }`,
26870
- explanation: "Next.js App Router layout with built-in Metadata API. Each page can override with its own metadata export."
26871
- };
26872
- }
26873
- function generateNextJsPageMetadata(options) {
26874
- const { siteName, description, pageName } = options;
26875
- return {
26876
- file: `app/${pageName}/page.tsx`,
26877
- code: `import type { Metadata } from 'next';
26878
-
26879
- export const metadata: Metadata = {
26880
- title: '${pageName.charAt(0).toUpperCase() + pageName.slice(1)}',
26881
- description: '${description || `Learn more about ${pageName} on ${siteName}.`}',
26882
- };
26883
-
26884
- export default function ${pageName.charAt(0).toUpperCase() + pageName.slice(1)}Page() {
26885
- return (
26886
- <main>
26887
- <h1>${pageName.charAt(0).toUpperCase() + pageName.slice(1)}</h1>
26888
- {/* Your content here */}
26889
- </main>
26890
- );
26891
- }`,
26892
- explanation: `Next.js page with metadata export. The title will be "${pageName} | ${siteName}" using the template.`
26893
- };
26894
- }
26895
- function generateNextJsDynamicMetadata() {
26896
- return {
26897
- file: "app/[slug]/page.tsx",
26898
- code: `import type { Metadata } from 'next';
26899
-
26900
- interface PageProps {
26901
- params: { slug: string };
26902
- }
26903
-
26904
- // Generate metadata dynamically based on the slug
26905
- export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
26906
- // Fetch data based on slug (replace with your data fetching logic)
26907
- const data = await fetchPageData(params.slug);
26908
-
26909
- return {
26910
- title: data.title,
26911
- description: data.description,
26912
- openGraph: {
26913
- title: data.title,
26914
- description: data.description,
26915
- images: data.image ? [{ url: data.image }] : [],
26916
- },
26917
- };
26918
- }
26919
-
26920
- // Pre-generate static pages for known slugs (improves SEO)
26921
- export async function generateStaticParams() {
26922
- const pages = await fetchAllPageSlugs();
26923
- return pages.map((slug) => ({ slug }));
26924
- }
26925
-
26926
- async function fetchPageData(slug: string) {
26927
- // Replace with your actual data fetching
26928
- return {
26929
- title: slug.replace(/-/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()),
26930
- description: \`Learn about \${slug}\`,
26931
- image: null,
26932
- };
26933
- }
26934
-
26935
- async function fetchAllPageSlugs() {
26936
- // Replace with your actual data source
26937
- return ['about', 'contact', 'pricing'];
26938
- }
26939
-
26940
- export default function DynamicPage({ params }: PageProps) {
26941
- return (
26942
- <main>
26943
- <h1>{params.slug}</h1>
26944
- </main>
26945
- );
26946
- }`,
26947
- explanation: "Next.js dynamic route with generateMetadata and generateStaticParams for SEO-optimized dynamic pages."
26948
- };
26949
- }
26950
- function generateNextJsRobots(siteUrl) {
26951
- return {
26952
- file: "app/robots.ts",
26953
- code: `import type { MetadataRoute } from 'next';
27222
+ explanation: `Next.js App Router layout with comprehensive SEO:
27223
+ \u2022 Metadata API with title templates
27224
+ \u2022 Full Open Graph and Twitter Card support
27225
+ \u2022 Viewport configuration (Next.js 14+)
27226
+ \u2022 JSON-LD Organization schema
27227
+ \u2022 Verification tags for search consoles
27228
+ \u2022 Internationalization ready
27229
+ \u2022 Web font optimization with next/font`,
27230
+ additionalFiles: [
27231
+ {
27232
+ file: "app/robots.ts",
27233
+ code: `import type { MetadataRoute } from 'next';
26954
27234
 
26955
27235
  export default function robots(): MetadataRoute.Robots {
27236
+ const baseUrl = '${siteUrl}';
27237
+
26956
27238
  return {
26957
27239
  rules: [
26958
27240
  {
26959
27241
  userAgent: '*',
26960
27242
  allow: '/',
26961
- disallow: ['/api/', '/admin/', '/_next/'],
27243
+ disallow: ['/api/', '/admin/', '/_next/', '/private/'],
27244
+ },
27245
+ {
27246
+ userAgent: 'GPTBot',
27247
+ allow: '/',
26962
27248
  },
26963
27249
  ],
26964
- sitemap: '${siteUrl}/sitemap.xml',
27250
+ sitemap: \`\${baseUrl}/sitemap.xml\`,
26965
27251
  };
26966
27252
  }`,
26967
- explanation: "Next.js App Router robots.txt generator. Automatically served at /robots.txt."
26968
- };
26969
- }
26970
- function generateNextJsSitemap(siteUrl) {
26971
- return {
26972
- file: "app/sitemap.ts",
26973
- code: `import type { MetadataRoute } from 'next';
27253
+ explanation: "Robots.txt with AI crawler support."
27254
+ },
27255
+ {
27256
+ file: "app/sitemap.ts",
27257
+ code: `import type { MetadataRoute } from 'next';
26974
27258
 
26975
27259
  export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
27260
+ const baseUrl = '${siteUrl}';
27261
+
26976
27262
  // Static pages
26977
27263
  const staticPages: MetadataRoute.Sitemap = [
26978
27264
  {
26979
- url: '${siteUrl}',
27265
+ url: baseUrl,
26980
27266
  lastModified: new Date(),
26981
27267
  changeFrequency: 'daily',
26982
27268
  priority: 1,
26983
27269
  },
26984
27270
  {
26985
- url: '${siteUrl}/about',
27271
+ url: \`\${baseUrl}/about\`,
26986
27272
  lastModified: new Date(),
26987
27273
  changeFrequency: 'monthly',
26988
27274
  priority: 0.8,
26989
27275
  },
26990
27276
  {
26991
- url: '${siteUrl}/pricing',
27277
+ url: \`\${baseUrl}/pricing\`,
26992
27278
  lastModified: new Date(),
26993
27279
  changeFrequency: 'weekly',
26994
27280
  priority: 0.9,
26995
27281
  },
27282
+ {
27283
+ url: \`\${baseUrl}/blog\`,
27284
+ lastModified: new Date(),
27285
+ changeFrequency: 'daily',
27286
+ priority: 0.8,
27287
+ },
26996
27288
  ];
26997
27289
 
26998
- // Dynamic pages (fetch from your database/CMS)
26999
- // const posts = await fetchAllPosts();
27290
+ // Dynamic pages - fetch from your database/CMS
27291
+ // const posts = await db.post.findMany({ select: { slug: true, updatedAt: true } });
27000
27292
  // const dynamicPages = posts.map((post) => ({
27001
- // url: \`${siteUrl}/blog/\${post.slug}\`,
27293
+ // url: \`\${baseUrl}/blog/\${post.slug}\`,
27002
27294
  // lastModified: post.updatedAt,
27003
27295
  // changeFrequency: 'weekly' as const,
27004
27296
  // priority: 0.7,
@@ -27006,209 +27298,628 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
27006
27298
 
27007
27299
  return [...staticPages];
27008
27300
  }`,
27009
- explanation: "Next.js App Router sitemap generator. Automatically served at /sitemap.xml."
27010
- };
27011
- }
27012
- function generateNextJsPagesRouterHead(options) {
27013
- const { siteName, siteUrl, title, description, image } = options;
27014
- return {
27015
- file: "components/SEOHead.tsx",
27016
- code: `import Head from 'next/head';
27301
+ explanation: "Dynamic sitemap generator."
27302
+ },
27303
+ {
27304
+ file: "lib/seo.ts",
27305
+ code: `import type { Metadata } from 'next';
27017
27306
 
27018
- interface SEOHeadProps {
27019
- title?: string;
27020
- description?: string;
27307
+ const baseUrl = '${siteUrl}';
27308
+ const siteName = '${siteName}';
27309
+
27310
+ interface PageSEOProps {
27311
+ title: string;
27312
+ description: string;
27313
+ path?: string;
27021
27314
  image?: string;
27022
- url?: string;
27023
27315
  type?: 'website' | 'article';
27316
+ publishedTime?: string;
27317
+ modifiedTime?: string;
27318
+ authors?: string[];
27319
+ tags?: string[];
27320
+ noIndex?: boolean;
27321
+ }
27322
+
27323
+ /**
27324
+ * Generate metadata for a page
27325
+ * Use in page.tsx: export const metadata = generateMetadata({ ... })
27326
+ */
27327
+ export function generatePageMetadata({
27328
+ title,
27329
+ description,
27330
+ path = '',
27331
+ image,
27332
+ type = 'website',
27333
+ publishedTime,
27334
+ modifiedTime,
27335
+ authors,
27336
+ tags,
27337
+ noIndex = false,
27338
+ }: PageSEOProps): Metadata {
27339
+ const url = \`\${baseUrl}\${path}\`;
27340
+ const ogImage = image || '/og-image.png';
27341
+
27342
+ return {
27343
+ title,
27344
+ description,
27345
+
27346
+ robots: noIndex ? { index: false, follow: false } : undefined,
27347
+
27348
+ alternates: {
27349
+ canonical: url,
27350
+ },
27351
+
27352
+ openGraph: {
27353
+ title,
27354
+ description,
27355
+ url,
27356
+ siteName,
27357
+ type,
27358
+ images: [{ url: ogImage, width: 1200, height: 630 }],
27359
+ ...(type === 'article' && {
27360
+ publishedTime,
27361
+ modifiedTime,
27362
+ authors,
27363
+ tags,
27364
+ }),
27365
+ },
27366
+
27367
+ twitter: {
27368
+ card: 'summary_large_image',
27369
+ title,
27370
+ description,
27371
+ images: [ogImage],
27372
+ },
27373
+ };
27024
27374
  }
27025
27375
 
27026
- export function SEOHead({
27027
- title = '${title || siteName}',
27028
- description = '${description || `${siteName} - A compelling description.`}',
27029
- image = '${image || `${siteUrl}/og-image.png`}',
27030
- url = '${siteUrl}',
27031
- type = 'website',
27032
- }: SEOHeadProps) {
27033
- const fullTitle = title.includes('${siteName}') ? title : \`\${title} | ${siteName}\`;
27376
+ /**
27377
+ * Generate JSON-LD for articles
27378
+ */
27379
+ export function generateArticleJsonLd(article: {
27380
+ title: string;
27381
+ description: string;
27382
+ url: string;
27383
+ image: string;
27384
+ datePublished: string;
27385
+ dateModified?: string;
27386
+ author: { name: string; url?: string };
27387
+ }) {
27388
+ return {
27389
+ '@context': 'https://schema.org',
27390
+ '@type': 'Article',
27391
+ headline: article.title,
27392
+ description: article.description,
27393
+ url: article.url,
27394
+ image: article.image,
27395
+ datePublished: article.datePublished,
27396
+ dateModified: article.dateModified || article.datePublished,
27397
+ author: {
27398
+ '@type': 'Person',
27399
+ name: article.author.name,
27400
+ url: article.author.url,
27401
+ },
27402
+ publisher: {
27403
+ '@type': 'Organization',
27404
+ name: siteName,
27405
+ logo: {
27406
+ '@type': 'ImageObject',
27407
+ url: \`\${baseUrl}/logo.png\`,
27408
+ },
27409
+ },
27410
+ };
27411
+ }
27034
27412
 
27035
- return (
27036
- <Head>
27037
- <title>{fullTitle}</title>
27038
- <meta name="description" content={description} />
27039
- <link rel="canonical" href={url} />
27040
-
27041
- <meta property="og:type" content={type} />
27042
- <meta property="og:url" content={url} />
27043
- <meta property="og:title" content={fullTitle} />
27044
- <meta property="og:description" content={description} />
27045
- <meta property="og:image" content={image} />
27046
- <meta property="og:site_name" content="${siteName}" />
27047
-
27048
- <meta name="twitter:card" content="summary_large_image" />
27049
- <meta name="twitter:title" content={fullTitle} />
27050
- <meta name="twitter:description" content={description} />
27051
- <meta name="twitter:image" content={image} />
27052
- </Head>
27053
- );
27413
+ /**
27414
+ * Generate JSON-LD for products
27415
+ */
27416
+ export function generateProductJsonLd(product: {
27417
+ name: string;
27418
+ description: string;
27419
+ image: string;
27420
+ price: number;
27421
+ currency?: string;
27422
+ availability?: 'InStock' | 'OutOfStock' | 'PreOrder';
27423
+ rating?: { value: number; count: number };
27424
+ brand?: string;
27425
+ sku?: string;
27426
+ }) {
27427
+ return {
27428
+ '@context': 'https://schema.org',
27429
+ '@type': 'Product',
27430
+ name: product.name,
27431
+ description: product.description,
27432
+ image: product.image,
27433
+ brand: product.brand ? { '@type': 'Brand', name: product.brand } : undefined,
27434
+ sku: product.sku,
27435
+ offers: {
27436
+ '@type': 'Offer',
27437
+ price: product.price,
27438
+ priceCurrency: product.currency || 'USD',
27439
+ availability: \`https://schema.org/\${product.availability || 'InStock'}\`,
27440
+ },
27441
+ ...(product.rating && {
27442
+ aggregateRating: {
27443
+ '@type': 'AggregateRating',
27444
+ ratingValue: product.rating.value,
27445
+ reviewCount: product.rating.count,
27446
+ },
27447
+ }),
27448
+ };
27449
+ }
27450
+
27451
+ /**
27452
+ * Generate JSON-LD for FAQ pages
27453
+ */
27454
+ export function generateFAQJsonLd(items: { question: string; answer: string }[]) {
27455
+ return {
27456
+ '@context': 'https://schema.org',
27457
+ '@type': 'FAQPage',
27458
+ mainEntity: items.map(item => ({
27459
+ '@type': 'Question',
27460
+ name: item.question,
27461
+ acceptedAnswer: {
27462
+ '@type': 'Answer',
27463
+ text: item.answer,
27464
+ },
27465
+ })),
27466
+ };
27054
27467
  }`,
27055
- explanation: "Next.js Pages Router SEO component using next/head. Import and use on each page."
27468
+ explanation: "SEO utility functions for generating metadata and JSON-LD."
27469
+ }
27470
+ ]
27056
27471
  };
27057
27472
  }
27058
27473
  function generateNuxtSEOHead(options) {
27059
- const { siteName, siteUrl, title, description, image } = options;
27474
+ const { siteName, siteUrl, title, description, image, twitterHandle, locale } = options;
27060
27475
  return {
27061
27476
  file: "composables/useSEO.ts",
27062
- code: `export function useSEO(options: {
27477
+ code: `/**
27478
+ * Comprehensive SEO composable for Nuxt 3
27479
+ *
27480
+ * Features:
27481
+ * - Full Open Graph support
27482
+ * - Twitter Cards
27483
+ * - JSON-LD structured data
27484
+ * - Canonical URLs
27485
+ * - Robots directives
27486
+ * - Internationalization
27487
+ */
27488
+
27489
+ interface SEOOptions {
27063
27490
  title?: string;
27064
27491
  description?: string;
27065
27492
  image?: string;
27066
27493
  url?: string;
27067
- type?: 'website' | 'article';
27068
- } = {}) {
27494
+ type?: 'website' | 'article' | 'product';
27495
+
27496
+ // Article-specific
27497
+ publishedTime?: string;
27498
+ modifiedTime?: string;
27499
+ author?: string;
27500
+ tags?: string[];
27501
+
27502
+ // Robots
27503
+ noIndex?: boolean;
27504
+ noFollow?: boolean;
27505
+
27506
+ // Structured data
27507
+ schema?: Record<string, unknown> | Record<string, unknown>[];
27508
+ }
27509
+
27510
+ const SITE_NAME = '${siteName}';
27511
+ const SITE_URL = '${siteUrl}';
27512
+ const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
27513
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
27514
+ const DEFAULT_LOCALE = '${locale || "en_US"}';
27515
+ const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
27516
+
27517
+ export function useSEO(options: SEOOptions = {}) {
27069
27518
  const route = useRoute();
27070
- const config = useRuntimeConfig();
27071
27519
 
27072
- const defaults = {
27073
- title: '${title || siteName}',
27074
- description: '${description || `${siteName} - A compelling description.`}',
27075
- image: '${image || `${siteUrl}/og-image.png`}',
27076
- url: \`${siteUrl}\${route.path}\`,
27077
- type: 'website' as const,
27520
+ const {
27521
+ title,
27522
+ description = DEFAULT_DESCRIPTION,
27523
+ image = DEFAULT_IMAGE,
27524
+ url,
27525
+ type = 'website',
27526
+ publishedTime,
27527
+ modifiedTime,
27528
+ author,
27529
+ tags,
27530
+ noIndex = false,
27531
+ noFollow = false,
27532
+ schema,
27533
+ } = options;
27534
+
27535
+ const pageUrl = url || \`\${SITE_URL}\${route.path}\`;
27536
+ const fullTitle = title
27537
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
27538
+ : SITE_NAME;
27539
+ const imageUrl = image.startsWith('http') ? image : \`\${SITE_URL}\${image}\`;
27540
+
27541
+ const robotsContent = [
27542
+ noIndex ? 'noindex' : 'index',
27543
+ noFollow ? 'nofollow' : 'follow',
27544
+ ].join(', ');
27545
+
27546
+ // Build meta array
27547
+ const meta = [
27548
+ { name: 'description', content: description },
27549
+ { name: 'robots', content: robotsContent },
27550
+
27551
+ // Open Graph
27552
+ { property: 'og:type', content: type },
27553
+ { property: 'og:url', content: pageUrl },
27554
+ { property: 'og:title', content: fullTitle },
27555
+ { property: 'og:description', content: description },
27556
+ { property: 'og:image', content: imageUrl },
27557
+ { property: 'og:image:width', content: '1200' },
27558
+ { property: 'og:image:height', content: '630' },
27559
+ { property: 'og:site_name', content: SITE_NAME },
27560
+ { property: 'og:locale', content: DEFAULT_LOCALE },
27561
+
27562
+ // Twitter
27563
+ { name: 'twitter:card', content: 'summary_large_image' },
27564
+ { name: 'twitter:title', content: fullTitle },
27565
+ { name: 'twitter:description', content: description },
27566
+ { name: 'twitter:image', content: imageUrl },
27567
+ ];
27568
+
27569
+ // Add Twitter handle if configured
27570
+ if (TWITTER_HANDLE) {
27571
+ meta.push(
27572
+ { name: 'twitter:site', content: TWITTER_HANDLE },
27573
+ { name: 'twitter:creator', content: TWITTER_HANDLE }
27574
+ );
27575
+ }
27576
+
27577
+ // Add article-specific meta
27578
+ if (type === 'article') {
27579
+ if (publishedTime) meta.push({ property: 'article:published_time', content: publishedTime });
27580
+ if (modifiedTime) meta.push({ property: 'article:modified_time', content: modifiedTime });
27581
+ if (author) meta.push({ property: 'article:author', content: author });
27582
+ tags?.forEach(tag => meta.push({ property: 'article:tag', content: tag }));
27583
+ }
27584
+
27585
+ // Build JSON-LD
27586
+ const defaultSchema = {
27587
+ '@context': 'https://schema.org',
27588
+ '@type': 'WebSite',
27589
+ name: SITE_NAME,
27590
+ url: SITE_URL,
27078
27591
  };
27079
27592
 
27080
- const meta = { ...defaults, ...options };
27081
- const fullTitle = meta.title.includes('${siteName}')
27082
- ? meta.title
27083
- : \`\${meta.title} | ${siteName}\`;
27593
+ const jsonLd = schema
27594
+ ? Array.isArray(schema)
27595
+ ? [defaultSchema, ...schema]
27596
+ : [defaultSchema, schema]
27597
+ : [defaultSchema];
27084
27598
 
27085
27599
  useHead({
27086
27600
  title: fullTitle,
27087
- meta: [
27088
- { name: 'description', content: meta.description },
27089
- // Open Graph
27090
- { property: 'og:type', content: meta.type },
27091
- { property: 'og:url', content: meta.url },
27092
- { property: 'og:title', content: fullTitle },
27093
- { property: 'og:description', content: meta.description },
27094
- { property: 'og:image', content: meta.image },
27095
- { property: 'og:site_name', content: '${siteName}' },
27096
- // Twitter
27097
- { name: 'twitter:card', content: 'summary_large_image' },
27098
- { name: 'twitter:title', content: fullTitle },
27099
- { name: 'twitter:description', content: meta.description },
27100
- { name: 'twitter:image', content: meta.image },
27101
- ],
27601
+ meta,
27102
27602
  link: [
27103
- { rel: 'canonical', href: meta.url },
27603
+ { rel: 'canonical', href: pageUrl },
27604
+ ],
27605
+ script: [
27606
+ {
27607
+ type: 'application/ld+json',
27608
+ innerHTML: JSON.stringify(jsonLd),
27609
+ },
27104
27610
  ],
27105
27611
  });
27106
- }`,
27107
- explanation: "Nuxt 3 SEO composable using useHead(). Call useSEO() in any page to set meta tags."
27108
- };
27109
27612
  }
27110
- function generateNuxtPageExample() {
27111
- return {
27112
- file: "pages/about.vue",
27113
- code: `<script setup lang="ts">
27114
- useSEO({
27115
- title: 'About Us',
27116
- description: 'Learn more about our company and mission.',
27117
- });
27118
- </script>
27119
27613
 
27120
- <template>
27121
- <main>
27122
- <h1>About Us</h1>
27123
- <!-- Your content here -->
27124
- </main>
27125
- </template>`,
27126
- explanation: "Example Nuxt page using the useSEO composable."
27614
+ /**
27615
+ * Schema generators for common types
27616
+ */
27617
+ export const Schema = {
27618
+ article: (data: {
27619
+ headline: string;
27620
+ description: string;
27621
+ image: string;
27622
+ datePublished: string;
27623
+ dateModified?: string;
27624
+ author: { name: string; url?: string };
27625
+ }) => ({
27626
+ '@context': 'https://schema.org',
27627
+ '@type': 'Article',
27628
+ headline: data.headline,
27629
+ description: data.description,
27630
+ image: data.image,
27631
+ datePublished: data.datePublished,
27632
+ dateModified: data.dateModified || data.datePublished,
27633
+ author: { '@type': 'Person', ...data.author },
27634
+ publisher: {
27635
+ '@type': 'Organization',
27636
+ name: SITE_NAME,
27637
+ url: SITE_URL,
27638
+ },
27639
+ }),
27640
+
27641
+ product: (data: {
27642
+ name: string;
27643
+ description: string;
27644
+ image: string;
27645
+ price: number;
27646
+ currency?: string;
27647
+ availability?: 'InStock' | 'OutOfStock' | 'PreOrder';
27648
+ }) => ({
27649
+ '@context': 'https://schema.org',
27650
+ '@type': 'Product',
27651
+ name: data.name,
27652
+ description: data.description,
27653
+ image: data.image,
27654
+ offers: {
27655
+ '@type': 'Offer',
27656
+ price: data.price,
27657
+ priceCurrency: data.currency || 'USD',
27658
+ availability: \`https://schema.org/\${data.availability || 'InStock'}\`,
27659
+ },
27660
+ }),
27661
+
27662
+ faq: (items: { question: string; answer: string }[]) => ({
27663
+ '@context': 'https://schema.org',
27664
+ '@type': 'FAQPage',
27665
+ mainEntity: items.map(item => ({
27666
+ '@type': 'Question',
27667
+ name: item.question,
27668
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
27669
+ })),
27670
+ }),
27671
+
27672
+ breadcrumb: (items: { name: string; url: string }[]) => ({
27673
+ '@context': 'https://schema.org',
27674
+ '@type': 'BreadcrumbList',
27675
+ itemListElement: items.map((item, i) => ({
27676
+ '@type': 'ListItem',
27677
+ position: i + 1,
27678
+ name: item.name,
27679
+ item: item.url,
27680
+ })),
27681
+ }),
27682
+ };`,
27683
+ explanation: `Nuxt 3 comprehensive SEO composable with:
27684
+ \u2022 Full useHead integration
27685
+ \u2022 Open Graph with article support
27686
+ \u2022 Twitter Cards
27687
+ \u2022 JSON-LD schema generators
27688
+ \u2022 Robots directives
27689
+ \u2022 Canonical URLs
27690
+
27691
+ Usage: useSEO({ title: 'Page', description: '...' })`,
27692
+ additionalFiles: [
27693
+ {
27694
+ file: "server/routes/sitemap.xml.ts",
27695
+ code: `import { SitemapStream, streamToPromise } from 'sitemap';
27696
+ import { Readable } from 'stream';
27697
+
27698
+ export default defineEventHandler(async () => {
27699
+ const baseUrl = '${siteUrl}';
27700
+
27701
+ // Define your pages
27702
+ const pages = [
27703
+ { url: '/', changefreq: 'daily', priority: 1 },
27704
+ { url: '/about', changefreq: 'monthly', priority: 0.8 },
27705
+ { url: '/pricing', changefreq: 'weekly', priority: 0.9 },
27706
+ { url: '/blog', changefreq: 'daily', priority: 0.8 },
27707
+ ];
27708
+
27709
+ // Add dynamic pages from your database
27710
+ // const posts = await $fetch('/api/posts');
27711
+ // posts.forEach(post => pages.push({
27712
+ // url: \`/blog/\${post.slug}\`,
27713
+ // changefreq: 'weekly',
27714
+ // priority: 0.7,
27715
+ // lastmod: post.updatedAt,
27716
+ // }));
27717
+
27718
+ const stream = new SitemapStream({ hostname: baseUrl });
27719
+
27720
+ return streamToPromise(Readable.from(pages).pipe(stream)).then((data) =>
27721
+ data.toString()
27722
+ );
27723
+ });`,
27724
+ explanation: "Dynamic sitemap generator for Nuxt."
27725
+ },
27726
+ {
27727
+ file: "public/robots.txt",
27728
+ code: `User-agent: *
27729
+ Allow: /
27730
+ Disallow: /api/
27731
+ Disallow: /admin/
27732
+
27733
+ User-agent: GPTBot
27734
+ Allow: /
27735
+
27736
+ Sitemap: ${siteUrl}/sitemap.xml`,
27737
+ explanation: "Robots.txt with AI crawler support."
27738
+ }
27739
+ ]
27127
27740
  };
27128
27741
  }
27129
27742
  function generateVueSEOHead(options) {
27130
- const { siteName, siteUrl, title, description, image } = options;
27743
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
27131
27744
  return {
27132
27745
  file: "src/composables/useSEO.ts",
27133
- code: `import { useHead } from '@unhead/vue';
27134
- import { computed, ref } from 'vue';
27746
+ code: `import { useHead, useServerHead } from '@unhead/vue';
27747
+ import { computed, unref, MaybeRef } from 'vue';
27748
+ import { useRoute } from 'vue-router';
27135
27749
 
27136
27750
  interface SEOOptions {
27137
- title?: string;
27138
- description?: string;
27139
- image?: string;
27140
- url?: string;
27751
+ title?: MaybeRef<string>;
27752
+ description?: MaybeRef<string>;
27753
+ image?: MaybeRef<string>;
27141
27754
  type?: 'website' | 'article';
27755
+ noIndex?: boolean;
27756
+ schema?: Record<string, unknown>;
27142
27757
  }
27143
27758
 
27144
- export function useSEO(options: SEOOptions = {}) {
27145
- const defaults = {
27146
- title: '${title || siteName}',
27147
- description: '${description || `${siteName} - A compelling description.`}',
27148
- image: '${image || `${siteUrl}/og-image.png`}',
27149
- url: typeof window !== 'undefined' ? window.location.href : '${siteUrl}',
27150
- type: 'website' as const,
27151
- };
27152
-
27153
- const meta = { ...defaults, ...options };
27154
- const fullTitle = computed(() =>
27155
- meta.title.includes('${siteName}') ? meta.title : \`\${meta.title} | ${siteName}\`
27156
- );
27759
+ const SITE_NAME = '${siteName}';
27760
+ const SITE_URL = '${siteUrl}';
27761
+ const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
27762
+ const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
27763
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
27157
27764
 
27765
+ export function useSEO(options: SEOOptions = {}) {
27766
+ const route = useRoute();
27767
+
27768
+ const title = computed(() => {
27769
+ const t = unref(options.title);
27770
+ return t ? (t.includes(SITE_NAME) ? t : \`\${t} | \${SITE_NAME}\`) : SITE_NAME;
27771
+ });
27772
+
27773
+ const description = computed(() => unref(options.description) || DEFAULT_DESCRIPTION);
27774
+ const image = computed(() => {
27775
+ const img = unref(options.image) || DEFAULT_IMAGE;
27776
+ return img.startsWith('http') ? img : \`\${SITE_URL}\${img}\`;
27777
+ });
27778
+ const url = computed(() => \`\${SITE_URL}\${route.path}\`);
27779
+
27158
27780
  useHead({
27159
- title: fullTitle,
27781
+ title,
27160
27782
  meta: [
27161
- { name: 'description', content: meta.description },
27162
- { property: 'og:type', content: meta.type },
27163
- { property: 'og:url', content: meta.url },
27164
- { property: 'og:title', content: fullTitle.value },
27165
- { property: 'og:description', content: meta.description },
27166
- { property: 'og:image', content: meta.image },
27167
- { property: 'og:site_name', content: '${siteName}' },
27783
+ { name: 'description', content: description },
27784
+ { name: 'robots', content: options.noIndex ? 'noindex, nofollow' : 'index, follow' },
27785
+
27786
+ // Open Graph
27787
+ { property: 'og:type', content: options.type || 'website' },
27788
+ { property: 'og:url', content: url },
27789
+ { property: 'og:title', content: title },
27790
+ { property: 'og:description', content: description },
27791
+ { property: 'og:image', content: image },
27792
+ { property: 'og:site_name', content: SITE_NAME },
27793
+
27794
+ // Twitter
27168
27795
  { name: 'twitter:card', content: 'summary_large_image' },
27169
- { name: 'twitter:title', content: fullTitle.value },
27170
- { name: 'twitter:description', content: meta.description },
27171
- { name: 'twitter:image', content: meta.image },
27796
+ { name: 'twitter:title', content: title },
27797
+ { name: 'twitter:description', content: description },
27798
+ { name: 'twitter:image', content: image },
27799
+ ...(TWITTER_HANDLE ? [
27800
+ { name: 'twitter:site', content: TWITTER_HANDLE },
27801
+ { name: 'twitter:creator', content: TWITTER_HANDLE },
27802
+ ] : []),
27172
27803
  ],
27173
27804
  link: [
27174
- { rel: 'canonical', href: meta.url },
27805
+ { rel: 'canonical', href: url },
27175
27806
  ],
27807
+ script: options.schema ? [
27808
+ { type: 'application/ld+json', innerHTML: JSON.stringify(options.schema) },
27809
+ ] : [],
27176
27810
  });
27177
27811
  }`,
27178
- explanation: "Vue 3 SEO composable using @unhead/vue. Install with: npm install @unhead/vue",
27179
- installCommands: ["npm install @unhead/vue"]
27812
+ explanation: `Vue 3 SEO composable using @unhead/vue with:
27813
+ \u2022 Reactive title/description
27814
+ \u2022 Open Graph and Twitter Cards
27815
+ \u2022 JSON-LD schema support
27816
+ \u2022 Canonical URLs
27817
+
27818
+ Install: npm install @unhead/vue`,
27819
+ installCommands: ["npm install @unhead/vue"],
27820
+ additionalFiles: [
27821
+ {
27822
+ file: "src/main.ts",
27823
+ code: `import { createApp } from 'vue';
27824
+ import { createHead } from '@unhead/vue';
27825
+ import { createRouter, createWebHistory } from 'vue-router';
27826
+ import App from './App.vue';
27827
+
27828
+ const app = createApp(App);
27829
+ const head = createHead();
27830
+ const router = createRouter({
27831
+ history: createWebHistory(),
27832
+ routes: [/* your routes */],
27833
+ });
27834
+
27835
+ app.use(head);
27836
+ app.use(router);
27837
+ app.mount('#app');`,
27838
+ explanation: "Vue app setup with @unhead/vue."
27839
+ }
27840
+ ]
27180
27841
  };
27181
27842
  }
27182
27843
  function generateAstroBaseHead(options) {
27183
- const { siteName, siteUrl, title, description, image } = options;
27844
+ const { siteName, siteUrl, description, image, twitterHandle, locale } = options;
27184
27845
  return {
27185
27846
  file: "src/components/BaseHead.astro",
27186
27847
  code: `---
27848
+ /**
27849
+ * Comprehensive SEO Head Component for Astro
27850
+ *
27851
+ * Features:
27852
+ * - Full Open Graph support
27853
+ * - Twitter Cards
27854
+ * - JSON-LD structured data
27855
+ * - Canonical URLs
27856
+ * - Robots directives
27857
+ * - Performance optimizations
27858
+ */
27859
+
27187
27860
  interface Props {
27188
27861
  title?: string;
27189
27862
  description?: string;
27190
27863
  image?: string;
27191
27864
  type?: 'website' | 'article';
27865
+ publishedTime?: string;
27866
+ modifiedTime?: string;
27867
+ author?: string;
27868
+ tags?: string[];
27869
+ noIndex?: boolean;
27870
+ schema?: Record<string, unknown>;
27192
27871
  }
27193
27872
 
27873
+ const SITE_NAME = '${siteName}';
27874
+ const SITE_URL = '${siteUrl}';
27875
+ const DEFAULT_IMAGE = '${image || "/og-image.png"}';
27876
+ const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
27877
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
27878
+ const DEFAULT_LOCALE = '${locale || "en_US"}';
27879
+
27194
27880
  const {
27195
- title = '${title || siteName}',
27196
- description = '${description || `${siteName} - A compelling description.`}',
27197
- image = '${image || "/og-image.png"}',
27881
+ title,
27882
+ description = DEFAULT_DESCRIPTION,
27883
+ image = DEFAULT_IMAGE,
27198
27884
  type = 'website',
27885
+ publishedTime,
27886
+ modifiedTime,
27887
+ author,
27888
+ tags,
27889
+ noIndex = false,
27890
+ schema,
27199
27891
  } = Astro.props;
27200
27892
 
27201
- const canonicalURL = new URL(Astro.url.pathname, Astro.site);
27202
- const fullTitle = title.includes('${siteName}') ? title : \`\${title} | ${siteName}\`;
27203
- const imageURL = new URL(image, Astro.site);
27893
+ const canonicalURL = new URL(Astro.url.pathname, Astro.site || SITE_URL);
27894
+ const fullTitle = title
27895
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
27896
+ : SITE_NAME;
27897
+ const imageURL = new URL(image, Astro.site || SITE_URL);
27898
+ const robotsContent = noIndex ? 'noindex, nofollow' : 'index, follow';
27899
+
27900
+ // Default website schema
27901
+ const defaultSchema = {
27902
+ '@context': 'https://schema.org',
27903
+ '@type': 'WebSite',
27904
+ name: SITE_NAME,
27905
+ url: SITE_URL,
27906
+ };
27907
+
27908
+ const jsonLd = schema ? [defaultSchema, schema] : [defaultSchema];
27204
27909
  ---
27205
27910
 
27206
27911
  <!-- Global Metadata -->
27207
27912
  <meta charset="utf-8" />
27208
- <meta name="viewport" content="width=device-width, initial-scale=1" />
27209
- <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
27913
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
27210
27914
  <meta name="generator" content={Astro.generator} />
27211
27915
 
27916
+ <!-- Favicon -->
27917
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
27918
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
27919
+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
27920
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
27921
+ <link rel="manifest" href="/site.webmanifest" />
27922
+
27212
27923
  <!-- Canonical URL -->
27213
27924
  <link rel="canonical" href={canonicalURL} />
27214
27925
 
@@ -27216,6 +27927,11 @@ const imageURL = new URL(image, Astro.site);
27216
27927
  <title>{fullTitle}</title>
27217
27928
  <meta name="title" content={fullTitle} />
27218
27929
  <meta name="description" content={description} />
27930
+ <meta name="robots" content={robotsContent} />
27931
+
27932
+ <!-- Theme -->
27933
+ <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
27934
+ <meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)" />
27219
27935
 
27220
27936
  <!-- Open Graph / Facebook -->
27221
27937
  <meta property="og:type" content={type} />
@@ -27223,143 +27939,449 @@ const imageURL = new URL(image, Astro.site);
27223
27939
  <meta property="og:title" content={fullTitle} />
27224
27940
  <meta property="og:description" content={description} />
27225
27941
  <meta property="og:image" content={imageURL} />
27226
- <meta property="og:site_name" content="${siteName}" />
27942
+ <meta property="og:image:width" content="1200" />
27943
+ <meta property="og:image:height" content="630" />
27944
+ <meta property="og:image:alt" content={fullTitle} />
27945
+ <meta property="og:site_name" content={SITE_NAME} />
27946
+ <meta property="og:locale" content={DEFAULT_LOCALE} />
27947
+
27948
+ {type === 'article' && publishedTime && (
27949
+ <meta property="article:published_time" content={publishedTime} />
27950
+ )}
27951
+ {type === 'article' && modifiedTime && (
27952
+ <meta property="article:modified_time" content={modifiedTime} />
27953
+ )}
27954
+ {type === 'article' && author && (
27955
+ <meta property="article:author" content={author} />
27956
+ )}
27957
+ {type === 'article' && tags?.map((tag) => (
27958
+ <meta property="article:tag" content={tag} />
27959
+ ))}
27227
27960
 
27228
27961
  <!-- Twitter -->
27229
27962
  <meta name="twitter:card" content="summary_large_image" />
27230
27963
  <meta name="twitter:url" content={canonicalURL} />
27231
27964
  <meta name="twitter:title" content={fullTitle} />
27232
27965
  <meta name="twitter:description" content={description} />
27233
- <meta name="twitter:image" content={imageURL} />`,
27234
- explanation: 'Astro BaseHead component. Import in your layout: <BaseHead title="Page Title" description="..." />'
27235
- };
27236
- }
27237
- function generateAstroLayout(siteName) {
27238
- return {
27239
- file: "src/layouts/BaseLayout.astro",
27240
- code: `---
27966
+ <meta name="twitter:image" content={imageURL} />
27967
+ <meta name="twitter:image:alt" content={fullTitle} />
27968
+ {TWITTER_HANDLE && <meta name="twitter:site" content={TWITTER_HANDLE} />}
27969
+ {TWITTER_HANDLE && <meta name="twitter:creator" content={TWITTER_HANDLE} />}
27970
+
27971
+ <!-- Performance: Preconnect to external origins -->
27972
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
27973
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
27974
+
27975
+ <!-- JSON-LD Structured Data -->
27976
+ <script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />`,
27977
+ explanation: `Astro comprehensive SEO component with:
27978
+ \u2022 Full Open Graph with article support
27979
+ \u2022 Twitter Cards
27980
+ \u2022 JSON-LD structured data
27981
+ \u2022 Performance optimizations (preconnect)
27982
+ \u2022 Theme color for PWA
27983
+ \u2022 Favicon configuration
27984
+
27985
+ Usage: <BaseHead title="Page" description="..." />`,
27986
+ additionalFiles: [
27987
+ {
27988
+ file: "src/layouts/BaseLayout.astro",
27989
+ code: `---
27241
27990
  import BaseHead from '../components/BaseHead.astro';
27242
27991
 
27243
27992
  interface Props {
27244
27993
  title?: string;
27245
27994
  description?: string;
27246
27995
  image?: string;
27996
+ type?: 'website' | 'article';
27997
+ schema?: Record<string, unknown>;
27247
27998
  }
27248
27999
 
27249
- const { title, description, image } = Astro.props;
28000
+ const { title, description, image, type, schema } = Astro.props;
27250
28001
  ---
27251
28002
 
27252
28003
  <!DOCTYPE html>
27253
- <html lang="en">
28004
+ <html lang="${(locale || "en_US").split("_")[0]}">
27254
28005
  <head>
27255
- <BaseHead title={title} description={description} image={image} />
28006
+ <BaseHead
28007
+ title={title}
28008
+ description={description}
28009
+ image={image}
28010
+ type={type}
28011
+ schema={schema}
28012
+ />
27256
28013
  </head>
27257
28014
  <body>
27258
- <main>
27259
- <slot />
27260
- </main>
28015
+ <slot />
27261
28016
  </body>
27262
28017
  </html>`,
27263
- explanation: "Astro layout using BaseHead component. Use in pages with frontmatter."
28018
+ explanation: "Base layout using the SEO head component."
28019
+ },
28020
+ {
28021
+ file: "public/robots.txt",
28022
+ code: `User-agent: *
28023
+ Allow: /
28024
+ Disallow: /api/
28025
+
28026
+ User-agent: GPTBot
28027
+ Allow: /
28028
+
28029
+ Sitemap: ${siteUrl}/sitemap-index.xml`,
28030
+ explanation: "Robots.txt with AI crawler support."
28031
+ }
28032
+ ]
27264
28033
  };
27265
28034
  }
27266
28035
  function generateSvelteKitSEOHead(options) {
27267
- const { siteName, siteUrl, title, description, image } = options;
28036
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
27268
28037
  return {
27269
28038
  file: "src/lib/components/SEOHead.svelte",
27270
28039
  code: `<script lang="ts">
27271
28040
  import { page } from '$app/stores';
27272
28041
 
27273
- export let title = '${title || siteName}';
27274
- export let description = '${description || `${siteName} - A compelling description.`}';
27275
- export let image = '${image || `${siteUrl}/og-image.png`}';
28042
+ export let title: string | undefined = undefined;
28043
+ export let description: string = '${description || `${siteName} - A compelling description.`}';
28044
+ export let image: string = '${image || `${siteUrl}/og-image.png`}';
27276
28045
  export let type: 'website' | 'article' = 'website';
27277
-
27278
- $: fullTitle = title.includes('${siteName}') ? title : \`\${title} | ${siteName}\`;
28046
+ export let publishedTime: string | undefined = undefined;
28047
+ export let modifiedTime: string | undefined = undefined;
28048
+ export let author: string | undefined = undefined;
28049
+ export let tags: string[] = [];
28050
+ export let noIndex: boolean = false;
28051
+ export let schema: Record<string, unknown> | undefined = undefined;
28052
+
28053
+ const SITE_NAME = '${siteName}';
28054
+ const SITE_URL = '${siteUrl}';
28055
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
28056
+
28057
+ $: fullTitle = title
28058
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
28059
+ : SITE_NAME;
27279
28060
  $: canonicalUrl = $page.url.href;
28061
+ $: imageUrl = image.startsWith('http') ? image : \`\${SITE_URL}\${image}\`;
28062
+ $: robotsContent = noIndex ? 'noindex, nofollow' : 'index, follow';
28063
+
28064
+ $: defaultSchema = {
28065
+ '@context': 'https://schema.org',
28066
+ '@type': 'WebSite',
28067
+ name: SITE_NAME,
28068
+ url: SITE_URL,
28069
+ };
28070
+
28071
+ $: jsonLd = schema ? [defaultSchema, schema] : [defaultSchema];
27280
28072
  </script>
27281
28073
 
27282
28074
  <svelte:head>
28075
+ <!-- Primary Meta Tags -->
27283
28076
  <title>{fullTitle}</title>
28077
+ <meta name="title" content={fullTitle} />
27284
28078
  <meta name="description" content={description} />
28079
+ <meta name="robots" content={robotsContent} />
27285
28080
  <link rel="canonical" href={canonicalUrl} />
27286
-
28081
+
28082
+ <!-- Open Graph / Facebook -->
27287
28083
  <meta property="og:type" content={type} />
27288
28084
  <meta property="og:url" content={canonicalUrl} />
27289
28085
  <meta property="og:title" content={fullTitle} />
27290
28086
  <meta property="og:description" content={description} />
27291
- <meta property="og:image" content={image} />
27292
- <meta property="og:site_name" content="${siteName}" />
27293
-
28087
+ <meta property="og:image" content={imageUrl} />
28088
+ <meta property="og:image:width" content="1200" />
28089
+ <meta property="og:image:height" content="630" />
28090
+ <meta property="og:site_name" content={SITE_NAME} />
28091
+
28092
+ {#if type === 'article'}
28093
+ {#if publishedTime}
28094
+ <meta property="article:published_time" content={publishedTime} />
28095
+ {/if}
28096
+ {#if modifiedTime}
28097
+ <meta property="article:modified_time" content={modifiedTime} />
28098
+ {/if}
28099
+ {#if author}
28100
+ <meta property="article:author" content={author} />
28101
+ {/if}
28102
+ {#each tags as tag}
28103
+ <meta property="article:tag" content={tag} />
28104
+ {/each}
28105
+ {/if}
28106
+
28107
+ <!-- Twitter -->
27294
28108
  <meta name="twitter:card" content="summary_large_image" />
27295
28109
  <meta name="twitter:title" content={fullTitle} />
27296
28110
  <meta name="twitter:description" content={description} />
27297
- <meta name="twitter:image" content={image} />
28111
+ <meta name="twitter:image" content={imageUrl} />
28112
+ {#if TWITTER_HANDLE}
28113
+ <meta name="twitter:site" content={TWITTER_HANDLE} />
28114
+ <meta name="twitter:creator" content={TWITTER_HANDLE} />
28115
+ {/if}
28116
+
28117
+ <!-- JSON-LD Structured Data -->
28118
+ {@html \`<script type="application/ld+json">\${JSON.stringify(jsonLd)}</script>\`}
27298
28119
  </svelte:head>`,
27299
- explanation: "SvelteKit SEO component using svelte:head. Import and use in +page.svelte files."
28120
+ explanation: `SvelteKit comprehensive SEO component with:
28121
+ \u2022 Reactive props
28122
+ \u2022 Full Open Graph with article support
28123
+ \u2022 Twitter Cards
28124
+ \u2022 JSON-LD structured data
28125
+ \u2022 Robots directives
28126
+
28127
+ Usage: <SEOHead title="Page" description="..." />`,
28128
+ additionalFiles: [
28129
+ {
28130
+ file: "src/routes/+layout.svelte",
28131
+ code: `<script lang="ts">
28132
+ import '../app.css';
28133
+ </script>
28134
+
28135
+ <slot />`,
28136
+ explanation: "Root layout."
28137
+ },
28138
+ {
28139
+ file: "static/robots.txt",
28140
+ code: `User-agent: *
28141
+ Allow: /
28142
+ Disallow: /api/
28143
+
28144
+ User-agent: GPTBot
28145
+ Allow: /
28146
+
28147
+ Sitemap: ${siteUrl}/sitemap.xml`,
28148
+ explanation: "Robots.txt with AI crawler support."
28149
+ }
28150
+ ]
27300
28151
  };
27301
28152
  }
27302
28153
  function generateAngularSEOService(options) {
27303
- const { siteName, siteUrl, title, description, image } = options;
28154
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
27304
28155
  return {
27305
- file: "src/app/services/seo.service.ts",
27306
- code: `import { Injectable } from '@angular/core';
28156
+ file: "src/app/core/services/seo.service.ts",
28157
+ code: `import { Injectable, Inject } from '@angular/core';
27307
28158
  import { Meta, Title } from '@angular/platform-browser';
27308
- import { Router } from '@angular/router';
28159
+ import { Router, NavigationEnd } from '@angular/router';
28160
+ import { DOCUMENT } from '@angular/common';
28161
+ import { filter } from 'rxjs/operators';
27309
28162
 
27310
28163
  interface SEOConfig {
27311
28164
  title?: string;
27312
28165
  description?: string;
27313
28166
  image?: string;
27314
28167
  type?: 'website' | 'article';
28168
+ publishedTime?: string;
28169
+ modifiedTime?: string;
28170
+ author?: string;
28171
+ tags?: string[];
28172
+ noIndex?: boolean;
28173
+ schema?: Record<string, unknown>;
27315
28174
  }
27316
28175
 
27317
28176
  @Injectable({
27318
28177
  providedIn: 'root'
27319
28178
  })
27320
28179
  export class SEOService {
27321
- private siteName = '${siteName}';
27322
- private siteUrl = '${siteUrl}';
27323
- private defaultDescription = '${description || `${siteName} - A compelling description.`}';
27324
- private defaultImage = '${image || `${siteUrl}/og-image.png`}';
28180
+ private readonly siteName = '${siteName}';
28181
+ private readonly siteUrl = '${siteUrl}';
28182
+ private readonly defaultDescription = '${description || `${siteName} - A compelling description.`}';
28183
+ private readonly defaultImage = '${image || `${siteUrl}/og-image.png`}';
28184
+ private readonly twitterHandle = '${twitterHandle || ""}';
27325
28185
 
27326
28186
  constructor(
27327
28187
  private meta: Meta,
27328
28188
  private titleService: Title,
27329
- private router: Router
27330
- ) {}
28189
+ private router: Router,
28190
+ @Inject(DOCUMENT) private document: Document
28191
+ ) {
28192
+ // Update canonical URL on route change
28193
+ this.router.events.pipe(
28194
+ filter(event => event instanceof NavigationEnd)
28195
+ ).subscribe(() => {
28196
+ this.updateCanonical();
28197
+ });
28198
+ }
27331
28199
 
28200
+ /**
28201
+ * Update all SEO meta tags
28202
+ */
27332
28203
  updateMeta(config: SEOConfig = {}): void {
27333
- const title = config.title || this.siteName;
27334
- const fullTitle = title.includes(this.siteName) ? title : \`\${title} | \${this.siteName}\`;
27335
- const description = config.description || this.defaultDescription;
27336
- const image = config.image || this.defaultImage;
27337
- const url = this.siteUrl + this.router.url;
27338
- const type = config.type || 'website';
28204
+ const {
28205
+ title,
28206
+ description = this.defaultDescription,
28207
+ image = this.defaultImage,
28208
+ type = 'website',
28209
+ publishedTime,
28210
+ modifiedTime,
28211
+ author,
28212
+ tags,
28213
+ noIndex = false,
28214
+ schema,
28215
+ } = config;
28216
+
28217
+ const fullTitle = title
28218
+ ? (title.includes(this.siteName) ? title : \`\${title} | \${this.siteName}\`)
28219
+ : this.siteName;
28220
+ const pageUrl = this.siteUrl + this.router.url;
28221
+ const imageUrl = image.startsWith('http') ? image : \`\${this.siteUrl}\${image}\`;
28222
+ const robotsContent = noIndex ? 'noindex, nofollow' : 'index, follow';
27339
28223
 
27340
28224
  // Title
27341
28225
  this.titleService.setTitle(fullTitle);
27342
28226
 
27343
- // Primary Meta
27344
- this.meta.updateTag({ name: 'description', content: description });
27345
- this.meta.updateTag({ rel: 'canonical', href: url });
28227
+ // Primary Meta Tags
28228
+ this.setMetaTag('description', description);
28229
+ this.setMetaTag('robots', robotsContent);
27346
28230
 
27347
28231
  // Open Graph
27348
- this.meta.updateTag({ property: 'og:type', content: type });
27349
- this.meta.updateTag({ property: 'og:url', content: url });
27350
- this.meta.updateTag({ property: 'og:title', content: fullTitle });
27351
- this.meta.updateTag({ property: 'og:description', content: description });
27352
- this.meta.updateTag({ property: 'og:image', content: image });
27353
- this.meta.updateTag({ property: 'og:site_name', content: this.siteName });
28232
+ this.setMetaProperty('og:type', type);
28233
+ this.setMetaProperty('og:url', pageUrl);
28234
+ this.setMetaProperty('og:title', fullTitle);
28235
+ this.setMetaProperty('og:description', description);
28236
+ this.setMetaProperty('og:image', imageUrl);
28237
+ this.setMetaProperty('og:image:width', '1200');
28238
+ this.setMetaProperty('og:image:height', '630');
28239
+ this.setMetaProperty('og:site_name', this.siteName);
28240
+
28241
+ // Article-specific
28242
+ if (type === 'article') {
28243
+ if (publishedTime) this.setMetaProperty('article:published_time', publishedTime);
28244
+ if (modifiedTime) this.setMetaProperty('article:modified_time', modifiedTime);
28245
+ if (author) this.setMetaProperty('article:author', author);
28246
+ tags?.forEach(tag => this.setMetaProperty('article:tag', tag));
28247
+ }
27354
28248
 
27355
28249
  // Twitter
27356
- this.meta.updateTag({ name: 'twitter:card', content: 'summary_large_image' });
27357
- this.meta.updateTag({ name: 'twitter:title', content: fullTitle });
27358
- this.meta.updateTag({ name: 'twitter:description', content: description });
27359
- this.meta.updateTag({ name: 'twitter:image', content: image });
28250
+ this.setMetaTag('twitter:card', 'summary_large_image');
28251
+ this.setMetaTag('twitter:title', fullTitle);
28252
+ this.setMetaTag('twitter:description', description);
28253
+ this.setMetaTag('twitter:image', imageUrl);
28254
+ if (this.twitterHandle) {
28255
+ this.setMetaTag('twitter:site', this.twitterHandle);
28256
+ this.setMetaTag('twitter:creator', this.twitterHandle);
28257
+ }
28258
+
28259
+ // Update canonical
28260
+ this.updateCanonical(pageUrl);
28261
+
28262
+ // Update JSON-LD
28263
+ this.updateJsonLd(schema);
28264
+ }
28265
+
28266
+ private setMetaTag(name: string, content: string): void {
28267
+ this.meta.updateTag({ name, content });
28268
+ }
28269
+
28270
+ private setMetaProperty(property: string, content: string): void {
28271
+ this.meta.updateTag({ property, content });
28272
+ }
28273
+
28274
+ private updateCanonical(url?: string): void {
28275
+ const canonicalUrl = url || this.siteUrl + this.router.url;
28276
+ let link = this.document.querySelector('link[rel="canonical"]') as HTMLLinkElement;
28277
+
28278
+ if (!link) {
28279
+ link = this.document.createElement('link');
28280
+ link.setAttribute('rel', 'canonical');
28281
+ this.document.head.appendChild(link);
28282
+ }
28283
+
28284
+ link.setAttribute('href', canonicalUrl);
28285
+ }
28286
+
28287
+ private updateJsonLd(schema?: Record<string, unknown>): void {
28288
+ // Remove existing JSON-LD
28289
+ const existing = this.document.querySelector('script[type="application/ld+json"]');
28290
+ if (existing) existing.remove();
28291
+
28292
+ // Add new JSON-LD
28293
+ const defaultSchema = {
28294
+ '@context': 'https://schema.org',
28295
+ '@type': 'WebSite',
28296
+ name: this.siteName,
28297
+ url: this.siteUrl,
28298
+ };
28299
+
28300
+ const jsonLd = schema ? [defaultSchema, schema] : [defaultSchema];
28301
+
28302
+ const script = this.document.createElement('script');
28303
+ script.type = 'application/ld+json';
28304
+ script.text = JSON.stringify(jsonLd);
28305
+ this.document.head.appendChild(script);
28306
+ }
28307
+
28308
+ /**
28309
+ * Generate Article schema
28310
+ */
28311
+ articleSchema(data: {
28312
+ headline: string;
28313
+ description: string;
28314
+ image: string;
28315
+ datePublished: string;
28316
+ dateModified?: string;
28317
+ author: { name: string; url?: string };
28318
+ }): Record<string, unknown> {
28319
+ return {
28320
+ '@context': 'https://schema.org',
28321
+ '@type': 'Article',
28322
+ headline: data.headline,
28323
+ description: data.description,
28324
+ image: data.image,
28325
+ datePublished: data.datePublished,
28326
+ dateModified: data.dateModified || data.datePublished,
28327
+ author: { '@type': 'Person', ...data.author },
28328
+ publisher: {
28329
+ '@type': 'Organization',
28330
+ name: this.siteName,
28331
+ url: this.siteUrl,
28332
+ },
28333
+ };
28334
+ }
28335
+
28336
+ /**
28337
+ * Generate Product schema
28338
+ */
28339
+ productSchema(data: {
28340
+ name: string;
28341
+ description: string;
28342
+ image: string;
28343
+ price: number;
28344
+ currency?: string;
28345
+ }): Record<string, unknown> {
28346
+ return {
28347
+ '@context': 'https://schema.org',
28348
+ '@type': 'Product',
28349
+ name: data.name,
28350
+ description: data.description,
28351
+ image: data.image,
28352
+ offers: {
28353
+ '@type': 'Offer',
28354
+ price: data.price,
28355
+ priceCurrency: data.currency || 'USD',
28356
+ availability: 'https://schema.org/InStock',
28357
+ },
28358
+ };
28359
+ }
28360
+
28361
+ /**
28362
+ * Generate FAQ schema
28363
+ */
28364
+ faqSchema(items: { question: string; answer: string }[]): Record<string, unknown> {
28365
+ return {
28366
+ '@context': 'https://schema.org',
28367
+ '@type': 'FAQPage',
28368
+ mainEntity: items.map(item => ({
28369
+ '@type': 'Question',
28370
+ name: item.question,
28371
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
28372
+ })),
28373
+ };
27360
28374
  }
27361
28375
  }`,
27362
- explanation: "Angular SEO service using Meta and Title services. Inject and call updateMeta() in components."
28376
+ explanation: `Angular comprehensive SEO service with:
28377
+ \u2022 Meta and Title service integration
28378
+ \u2022 Dynamic canonical URL updates
28379
+ \u2022 Full Open Graph with article support
28380
+ \u2022 Twitter Cards
28381
+ \u2022 JSON-LD schema generators
28382
+ \u2022 Automatic route change handling
28383
+
28384
+ Usage: Inject SEOService and call updateMeta()`
27363
28385
  };
27364
28386
  }
27365
28387
  function getFrameworkSpecificFix(framework, options) {
@@ -27388,6 +28410,102 @@ function getFrameworkSpecificFix(framework, options) {
27388
28410
  }
27389
28411
  return generateReactSEOHead(options);
27390
28412
  }
28413
+ function generateNextJsPagesRouterHead(options) {
28414
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
28415
+ return {
28416
+ file: "components/SEOHead.tsx",
28417
+ code: `import Head from 'next/head';
28418
+ import { useRouter } from 'next/router';
28419
+
28420
+ interface SEOHeadProps {
28421
+ title?: string;
28422
+ description?: string;
28423
+ image?: string;
28424
+ type?: 'website' | 'article';
28425
+ publishedTime?: string;
28426
+ modifiedTime?: string;
28427
+ noIndex?: boolean;
28428
+ schema?: Record<string, unknown>;
28429
+ }
28430
+
28431
+ const SITE_NAME = '${siteName}';
28432
+ const SITE_URL = '${siteUrl}';
28433
+ const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
28434
+ const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
28435
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
28436
+
28437
+ export function SEOHead({
28438
+ title,
28439
+ description = DEFAULT_DESCRIPTION,
28440
+ image = DEFAULT_IMAGE,
28441
+ type = 'website',
28442
+ publishedTime,
28443
+ modifiedTime,
28444
+ noIndex = false,
28445
+ schema,
28446
+ }: SEOHeadProps) {
28447
+ const router = useRouter();
28448
+
28449
+ const fullTitle = title
28450
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
28451
+ : SITE_NAME;
28452
+ const pageUrl = \`\${SITE_URL}\${router.asPath}\`;
28453
+ const imageUrl = image.startsWith('http') ? image : \`\${SITE_URL}\${image}\`;
28454
+ const robotsContent = noIndex ? 'noindex, nofollow' : 'index, follow';
28455
+
28456
+ const defaultSchema = {
28457
+ '@context': 'https://schema.org',
28458
+ '@type': 'WebSite',
28459
+ name: SITE_NAME,
28460
+ url: SITE_URL,
28461
+ };
28462
+
28463
+ const jsonLd = schema ? [defaultSchema, schema] : [defaultSchema];
28464
+
28465
+ return (
28466
+ <Head>
28467
+ <title>{fullTitle}</title>
28468
+ <meta name="description" content={description} />
28469
+ <meta name="robots" content={robotsContent} />
28470
+ <link rel="canonical" href={pageUrl} />
28471
+
28472
+ <meta property="og:type" content={type} />
28473
+ <meta property="og:url" content={pageUrl} />
28474
+ <meta property="og:title" content={fullTitle} />
28475
+ <meta property="og:description" content={description} />
28476
+ <meta property="og:image" content={imageUrl} />
28477
+ <meta property="og:site_name" content={SITE_NAME} />
28478
+
28479
+ {type === 'article' && publishedTime && (
28480
+ <meta property="article:published_time" content={publishedTime} />
28481
+ )}
28482
+ {type === 'article' && modifiedTime && (
28483
+ <meta property="article:modified_time" content={modifiedTime} />
28484
+ )}
28485
+
28486
+ <meta name="twitter:card" content="summary_large_image" />
28487
+ <meta name="twitter:title" content={fullTitle} />
28488
+ <meta name="twitter:description" content={description} />
28489
+ <meta name="twitter:image" content={imageUrl} />
28490
+ {TWITTER_HANDLE && <meta name="twitter:site" content={TWITTER_HANDLE} />}
28491
+
28492
+ <script
28493
+ type="application/ld+json"
28494
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
28495
+ />
28496
+ </Head>
28497
+ );
28498
+ }`,
28499
+ explanation: `Next.js Pages Router SEO component with:
28500
+ \u2022 Full Open Graph support
28501
+ \u2022 Twitter Cards
28502
+ \u2022 JSON-LD structured data
28503
+ \u2022 Article metadata
28504
+ \u2022 Canonical URLs
28505
+
28506
+ Usage: <SEOHead title="Page" description="..." />`
28507
+ };
28508
+ }
27391
28509
 
27392
28510
  // src/fixer.ts
27393
28511
  async function generateFixes(issues, options) {
@@ -30624,7 +31742,6 @@ if (typeof globalThis !== "undefined") {
30624
31742
  generateAllFixes,
30625
31743
  generateAngularSEOService,
30626
31744
  generateAstroBaseHead,
30627
- generateAstroLayout,
30628
31745
  generateAstroMeta,
30629
31746
  generateBlogPost,
30630
31747
  generateBranchName,
@@ -30650,17 +31767,11 @@ if (typeof globalThis !== "undefined") {
30650
31767
  generateMarkdownReport,
30651
31768
  generateNextAppMetadata,
30652
31769
  generateNextJsAppRouterMetadata,
30653
- generateNextJsDynamicMetadata,
30654
- generateNextJsPageMetadata,
30655
31770
  generateNextJsPagesRouterHead,
30656
- generateNextJsRobots,
30657
- generateNextJsSitemap,
30658
31771
  generateNextPagesHead,
30659
- generateNuxtPageExample,
30660
31772
  generateNuxtSEOHead,
30661
31773
  generatePDFReport,
30662
31774
  generatePRDescription,
30663
- generateReactAppWrapper,
30664
31775
  generateReactHelmetSocialMeta,
30665
31776
  generateReactSEOHead,
30666
31777
  generateRecommendationQueries,