@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.mjs CHANGED
@@ -25653,61 +25653,332 @@ import { join as join6 } from "path";
25653
25653
 
25654
25654
  // src/fixer/framework-fixes.ts
25655
25655
  function generateReactSEOHead(options) {
25656
- const { siteName, siteUrl, title, description, image } = options;
25656
+ const { siteName, siteUrl, title, description, image, twitterHandle, locale } = options;
25657
25657
  return {
25658
25658
  file: "src/components/SEOHead.tsx",
25659
25659
  code: `import { Helmet } from 'react-helmet-async';
25660
25660
 
25661
+ /**
25662
+ * SEO Head Component
25663
+ *
25664
+ * Comprehensive SEO meta tags following best practices:
25665
+ * - Primary meta tags (title, description)
25666
+ * - Open Graph for Facebook/LinkedIn
25667
+ * - Twitter Card for X/Twitter
25668
+ * - JSON-LD structured data
25669
+ * - Canonical URLs
25670
+ *
25671
+ * @example
25672
+ * <SEOHead
25673
+ * title="Product Name"
25674
+ * description="Product description"
25675
+ * type="product"
25676
+ * schema={{
25677
+ * "@type": "Product",
25678
+ * name: "Product Name",
25679
+ * price: "99.00"
25680
+ * }}
25681
+ * />
25682
+ */
25683
+
25661
25684
  interface SEOHeadProps {
25685
+ // Required
25662
25686
  title?: string;
25663
25687
  description?: string;
25664
- image?: string;
25688
+
25689
+ // URLs
25665
25690
  url?: string;
25666
- type?: 'website' | 'article';
25691
+ canonical?: string;
25692
+ image?: string;
25693
+
25694
+ // Page type
25695
+ type?: 'website' | 'article' | 'product' | 'profile';
25696
+
25697
+ // Article-specific
25698
+ publishedTime?: string;
25699
+ modifiedTime?: string;
25700
+ author?: string;
25701
+ section?: string;
25702
+ tags?: string[];
25703
+
25704
+ // Twitter
25705
+ twitterCard?: 'summary' | 'summary_large_image' | 'player';
25706
+
25707
+ // Structured data
25708
+ schema?: Record<string, unknown> | Record<string, unknown>[];
25709
+
25710
+ // Robots
25711
+ noindex?: boolean;
25712
+ nofollow?: boolean;
25713
+
25714
+ // Alternate languages
25715
+ alternates?: { hrefLang: string; href: string }[];
25667
25716
  }
25668
25717
 
25718
+ const SITE_NAME = '${siteName}';
25719
+ const SITE_URL = '${siteUrl}';
25720
+ const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
25721
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
25722
+ const DEFAULT_LOCALE = '${locale || "en_US"}';
25723
+
25669
25724
  export function SEOHead({
25670
- title = '${title || `${siteName} - Your tagline here`}',
25725
+ title,
25671
25726
  description = '${description || `${siteName} - A compelling description of your product or service.`}',
25672
- image = '${image || `${siteUrl}/og-image.png`}',
25673
- url = typeof window !== 'undefined' ? window.location.href : '${siteUrl}',
25727
+ url,
25728
+ canonical,
25729
+ image = DEFAULT_IMAGE,
25674
25730
  type = 'website',
25731
+ publishedTime,
25732
+ modifiedTime,
25733
+ author,
25734
+ section,
25735
+ tags,
25736
+ twitterCard = 'summary_large_image',
25737
+ schema,
25738
+ noindex = false,
25739
+ nofollow = false,
25740
+ alternates,
25675
25741
  }: SEOHeadProps) {
25676
- const fullTitle = title.includes('${siteName}') ? title : \`\${title} | ${siteName}\`;
25742
+ const pageUrl = url || (typeof window !== 'undefined' ? window.location.href : SITE_URL);
25743
+ const canonicalUrl = canonical || pageUrl;
25744
+ const fullTitle = title
25745
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
25746
+ : SITE_NAME;
25747
+
25748
+ // Ensure image is absolute URL
25749
+ const imageUrl = image.startsWith('http') ? image : \`\${SITE_URL}\${image}\`;
25750
+
25751
+ // Build robots directive
25752
+ const robotsContent = [
25753
+ noindex ? 'noindex' : 'index',
25754
+ nofollow ? 'nofollow' : 'follow',
25755
+ ].join(', ');
25756
+
25757
+ // Default Organization schema
25758
+ const defaultSchema = {
25759
+ '@context': 'https://schema.org',
25760
+ '@type': 'WebSite',
25761
+ name: SITE_NAME,
25762
+ url: SITE_URL,
25763
+ };
25764
+
25765
+ // Merge with provided schema
25766
+ const jsonLd = schema
25767
+ ? Array.isArray(schema)
25768
+ ? [defaultSchema, ...schema]
25769
+ : [defaultSchema, schema]
25770
+ : [defaultSchema];
25677
25771
 
25678
25772
  return (
25679
25773
  <Helmet>
25680
25774
  {/* Primary Meta Tags */}
25681
25775
  <title>{fullTitle}</title>
25776
+ <meta name="title" content={fullTitle} />
25682
25777
  <meta name="description" content={description} />
25683
- <link rel="canonical" href={url} />
25778
+ <meta name="robots" content={robotsContent} />
25779
+ <link rel="canonical" href={canonicalUrl} />
25684
25780
 
25685
25781
  {/* Open Graph / Facebook */}
25686
25782
  <meta property="og:type" content={type} />
25687
- <meta property="og:url" content={url} />
25783
+ <meta property="og:url" content={pageUrl} />
25688
25784
  <meta property="og:title" content={fullTitle} />
25689
25785
  <meta property="og:description" content={description} />
25690
- <meta property="og:image" content={image} />
25691
- <meta property="og:site_name" content="${siteName}" />
25786
+ <meta property="og:image" content={imageUrl} />
25787
+ <meta property="og:image:width" content="1200" />
25788
+ <meta property="og:image:height" content="630" />
25789
+ <meta property="og:image:alt" content={fullTitle} />
25790
+ <meta property="og:site_name" content={SITE_NAME} />
25791
+ <meta property="og:locale" content={DEFAULT_LOCALE} />
25792
+
25793
+ {/* Article-specific Open Graph */}
25794
+ {type === 'article' && publishedTime && (
25795
+ <meta property="article:published_time" content={publishedTime} />
25796
+ )}
25797
+ {type === 'article' && modifiedTime && (
25798
+ <meta property="article:modified_time" content={modifiedTime} />
25799
+ )}
25800
+ {type === 'article' && author && (
25801
+ <meta property="article:author" content={author} />
25802
+ )}
25803
+ {type === 'article' && section && (
25804
+ <meta property="article:section" content={section} />
25805
+ )}
25806
+ {type === 'article' && tags?.map((tag, i) => (
25807
+ <meta key={i} property="article:tag" content={tag} />
25808
+ ))}
25692
25809
 
25693
25810
  {/* Twitter */}
25694
- <meta name="twitter:card" content="summary_large_image" />
25695
- <meta name="twitter:url" content={url} />
25811
+ <meta name="twitter:card" content={twitterCard} />
25812
+ <meta name="twitter:url" content={pageUrl} />
25696
25813
  <meta name="twitter:title" content={fullTitle} />
25697
25814
  <meta name="twitter:description" content={description} />
25698
- <meta name="twitter:image" content={image} />
25815
+ <meta name="twitter:image" content={imageUrl} />
25816
+ <meta name="twitter:image:alt" content={fullTitle} />
25817
+ {TWITTER_HANDLE && <meta name="twitter:site" content={TWITTER_HANDLE} />}
25818
+ {TWITTER_HANDLE && <meta name="twitter:creator" content={TWITTER_HANDLE} />}
25819
+
25820
+ {/* Alternate Languages */}
25821
+ {alternates?.map((alt, i) => (
25822
+ <link key={i} rel="alternate" hrefLang={alt.hrefLang} href={alt.href} />
25823
+ ))}
25824
+
25825
+ {/* JSON-LD Structured Data */}
25826
+ <script type="application/ld+json">
25827
+ {JSON.stringify(jsonLd)}
25828
+ </script>
25699
25829
  </Helmet>
25700
25830
  );
25701
- }`,
25702
- explanation: "React SEO component using react-helmet-async. Wrap your app in <HelmetProvider> and use <SEOHead /> on each page.",
25703
- installCommands: ["npm install react-helmet-async"],
25704
- imports: ["import { HelmetProvider } from 'react-helmet-async';"]
25705
- };
25706
25831
  }
25707
- function generateReactAppWrapper() {
25708
- return {
25709
- file: "src/main.tsx",
25710
- code: `import React from 'react';
25832
+
25833
+ /**
25834
+ * Pre-built schema generators for common page types
25835
+ */
25836
+ export const SchemaGenerators = {
25837
+ organization: (data: {
25838
+ name: string;
25839
+ url: string;
25840
+ logo?: string;
25841
+ sameAs?: string[];
25842
+ }) => ({
25843
+ '@context': 'https://schema.org',
25844
+ '@type': 'Organization',
25845
+ name: data.name,
25846
+ url: data.url,
25847
+ logo: data.logo,
25848
+ sameAs: data.sameAs,
25849
+ }),
25850
+
25851
+ article: (data: {
25852
+ headline: string;
25853
+ description: string;
25854
+ image: string;
25855
+ datePublished: string;
25856
+ dateModified?: string;
25857
+ author: { name: string; url?: string };
25858
+ }) => ({
25859
+ '@context': 'https://schema.org',
25860
+ '@type': 'Article',
25861
+ headline: data.headline,
25862
+ description: data.description,
25863
+ image: data.image,
25864
+ datePublished: data.datePublished,
25865
+ dateModified: data.dateModified || data.datePublished,
25866
+ author: {
25867
+ '@type': 'Person',
25868
+ name: data.author.name,
25869
+ url: data.author.url,
25870
+ },
25871
+ }),
25872
+
25873
+ product: (data: {
25874
+ name: string;
25875
+ description: string;
25876
+ image: string;
25877
+ price: string;
25878
+ currency?: string;
25879
+ availability?: 'InStock' | 'OutOfStock' | 'PreOrder';
25880
+ brand?: string;
25881
+ sku?: string;
25882
+ rating?: { value: number; count: number };
25883
+ }) => ({
25884
+ '@context': 'https://schema.org',
25885
+ '@type': 'Product',
25886
+ name: data.name,
25887
+ description: data.description,
25888
+ image: data.image,
25889
+ brand: data.brand ? { '@type': 'Brand', name: data.brand } : undefined,
25890
+ sku: data.sku,
25891
+ offers: {
25892
+ '@type': 'Offer',
25893
+ price: data.price,
25894
+ priceCurrency: data.currency || 'USD',
25895
+ availability: \`https://schema.org/\${data.availability || 'InStock'}\`,
25896
+ },
25897
+ aggregateRating: data.rating ? {
25898
+ '@type': 'AggregateRating',
25899
+ ratingValue: data.rating.value,
25900
+ reviewCount: data.rating.count,
25901
+ } : undefined,
25902
+ }),
25903
+
25904
+ faq: (items: { question: string; answer: string }[]) => ({
25905
+ '@context': 'https://schema.org',
25906
+ '@type': 'FAQPage',
25907
+ mainEntity: items.map(item => ({
25908
+ '@type': 'Question',
25909
+ name: item.question,
25910
+ acceptedAnswer: {
25911
+ '@type': 'Answer',
25912
+ text: item.answer,
25913
+ },
25914
+ })),
25915
+ }),
25916
+
25917
+ breadcrumb: (items: { name: string; url: string }[]) => ({
25918
+ '@context': 'https://schema.org',
25919
+ '@type': 'BreadcrumbList',
25920
+ itemListElement: items.map((item, index) => ({
25921
+ '@type': 'ListItem',
25922
+ position: index + 1,
25923
+ name: item.name,
25924
+ item: item.url,
25925
+ })),
25926
+ }),
25927
+
25928
+ localBusiness: (data: {
25929
+ name: string;
25930
+ description: string;
25931
+ url: string;
25932
+ phone: string;
25933
+ address: {
25934
+ street: string;
25935
+ city: string;
25936
+ state: string;
25937
+ zip: string;
25938
+ country: string;
25939
+ };
25940
+ geo?: { lat: number; lng: number };
25941
+ hours?: string[];
25942
+ priceRange?: string;
25943
+ }) => ({
25944
+ '@context': 'https://schema.org',
25945
+ '@type': 'LocalBusiness',
25946
+ name: data.name,
25947
+ description: data.description,
25948
+ url: data.url,
25949
+ telephone: data.phone,
25950
+ address: {
25951
+ '@type': 'PostalAddress',
25952
+ streetAddress: data.address.street,
25953
+ addressLocality: data.address.city,
25954
+ addressRegion: data.address.state,
25955
+ postalCode: data.address.zip,
25956
+ addressCountry: data.address.country,
25957
+ },
25958
+ geo: data.geo ? {
25959
+ '@type': 'GeoCoordinates',
25960
+ latitude: data.geo.lat,
25961
+ longitude: data.geo.lng,
25962
+ } : undefined,
25963
+ openingHours: data.hours,
25964
+ priceRange: data.priceRange,
25965
+ }),
25966
+ };`,
25967
+ explanation: `Comprehensive React SEO component with:
25968
+ \u2022 Full Open Graph support (including article metadata)
25969
+ \u2022 Twitter Cards with all variants
25970
+ \u2022 JSON-LD structured data with pre-built schema generators
25971
+ \u2022 Robots directives (noindex/nofollow)
25972
+ \u2022 Hreflang for internationalization
25973
+ \u2022 Canonical URL handling
25974
+
25975
+ Install: npm install react-helmet-async
25976
+ Wrap app: <HelmetProvider><App /></HelmetProvider>`,
25977
+ installCommands: ["npm install react-helmet-async"],
25978
+ additionalFiles: [
25979
+ {
25980
+ file: "src/main.tsx",
25981
+ code: `import React from 'react';
25711
25982
  import ReactDOM from 'react-dom/client';
25712
25983
  import { HelmetProvider } from 'react-helmet-async';
25713
25984
  import App from './App';
@@ -25720,29 +25991,63 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
25720
25991
  </HelmetProvider>
25721
25992
  </React.StrictMode>,
25722
25993
  );`,
25723
- explanation: "Updated main.tsx with HelmetProvider wrapper for react-helmet-async."
25994
+ explanation: "Updated main.tsx with HelmetProvider wrapper."
25995
+ }
25996
+ ]
25724
25997
  };
25725
25998
  }
25726
25999
  function generateNextJsAppRouterMetadata(options) {
25727
- const { siteName, siteUrl, title, description, image } = options;
26000
+ const { siteName, siteUrl, title, description, image, twitterHandle, locale } = options;
25728
26001
  return {
25729
26002
  file: "app/layout.tsx",
25730
- code: `import type { Metadata } from 'next';
26003
+ code: `import type { Metadata, Viewport } from 'next';
25731
26004
  import { Inter } from 'next/font/google';
25732
26005
  import './globals.css';
25733
26006
 
25734
- const inter = Inter({ subsets: ['latin'] });
26007
+ const inter = Inter({ subsets: ['latin'], display: 'swap' });
25735
26008
 
26009
+ /**
26010
+ * Default metadata for all pages
26011
+ * Individual pages can override with their own metadata export
26012
+ */
25736
26013
  export const metadata: Metadata = {
25737
26014
  metadataBase: new URL('${siteUrl}'),
26015
+
26016
+ // Default title with template
25738
26017
  title: {
25739
26018
  default: '${title || siteName}',
25740
26019
  template: \`%s | ${siteName}\`,
25741
26020
  },
26021
+
25742
26022
  description: '${description || `${siteName} - A compelling description of your product or service.`}',
26023
+
26024
+ // Indexing
26025
+ robots: {
26026
+ index: true,
26027
+ follow: true,
26028
+ googleBot: {
26029
+ index: true,
26030
+ follow: true,
26031
+ 'max-video-preview': -1,
26032
+ 'max-image-preview': 'large',
26033
+ 'max-snippet': -1,
26034
+ },
26035
+ },
26036
+
26037
+ // Icons
26038
+ icons: {
26039
+ icon: '/favicon.ico',
26040
+ shortcut: '/favicon-16x16.png',
26041
+ apple: '/apple-touch-icon.png',
26042
+ },
26043
+
26044
+ // Manifest
26045
+ manifest: '/site.webmanifest',
26046
+
26047
+ // Open Graph
25743
26048
  openGraph: {
25744
26049
  type: 'website',
25745
- locale: 'en_US',
26050
+ locale: '${locale || "en_US"}',
25746
26051
  url: '${siteUrl}',
25747
26052
  siteName: '${siteName}',
25748
26053
  title: '${title || siteName}',
@@ -25756,16 +26061,49 @@ export const metadata: Metadata = {
25756
26061
  },
25757
26062
  ],
25758
26063
  },
26064
+
26065
+ // Twitter
25759
26066
  twitter: {
25760
26067
  card: 'summary_large_image',
25761
26068
  title: '${title || siteName}',
25762
26069
  description: '${description || `${siteName} - A compelling description.`}',
25763
26070
  images: ['${image || "/og-image.png"}'],
26071
+ ${twitterHandle ? `site: '${twitterHandle}',
26072
+ creator: '${twitterHandle}',` : ""}
25764
26073
  },
25765
- robots: {
25766
- index: true,
25767
- follow: true,
26074
+
26075
+ // Verification (add your IDs)
26076
+ verification: {
26077
+ // google: 'your-google-verification-code',
26078
+ // yandex: 'your-yandex-verification-code',
26079
+ // bing: 'your-bing-verification-code',
25768
26080
  },
26081
+
26082
+ // Alternate languages (uncomment and customize)
26083
+ // alternates: {
26084
+ // canonical: '${siteUrl}',
26085
+ // languages: {
26086
+ // 'en-US': '${siteUrl}/en',
26087
+ // 'es-ES': '${siteUrl}/es',
26088
+ // },
26089
+ // },
26090
+
26091
+ // Category
26092
+ category: 'technology',
26093
+ };
26094
+
26095
+ /**
26096
+ * Viewport configuration
26097
+ * Separated from metadata in Next.js 14+
26098
+ */
26099
+ export const viewport: Viewport = {
26100
+ themeColor: [
26101
+ { media: '(prefers-color-scheme: light)', color: '#ffffff' },
26102
+ { media: '(prefers-color-scheme: dark)', color: '#000000' },
26103
+ ],
26104
+ width: 'device-width',
26105
+ initialScale: 1,
26106
+ maximumScale: 5,
25769
26107
  };
25770
26108
 
25771
26109
  export default function RootLayout({
@@ -25774,143 +26112,104 @@ export default function RootLayout({
25774
26112
  children: React.ReactNode;
25775
26113
  }) {
25776
26114
  return (
25777
- <html lang="en">
25778
- <body className={inter.className}>{children}</body>
26115
+ <html lang="${(locale || "en_US").split("_")[0]}" className={inter.className}>
26116
+ <body>
26117
+ {children}
26118
+
26119
+ {/* JSON-LD Organization Schema */}
26120
+ <script
26121
+ type="application/ld+json"
26122
+ dangerouslySetInnerHTML={{
26123
+ __html: JSON.stringify({
26124
+ '@context': 'https://schema.org',
26125
+ '@type': 'Organization',
26126
+ name: '${siteName}',
26127
+ url: '${siteUrl}',
26128
+ logo: '${siteUrl}/logo.png',
26129
+ sameAs: [
26130
+ // Add your social profiles
26131
+ // 'https://twitter.com/yourhandle',
26132
+ // 'https://linkedin.com/company/yourcompany',
26133
+ ],
26134
+ }),
26135
+ }}
26136
+ />
26137
+ </body>
25779
26138
  </html>
25780
26139
  );
25781
26140
  }`,
25782
- explanation: "Next.js App Router layout with built-in Metadata API. Each page can override with its own metadata export."
25783
- };
25784
- }
25785
- function generateNextJsPageMetadata(options) {
25786
- const { siteName, description, pageName } = options;
25787
- return {
25788
- file: `app/${pageName}/page.tsx`,
25789
- code: `import type { Metadata } from 'next';
25790
-
25791
- export const metadata: Metadata = {
25792
- title: '${pageName.charAt(0).toUpperCase() + pageName.slice(1)}',
25793
- description: '${description || `Learn more about ${pageName} on ${siteName}.`}',
25794
- };
25795
-
25796
- export default function ${pageName.charAt(0).toUpperCase() + pageName.slice(1)}Page() {
25797
- return (
25798
- <main>
25799
- <h1>${pageName.charAt(0).toUpperCase() + pageName.slice(1)}</h1>
25800
- {/* Your content here */}
25801
- </main>
25802
- );
25803
- }`,
25804
- explanation: `Next.js page with metadata export. The title will be "${pageName} | ${siteName}" using the template.`
25805
- };
25806
- }
25807
- function generateNextJsDynamicMetadata() {
25808
- return {
25809
- file: "app/[slug]/page.tsx",
25810
- code: `import type { Metadata } from 'next';
25811
-
25812
- interface PageProps {
25813
- params: { slug: string };
25814
- }
25815
-
25816
- // Generate metadata dynamically based on the slug
25817
- export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
25818
- // Fetch data based on slug (replace with your data fetching logic)
25819
- const data = await fetchPageData(params.slug);
25820
-
25821
- return {
25822
- title: data.title,
25823
- description: data.description,
25824
- openGraph: {
25825
- title: data.title,
25826
- description: data.description,
25827
- images: data.image ? [{ url: data.image }] : [],
25828
- },
25829
- };
25830
- }
25831
-
25832
- // Pre-generate static pages for known slugs (improves SEO)
25833
- export async function generateStaticParams() {
25834
- const pages = await fetchAllPageSlugs();
25835
- return pages.map((slug) => ({ slug }));
25836
- }
25837
-
25838
- async function fetchPageData(slug: string) {
25839
- // Replace with your actual data fetching
25840
- return {
25841
- title: slug.replace(/-/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()),
25842
- description: \`Learn about \${slug}\`,
25843
- image: null,
25844
- };
25845
- }
25846
-
25847
- async function fetchAllPageSlugs() {
25848
- // Replace with your actual data source
25849
- return ['about', 'contact', 'pricing'];
25850
- }
25851
-
25852
- export default function DynamicPage({ params }: PageProps) {
25853
- return (
25854
- <main>
25855
- <h1>{params.slug}</h1>
25856
- </main>
25857
- );
25858
- }`,
25859
- explanation: "Next.js dynamic route with generateMetadata and generateStaticParams for SEO-optimized dynamic pages."
25860
- };
25861
- }
25862
- function generateNextJsRobots(siteUrl) {
25863
- return {
25864
- file: "app/robots.ts",
25865
- code: `import type { MetadataRoute } from 'next';
26141
+ explanation: `Next.js App Router layout with comprehensive SEO:
26142
+ \u2022 Metadata API with title templates
26143
+ \u2022 Full Open Graph and Twitter Card support
26144
+ \u2022 Viewport configuration (Next.js 14+)
26145
+ \u2022 JSON-LD Organization schema
26146
+ \u2022 Verification tags for search consoles
26147
+ \u2022 Internationalization ready
26148
+ \u2022 Web font optimization with next/font`,
26149
+ additionalFiles: [
26150
+ {
26151
+ file: "app/robots.ts",
26152
+ code: `import type { MetadataRoute } from 'next';
25866
26153
 
25867
26154
  export default function robots(): MetadataRoute.Robots {
26155
+ const baseUrl = '${siteUrl}';
26156
+
25868
26157
  return {
25869
26158
  rules: [
25870
26159
  {
25871
26160
  userAgent: '*',
25872
26161
  allow: '/',
25873
- disallow: ['/api/', '/admin/', '/_next/'],
26162
+ disallow: ['/api/', '/admin/', '/_next/', '/private/'],
26163
+ },
26164
+ {
26165
+ userAgent: 'GPTBot',
26166
+ allow: '/',
25874
26167
  },
25875
26168
  ],
25876
- sitemap: '${siteUrl}/sitemap.xml',
26169
+ sitemap: \`\${baseUrl}/sitemap.xml\`,
25877
26170
  };
25878
26171
  }`,
25879
- explanation: "Next.js App Router robots.txt generator. Automatically served at /robots.txt."
25880
- };
25881
- }
25882
- function generateNextJsSitemap(siteUrl) {
25883
- return {
25884
- file: "app/sitemap.ts",
25885
- code: `import type { MetadataRoute } from 'next';
26172
+ explanation: "Robots.txt with AI crawler support."
26173
+ },
26174
+ {
26175
+ file: "app/sitemap.ts",
26176
+ code: `import type { MetadataRoute } from 'next';
25886
26177
 
25887
26178
  export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
26179
+ const baseUrl = '${siteUrl}';
26180
+
25888
26181
  // Static pages
25889
26182
  const staticPages: MetadataRoute.Sitemap = [
25890
26183
  {
25891
- url: '${siteUrl}',
26184
+ url: baseUrl,
25892
26185
  lastModified: new Date(),
25893
26186
  changeFrequency: 'daily',
25894
26187
  priority: 1,
25895
26188
  },
25896
26189
  {
25897
- url: '${siteUrl}/about',
26190
+ url: \`\${baseUrl}/about\`,
25898
26191
  lastModified: new Date(),
25899
26192
  changeFrequency: 'monthly',
25900
26193
  priority: 0.8,
25901
26194
  },
25902
26195
  {
25903
- url: '${siteUrl}/pricing',
26196
+ url: \`\${baseUrl}/pricing\`,
25904
26197
  lastModified: new Date(),
25905
26198
  changeFrequency: 'weekly',
25906
26199
  priority: 0.9,
25907
26200
  },
26201
+ {
26202
+ url: \`\${baseUrl}/blog\`,
26203
+ lastModified: new Date(),
26204
+ changeFrequency: 'daily',
26205
+ priority: 0.8,
26206
+ },
25908
26207
  ];
25909
26208
 
25910
- // Dynamic pages (fetch from your database/CMS)
25911
- // const posts = await fetchAllPosts();
26209
+ // Dynamic pages - fetch from your database/CMS
26210
+ // const posts = await db.post.findMany({ select: { slug: true, updatedAt: true } });
25912
26211
  // const dynamicPages = posts.map((post) => ({
25913
- // url: \`${siteUrl}/blog/\${post.slug}\`,
26212
+ // url: \`\${baseUrl}/blog/\${post.slug}\`,
25914
26213
  // lastModified: post.updatedAt,
25915
26214
  // changeFrequency: 'weekly' as const,
25916
26215
  // priority: 0.7,
@@ -25918,209 +26217,628 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
25918
26217
 
25919
26218
  return [...staticPages];
25920
26219
  }`,
25921
- explanation: "Next.js App Router sitemap generator. Automatically served at /sitemap.xml."
25922
- };
25923
- }
25924
- function generateNextJsPagesRouterHead(options) {
25925
- const { siteName, siteUrl, title, description, image } = options;
25926
- return {
25927
- file: "components/SEOHead.tsx",
25928
- code: `import Head from 'next/head';
26220
+ explanation: "Dynamic sitemap generator."
26221
+ },
26222
+ {
26223
+ file: "lib/seo.ts",
26224
+ code: `import type { Metadata } from 'next';
25929
26225
 
25930
- interface SEOHeadProps {
25931
- title?: string;
25932
- description?: string;
26226
+ const baseUrl = '${siteUrl}';
26227
+ const siteName = '${siteName}';
26228
+
26229
+ interface PageSEOProps {
26230
+ title: string;
26231
+ description: string;
26232
+ path?: string;
25933
26233
  image?: string;
25934
- url?: string;
25935
26234
  type?: 'website' | 'article';
26235
+ publishedTime?: string;
26236
+ modifiedTime?: string;
26237
+ authors?: string[];
26238
+ tags?: string[];
26239
+ noIndex?: boolean;
26240
+ }
26241
+
26242
+ /**
26243
+ * Generate metadata for a page
26244
+ * Use in page.tsx: export const metadata = generateMetadata({ ... })
26245
+ */
26246
+ export function generatePageMetadata({
26247
+ title,
26248
+ description,
26249
+ path = '',
26250
+ image,
26251
+ type = 'website',
26252
+ publishedTime,
26253
+ modifiedTime,
26254
+ authors,
26255
+ tags,
26256
+ noIndex = false,
26257
+ }: PageSEOProps): Metadata {
26258
+ const url = \`\${baseUrl}\${path}\`;
26259
+ const ogImage = image || '/og-image.png';
26260
+
26261
+ return {
26262
+ title,
26263
+ description,
26264
+
26265
+ robots: noIndex ? { index: false, follow: false } : undefined,
26266
+
26267
+ alternates: {
26268
+ canonical: url,
26269
+ },
26270
+
26271
+ openGraph: {
26272
+ title,
26273
+ description,
26274
+ url,
26275
+ siteName,
26276
+ type,
26277
+ images: [{ url: ogImage, width: 1200, height: 630 }],
26278
+ ...(type === 'article' && {
26279
+ publishedTime,
26280
+ modifiedTime,
26281
+ authors,
26282
+ tags,
26283
+ }),
26284
+ },
26285
+
26286
+ twitter: {
26287
+ card: 'summary_large_image',
26288
+ title,
26289
+ description,
26290
+ images: [ogImage],
26291
+ },
26292
+ };
25936
26293
  }
25937
26294
 
25938
- export function SEOHead({
25939
- title = '${title || siteName}',
25940
- description = '${description || `${siteName} - A compelling description.`}',
25941
- image = '${image || `${siteUrl}/og-image.png`}',
25942
- url = '${siteUrl}',
25943
- type = 'website',
25944
- }: SEOHeadProps) {
25945
- const fullTitle = title.includes('${siteName}') ? title : \`\${title} | ${siteName}\`;
26295
+ /**
26296
+ * Generate JSON-LD for articles
26297
+ */
26298
+ export function generateArticleJsonLd(article: {
26299
+ title: string;
26300
+ description: string;
26301
+ url: string;
26302
+ image: string;
26303
+ datePublished: string;
26304
+ dateModified?: string;
26305
+ author: { name: string; url?: string };
26306
+ }) {
26307
+ return {
26308
+ '@context': 'https://schema.org',
26309
+ '@type': 'Article',
26310
+ headline: article.title,
26311
+ description: article.description,
26312
+ url: article.url,
26313
+ image: article.image,
26314
+ datePublished: article.datePublished,
26315
+ dateModified: article.dateModified || article.datePublished,
26316
+ author: {
26317
+ '@type': 'Person',
26318
+ name: article.author.name,
26319
+ url: article.author.url,
26320
+ },
26321
+ publisher: {
26322
+ '@type': 'Organization',
26323
+ name: siteName,
26324
+ logo: {
26325
+ '@type': 'ImageObject',
26326
+ url: \`\${baseUrl}/logo.png\`,
26327
+ },
26328
+ },
26329
+ };
26330
+ }
25946
26331
 
25947
- return (
25948
- <Head>
25949
- <title>{fullTitle}</title>
25950
- <meta name="description" content={description} />
25951
- <link rel="canonical" href={url} />
25952
-
25953
- <meta property="og:type" content={type} />
25954
- <meta property="og:url" content={url} />
25955
- <meta property="og:title" content={fullTitle} />
25956
- <meta property="og:description" content={description} />
25957
- <meta property="og:image" content={image} />
25958
- <meta property="og:site_name" content="${siteName}" />
25959
-
25960
- <meta name="twitter:card" content="summary_large_image" />
25961
- <meta name="twitter:title" content={fullTitle} />
25962
- <meta name="twitter:description" content={description} />
25963
- <meta name="twitter:image" content={image} />
25964
- </Head>
25965
- );
26332
+ /**
26333
+ * Generate JSON-LD for products
26334
+ */
26335
+ export function generateProductJsonLd(product: {
26336
+ name: string;
26337
+ description: string;
26338
+ image: string;
26339
+ price: number;
26340
+ currency?: string;
26341
+ availability?: 'InStock' | 'OutOfStock' | 'PreOrder';
26342
+ rating?: { value: number; count: number };
26343
+ brand?: string;
26344
+ sku?: string;
26345
+ }) {
26346
+ return {
26347
+ '@context': 'https://schema.org',
26348
+ '@type': 'Product',
26349
+ name: product.name,
26350
+ description: product.description,
26351
+ image: product.image,
26352
+ brand: product.brand ? { '@type': 'Brand', name: product.brand } : undefined,
26353
+ sku: product.sku,
26354
+ offers: {
26355
+ '@type': 'Offer',
26356
+ price: product.price,
26357
+ priceCurrency: product.currency || 'USD',
26358
+ availability: \`https://schema.org/\${product.availability || 'InStock'}\`,
26359
+ },
26360
+ ...(product.rating && {
26361
+ aggregateRating: {
26362
+ '@type': 'AggregateRating',
26363
+ ratingValue: product.rating.value,
26364
+ reviewCount: product.rating.count,
26365
+ },
26366
+ }),
26367
+ };
26368
+ }
26369
+
26370
+ /**
26371
+ * Generate JSON-LD for FAQ pages
26372
+ */
26373
+ export function generateFAQJsonLd(items: { question: string; answer: string }[]) {
26374
+ return {
26375
+ '@context': 'https://schema.org',
26376
+ '@type': 'FAQPage',
26377
+ mainEntity: items.map(item => ({
26378
+ '@type': 'Question',
26379
+ name: item.question,
26380
+ acceptedAnswer: {
26381
+ '@type': 'Answer',
26382
+ text: item.answer,
26383
+ },
26384
+ })),
26385
+ };
25966
26386
  }`,
25967
- explanation: "Next.js Pages Router SEO component using next/head. Import and use on each page."
26387
+ explanation: "SEO utility functions for generating metadata and JSON-LD."
26388
+ }
26389
+ ]
25968
26390
  };
25969
26391
  }
25970
26392
  function generateNuxtSEOHead(options) {
25971
- const { siteName, siteUrl, title, description, image } = options;
26393
+ const { siteName, siteUrl, title, description, image, twitterHandle, locale } = options;
25972
26394
  return {
25973
26395
  file: "composables/useSEO.ts",
25974
- code: `export function useSEO(options: {
26396
+ code: `/**
26397
+ * Comprehensive SEO composable for Nuxt 3
26398
+ *
26399
+ * Features:
26400
+ * - Full Open Graph support
26401
+ * - Twitter Cards
26402
+ * - JSON-LD structured data
26403
+ * - Canonical URLs
26404
+ * - Robots directives
26405
+ * - Internationalization
26406
+ */
26407
+
26408
+ interface SEOOptions {
25975
26409
  title?: string;
25976
26410
  description?: string;
25977
26411
  image?: string;
25978
26412
  url?: string;
25979
- type?: 'website' | 'article';
25980
- } = {}) {
26413
+ type?: 'website' | 'article' | 'product';
26414
+
26415
+ // Article-specific
26416
+ publishedTime?: string;
26417
+ modifiedTime?: string;
26418
+ author?: string;
26419
+ tags?: string[];
26420
+
26421
+ // Robots
26422
+ noIndex?: boolean;
26423
+ noFollow?: boolean;
26424
+
26425
+ // Structured data
26426
+ schema?: Record<string, unknown> | Record<string, unknown>[];
26427
+ }
26428
+
26429
+ const SITE_NAME = '${siteName}';
26430
+ const SITE_URL = '${siteUrl}';
26431
+ const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
26432
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
26433
+ const DEFAULT_LOCALE = '${locale || "en_US"}';
26434
+ const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
26435
+
26436
+ export function useSEO(options: SEOOptions = {}) {
25981
26437
  const route = useRoute();
25982
- const config = useRuntimeConfig();
25983
26438
 
25984
- const defaults = {
25985
- title: '${title || siteName}',
25986
- description: '${description || `${siteName} - A compelling description.`}',
25987
- image: '${image || `${siteUrl}/og-image.png`}',
25988
- url: \`${siteUrl}\${route.path}\`,
25989
- type: 'website' as const,
26439
+ const {
26440
+ title,
26441
+ description = DEFAULT_DESCRIPTION,
26442
+ image = DEFAULT_IMAGE,
26443
+ url,
26444
+ type = 'website',
26445
+ publishedTime,
26446
+ modifiedTime,
26447
+ author,
26448
+ tags,
26449
+ noIndex = false,
26450
+ noFollow = false,
26451
+ schema,
26452
+ } = options;
26453
+
26454
+ const pageUrl = url || \`\${SITE_URL}\${route.path}\`;
26455
+ const fullTitle = title
26456
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
26457
+ : SITE_NAME;
26458
+ const imageUrl = image.startsWith('http') ? image : \`\${SITE_URL}\${image}\`;
26459
+
26460
+ const robotsContent = [
26461
+ noIndex ? 'noindex' : 'index',
26462
+ noFollow ? 'nofollow' : 'follow',
26463
+ ].join(', ');
26464
+
26465
+ // Build meta array
26466
+ const meta = [
26467
+ { name: 'description', content: description },
26468
+ { name: 'robots', content: robotsContent },
26469
+
26470
+ // Open Graph
26471
+ { property: 'og:type', content: type },
26472
+ { property: 'og:url', content: pageUrl },
26473
+ { property: 'og:title', content: fullTitle },
26474
+ { property: 'og:description', content: description },
26475
+ { property: 'og:image', content: imageUrl },
26476
+ { property: 'og:image:width', content: '1200' },
26477
+ { property: 'og:image:height', content: '630' },
26478
+ { property: 'og:site_name', content: SITE_NAME },
26479
+ { property: 'og:locale', content: DEFAULT_LOCALE },
26480
+
26481
+ // Twitter
26482
+ { name: 'twitter:card', content: 'summary_large_image' },
26483
+ { name: 'twitter:title', content: fullTitle },
26484
+ { name: 'twitter:description', content: description },
26485
+ { name: 'twitter:image', content: imageUrl },
26486
+ ];
26487
+
26488
+ // Add Twitter handle if configured
26489
+ if (TWITTER_HANDLE) {
26490
+ meta.push(
26491
+ { name: 'twitter:site', content: TWITTER_HANDLE },
26492
+ { name: 'twitter:creator', content: TWITTER_HANDLE }
26493
+ );
26494
+ }
26495
+
26496
+ // Add article-specific meta
26497
+ if (type === 'article') {
26498
+ if (publishedTime) meta.push({ property: 'article:published_time', content: publishedTime });
26499
+ if (modifiedTime) meta.push({ property: 'article:modified_time', content: modifiedTime });
26500
+ if (author) meta.push({ property: 'article:author', content: author });
26501
+ tags?.forEach(tag => meta.push({ property: 'article:tag', content: tag }));
26502
+ }
26503
+
26504
+ // Build JSON-LD
26505
+ const defaultSchema = {
26506
+ '@context': 'https://schema.org',
26507
+ '@type': 'WebSite',
26508
+ name: SITE_NAME,
26509
+ url: SITE_URL,
25990
26510
  };
25991
26511
 
25992
- const meta = { ...defaults, ...options };
25993
- const fullTitle = meta.title.includes('${siteName}')
25994
- ? meta.title
25995
- : \`\${meta.title} | ${siteName}\`;
26512
+ const jsonLd = schema
26513
+ ? Array.isArray(schema)
26514
+ ? [defaultSchema, ...schema]
26515
+ : [defaultSchema, schema]
26516
+ : [defaultSchema];
25996
26517
 
25997
26518
  useHead({
25998
26519
  title: fullTitle,
25999
- meta: [
26000
- { name: 'description', content: meta.description },
26001
- // Open Graph
26002
- { property: 'og:type', content: meta.type },
26003
- { property: 'og:url', content: meta.url },
26004
- { property: 'og:title', content: fullTitle },
26005
- { property: 'og:description', content: meta.description },
26006
- { property: 'og:image', content: meta.image },
26007
- { property: 'og:site_name', content: '${siteName}' },
26008
- // Twitter
26009
- { name: 'twitter:card', content: 'summary_large_image' },
26010
- { name: 'twitter:title', content: fullTitle },
26011
- { name: 'twitter:description', content: meta.description },
26012
- { name: 'twitter:image', content: meta.image },
26013
- ],
26520
+ meta,
26014
26521
  link: [
26015
- { rel: 'canonical', href: meta.url },
26522
+ { rel: 'canonical', href: pageUrl },
26523
+ ],
26524
+ script: [
26525
+ {
26526
+ type: 'application/ld+json',
26527
+ innerHTML: JSON.stringify(jsonLd),
26528
+ },
26016
26529
  ],
26017
26530
  });
26018
- }`,
26019
- explanation: "Nuxt 3 SEO composable using useHead(). Call useSEO() in any page to set meta tags."
26020
- };
26021
26531
  }
26022
- function generateNuxtPageExample() {
26023
- return {
26024
- file: "pages/about.vue",
26025
- code: `<script setup lang="ts">
26026
- useSEO({
26027
- title: 'About Us',
26028
- description: 'Learn more about our company and mission.',
26029
- });
26030
- </script>
26031
26532
 
26032
- <template>
26033
- <main>
26034
- <h1>About Us</h1>
26035
- <!-- Your content here -->
26036
- </main>
26037
- </template>`,
26038
- explanation: "Example Nuxt page using the useSEO composable."
26533
+ /**
26534
+ * Schema generators for common types
26535
+ */
26536
+ export const Schema = {
26537
+ article: (data: {
26538
+ headline: string;
26539
+ description: string;
26540
+ image: string;
26541
+ datePublished: string;
26542
+ dateModified?: string;
26543
+ author: { name: string; url?: string };
26544
+ }) => ({
26545
+ '@context': 'https://schema.org',
26546
+ '@type': 'Article',
26547
+ headline: data.headline,
26548
+ description: data.description,
26549
+ image: data.image,
26550
+ datePublished: data.datePublished,
26551
+ dateModified: data.dateModified || data.datePublished,
26552
+ author: { '@type': 'Person', ...data.author },
26553
+ publisher: {
26554
+ '@type': 'Organization',
26555
+ name: SITE_NAME,
26556
+ url: SITE_URL,
26557
+ },
26558
+ }),
26559
+
26560
+ product: (data: {
26561
+ name: string;
26562
+ description: string;
26563
+ image: string;
26564
+ price: number;
26565
+ currency?: string;
26566
+ availability?: 'InStock' | 'OutOfStock' | 'PreOrder';
26567
+ }) => ({
26568
+ '@context': 'https://schema.org',
26569
+ '@type': 'Product',
26570
+ name: data.name,
26571
+ description: data.description,
26572
+ image: data.image,
26573
+ offers: {
26574
+ '@type': 'Offer',
26575
+ price: data.price,
26576
+ priceCurrency: data.currency || 'USD',
26577
+ availability: \`https://schema.org/\${data.availability || 'InStock'}\`,
26578
+ },
26579
+ }),
26580
+
26581
+ faq: (items: { question: string; answer: string }[]) => ({
26582
+ '@context': 'https://schema.org',
26583
+ '@type': 'FAQPage',
26584
+ mainEntity: items.map(item => ({
26585
+ '@type': 'Question',
26586
+ name: item.question,
26587
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
26588
+ })),
26589
+ }),
26590
+
26591
+ breadcrumb: (items: { name: string; url: string }[]) => ({
26592
+ '@context': 'https://schema.org',
26593
+ '@type': 'BreadcrumbList',
26594
+ itemListElement: items.map((item, i) => ({
26595
+ '@type': 'ListItem',
26596
+ position: i + 1,
26597
+ name: item.name,
26598
+ item: item.url,
26599
+ })),
26600
+ }),
26601
+ };`,
26602
+ explanation: `Nuxt 3 comprehensive SEO composable with:
26603
+ \u2022 Full useHead integration
26604
+ \u2022 Open Graph with article support
26605
+ \u2022 Twitter Cards
26606
+ \u2022 JSON-LD schema generators
26607
+ \u2022 Robots directives
26608
+ \u2022 Canonical URLs
26609
+
26610
+ Usage: useSEO({ title: 'Page', description: '...' })`,
26611
+ additionalFiles: [
26612
+ {
26613
+ file: "server/routes/sitemap.xml.ts",
26614
+ code: `import { SitemapStream, streamToPromise } from 'sitemap';
26615
+ import { Readable } from 'stream';
26616
+
26617
+ export default defineEventHandler(async () => {
26618
+ const baseUrl = '${siteUrl}';
26619
+
26620
+ // Define your pages
26621
+ const pages = [
26622
+ { url: '/', changefreq: 'daily', priority: 1 },
26623
+ { url: '/about', changefreq: 'monthly', priority: 0.8 },
26624
+ { url: '/pricing', changefreq: 'weekly', priority: 0.9 },
26625
+ { url: '/blog', changefreq: 'daily', priority: 0.8 },
26626
+ ];
26627
+
26628
+ // Add dynamic pages from your database
26629
+ // const posts = await $fetch('/api/posts');
26630
+ // posts.forEach(post => pages.push({
26631
+ // url: \`/blog/\${post.slug}\`,
26632
+ // changefreq: 'weekly',
26633
+ // priority: 0.7,
26634
+ // lastmod: post.updatedAt,
26635
+ // }));
26636
+
26637
+ const stream = new SitemapStream({ hostname: baseUrl });
26638
+
26639
+ return streamToPromise(Readable.from(pages).pipe(stream)).then((data) =>
26640
+ data.toString()
26641
+ );
26642
+ });`,
26643
+ explanation: "Dynamic sitemap generator for Nuxt."
26644
+ },
26645
+ {
26646
+ file: "public/robots.txt",
26647
+ code: `User-agent: *
26648
+ Allow: /
26649
+ Disallow: /api/
26650
+ Disallow: /admin/
26651
+
26652
+ User-agent: GPTBot
26653
+ Allow: /
26654
+
26655
+ Sitemap: ${siteUrl}/sitemap.xml`,
26656
+ explanation: "Robots.txt with AI crawler support."
26657
+ }
26658
+ ]
26039
26659
  };
26040
26660
  }
26041
26661
  function generateVueSEOHead(options) {
26042
- const { siteName, siteUrl, title, description, image } = options;
26662
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
26043
26663
  return {
26044
26664
  file: "src/composables/useSEO.ts",
26045
- code: `import { useHead } from '@unhead/vue';
26046
- import { computed, ref } from 'vue';
26665
+ code: `import { useHead, useServerHead } from '@unhead/vue';
26666
+ import { computed, unref, MaybeRef } from 'vue';
26667
+ import { useRoute } from 'vue-router';
26047
26668
 
26048
26669
  interface SEOOptions {
26049
- title?: string;
26050
- description?: string;
26051
- image?: string;
26052
- url?: string;
26670
+ title?: MaybeRef<string>;
26671
+ description?: MaybeRef<string>;
26672
+ image?: MaybeRef<string>;
26053
26673
  type?: 'website' | 'article';
26674
+ noIndex?: boolean;
26675
+ schema?: Record<string, unknown>;
26054
26676
  }
26055
26677
 
26056
- export function useSEO(options: SEOOptions = {}) {
26057
- const defaults = {
26058
- title: '${title || siteName}',
26059
- description: '${description || `${siteName} - A compelling description.`}',
26060
- image: '${image || `${siteUrl}/og-image.png`}',
26061
- url: typeof window !== 'undefined' ? window.location.href : '${siteUrl}',
26062
- type: 'website' as const,
26063
- };
26064
-
26065
- const meta = { ...defaults, ...options };
26066
- const fullTitle = computed(() =>
26067
- meta.title.includes('${siteName}') ? meta.title : \`\${meta.title} | ${siteName}\`
26068
- );
26678
+ const SITE_NAME = '${siteName}';
26679
+ const SITE_URL = '${siteUrl}';
26680
+ const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
26681
+ const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
26682
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
26069
26683
 
26684
+ export function useSEO(options: SEOOptions = {}) {
26685
+ const route = useRoute();
26686
+
26687
+ const title = computed(() => {
26688
+ const t = unref(options.title);
26689
+ return t ? (t.includes(SITE_NAME) ? t : \`\${t} | \${SITE_NAME}\`) : SITE_NAME;
26690
+ });
26691
+
26692
+ const description = computed(() => unref(options.description) || DEFAULT_DESCRIPTION);
26693
+ const image = computed(() => {
26694
+ const img = unref(options.image) || DEFAULT_IMAGE;
26695
+ return img.startsWith('http') ? img : \`\${SITE_URL}\${img}\`;
26696
+ });
26697
+ const url = computed(() => \`\${SITE_URL}\${route.path}\`);
26698
+
26070
26699
  useHead({
26071
- title: fullTitle,
26700
+ title,
26072
26701
  meta: [
26073
- { name: 'description', content: meta.description },
26074
- { property: 'og:type', content: meta.type },
26075
- { property: 'og:url', content: meta.url },
26076
- { property: 'og:title', content: fullTitle.value },
26077
- { property: 'og:description', content: meta.description },
26078
- { property: 'og:image', content: meta.image },
26079
- { property: 'og:site_name', content: '${siteName}' },
26702
+ { name: 'description', content: description },
26703
+ { name: 'robots', content: options.noIndex ? 'noindex, nofollow' : 'index, follow' },
26704
+
26705
+ // Open Graph
26706
+ { property: 'og:type', content: options.type || 'website' },
26707
+ { property: 'og:url', content: url },
26708
+ { property: 'og:title', content: title },
26709
+ { property: 'og:description', content: description },
26710
+ { property: 'og:image', content: image },
26711
+ { property: 'og:site_name', content: SITE_NAME },
26712
+
26713
+ // Twitter
26080
26714
  { name: 'twitter:card', content: 'summary_large_image' },
26081
- { name: 'twitter:title', content: fullTitle.value },
26082
- { name: 'twitter:description', content: meta.description },
26083
- { name: 'twitter:image', content: meta.image },
26715
+ { name: 'twitter:title', content: title },
26716
+ { name: 'twitter:description', content: description },
26717
+ { name: 'twitter:image', content: image },
26718
+ ...(TWITTER_HANDLE ? [
26719
+ { name: 'twitter:site', content: TWITTER_HANDLE },
26720
+ { name: 'twitter:creator', content: TWITTER_HANDLE },
26721
+ ] : []),
26084
26722
  ],
26085
26723
  link: [
26086
- { rel: 'canonical', href: meta.url },
26724
+ { rel: 'canonical', href: url },
26087
26725
  ],
26726
+ script: options.schema ? [
26727
+ { type: 'application/ld+json', innerHTML: JSON.stringify(options.schema) },
26728
+ ] : [],
26088
26729
  });
26089
26730
  }`,
26090
- explanation: "Vue 3 SEO composable using @unhead/vue. Install with: npm install @unhead/vue",
26091
- installCommands: ["npm install @unhead/vue"]
26731
+ explanation: `Vue 3 SEO composable using @unhead/vue with:
26732
+ \u2022 Reactive title/description
26733
+ \u2022 Open Graph and Twitter Cards
26734
+ \u2022 JSON-LD schema support
26735
+ \u2022 Canonical URLs
26736
+
26737
+ Install: npm install @unhead/vue`,
26738
+ installCommands: ["npm install @unhead/vue"],
26739
+ additionalFiles: [
26740
+ {
26741
+ file: "src/main.ts",
26742
+ code: `import { createApp } from 'vue';
26743
+ import { createHead } from '@unhead/vue';
26744
+ import { createRouter, createWebHistory } from 'vue-router';
26745
+ import App from './App.vue';
26746
+
26747
+ const app = createApp(App);
26748
+ const head = createHead();
26749
+ const router = createRouter({
26750
+ history: createWebHistory(),
26751
+ routes: [/* your routes */],
26752
+ });
26753
+
26754
+ app.use(head);
26755
+ app.use(router);
26756
+ app.mount('#app');`,
26757
+ explanation: "Vue app setup with @unhead/vue."
26758
+ }
26759
+ ]
26092
26760
  };
26093
26761
  }
26094
26762
  function generateAstroBaseHead(options) {
26095
- const { siteName, siteUrl, title, description, image } = options;
26763
+ const { siteName, siteUrl, description, image, twitterHandle, locale } = options;
26096
26764
  return {
26097
26765
  file: "src/components/BaseHead.astro",
26098
26766
  code: `---
26767
+ /**
26768
+ * Comprehensive SEO Head Component for Astro
26769
+ *
26770
+ * Features:
26771
+ * - Full Open Graph support
26772
+ * - Twitter Cards
26773
+ * - JSON-LD structured data
26774
+ * - Canonical URLs
26775
+ * - Robots directives
26776
+ * - Performance optimizations
26777
+ */
26778
+
26099
26779
  interface Props {
26100
26780
  title?: string;
26101
26781
  description?: string;
26102
26782
  image?: string;
26103
26783
  type?: 'website' | 'article';
26784
+ publishedTime?: string;
26785
+ modifiedTime?: string;
26786
+ author?: string;
26787
+ tags?: string[];
26788
+ noIndex?: boolean;
26789
+ schema?: Record<string, unknown>;
26104
26790
  }
26105
26791
 
26792
+ const SITE_NAME = '${siteName}';
26793
+ const SITE_URL = '${siteUrl}';
26794
+ const DEFAULT_IMAGE = '${image || "/og-image.png"}';
26795
+ const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
26796
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
26797
+ const DEFAULT_LOCALE = '${locale || "en_US"}';
26798
+
26106
26799
  const {
26107
- title = '${title || siteName}',
26108
- description = '${description || `${siteName} - A compelling description.`}',
26109
- image = '${image || "/og-image.png"}',
26800
+ title,
26801
+ description = DEFAULT_DESCRIPTION,
26802
+ image = DEFAULT_IMAGE,
26110
26803
  type = 'website',
26804
+ publishedTime,
26805
+ modifiedTime,
26806
+ author,
26807
+ tags,
26808
+ noIndex = false,
26809
+ schema,
26111
26810
  } = Astro.props;
26112
26811
 
26113
- const canonicalURL = new URL(Astro.url.pathname, Astro.site);
26114
- const fullTitle = title.includes('${siteName}') ? title : \`\${title} | ${siteName}\`;
26115
- const imageURL = new URL(image, Astro.site);
26812
+ const canonicalURL = new URL(Astro.url.pathname, Astro.site || SITE_URL);
26813
+ const fullTitle = title
26814
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
26815
+ : SITE_NAME;
26816
+ const imageURL = new URL(image, Astro.site || SITE_URL);
26817
+ const robotsContent = noIndex ? 'noindex, nofollow' : 'index, follow';
26818
+
26819
+ // Default website schema
26820
+ const defaultSchema = {
26821
+ '@context': 'https://schema.org',
26822
+ '@type': 'WebSite',
26823
+ name: SITE_NAME,
26824
+ url: SITE_URL,
26825
+ };
26826
+
26827
+ const jsonLd = schema ? [defaultSchema, schema] : [defaultSchema];
26116
26828
  ---
26117
26829
 
26118
26830
  <!-- Global Metadata -->
26119
26831
  <meta charset="utf-8" />
26120
- <meta name="viewport" content="width=device-width, initial-scale=1" />
26121
- <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
26832
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
26122
26833
  <meta name="generator" content={Astro.generator} />
26123
26834
 
26835
+ <!-- Favicon -->
26836
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
26837
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
26838
+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
26839
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
26840
+ <link rel="manifest" href="/site.webmanifest" />
26841
+
26124
26842
  <!-- Canonical URL -->
26125
26843
  <link rel="canonical" href={canonicalURL} />
26126
26844
 
@@ -26128,6 +26846,11 @@ const imageURL = new URL(image, Astro.site);
26128
26846
  <title>{fullTitle}</title>
26129
26847
  <meta name="title" content={fullTitle} />
26130
26848
  <meta name="description" content={description} />
26849
+ <meta name="robots" content={robotsContent} />
26850
+
26851
+ <!-- Theme -->
26852
+ <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
26853
+ <meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)" />
26131
26854
 
26132
26855
  <!-- Open Graph / Facebook -->
26133
26856
  <meta property="og:type" content={type} />
@@ -26135,143 +26858,449 @@ const imageURL = new URL(image, Astro.site);
26135
26858
  <meta property="og:title" content={fullTitle} />
26136
26859
  <meta property="og:description" content={description} />
26137
26860
  <meta property="og:image" content={imageURL} />
26138
- <meta property="og:site_name" content="${siteName}" />
26861
+ <meta property="og:image:width" content="1200" />
26862
+ <meta property="og:image:height" content="630" />
26863
+ <meta property="og:image:alt" content={fullTitle} />
26864
+ <meta property="og:site_name" content={SITE_NAME} />
26865
+ <meta property="og:locale" content={DEFAULT_LOCALE} />
26866
+
26867
+ {type === 'article' && publishedTime && (
26868
+ <meta property="article:published_time" content={publishedTime} />
26869
+ )}
26870
+ {type === 'article' && modifiedTime && (
26871
+ <meta property="article:modified_time" content={modifiedTime} />
26872
+ )}
26873
+ {type === 'article' && author && (
26874
+ <meta property="article:author" content={author} />
26875
+ )}
26876
+ {type === 'article' && tags?.map((tag) => (
26877
+ <meta property="article:tag" content={tag} />
26878
+ ))}
26139
26879
 
26140
26880
  <!-- Twitter -->
26141
26881
  <meta name="twitter:card" content="summary_large_image" />
26142
26882
  <meta name="twitter:url" content={canonicalURL} />
26143
26883
  <meta name="twitter:title" content={fullTitle} />
26144
26884
  <meta name="twitter:description" content={description} />
26145
- <meta name="twitter:image" content={imageURL} />`,
26146
- explanation: 'Astro BaseHead component. Import in your layout: <BaseHead title="Page Title" description="..." />'
26147
- };
26148
- }
26149
- function generateAstroLayout(siteName) {
26150
- return {
26151
- file: "src/layouts/BaseLayout.astro",
26152
- code: `---
26885
+ <meta name="twitter:image" content={imageURL} />
26886
+ <meta name="twitter:image:alt" content={fullTitle} />
26887
+ {TWITTER_HANDLE && <meta name="twitter:site" content={TWITTER_HANDLE} />}
26888
+ {TWITTER_HANDLE && <meta name="twitter:creator" content={TWITTER_HANDLE} />}
26889
+
26890
+ <!-- Performance: Preconnect to external origins -->
26891
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
26892
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
26893
+
26894
+ <!-- JSON-LD Structured Data -->
26895
+ <script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />`,
26896
+ explanation: `Astro comprehensive SEO component with:
26897
+ \u2022 Full Open Graph with article support
26898
+ \u2022 Twitter Cards
26899
+ \u2022 JSON-LD structured data
26900
+ \u2022 Performance optimizations (preconnect)
26901
+ \u2022 Theme color for PWA
26902
+ \u2022 Favicon configuration
26903
+
26904
+ Usage: <BaseHead title="Page" description="..." />`,
26905
+ additionalFiles: [
26906
+ {
26907
+ file: "src/layouts/BaseLayout.astro",
26908
+ code: `---
26153
26909
  import BaseHead from '../components/BaseHead.astro';
26154
26910
 
26155
26911
  interface Props {
26156
26912
  title?: string;
26157
26913
  description?: string;
26158
26914
  image?: string;
26915
+ type?: 'website' | 'article';
26916
+ schema?: Record<string, unknown>;
26159
26917
  }
26160
26918
 
26161
- const { title, description, image } = Astro.props;
26919
+ const { title, description, image, type, schema } = Astro.props;
26162
26920
  ---
26163
26921
 
26164
26922
  <!DOCTYPE html>
26165
- <html lang="en">
26923
+ <html lang="${(locale || "en_US").split("_")[0]}">
26166
26924
  <head>
26167
- <BaseHead title={title} description={description} image={image} />
26925
+ <BaseHead
26926
+ title={title}
26927
+ description={description}
26928
+ image={image}
26929
+ type={type}
26930
+ schema={schema}
26931
+ />
26168
26932
  </head>
26169
26933
  <body>
26170
- <main>
26171
- <slot />
26172
- </main>
26934
+ <slot />
26173
26935
  </body>
26174
26936
  </html>`,
26175
- explanation: "Astro layout using BaseHead component. Use in pages with frontmatter."
26937
+ explanation: "Base layout using the SEO head component."
26938
+ },
26939
+ {
26940
+ file: "public/robots.txt",
26941
+ code: `User-agent: *
26942
+ Allow: /
26943
+ Disallow: /api/
26944
+
26945
+ User-agent: GPTBot
26946
+ Allow: /
26947
+
26948
+ Sitemap: ${siteUrl}/sitemap-index.xml`,
26949
+ explanation: "Robots.txt with AI crawler support."
26950
+ }
26951
+ ]
26176
26952
  };
26177
26953
  }
26178
26954
  function generateSvelteKitSEOHead(options) {
26179
- const { siteName, siteUrl, title, description, image } = options;
26955
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
26180
26956
  return {
26181
26957
  file: "src/lib/components/SEOHead.svelte",
26182
26958
  code: `<script lang="ts">
26183
26959
  import { page } from '$app/stores';
26184
26960
 
26185
- export let title = '${title || siteName}';
26186
- export let description = '${description || `${siteName} - A compelling description.`}';
26187
- export let image = '${image || `${siteUrl}/og-image.png`}';
26961
+ export let title: string | undefined = undefined;
26962
+ export let description: string = '${description || `${siteName} - A compelling description.`}';
26963
+ export let image: string = '${image || `${siteUrl}/og-image.png`}';
26188
26964
  export let type: 'website' | 'article' = 'website';
26189
-
26190
- $: fullTitle = title.includes('${siteName}') ? title : \`\${title} | ${siteName}\`;
26965
+ export let publishedTime: string | undefined = undefined;
26966
+ export let modifiedTime: string | undefined = undefined;
26967
+ export let author: string | undefined = undefined;
26968
+ export let tags: string[] = [];
26969
+ export let noIndex: boolean = false;
26970
+ export let schema: Record<string, unknown> | undefined = undefined;
26971
+
26972
+ const SITE_NAME = '${siteName}';
26973
+ const SITE_URL = '${siteUrl}';
26974
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
26975
+
26976
+ $: fullTitle = title
26977
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
26978
+ : SITE_NAME;
26191
26979
  $: canonicalUrl = $page.url.href;
26980
+ $: imageUrl = image.startsWith('http') ? image : \`\${SITE_URL}\${image}\`;
26981
+ $: robotsContent = noIndex ? 'noindex, nofollow' : 'index, follow';
26982
+
26983
+ $: defaultSchema = {
26984
+ '@context': 'https://schema.org',
26985
+ '@type': 'WebSite',
26986
+ name: SITE_NAME,
26987
+ url: SITE_URL,
26988
+ };
26989
+
26990
+ $: jsonLd = schema ? [defaultSchema, schema] : [defaultSchema];
26192
26991
  </script>
26193
26992
 
26194
26993
  <svelte:head>
26994
+ <!-- Primary Meta Tags -->
26195
26995
  <title>{fullTitle}</title>
26996
+ <meta name="title" content={fullTitle} />
26196
26997
  <meta name="description" content={description} />
26998
+ <meta name="robots" content={robotsContent} />
26197
26999
  <link rel="canonical" href={canonicalUrl} />
26198
-
27000
+
27001
+ <!-- Open Graph / Facebook -->
26199
27002
  <meta property="og:type" content={type} />
26200
27003
  <meta property="og:url" content={canonicalUrl} />
26201
27004
  <meta property="og:title" content={fullTitle} />
26202
27005
  <meta property="og:description" content={description} />
26203
- <meta property="og:image" content={image} />
26204
- <meta property="og:site_name" content="${siteName}" />
26205
-
27006
+ <meta property="og:image" content={imageUrl} />
27007
+ <meta property="og:image:width" content="1200" />
27008
+ <meta property="og:image:height" content="630" />
27009
+ <meta property="og:site_name" content={SITE_NAME} />
27010
+
27011
+ {#if type === 'article'}
27012
+ {#if publishedTime}
27013
+ <meta property="article:published_time" content={publishedTime} />
27014
+ {/if}
27015
+ {#if modifiedTime}
27016
+ <meta property="article:modified_time" content={modifiedTime} />
27017
+ {/if}
27018
+ {#if author}
27019
+ <meta property="article:author" content={author} />
27020
+ {/if}
27021
+ {#each tags as tag}
27022
+ <meta property="article:tag" content={tag} />
27023
+ {/each}
27024
+ {/if}
27025
+
27026
+ <!-- Twitter -->
26206
27027
  <meta name="twitter:card" content="summary_large_image" />
26207
27028
  <meta name="twitter:title" content={fullTitle} />
26208
27029
  <meta name="twitter:description" content={description} />
26209
- <meta name="twitter:image" content={image} />
27030
+ <meta name="twitter:image" content={imageUrl} />
27031
+ {#if TWITTER_HANDLE}
27032
+ <meta name="twitter:site" content={TWITTER_HANDLE} />
27033
+ <meta name="twitter:creator" content={TWITTER_HANDLE} />
27034
+ {/if}
27035
+
27036
+ <!-- JSON-LD Structured Data -->
27037
+ {@html \`<script type="application/ld+json">\${JSON.stringify(jsonLd)}</script>\`}
26210
27038
  </svelte:head>`,
26211
- explanation: "SvelteKit SEO component using svelte:head. Import and use in +page.svelte files."
27039
+ explanation: `SvelteKit comprehensive SEO component with:
27040
+ \u2022 Reactive props
27041
+ \u2022 Full Open Graph with article support
27042
+ \u2022 Twitter Cards
27043
+ \u2022 JSON-LD structured data
27044
+ \u2022 Robots directives
27045
+
27046
+ Usage: <SEOHead title="Page" description="..." />`,
27047
+ additionalFiles: [
27048
+ {
27049
+ file: "src/routes/+layout.svelte",
27050
+ code: `<script lang="ts">
27051
+ import '../app.css';
27052
+ </script>
27053
+
27054
+ <slot />`,
27055
+ explanation: "Root layout."
27056
+ },
27057
+ {
27058
+ file: "static/robots.txt",
27059
+ code: `User-agent: *
27060
+ Allow: /
27061
+ Disallow: /api/
27062
+
27063
+ User-agent: GPTBot
27064
+ Allow: /
27065
+
27066
+ Sitemap: ${siteUrl}/sitemap.xml`,
27067
+ explanation: "Robots.txt with AI crawler support."
27068
+ }
27069
+ ]
26212
27070
  };
26213
27071
  }
26214
27072
  function generateAngularSEOService(options) {
26215
- const { siteName, siteUrl, title, description, image } = options;
27073
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
26216
27074
  return {
26217
- file: "src/app/services/seo.service.ts",
26218
- code: `import { Injectable } from '@angular/core';
27075
+ file: "src/app/core/services/seo.service.ts",
27076
+ code: `import { Injectable, Inject } from '@angular/core';
26219
27077
  import { Meta, Title } from '@angular/platform-browser';
26220
- import { Router } from '@angular/router';
27078
+ import { Router, NavigationEnd } from '@angular/router';
27079
+ import { DOCUMENT } from '@angular/common';
27080
+ import { filter } from 'rxjs/operators';
26221
27081
 
26222
27082
  interface SEOConfig {
26223
27083
  title?: string;
26224
27084
  description?: string;
26225
27085
  image?: string;
26226
27086
  type?: 'website' | 'article';
27087
+ publishedTime?: string;
27088
+ modifiedTime?: string;
27089
+ author?: string;
27090
+ tags?: string[];
27091
+ noIndex?: boolean;
27092
+ schema?: Record<string, unknown>;
26227
27093
  }
26228
27094
 
26229
27095
  @Injectable({
26230
27096
  providedIn: 'root'
26231
27097
  })
26232
27098
  export class SEOService {
26233
- private siteName = '${siteName}';
26234
- private siteUrl = '${siteUrl}';
26235
- private defaultDescription = '${description || `${siteName} - A compelling description.`}';
26236
- private defaultImage = '${image || `${siteUrl}/og-image.png`}';
27099
+ private readonly siteName = '${siteName}';
27100
+ private readonly siteUrl = '${siteUrl}';
27101
+ private readonly defaultDescription = '${description || `${siteName} - A compelling description.`}';
27102
+ private readonly defaultImage = '${image || `${siteUrl}/og-image.png`}';
27103
+ private readonly twitterHandle = '${twitterHandle || ""}';
26237
27104
 
26238
27105
  constructor(
26239
27106
  private meta: Meta,
26240
27107
  private titleService: Title,
26241
- private router: Router
26242
- ) {}
27108
+ private router: Router,
27109
+ @Inject(DOCUMENT) private document: Document
27110
+ ) {
27111
+ // Update canonical URL on route change
27112
+ this.router.events.pipe(
27113
+ filter(event => event instanceof NavigationEnd)
27114
+ ).subscribe(() => {
27115
+ this.updateCanonical();
27116
+ });
27117
+ }
26243
27118
 
27119
+ /**
27120
+ * Update all SEO meta tags
27121
+ */
26244
27122
  updateMeta(config: SEOConfig = {}): void {
26245
- const title = config.title || this.siteName;
26246
- const fullTitle = title.includes(this.siteName) ? title : \`\${title} | \${this.siteName}\`;
26247
- const description = config.description || this.defaultDescription;
26248
- const image = config.image || this.defaultImage;
26249
- const url = this.siteUrl + this.router.url;
26250
- const type = config.type || 'website';
27123
+ const {
27124
+ title,
27125
+ description = this.defaultDescription,
27126
+ image = this.defaultImage,
27127
+ type = 'website',
27128
+ publishedTime,
27129
+ modifiedTime,
27130
+ author,
27131
+ tags,
27132
+ noIndex = false,
27133
+ schema,
27134
+ } = config;
27135
+
27136
+ const fullTitle = title
27137
+ ? (title.includes(this.siteName) ? title : \`\${title} | \${this.siteName}\`)
27138
+ : this.siteName;
27139
+ const pageUrl = this.siteUrl + this.router.url;
27140
+ const imageUrl = image.startsWith('http') ? image : \`\${this.siteUrl}\${image}\`;
27141
+ const robotsContent = noIndex ? 'noindex, nofollow' : 'index, follow';
26251
27142
 
26252
27143
  // Title
26253
27144
  this.titleService.setTitle(fullTitle);
26254
27145
 
26255
- // Primary Meta
26256
- this.meta.updateTag({ name: 'description', content: description });
26257
- this.meta.updateTag({ rel: 'canonical', href: url });
27146
+ // Primary Meta Tags
27147
+ this.setMetaTag('description', description);
27148
+ this.setMetaTag('robots', robotsContent);
26258
27149
 
26259
27150
  // Open Graph
26260
- this.meta.updateTag({ property: 'og:type', content: type });
26261
- this.meta.updateTag({ property: 'og:url', content: url });
26262
- this.meta.updateTag({ property: 'og:title', content: fullTitle });
26263
- this.meta.updateTag({ property: 'og:description', content: description });
26264
- this.meta.updateTag({ property: 'og:image', content: image });
26265
- this.meta.updateTag({ property: 'og:site_name', content: this.siteName });
27151
+ this.setMetaProperty('og:type', type);
27152
+ this.setMetaProperty('og:url', pageUrl);
27153
+ this.setMetaProperty('og:title', fullTitle);
27154
+ this.setMetaProperty('og:description', description);
27155
+ this.setMetaProperty('og:image', imageUrl);
27156
+ this.setMetaProperty('og:image:width', '1200');
27157
+ this.setMetaProperty('og:image:height', '630');
27158
+ this.setMetaProperty('og:site_name', this.siteName);
27159
+
27160
+ // Article-specific
27161
+ if (type === 'article') {
27162
+ if (publishedTime) this.setMetaProperty('article:published_time', publishedTime);
27163
+ if (modifiedTime) this.setMetaProperty('article:modified_time', modifiedTime);
27164
+ if (author) this.setMetaProperty('article:author', author);
27165
+ tags?.forEach(tag => this.setMetaProperty('article:tag', tag));
27166
+ }
26266
27167
 
26267
27168
  // Twitter
26268
- this.meta.updateTag({ name: 'twitter:card', content: 'summary_large_image' });
26269
- this.meta.updateTag({ name: 'twitter:title', content: fullTitle });
26270
- this.meta.updateTag({ name: 'twitter:description', content: description });
26271
- this.meta.updateTag({ name: 'twitter:image', content: image });
27169
+ this.setMetaTag('twitter:card', 'summary_large_image');
27170
+ this.setMetaTag('twitter:title', fullTitle);
27171
+ this.setMetaTag('twitter:description', description);
27172
+ this.setMetaTag('twitter:image', imageUrl);
27173
+ if (this.twitterHandle) {
27174
+ this.setMetaTag('twitter:site', this.twitterHandle);
27175
+ this.setMetaTag('twitter:creator', this.twitterHandle);
27176
+ }
27177
+
27178
+ // Update canonical
27179
+ this.updateCanonical(pageUrl);
27180
+
27181
+ // Update JSON-LD
27182
+ this.updateJsonLd(schema);
27183
+ }
27184
+
27185
+ private setMetaTag(name: string, content: string): void {
27186
+ this.meta.updateTag({ name, content });
27187
+ }
27188
+
27189
+ private setMetaProperty(property: string, content: string): void {
27190
+ this.meta.updateTag({ property, content });
27191
+ }
27192
+
27193
+ private updateCanonical(url?: string): void {
27194
+ const canonicalUrl = url || this.siteUrl + this.router.url;
27195
+ let link = this.document.querySelector('link[rel="canonical"]') as HTMLLinkElement;
27196
+
27197
+ if (!link) {
27198
+ link = this.document.createElement('link');
27199
+ link.setAttribute('rel', 'canonical');
27200
+ this.document.head.appendChild(link);
27201
+ }
27202
+
27203
+ link.setAttribute('href', canonicalUrl);
27204
+ }
27205
+
27206
+ private updateJsonLd(schema?: Record<string, unknown>): void {
27207
+ // Remove existing JSON-LD
27208
+ const existing = this.document.querySelector('script[type="application/ld+json"]');
27209
+ if (existing) existing.remove();
27210
+
27211
+ // Add new JSON-LD
27212
+ const defaultSchema = {
27213
+ '@context': 'https://schema.org',
27214
+ '@type': 'WebSite',
27215
+ name: this.siteName,
27216
+ url: this.siteUrl,
27217
+ };
27218
+
27219
+ const jsonLd = schema ? [defaultSchema, schema] : [defaultSchema];
27220
+
27221
+ const script = this.document.createElement('script');
27222
+ script.type = 'application/ld+json';
27223
+ script.text = JSON.stringify(jsonLd);
27224
+ this.document.head.appendChild(script);
27225
+ }
27226
+
27227
+ /**
27228
+ * Generate Article schema
27229
+ */
27230
+ articleSchema(data: {
27231
+ headline: string;
27232
+ description: string;
27233
+ image: string;
27234
+ datePublished: string;
27235
+ dateModified?: string;
27236
+ author: { name: string; url?: string };
27237
+ }): Record<string, unknown> {
27238
+ return {
27239
+ '@context': 'https://schema.org',
27240
+ '@type': 'Article',
27241
+ headline: data.headline,
27242
+ description: data.description,
27243
+ image: data.image,
27244
+ datePublished: data.datePublished,
27245
+ dateModified: data.dateModified || data.datePublished,
27246
+ author: { '@type': 'Person', ...data.author },
27247
+ publisher: {
27248
+ '@type': 'Organization',
27249
+ name: this.siteName,
27250
+ url: this.siteUrl,
27251
+ },
27252
+ };
27253
+ }
27254
+
27255
+ /**
27256
+ * Generate Product schema
27257
+ */
27258
+ productSchema(data: {
27259
+ name: string;
27260
+ description: string;
27261
+ image: string;
27262
+ price: number;
27263
+ currency?: string;
27264
+ }): Record<string, unknown> {
27265
+ return {
27266
+ '@context': 'https://schema.org',
27267
+ '@type': 'Product',
27268
+ name: data.name,
27269
+ description: data.description,
27270
+ image: data.image,
27271
+ offers: {
27272
+ '@type': 'Offer',
27273
+ price: data.price,
27274
+ priceCurrency: data.currency || 'USD',
27275
+ availability: 'https://schema.org/InStock',
27276
+ },
27277
+ };
27278
+ }
27279
+
27280
+ /**
27281
+ * Generate FAQ schema
27282
+ */
27283
+ faqSchema(items: { question: string; answer: string }[]): Record<string, unknown> {
27284
+ return {
27285
+ '@context': 'https://schema.org',
27286
+ '@type': 'FAQPage',
27287
+ mainEntity: items.map(item => ({
27288
+ '@type': 'Question',
27289
+ name: item.question,
27290
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
27291
+ })),
27292
+ };
26272
27293
  }
26273
27294
  }`,
26274
- explanation: "Angular SEO service using Meta and Title services. Inject and call updateMeta() in components."
27295
+ explanation: `Angular comprehensive SEO service with:
27296
+ \u2022 Meta and Title service integration
27297
+ \u2022 Dynamic canonical URL updates
27298
+ \u2022 Full Open Graph with article support
27299
+ \u2022 Twitter Cards
27300
+ \u2022 JSON-LD schema generators
27301
+ \u2022 Automatic route change handling
27302
+
27303
+ Usage: Inject SEOService and call updateMeta()`
26275
27304
  };
26276
27305
  }
26277
27306
  function getFrameworkSpecificFix(framework, options) {
@@ -26300,6 +27329,102 @@ function getFrameworkSpecificFix(framework, options) {
26300
27329
  }
26301
27330
  return generateReactSEOHead(options);
26302
27331
  }
27332
+ function generateNextJsPagesRouterHead(options) {
27333
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
27334
+ return {
27335
+ file: "components/SEOHead.tsx",
27336
+ code: `import Head from 'next/head';
27337
+ import { useRouter } from 'next/router';
27338
+
27339
+ interface SEOHeadProps {
27340
+ title?: string;
27341
+ description?: string;
27342
+ image?: string;
27343
+ type?: 'website' | 'article';
27344
+ publishedTime?: string;
27345
+ modifiedTime?: string;
27346
+ noIndex?: boolean;
27347
+ schema?: Record<string, unknown>;
27348
+ }
27349
+
27350
+ const SITE_NAME = '${siteName}';
27351
+ const SITE_URL = '${siteUrl}';
27352
+ const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
27353
+ const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
27354
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
27355
+
27356
+ export function SEOHead({
27357
+ title,
27358
+ description = DEFAULT_DESCRIPTION,
27359
+ image = DEFAULT_IMAGE,
27360
+ type = 'website',
27361
+ publishedTime,
27362
+ modifiedTime,
27363
+ noIndex = false,
27364
+ schema,
27365
+ }: SEOHeadProps) {
27366
+ const router = useRouter();
27367
+
27368
+ const fullTitle = title
27369
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
27370
+ : SITE_NAME;
27371
+ const pageUrl = \`\${SITE_URL}\${router.asPath}\`;
27372
+ const imageUrl = image.startsWith('http') ? image : \`\${SITE_URL}\${image}\`;
27373
+ const robotsContent = noIndex ? 'noindex, nofollow' : 'index, follow';
27374
+
27375
+ const defaultSchema = {
27376
+ '@context': 'https://schema.org',
27377
+ '@type': 'WebSite',
27378
+ name: SITE_NAME,
27379
+ url: SITE_URL,
27380
+ };
27381
+
27382
+ const jsonLd = schema ? [defaultSchema, schema] : [defaultSchema];
27383
+
27384
+ return (
27385
+ <Head>
27386
+ <title>{fullTitle}</title>
27387
+ <meta name="description" content={description} />
27388
+ <meta name="robots" content={robotsContent} />
27389
+ <link rel="canonical" href={pageUrl} />
27390
+
27391
+ <meta property="og:type" content={type} />
27392
+ <meta property="og:url" content={pageUrl} />
27393
+ <meta property="og:title" content={fullTitle} />
27394
+ <meta property="og:description" content={description} />
27395
+ <meta property="og:image" content={imageUrl} />
27396
+ <meta property="og:site_name" content={SITE_NAME} />
27397
+
27398
+ {type === 'article' && publishedTime && (
27399
+ <meta property="article:published_time" content={publishedTime} />
27400
+ )}
27401
+ {type === 'article' && modifiedTime && (
27402
+ <meta property="article:modified_time" content={modifiedTime} />
27403
+ )}
27404
+
27405
+ <meta name="twitter:card" content="summary_large_image" />
27406
+ <meta name="twitter:title" content={fullTitle} />
27407
+ <meta name="twitter:description" content={description} />
27408
+ <meta name="twitter:image" content={imageUrl} />
27409
+ {TWITTER_HANDLE && <meta name="twitter:site" content={TWITTER_HANDLE} />}
27410
+
27411
+ <script
27412
+ type="application/ld+json"
27413
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
27414
+ />
27415
+ </Head>
27416
+ );
27417
+ }`,
27418
+ explanation: `Next.js Pages Router SEO component with:
27419
+ \u2022 Full Open Graph support
27420
+ \u2022 Twitter Cards
27421
+ \u2022 JSON-LD structured data
27422
+ \u2022 Article metadata
27423
+ \u2022 Canonical URLs
27424
+
27425
+ Usage: <SEOHead title="Page" description="..." />`
27426
+ };
27427
+ }
26303
27428
 
26304
27429
  // src/fixer.ts
26305
27430
  async function generateFixes(issues, options) {
@@ -29534,7 +30659,6 @@ export {
29534
30659
  generateAllFixes,
29535
30660
  generateAngularSEOService,
29536
30661
  generateAstroBaseHead,
29537
- generateAstroLayout,
29538
30662
  generateAstroMeta,
29539
30663
  generateBlogPost,
29540
30664
  generateBranchName,
@@ -29560,17 +30684,11 @@ export {
29560
30684
  generateMarkdownReport,
29561
30685
  generateNextAppMetadata,
29562
30686
  generateNextJsAppRouterMetadata,
29563
- generateNextJsDynamicMetadata,
29564
- generateNextJsPageMetadata,
29565
30687
  generateNextJsPagesRouterHead,
29566
- generateNextJsRobots,
29567
- generateNextJsSitemap,
29568
30688
  generateNextPagesHead,
29569
- generateNuxtPageExample,
29570
30689
  generateNuxtSEOHead,
29571
30690
  generatePDFReport,
29572
30691
  generatePRDescription,
29573
- generateReactAppWrapper,
29574
30692
  generateReactHelmetSocialMeta,
29575
30693
  generateReactSEOHead,
29576
30694
  generateRecommendationQueries,