@rankcli/agent-runtime 0.0.7 → 0.0.9

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
@@ -809,6 +809,7 @@ __export(index_exports, {
809
809
  PRIORITY_WEIGHTS: () => PRIORITY_WEIGHTS,
810
810
  SEO_SCOPES: () => SEO_SCOPES,
811
811
  SITE_PROFILE_QUESTIONS: () => SITE_PROFILE_QUESTIONS,
812
+ Schemas: () => Schemas,
812
813
  addTrackingResult: () => addTrackingResult,
813
814
  analyzeAnchorText: () => analyzeAnchorText,
814
815
  analyzeCanonicalAdvanced: () => analyzeCanonicalAdvanced,
@@ -946,7 +947,6 @@ __export(index_exports, {
946
947
  generateAllFixes: () => generateAllFixes,
947
948
  generateAngularSEOService: () => generateAngularSEOService,
948
949
  generateAstroBaseHead: () => generateAstroBaseHead,
949
- generateAstroLayout: () => generateAstroLayout,
950
950
  generateAstroMeta: () => generateAstroMeta,
951
951
  generateBlogPost: () => generateBlogPost,
952
952
  generateBranchName: () => generateBranchName,
@@ -955,7 +955,7 @@ __export(index_exports, {
955
955
  generateComparisonTable: () => generateComparisonTable,
956
956
  generateCompleteSocialMetaSetup: () => generateCompleteSocialMetaSetup,
957
957
  generateDuplicateIssues: () => generateDuplicateIssues,
958
- generateFAQSchema: () => generateFAQSchema,
958
+ generateFAQSchema: () => generateFAQSchema2,
959
959
  generateFixes: () => generateFixes,
960
960
  generateGA4EnvTemplate: () => generateGA4EnvTemplate,
961
961
  generateGA4ReactComponent: () => generateGA4ReactComponent,
@@ -972,17 +972,11 @@ __export(index_exports, {
972
972
  generateMarkdownReport: () => generateMarkdownReport,
973
973
  generateNextAppMetadata: () => generateNextAppMetadata,
974
974
  generateNextJsAppRouterMetadata: () => generateNextJsAppRouterMetadata,
975
- generateNextJsDynamicMetadata: () => generateNextJsDynamicMetadata,
976
- generateNextJsPageMetadata: () => generateNextJsPageMetadata,
977
975
  generateNextJsPagesRouterHead: () => generateNextJsPagesRouterHead,
978
- generateNextJsRobots: () => generateNextJsRobots,
979
- generateNextJsSitemap: () => generateNextJsSitemap,
980
976
  generateNextPagesHead: () => generateNextPagesHead,
981
- generateNuxtPageExample: () => generateNuxtPageExample,
982
977
  generateNuxtSEOHead: () => generateNuxtSEOHead,
983
978
  generatePDFReport: () => generatePDFReport,
984
979
  generatePRDescription: () => generatePRDescription,
985
- generateReactAppWrapper: () => generateReactAppWrapper,
986
980
  generateReactHelmetSocialMeta: () => generateReactHelmetSocialMeta,
987
981
  generateReactSEOHead: () => generateReactSEOHead,
988
982
  generateRecommendationQueries: () => generateRecommendationQueries,
@@ -26741,61 +26735,332 @@ var import_path4 = require("path");
26741
26735
 
26742
26736
  // src/fixer/framework-fixes.ts
26743
26737
  function generateReactSEOHead(options) {
26744
- const { siteName, siteUrl, title, description, image } = options;
26738
+ const { siteName, siteUrl, title, description, image, twitterHandle, locale } = options;
26745
26739
  return {
26746
26740
  file: "src/components/SEOHead.tsx",
26747
26741
  code: `import { Helmet } from 'react-helmet-async';
26748
26742
 
26743
+ /**
26744
+ * SEO Head Component
26745
+ *
26746
+ * Comprehensive SEO meta tags following best practices:
26747
+ * - Primary meta tags (title, description)
26748
+ * - Open Graph for Facebook/LinkedIn
26749
+ * - Twitter Card for X/Twitter
26750
+ * - JSON-LD structured data
26751
+ * - Canonical URLs
26752
+ *
26753
+ * @example
26754
+ * <SEOHead
26755
+ * title="Product Name"
26756
+ * description="Product description"
26757
+ * type="product"
26758
+ * schema={{
26759
+ * "@type": "Product",
26760
+ * name: "Product Name",
26761
+ * price: "99.00"
26762
+ * }}
26763
+ * />
26764
+ */
26765
+
26749
26766
  interface SEOHeadProps {
26767
+ // Required
26750
26768
  title?: string;
26751
26769
  description?: string;
26752
- image?: string;
26770
+
26771
+ // URLs
26753
26772
  url?: string;
26754
- type?: 'website' | 'article';
26773
+ canonical?: string;
26774
+ image?: string;
26775
+
26776
+ // Page type
26777
+ type?: 'website' | 'article' | 'product' | 'profile';
26778
+
26779
+ // Article-specific
26780
+ publishedTime?: string;
26781
+ modifiedTime?: string;
26782
+ author?: string;
26783
+ section?: string;
26784
+ tags?: string[];
26785
+
26786
+ // Twitter
26787
+ twitterCard?: 'summary' | 'summary_large_image' | 'player';
26788
+
26789
+ // Structured data
26790
+ schema?: Record<string, unknown> | Record<string, unknown>[];
26791
+
26792
+ // Robots
26793
+ noindex?: boolean;
26794
+ nofollow?: boolean;
26795
+
26796
+ // Alternate languages
26797
+ alternates?: { hrefLang: string; href: string }[];
26755
26798
  }
26756
26799
 
26800
+ const SITE_NAME = '${siteName}';
26801
+ const SITE_URL = '${siteUrl}';
26802
+ const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
26803
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
26804
+ const DEFAULT_LOCALE = '${locale || "en_US"}';
26805
+
26757
26806
  export function SEOHead({
26758
- title = '${title || `${siteName} - Your tagline here`}',
26807
+ title,
26759
26808
  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}',
26809
+ url,
26810
+ canonical,
26811
+ image = DEFAULT_IMAGE,
26762
26812
  type = 'website',
26813
+ publishedTime,
26814
+ modifiedTime,
26815
+ author,
26816
+ section,
26817
+ tags,
26818
+ twitterCard = 'summary_large_image',
26819
+ schema,
26820
+ noindex = false,
26821
+ nofollow = false,
26822
+ alternates,
26763
26823
  }: SEOHeadProps) {
26764
- const fullTitle = title.includes('${siteName}') ? title : \`\${title} | ${siteName}\`;
26824
+ const pageUrl = url || (typeof window !== 'undefined' ? window.location.href : SITE_URL);
26825
+ const canonicalUrl = canonical || pageUrl;
26826
+ const fullTitle = title
26827
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
26828
+ : SITE_NAME;
26829
+
26830
+ // Ensure image is absolute URL
26831
+ const imageUrl = image.startsWith('http') ? image : \`\${SITE_URL}\${image}\`;
26832
+
26833
+ // Build robots directive
26834
+ const robotsContent = [
26835
+ noindex ? 'noindex' : 'index',
26836
+ nofollow ? 'nofollow' : 'follow',
26837
+ ].join(', ');
26838
+
26839
+ // Default Organization schema
26840
+ const defaultSchema = {
26841
+ '@context': 'https://schema.org',
26842
+ '@type': 'WebSite',
26843
+ name: SITE_NAME,
26844
+ url: SITE_URL,
26845
+ };
26846
+
26847
+ // Merge with provided schema
26848
+ const jsonLd = schema
26849
+ ? Array.isArray(schema)
26850
+ ? [defaultSchema, ...schema]
26851
+ : [defaultSchema, schema]
26852
+ : [defaultSchema];
26765
26853
 
26766
26854
  return (
26767
26855
  <Helmet>
26768
26856
  {/* Primary Meta Tags */}
26769
26857
  <title>{fullTitle}</title>
26858
+ <meta name="title" content={fullTitle} />
26770
26859
  <meta name="description" content={description} />
26771
- <link rel="canonical" href={url} />
26860
+ <meta name="robots" content={robotsContent} />
26861
+ <link rel="canonical" href={canonicalUrl} />
26772
26862
 
26773
26863
  {/* Open Graph / Facebook */}
26774
26864
  <meta property="og:type" content={type} />
26775
- <meta property="og:url" content={url} />
26865
+ <meta property="og:url" content={pageUrl} />
26776
26866
  <meta property="og:title" content={fullTitle} />
26777
26867
  <meta property="og:description" content={description} />
26778
- <meta property="og:image" content={image} />
26779
- <meta property="og:site_name" content="${siteName}" />
26868
+ <meta property="og:image" content={imageUrl} />
26869
+ <meta property="og:image:width" content="1200" />
26870
+ <meta property="og:image:height" content="630" />
26871
+ <meta property="og:image:alt" content={fullTitle} />
26872
+ <meta property="og:site_name" content={SITE_NAME} />
26873
+ <meta property="og:locale" content={DEFAULT_LOCALE} />
26874
+
26875
+ {/* Article-specific Open Graph */}
26876
+ {type === 'article' && publishedTime && (
26877
+ <meta property="article:published_time" content={publishedTime} />
26878
+ )}
26879
+ {type === 'article' && modifiedTime && (
26880
+ <meta property="article:modified_time" content={modifiedTime} />
26881
+ )}
26882
+ {type === 'article' && author && (
26883
+ <meta property="article:author" content={author} />
26884
+ )}
26885
+ {type === 'article' && section && (
26886
+ <meta property="article:section" content={section} />
26887
+ )}
26888
+ {type === 'article' && tags?.map((tag, i) => (
26889
+ <meta key={i} property="article:tag" content={tag} />
26890
+ ))}
26780
26891
 
26781
26892
  {/* Twitter */}
26782
- <meta name="twitter:card" content="summary_large_image" />
26783
- <meta name="twitter:url" content={url} />
26893
+ <meta name="twitter:card" content={twitterCard} />
26894
+ <meta name="twitter:url" content={pageUrl} />
26784
26895
  <meta name="twitter:title" content={fullTitle} />
26785
26896
  <meta name="twitter:description" content={description} />
26786
- <meta name="twitter:image" content={image} />
26897
+ <meta name="twitter:image" content={imageUrl} />
26898
+ <meta name="twitter:image:alt" content={fullTitle} />
26899
+ {TWITTER_HANDLE && <meta name="twitter:site" content={TWITTER_HANDLE} />}
26900
+ {TWITTER_HANDLE && <meta name="twitter:creator" content={TWITTER_HANDLE} />}
26901
+
26902
+ {/* Alternate Languages */}
26903
+ {alternates?.map((alt, i) => (
26904
+ <link key={i} rel="alternate" hrefLang={alt.hrefLang} href={alt.href} />
26905
+ ))}
26906
+
26907
+ {/* JSON-LD Structured Data */}
26908
+ <script type="application/ld+json">
26909
+ {JSON.stringify(jsonLd)}
26910
+ </script>
26787
26911
  </Helmet>
26788
26912
  );
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
26913
  }
26795
- function generateReactAppWrapper() {
26796
- return {
26797
- file: "src/main.tsx",
26798
- code: `import React from 'react';
26914
+
26915
+ /**
26916
+ * Pre-built schema generators for common page types
26917
+ */
26918
+ export const SchemaGenerators = {
26919
+ organization: (data: {
26920
+ name: string;
26921
+ url: string;
26922
+ logo?: string;
26923
+ sameAs?: string[];
26924
+ }) => ({
26925
+ '@context': 'https://schema.org',
26926
+ '@type': 'Organization',
26927
+ name: data.name,
26928
+ url: data.url,
26929
+ logo: data.logo,
26930
+ sameAs: data.sameAs,
26931
+ }),
26932
+
26933
+ article: (data: {
26934
+ headline: string;
26935
+ description: string;
26936
+ image: string;
26937
+ datePublished: string;
26938
+ dateModified?: string;
26939
+ author: { name: string; url?: string };
26940
+ }) => ({
26941
+ '@context': 'https://schema.org',
26942
+ '@type': 'Article',
26943
+ headline: data.headline,
26944
+ description: data.description,
26945
+ image: data.image,
26946
+ datePublished: data.datePublished,
26947
+ dateModified: data.dateModified || data.datePublished,
26948
+ author: {
26949
+ '@type': 'Person',
26950
+ name: data.author.name,
26951
+ url: data.author.url,
26952
+ },
26953
+ }),
26954
+
26955
+ product: (data: {
26956
+ name: string;
26957
+ description: string;
26958
+ image: string;
26959
+ price: string;
26960
+ currency?: string;
26961
+ availability?: 'InStock' | 'OutOfStock' | 'PreOrder';
26962
+ brand?: string;
26963
+ sku?: string;
26964
+ rating?: { value: number; count: number };
26965
+ }) => ({
26966
+ '@context': 'https://schema.org',
26967
+ '@type': 'Product',
26968
+ name: data.name,
26969
+ description: data.description,
26970
+ image: data.image,
26971
+ brand: data.brand ? { '@type': 'Brand', name: data.brand } : undefined,
26972
+ sku: data.sku,
26973
+ offers: {
26974
+ '@type': 'Offer',
26975
+ price: data.price,
26976
+ priceCurrency: data.currency || 'USD',
26977
+ availability: \`https://schema.org/\${data.availability || 'InStock'}\`,
26978
+ },
26979
+ aggregateRating: data.rating ? {
26980
+ '@type': 'AggregateRating',
26981
+ ratingValue: data.rating.value,
26982
+ reviewCount: data.rating.count,
26983
+ } : undefined,
26984
+ }),
26985
+
26986
+ faq: (items: { question: string; answer: string }[]) => ({
26987
+ '@context': 'https://schema.org',
26988
+ '@type': 'FAQPage',
26989
+ mainEntity: items.map(item => ({
26990
+ '@type': 'Question',
26991
+ name: item.question,
26992
+ acceptedAnswer: {
26993
+ '@type': 'Answer',
26994
+ text: item.answer,
26995
+ },
26996
+ })),
26997
+ }),
26998
+
26999
+ breadcrumb: (items: { name: string; url: string }[]) => ({
27000
+ '@context': 'https://schema.org',
27001
+ '@type': 'BreadcrumbList',
27002
+ itemListElement: items.map((item, index) => ({
27003
+ '@type': 'ListItem',
27004
+ position: index + 1,
27005
+ name: item.name,
27006
+ item: item.url,
27007
+ })),
27008
+ }),
27009
+
27010
+ localBusiness: (data: {
27011
+ name: string;
27012
+ description: string;
27013
+ url: string;
27014
+ phone: string;
27015
+ address: {
27016
+ street: string;
27017
+ city: string;
27018
+ state: string;
27019
+ zip: string;
27020
+ country: string;
27021
+ };
27022
+ geo?: { lat: number; lng: number };
27023
+ hours?: string[];
27024
+ priceRange?: string;
27025
+ }) => ({
27026
+ '@context': 'https://schema.org',
27027
+ '@type': 'LocalBusiness',
27028
+ name: data.name,
27029
+ description: data.description,
27030
+ url: data.url,
27031
+ telephone: data.phone,
27032
+ address: {
27033
+ '@type': 'PostalAddress',
27034
+ streetAddress: data.address.street,
27035
+ addressLocality: data.address.city,
27036
+ addressRegion: data.address.state,
27037
+ postalCode: data.address.zip,
27038
+ addressCountry: data.address.country,
27039
+ },
27040
+ geo: data.geo ? {
27041
+ '@type': 'GeoCoordinates',
27042
+ latitude: data.geo.lat,
27043
+ longitude: data.geo.lng,
27044
+ } : undefined,
27045
+ openingHours: data.hours,
27046
+ priceRange: data.priceRange,
27047
+ }),
27048
+ };`,
27049
+ explanation: `Comprehensive React SEO component with:
27050
+ \u2022 Full Open Graph support (including article metadata)
27051
+ \u2022 Twitter Cards with all variants
27052
+ \u2022 JSON-LD structured data with pre-built schema generators
27053
+ \u2022 Robots directives (noindex/nofollow)
27054
+ \u2022 Hreflang for internationalization
27055
+ \u2022 Canonical URL handling
27056
+
27057
+ Install: npm install react-helmet-async
27058
+ Wrap app: <HelmetProvider><App /></HelmetProvider>`,
27059
+ installCommands: ["npm install react-helmet-async"],
27060
+ additionalFiles: [
27061
+ {
27062
+ file: "src/main.tsx",
27063
+ code: `import React from 'react';
26799
27064
  import ReactDOM from 'react-dom/client';
26800
27065
  import { HelmetProvider } from 'react-helmet-async';
26801
27066
  import App from './App';
@@ -26808,29 +27073,63 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
26808
27073
  </HelmetProvider>
26809
27074
  </React.StrictMode>,
26810
27075
  );`,
26811
- explanation: "Updated main.tsx with HelmetProvider wrapper for react-helmet-async."
27076
+ explanation: "Updated main.tsx with HelmetProvider wrapper."
27077
+ }
27078
+ ]
26812
27079
  };
26813
27080
  }
26814
27081
  function generateNextJsAppRouterMetadata(options) {
26815
- const { siteName, siteUrl, title, description, image } = options;
27082
+ const { siteName, siteUrl, title, description, image, twitterHandle, locale } = options;
26816
27083
  return {
26817
27084
  file: "app/layout.tsx",
26818
- code: `import type { Metadata } from 'next';
27085
+ code: `import type { Metadata, Viewport } from 'next';
26819
27086
  import { Inter } from 'next/font/google';
26820
27087
  import './globals.css';
26821
27088
 
26822
- const inter = Inter({ subsets: ['latin'] });
27089
+ const inter = Inter({ subsets: ['latin'], display: 'swap' });
26823
27090
 
27091
+ /**
27092
+ * Default metadata for all pages
27093
+ * Individual pages can override with their own metadata export
27094
+ */
26824
27095
  export const metadata: Metadata = {
26825
27096
  metadataBase: new URL('${siteUrl}'),
27097
+
27098
+ // Default title with template
26826
27099
  title: {
26827
27100
  default: '${title || siteName}',
26828
27101
  template: \`%s | ${siteName}\`,
26829
27102
  },
27103
+
26830
27104
  description: '${description || `${siteName} - A compelling description of your product or service.`}',
27105
+
27106
+ // Indexing
27107
+ robots: {
27108
+ index: true,
27109
+ follow: true,
27110
+ googleBot: {
27111
+ index: true,
27112
+ follow: true,
27113
+ 'max-video-preview': -1,
27114
+ 'max-image-preview': 'large',
27115
+ 'max-snippet': -1,
27116
+ },
27117
+ },
27118
+
27119
+ // Icons
27120
+ icons: {
27121
+ icon: '/favicon.ico',
27122
+ shortcut: '/favicon-16x16.png',
27123
+ apple: '/apple-touch-icon.png',
27124
+ },
27125
+
27126
+ // Manifest
27127
+ manifest: '/site.webmanifest',
27128
+
27129
+ // Open Graph
26831
27130
  openGraph: {
26832
27131
  type: 'website',
26833
- locale: 'en_US',
27132
+ locale: '${locale || "en_US"}',
26834
27133
  url: '${siteUrl}',
26835
27134
  siteName: '${siteName}',
26836
27135
  title: '${title || siteName}',
@@ -26844,16 +27143,49 @@ export const metadata: Metadata = {
26844
27143
  },
26845
27144
  ],
26846
27145
  },
27146
+
27147
+ // Twitter
26847
27148
  twitter: {
26848
27149
  card: 'summary_large_image',
26849
27150
  title: '${title || siteName}',
26850
27151
  description: '${description || `${siteName} - A compelling description.`}',
26851
27152
  images: ['${image || "/og-image.png"}'],
27153
+ ${twitterHandle ? `site: '${twitterHandle}',
27154
+ creator: '${twitterHandle}',` : ""}
26852
27155
  },
26853
- robots: {
26854
- index: true,
26855
- follow: true,
27156
+
27157
+ // Verification (add your IDs)
27158
+ verification: {
27159
+ // google: 'your-google-verification-code',
27160
+ // yandex: 'your-yandex-verification-code',
27161
+ // bing: 'your-bing-verification-code',
26856
27162
  },
27163
+
27164
+ // Alternate languages (uncomment and customize)
27165
+ // alternates: {
27166
+ // canonical: '${siteUrl}',
27167
+ // languages: {
27168
+ // 'en-US': '${siteUrl}/en',
27169
+ // 'es-ES': '${siteUrl}/es',
27170
+ // },
27171
+ // },
27172
+
27173
+ // Category
27174
+ category: 'technology',
27175
+ };
27176
+
27177
+ /**
27178
+ * Viewport configuration
27179
+ * Separated from metadata in Next.js 14+
27180
+ */
27181
+ export const viewport: Viewport = {
27182
+ themeColor: [
27183
+ { media: '(prefers-color-scheme: light)', color: '#ffffff' },
27184
+ { media: '(prefers-color-scheme: dark)', color: '#000000' },
27185
+ ],
27186
+ width: 'device-width',
27187
+ initialScale: 1,
27188
+ maximumScale: 5,
26857
27189
  };
26858
27190
 
26859
27191
  export default function RootLayout({
@@ -26862,143 +27194,104 @@ export default function RootLayout({
26862
27194
  children: React.ReactNode;
26863
27195
  }) {
26864
27196
  return (
26865
- <html lang="en">
26866
- <body className={inter.className}>{children}</body>
27197
+ <html lang="${(locale || "en_US").split("_")[0]}" className={inter.className}>
27198
+ <body>
27199
+ {children}
27200
+
27201
+ {/* JSON-LD Organization Schema */}
27202
+ <script
27203
+ type="application/ld+json"
27204
+ dangerouslySetInnerHTML={{
27205
+ __html: JSON.stringify({
27206
+ '@context': 'https://schema.org',
27207
+ '@type': 'Organization',
27208
+ name: '${siteName}',
27209
+ url: '${siteUrl}',
27210
+ logo: '${siteUrl}/logo.png',
27211
+ sameAs: [
27212
+ // Add your social profiles
27213
+ // 'https://twitter.com/yourhandle',
27214
+ // 'https://linkedin.com/company/yourcompany',
27215
+ ],
27216
+ }),
27217
+ }}
27218
+ />
27219
+ </body>
26867
27220
  </html>
26868
27221
  );
26869
27222
  }`,
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';
27223
+ explanation: `Next.js App Router layout with comprehensive SEO:
27224
+ \u2022 Metadata API with title templates
27225
+ \u2022 Full Open Graph and Twitter Card support
27226
+ \u2022 Viewport configuration (Next.js 14+)
27227
+ \u2022 JSON-LD Organization schema
27228
+ \u2022 Verification tags for search consoles
27229
+ \u2022 Internationalization ready
27230
+ \u2022 Web font optimization with next/font`,
27231
+ additionalFiles: [
27232
+ {
27233
+ file: "app/robots.ts",
27234
+ code: `import type { MetadataRoute } from 'next';
26954
27235
 
26955
27236
  export default function robots(): MetadataRoute.Robots {
27237
+ const baseUrl = '${siteUrl}';
27238
+
26956
27239
  return {
26957
27240
  rules: [
26958
27241
  {
26959
27242
  userAgent: '*',
26960
27243
  allow: '/',
26961
- disallow: ['/api/', '/admin/', '/_next/'],
27244
+ disallow: ['/api/', '/admin/', '/_next/', '/private/'],
27245
+ },
27246
+ {
27247
+ userAgent: 'GPTBot',
27248
+ allow: '/',
26962
27249
  },
26963
27250
  ],
26964
- sitemap: '${siteUrl}/sitemap.xml',
27251
+ sitemap: \`\${baseUrl}/sitemap.xml\`,
26965
27252
  };
26966
27253
  }`,
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';
27254
+ explanation: "Robots.txt with AI crawler support."
27255
+ },
27256
+ {
27257
+ file: "app/sitemap.ts",
27258
+ code: `import type { MetadataRoute } from 'next';
26974
27259
 
26975
27260
  export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
27261
+ const baseUrl = '${siteUrl}';
27262
+
26976
27263
  // Static pages
26977
27264
  const staticPages: MetadataRoute.Sitemap = [
26978
27265
  {
26979
- url: '${siteUrl}',
27266
+ url: baseUrl,
26980
27267
  lastModified: new Date(),
26981
27268
  changeFrequency: 'daily',
26982
27269
  priority: 1,
26983
27270
  },
26984
27271
  {
26985
- url: '${siteUrl}/about',
27272
+ url: \`\${baseUrl}/about\`,
26986
27273
  lastModified: new Date(),
26987
27274
  changeFrequency: 'monthly',
26988
27275
  priority: 0.8,
26989
27276
  },
26990
27277
  {
26991
- url: '${siteUrl}/pricing',
27278
+ url: \`\${baseUrl}/pricing\`,
26992
27279
  lastModified: new Date(),
26993
27280
  changeFrequency: 'weekly',
26994
27281
  priority: 0.9,
26995
27282
  },
27283
+ {
27284
+ url: \`\${baseUrl}/blog\`,
27285
+ lastModified: new Date(),
27286
+ changeFrequency: 'daily',
27287
+ priority: 0.8,
27288
+ },
26996
27289
  ];
26997
27290
 
26998
- // Dynamic pages (fetch from your database/CMS)
26999
- // const posts = await fetchAllPosts();
27291
+ // Dynamic pages - fetch from your database/CMS
27292
+ // const posts = await db.post.findMany({ select: { slug: true, updatedAt: true } });
27000
27293
  // const dynamicPages = posts.map((post) => ({
27001
- // url: \`${siteUrl}/blog/\${post.slug}\`,
27294
+ // url: \`\${baseUrl}/blog/\${post.slug}\`,
27002
27295
  // lastModified: post.updatedAt,
27003
27296
  // changeFrequency: 'weekly' as const,
27004
27297
  // priority: 0.7,
@@ -27006,209 +27299,628 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
27006
27299
 
27007
27300
  return [...staticPages];
27008
27301
  }`,
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';
27302
+ explanation: "Dynamic sitemap generator."
27303
+ },
27304
+ {
27305
+ file: "lib/seo.ts",
27306
+ code: `import type { Metadata } from 'next';
27017
27307
 
27018
- interface SEOHeadProps {
27019
- title?: string;
27020
- description?: string;
27308
+ const baseUrl = '${siteUrl}';
27309
+ const siteName = '${siteName}';
27310
+
27311
+ interface PageSEOProps {
27312
+ title: string;
27313
+ description: string;
27314
+ path?: string;
27021
27315
  image?: string;
27022
- url?: string;
27023
27316
  type?: 'website' | 'article';
27317
+ publishedTime?: string;
27318
+ modifiedTime?: string;
27319
+ authors?: string[];
27320
+ tags?: string[];
27321
+ noIndex?: boolean;
27322
+ }
27323
+
27324
+ /**
27325
+ * Generate metadata for a page
27326
+ * Use in page.tsx: export const metadata = generateMetadata({ ... })
27327
+ */
27328
+ export function generatePageMetadata({
27329
+ title,
27330
+ description,
27331
+ path = '',
27332
+ image,
27333
+ type = 'website',
27334
+ publishedTime,
27335
+ modifiedTime,
27336
+ authors,
27337
+ tags,
27338
+ noIndex = false,
27339
+ }: PageSEOProps): Metadata {
27340
+ const url = \`\${baseUrl}\${path}\`;
27341
+ const ogImage = image || '/og-image.png';
27342
+
27343
+ return {
27344
+ title,
27345
+ description,
27346
+
27347
+ robots: noIndex ? { index: false, follow: false } : undefined,
27348
+
27349
+ alternates: {
27350
+ canonical: url,
27351
+ },
27352
+
27353
+ openGraph: {
27354
+ title,
27355
+ description,
27356
+ url,
27357
+ siteName,
27358
+ type,
27359
+ images: [{ url: ogImage, width: 1200, height: 630 }],
27360
+ ...(type === 'article' && {
27361
+ publishedTime,
27362
+ modifiedTime,
27363
+ authors,
27364
+ tags,
27365
+ }),
27366
+ },
27367
+
27368
+ twitter: {
27369
+ card: 'summary_large_image',
27370
+ title,
27371
+ description,
27372
+ images: [ogImage],
27373
+ },
27374
+ };
27024
27375
  }
27025
27376
 
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}\`;
27377
+ /**
27378
+ * Generate JSON-LD for articles
27379
+ */
27380
+ export function generateArticleJsonLd(article: {
27381
+ title: string;
27382
+ description: string;
27383
+ url: string;
27384
+ image: string;
27385
+ datePublished: string;
27386
+ dateModified?: string;
27387
+ author: { name: string; url?: string };
27388
+ }) {
27389
+ return {
27390
+ '@context': 'https://schema.org',
27391
+ '@type': 'Article',
27392
+ headline: article.title,
27393
+ description: article.description,
27394
+ url: article.url,
27395
+ image: article.image,
27396
+ datePublished: article.datePublished,
27397
+ dateModified: article.dateModified || article.datePublished,
27398
+ author: {
27399
+ '@type': 'Person',
27400
+ name: article.author.name,
27401
+ url: article.author.url,
27402
+ },
27403
+ publisher: {
27404
+ '@type': 'Organization',
27405
+ name: siteName,
27406
+ logo: {
27407
+ '@type': 'ImageObject',
27408
+ url: \`\${baseUrl}/logo.png\`,
27409
+ },
27410
+ },
27411
+ };
27412
+ }
27034
27413
 
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
- );
27414
+ /**
27415
+ * Generate JSON-LD for products
27416
+ */
27417
+ export function generateProductJsonLd(product: {
27418
+ name: string;
27419
+ description: string;
27420
+ image: string;
27421
+ price: number;
27422
+ currency?: string;
27423
+ availability?: 'InStock' | 'OutOfStock' | 'PreOrder';
27424
+ rating?: { value: number; count: number };
27425
+ brand?: string;
27426
+ sku?: string;
27427
+ }) {
27428
+ return {
27429
+ '@context': 'https://schema.org',
27430
+ '@type': 'Product',
27431
+ name: product.name,
27432
+ description: product.description,
27433
+ image: product.image,
27434
+ brand: product.brand ? { '@type': 'Brand', name: product.brand } : undefined,
27435
+ sku: product.sku,
27436
+ offers: {
27437
+ '@type': 'Offer',
27438
+ price: product.price,
27439
+ priceCurrency: product.currency || 'USD',
27440
+ availability: \`https://schema.org/\${product.availability || 'InStock'}\`,
27441
+ },
27442
+ ...(product.rating && {
27443
+ aggregateRating: {
27444
+ '@type': 'AggregateRating',
27445
+ ratingValue: product.rating.value,
27446
+ reviewCount: product.rating.count,
27447
+ },
27448
+ }),
27449
+ };
27450
+ }
27451
+
27452
+ /**
27453
+ * Generate JSON-LD for FAQ pages
27454
+ */
27455
+ export function generateFAQJsonLd(items: { question: string; answer: string }[]) {
27456
+ return {
27457
+ '@context': 'https://schema.org',
27458
+ '@type': 'FAQPage',
27459
+ mainEntity: items.map(item => ({
27460
+ '@type': 'Question',
27461
+ name: item.question,
27462
+ acceptedAnswer: {
27463
+ '@type': 'Answer',
27464
+ text: item.answer,
27465
+ },
27466
+ })),
27467
+ };
27054
27468
  }`,
27055
- explanation: "Next.js Pages Router SEO component using next/head. Import and use on each page."
27469
+ explanation: "SEO utility functions for generating metadata and JSON-LD."
27470
+ }
27471
+ ]
27056
27472
  };
27057
27473
  }
27058
27474
  function generateNuxtSEOHead(options) {
27059
- const { siteName, siteUrl, title, description, image } = options;
27475
+ const { siteName, siteUrl, title, description, image, twitterHandle, locale } = options;
27060
27476
  return {
27061
27477
  file: "composables/useSEO.ts",
27062
- code: `export function useSEO(options: {
27478
+ code: `/**
27479
+ * Comprehensive SEO composable for Nuxt 3
27480
+ *
27481
+ * Features:
27482
+ * - Full Open Graph support
27483
+ * - Twitter Cards
27484
+ * - JSON-LD structured data
27485
+ * - Canonical URLs
27486
+ * - Robots directives
27487
+ * - Internationalization
27488
+ */
27489
+
27490
+ interface SEOOptions {
27063
27491
  title?: string;
27064
27492
  description?: string;
27065
27493
  image?: string;
27066
27494
  url?: string;
27067
- type?: 'website' | 'article';
27068
- } = {}) {
27495
+ type?: 'website' | 'article' | 'product';
27496
+
27497
+ // Article-specific
27498
+ publishedTime?: string;
27499
+ modifiedTime?: string;
27500
+ author?: string;
27501
+ tags?: string[];
27502
+
27503
+ // Robots
27504
+ noIndex?: boolean;
27505
+ noFollow?: boolean;
27506
+
27507
+ // Structured data
27508
+ schema?: Record<string, unknown> | Record<string, unknown>[];
27509
+ }
27510
+
27511
+ const SITE_NAME = '${siteName}';
27512
+ const SITE_URL = '${siteUrl}';
27513
+ const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
27514
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
27515
+ const DEFAULT_LOCALE = '${locale || "en_US"}';
27516
+ const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
27517
+
27518
+ export function useSEO(options: SEOOptions = {}) {
27069
27519
  const route = useRoute();
27070
- const config = useRuntimeConfig();
27071
27520
 
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,
27521
+ const {
27522
+ title,
27523
+ description = DEFAULT_DESCRIPTION,
27524
+ image = DEFAULT_IMAGE,
27525
+ url,
27526
+ type = 'website',
27527
+ publishedTime,
27528
+ modifiedTime,
27529
+ author,
27530
+ tags,
27531
+ noIndex = false,
27532
+ noFollow = false,
27533
+ schema,
27534
+ } = options;
27535
+
27536
+ const pageUrl = url || \`\${SITE_URL}\${route.path}\`;
27537
+ const fullTitle = title
27538
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
27539
+ : SITE_NAME;
27540
+ const imageUrl = image.startsWith('http') ? image : \`\${SITE_URL}\${image}\`;
27541
+
27542
+ const robotsContent = [
27543
+ noIndex ? 'noindex' : 'index',
27544
+ noFollow ? 'nofollow' : 'follow',
27545
+ ].join(', ');
27546
+
27547
+ // Build meta array
27548
+ const meta = [
27549
+ { name: 'description', content: description },
27550
+ { name: 'robots', content: robotsContent },
27551
+
27552
+ // Open Graph
27553
+ { property: 'og:type', content: type },
27554
+ { property: 'og:url', content: pageUrl },
27555
+ { property: 'og:title', content: fullTitle },
27556
+ { property: 'og:description', content: description },
27557
+ { property: 'og:image', content: imageUrl },
27558
+ { property: 'og:image:width', content: '1200' },
27559
+ { property: 'og:image:height', content: '630' },
27560
+ { property: 'og:site_name', content: SITE_NAME },
27561
+ { property: 'og:locale', content: DEFAULT_LOCALE },
27562
+
27563
+ // Twitter
27564
+ { name: 'twitter:card', content: 'summary_large_image' },
27565
+ { name: 'twitter:title', content: fullTitle },
27566
+ { name: 'twitter:description', content: description },
27567
+ { name: 'twitter:image', content: imageUrl },
27568
+ ];
27569
+
27570
+ // Add Twitter handle if configured
27571
+ if (TWITTER_HANDLE) {
27572
+ meta.push(
27573
+ { name: 'twitter:site', content: TWITTER_HANDLE },
27574
+ { name: 'twitter:creator', content: TWITTER_HANDLE }
27575
+ );
27576
+ }
27577
+
27578
+ // Add article-specific meta
27579
+ if (type === 'article') {
27580
+ if (publishedTime) meta.push({ property: 'article:published_time', content: publishedTime });
27581
+ if (modifiedTime) meta.push({ property: 'article:modified_time', content: modifiedTime });
27582
+ if (author) meta.push({ property: 'article:author', content: author });
27583
+ tags?.forEach(tag => meta.push({ property: 'article:tag', content: tag }));
27584
+ }
27585
+
27586
+ // Build JSON-LD
27587
+ const defaultSchema = {
27588
+ '@context': 'https://schema.org',
27589
+ '@type': 'WebSite',
27590
+ name: SITE_NAME,
27591
+ url: SITE_URL,
27078
27592
  };
27079
27593
 
27080
- const meta = { ...defaults, ...options };
27081
- const fullTitle = meta.title.includes('${siteName}')
27082
- ? meta.title
27083
- : \`\${meta.title} | ${siteName}\`;
27594
+ const jsonLd = schema
27595
+ ? Array.isArray(schema)
27596
+ ? [defaultSchema, ...schema]
27597
+ : [defaultSchema, schema]
27598
+ : [defaultSchema];
27084
27599
 
27085
27600
  useHead({
27086
27601
  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
- ],
27602
+ meta,
27102
27603
  link: [
27103
- { rel: 'canonical', href: meta.url },
27604
+ { rel: 'canonical', href: pageUrl },
27605
+ ],
27606
+ script: [
27607
+ {
27608
+ type: 'application/ld+json',
27609
+ innerHTML: JSON.stringify(jsonLd),
27610
+ },
27104
27611
  ],
27105
27612
  });
27106
- }`,
27107
- explanation: "Nuxt 3 SEO composable using useHead(). Call useSEO() in any page to set meta tags."
27108
- };
27109
27613
  }
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
27614
 
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."
27615
+ /**
27616
+ * Schema generators for common types
27617
+ */
27618
+ export const Schema = {
27619
+ article: (data: {
27620
+ headline: string;
27621
+ description: string;
27622
+ image: string;
27623
+ datePublished: string;
27624
+ dateModified?: string;
27625
+ author: { name: string; url?: string };
27626
+ }) => ({
27627
+ '@context': 'https://schema.org',
27628
+ '@type': 'Article',
27629
+ headline: data.headline,
27630
+ description: data.description,
27631
+ image: data.image,
27632
+ datePublished: data.datePublished,
27633
+ dateModified: data.dateModified || data.datePublished,
27634
+ author: { '@type': 'Person', ...data.author },
27635
+ publisher: {
27636
+ '@type': 'Organization',
27637
+ name: SITE_NAME,
27638
+ url: SITE_URL,
27639
+ },
27640
+ }),
27641
+
27642
+ product: (data: {
27643
+ name: string;
27644
+ description: string;
27645
+ image: string;
27646
+ price: number;
27647
+ currency?: string;
27648
+ availability?: 'InStock' | 'OutOfStock' | 'PreOrder';
27649
+ }) => ({
27650
+ '@context': 'https://schema.org',
27651
+ '@type': 'Product',
27652
+ name: data.name,
27653
+ description: data.description,
27654
+ image: data.image,
27655
+ offers: {
27656
+ '@type': 'Offer',
27657
+ price: data.price,
27658
+ priceCurrency: data.currency || 'USD',
27659
+ availability: \`https://schema.org/\${data.availability || 'InStock'}\`,
27660
+ },
27661
+ }),
27662
+
27663
+ faq: (items: { question: string; answer: string }[]) => ({
27664
+ '@context': 'https://schema.org',
27665
+ '@type': 'FAQPage',
27666
+ mainEntity: items.map(item => ({
27667
+ '@type': 'Question',
27668
+ name: item.question,
27669
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
27670
+ })),
27671
+ }),
27672
+
27673
+ breadcrumb: (items: { name: string; url: string }[]) => ({
27674
+ '@context': 'https://schema.org',
27675
+ '@type': 'BreadcrumbList',
27676
+ itemListElement: items.map((item, i) => ({
27677
+ '@type': 'ListItem',
27678
+ position: i + 1,
27679
+ name: item.name,
27680
+ item: item.url,
27681
+ })),
27682
+ }),
27683
+ };`,
27684
+ explanation: `Nuxt 3 comprehensive SEO composable with:
27685
+ \u2022 Full useHead integration
27686
+ \u2022 Open Graph with article support
27687
+ \u2022 Twitter Cards
27688
+ \u2022 JSON-LD schema generators
27689
+ \u2022 Robots directives
27690
+ \u2022 Canonical URLs
27691
+
27692
+ Usage: useSEO({ title: 'Page', description: '...' })`,
27693
+ additionalFiles: [
27694
+ {
27695
+ file: "server/routes/sitemap.xml.ts",
27696
+ code: `import { SitemapStream, streamToPromise } from 'sitemap';
27697
+ import { Readable } from 'stream';
27698
+
27699
+ export default defineEventHandler(async () => {
27700
+ const baseUrl = '${siteUrl}';
27701
+
27702
+ // Define your pages
27703
+ const pages = [
27704
+ { url: '/', changefreq: 'daily', priority: 1 },
27705
+ { url: '/about', changefreq: 'monthly', priority: 0.8 },
27706
+ { url: '/pricing', changefreq: 'weekly', priority: 0.9 },
27707
+ { url: '/blog', changefreq: 'daily', priority: 0.8 },
27708
+ ];
27709
+
27710
+ // Add dynamic pages from your database
27711
+ // const posts = await $fetch('/api/posts');
27712
+ // posts.forEach(post => pages.push({
27713
+ // url: \`/blog/\${post.slug}\`,
27714
+ // changefreq: 'weekly',
27715
+ // priority: 0.7,
27716
+ // lastmod: post.updatedAt,
27717
+ // }));
27718
+
27719
+ const stream = new SitemapStream({ hostname: baseUrl });
27720
+
27721
+ return streamToPromise(Readable.from(pages).pipe(stream)).then((data) =>
27722
+ data.toString()
27723
+ );
27724
+ });`,
27725
+ explanation: "Dynamic sitemap generator for Nuxt."
27726
+ },
27727
+ {
27728
+ file: "public/robots.txt",
27729
+ code: `User-agent: *
27730
+ Allow: /
27731
+ Disallow: /api/
27732
+ Disallow: /admin/
27733
+
27734
+ User-agent: GPTBot
27735
+ Allow: /
27736
+
27737
+ Sitemap: ${siteUrl}/sitemap.xml`,
27738
+ explanation: "Robots.txt with AI crawler support."
27739
+ }
27740
+ ]
27127
27741
  };
27128
27742
  }
27129
27743
  function generateVueSEOHead(options) {
27130
- const { siteName, siteUrl, title, description, image } = options;
27744
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
27131
27745
  return {
27132
27746
  file: "src/composables/useSEO.ts",
27133
- code: `import { useHead } from '@unhead/vue';
27134
- import { computed, ref } from 'vue';
27747
+ code: `import { useHead, useServerHead } from '@unhead/vue';
27748
+ import { computed, unref, MaybeRef } from 'vue';
27749
+ import { useRoute } from 'vue-router';
27135
27750
 
27136
27751
  interface SEOOptions {
27137
- title?: string;
27138
- description?: string;
27139
- image?: string;
27140
- url?: string;
27752
+ title?: MaybeRef<string>;
27753
+ description?: MaybeRef<string>;
27754
+ image?: MaybeRef<string>;
27141
27755
  type?: 'website' | 'article';
27756
+ noIndex?: boolean;
27757
+ schema?: Record<string, unknown>;
27142
27758
  }
27143
27759
 
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
- );
27760
+ const SITE_NAME = '${siteName}';
27761
+ const SITE_URL = '${siteUrl}';
27762
+ const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
27763
+ const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
27764
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
27157
27765
 
27766
+ export function useSEO(options: SEOOptions = {}) {
27767
+ const route = useRoute();
27768
+
27769
+ const title = computed(() => {
27770
+ const t = unref(options.title);
27771
+ return t ? (t.includes(SITE_NAME) ? t : \`\${t} | \${SITE_NAME}\`) : SITE_NAME;
27772
+ });
27773
+
27774
+ const description = computed(() => unref(options.description) || DEFAULT_DESCRIPTION);
27775
+ const image = computed(() => {
27776
+ const img = unref(options.image) || DEFAULT_IMAGE;
27777
+ return img.startsWith('http') ? img : \`\${SITE_URL}\${img}\`;
27778
+ });
27779
+ const url = computed(() => \`\${SITE_URL}\${route.path}\`);
27780
+
27158
27781
  useHead({
27159
- title: fullTitle,
27782
+ title,
27160
27783
  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}' },
27784
+ { name: 'description', content: description },
27785
+ { name: 'robots', content: options.noIndex ? 'noindex, nofollow' : 'index, follow' },
27786
+
27787
+ // Open Graph
27788
+ { property: 'og:type', content: options.type || 'website' },
27789
+ { property: 'og:url', content: url },
27790
+ { property: 'og:title', content: title },
27791
+ { property: 'og:description', content: description },
27792
+ { property: 'og:image', content: image },
27793
+ { property: 'og:site_name', content: SITE_NAME },
27794
+
27795
+ // Twitter
27168
27796
  { 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 },
27797
+ { name: 'twitter:title', content: title },
27798
+ { name: 'twitter:description', content: description },
27799
+ { name: 'twitter:image', content: image },
27800
+ ...(TWITTER_HANDLE ? [
27801
+ { name: 'twitter:site', content: TWITTER_HANDLE },
27802
+ { name: 'twitter:creator', content: TWITTER_HANDLE },
27803
+ ] : []),
27172
27804
  ],
27173
27805
  link: [
27174
- { rel: 'canonical', href: meta.url },
27806
+ { rel: 'canonical', href: url },
27175
27807
  ],
27808
+ script: options.schema ? [
27809
+ { type: 'application/ld+json', innerHTML: JSON.stringify(options.schema) },
27810
+ ] : [],
27176
27811
  });
27177
27812
  }`,
27178
- explanation: "Vue 3 SEO composable using @unhead/vue. Install with: npm install @unhead/vue",
27179
- installCommands: ["npm install @unhead/vue"]
27813
+ explanation: `Vue 3 SEO composable using @unhead/vue with:
27814
+ \u2022 Reactive title/description
27815
+ \u2022 Open Graph and Twitter Cards
27816
+ \u2022 JSON-LD schema support
27817
+ \u2022 Canonical URLs
27818
+
27819
+ Install: npm install @unhead/vue`,
27820
+ installCommands: ["npm install @unhead/vue"],
27821
+ additionalFiles: [
27822
+ {
27823
+ file: "src/main.ts",
27824
+ code: `import { createApp } from 'vue';
27825
+ import { createHead } from '@unhead/vue';
27826
+ import { createRouter, createWebHistory } from 'vue-router';
27827
+ import App from './App.vue';
27828
+
27829
+ const app = createApp(App);
27830
+ const head = createHead();
27831
+ const router = createRouter({
27832
+ history: createWebHistory(),
27833
+ routes: [/* your routes */],
27834
+ });
27835
+
27836
+ app.use(head);
27837
+ app.use(router);
27838
+ app.mount('#app');`,
27839
+ explanation: "Vue app setup with @unhead/vue."
27840
+ }
27841
+ ]
27180
27842
  };
27181
27843
  }
27182
27844
  function generateAstroBaseHead(options) {
27183
- const { siteName, siteUrl, title, description, image } = options;
27845
+ const { siteName, siteUrl, description, image, twitterHandle, locale } = options;
27184
27846
  return {
27185
27847
  file: "src/components/BaseHead.astro",
27186
27848
  code: `---
27849
+ /**
27850
+ * Comprehensive SEO Head Component for Astro
27851
+ *
27852
+ * Features:
27853
+ * - Full Open Graph support
27854
+ * - Twitter Cards
27855
+ * - JSON-LD structured data
27856
+ * - Canonical URLs
27857
+ * - Robots directives
27858
+ * - Performance optimizations
27859
+ */
27860
+
27187
27861
  interface Props {
27188
27862
  title?: string;
27189
27863
  description?: string;
27190
27864
  image?: string;
27191
27865
  type?: 'website' | 'article';
27866
+ publishedTime?: string;
27867
+ modifiedTime?: string;
27868
+ author?: string;
27869
+ tags?: string[];
27870
+ noIndex?: boolean;
27871
+ schema?: Record<string, unknown>;
27192
27872
  }
27193
27873
 
27874
+ const SITE_NAME = '${siteName}';
27875
+ const SITE_URL = '${siteUrl}';
27876
+ const DEFAULT_IMAGE = '${image || "/og-image.png"}';
27877
+ const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
27878
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
27879
+ const DEFAULT_LOCALE = '${locale || "en_US"}';
27880
+
27194
27881
  const {
27195
- title = '${title || siteName}',
27196
- description = '${description || `${siteName} - A compelling description.`}',
27197
- image = '${image || "/og-image.png"}',
27882
+ title,
27883
+ description = DEFAULT_DESCRIPTION,
27884
+ image = DEFAULT_IMAGE,
27198
27885
  type = 'website',
27886
+ publishedTime,
27887
+ modifiedTime,
27888
+ author,
27889
+ tags,
27890
+ noIndex = false,
27891
+ schema,
27199
27892
  } = Astro.props;
27200
27893
 
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);
27894
+ const canonicalURL = new URL(Astro.url.pathname, Astro.site || SITE_URL);
27895
+ const fullTitle = title
27896
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
27897
+ : SITE_NAME;
27898
+ const imageURL = new URL(image, Astro.site || SITE_URL);
27899
+ const robotsContent = noIndex ? 'noindex, nofollow' : 'index, follow';
27900
+
27901
+ // Default website schema
27902
+ const defaultSchema = {
27903
+ '@context': 'https://schema.org',
27904
+ '@type': 'WebSite',
27905
+ name: SITE_NAME,
27906
+ url: SITE_URL,
27907
+ };
27908
+
27909
+ const jsonLd = schema ? [defaultSchema, schema] : [defaultSchema];
27204
27910
  ---
27205
27911
 
27206
27912
  <!-- Global Metadata -->
27207
27913
  <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" />
27914
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
27210
27915
  <meta name="generator" content={Astro.generator} />
27211
27916
 
27917
+ <!-- Favicon -->
27918
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
27919
+ <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
27920
+ <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
27921
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
27922
+ <link rel="manifest" href="/site.webmanifest" />
27923
+
27212
27924
  <!-- Canonical URL -->
27213
27925
  <link rel="canonical" href={canonicalURL} />
27214
27926
 
@@ -27216,6 +27928,11 @@ const imageURL = new URL(image, Astro.site);
27216
27928
  <title>{fullTitle}</title>
27217
27929
  <meta name="title" content={fullTitle} />
27218
27930
  <meta name="description" content={description} />
27931
+ <meta name="robots" content={robotsContent} />
27932
+
27933
+ <!-- Theme -->
27934
+ <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
27935
+ <meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)" />
27219
27936
 
27220
27937
  <!-- Open Graph / Facebook -->
27221
27938
  <meta property="og:type" content={type} />
@@ -27223,143 +27940,449 @@ const imageURL = new URL(image, Astro.site);
27223
27940
  <meta property="og:title" content={fullTitle} />
27224
27941
  <meta property="og:description" content={description} />
27225
27942
  <meta property="og:image" content={imageURL} />
27226
- <meta property="og:site_name" content="${siteName}" />
27943
+ <meta property="og:image:width" content="1200" />
27944
+ <meta property="og:image:height" content="630" />
27945
+ <meta property="og:image:alt" content={fullTitle} />
27946
+ <meta property="og:site_name" content={SITE_NAME} />
27947
+ <meta property="og:locale" content={DEFAULT_LOCALE} />
27948
+
27949
+ {type === 'article' && publishedTime && (
27950
+ <meta property="article:published_time" content={publishedTime} />
27951
+ )}
27952
+ {type === 'article' && modifiedTime && (
27953
+ <meta property="article:modified_time" content={modifiedTime} />
27954
+ )}
27955
+ {type === 'article' && author && (
27956
+ <meta property="article:author" content={author} />
27957
+ )}
27958
+ {type === 'article' && tags?.map((tag) => (
27959
+ <meta property="article:tag" content={tag} />
27960
+ ))}
27227
27961
 
27228
27962
  <!-- Twitter -->
27229
27963
  <meta name="twitter:card" content="summary_large_image" />
27230
27964
  <meta name="twitter:url" content={canonicalURL} />
27231
27965
  <meta name="twitter:title" content={fullTitle} />
27232
27966
  <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: `---
27967
+ <meta name="twitter:image" content={imageURL} />
27968
+ <meta name="twitter:image:alt" content={fullTitle} />
27969
+ {TWITTER_HANDLE && <meta name="twitter:site" content={TWITTER_HANDLE} />}
27970
+ {TWITTER_HANDLE && <meta name="twitter:creator" content={TWITTER_HANDLE} />}
27971
+
27972
+ <!-- Performance: Preconnect to external origins -->
27973
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
27974
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
27975
+
27976
+ <!-- JSON-LD Structured Data -->
27977
+ <script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />`,
27978
+ explanation: `Astro comprehensive SEO component with:
27979
+ \u2022 Full Open Graph with article support
27980
+ \u2022 Twitter Cards
27981
+ \u2022 JSON-LD structured data
27982
+ \u2022 Performance optimizations (preconnect)
27983
+ \u2022 Theme color for PWA
27984
+ \u2022 Favicon configuration
27985
+
27986
+ Usage: <BaseHead title="Page" description="..." />`,
27987
+ additionalFiles: [
27988
+ {
27989
+ file: "src/layouts/BaseLayout.astro",
27990
+ code: `---
27241
27991
  import BaseHead from '../components/BaseHead.astro';
27242
27992
 
27243
27993
  interface Props {
27244
27994
  title?: string;
27245
27995
  description?: string;
27246
27996
  image?: string;
27997
+ type?: 'website' | 'article';
27998
+ schema?: Record<string, unknown>;
27247
27999
  }
27248
28000
 
27249
- const { title, description, image } = Astro.props;
28001
+ const { title, description, image, type, schema } = Astro.props;
27250
28002
  ---
27251
28003
 
27252
28004
  <!DOCTYPE html>
27253
- <html lang="en">
28005
+ <html lang="${(locale || "en_US").split("_")[0]}">
27254
28006
  <head>
27255
- <BaseHead title={title} description={description} image={image} />
28007
+ <BaseHead
28008
+ title={title}
28009
+ description={description}
28010
+ image={image}
28011
+ type={type}
28012
+ schema={schema}
28013
+ />
27256
28014
  </head>
27257
28015
  <body>
27258
- <main>
27259
- <slot />
27260
- </main>
28016
+ <slot />
27261
28017
  </body>
27262
28018
  </html>`,
27263
- explanation: "Astro layout using BaseHead component. Use in pages with frontmatter."
28019
+ explanation: "Base layout using the SEO head component."
28020
+ },
28021
+ {
28022
+ file: "public/robots.txt",
28023
+ code: `User-agent: *
28024
+ Allow: /
28025
+ Disallow: /api/
28026
+
28027
+ User-agent: GPTBot
28028
+ Allow: /
28029
+
28030
+ Sitemap: ${siteUrl}/sitemap-index.xml`,
28031
+ explanation: "Robots.txt with AI crawler support."
28032
+ }
28033
+ ]
27264
28034
  };
27265
28035
  }
27266
28036
  function generateSvelteKitSEOHead(options) {
27267
- const { siteName, siteUrl, title, description, image } = options;
28037
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
27268
28038
  return {
27269
28039
  file: "src/lib/components/SEOHead.svelte",
27270
28040
  code: `<script lang="ts">
27271
28041
  import { page } from '$app/stores';
27272
28042
 
27273
- export let title = '${title || siteName}';
27274
- export let description = '${description || `${siteName} - A compelling description.`}';
27275
- export let image = '${image || `${siteUrl}/og-image.png`}';
28043
+ export let title: string | undefined = undefined;
28044
+ export let description: string = '${description || `${siteName} - A compelling description.`}';
28045
+ export let image: string = '${image || `${siteUrl}/og-image.png`}';
27276
28046
  export let type: 'website' | 'article' = 'website';
27277
-
27278
- $: fullTitle = title.includes('${siteName}') ? title : \`\${title} | ${siteName}\`;
28047
+ export let publishedTime: string | undefined = undefined;
28048
+ export let modifiedTime: string | undefined = undefined;
28049
+ export let author: string | undefined = undefined;
28050
+ export let tags: string[] = [];
28051
+ export let noIndex: boolean = false;
28052
+ export let schema: Record<string, unknown> | undefined = undefined;
28053
+
28054
+ const SITE_NAME = '${siteName}';
28055
+ const SITE_URL = '${siteUrl}';
28056
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
28057
+
28058
+ $: fullTitle = title
28059
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
28060
+ : SITE_NAME;
27279
28061
  $: canonicalUrl = $page.url.href;
28062
+ $: imageUrl = image.startsWith('http') ? image : \`\${SITE_URL}\${image}\`;
28063
+ $: robotsContent = noIndex ? 'noindex, nofollow' : 'index, follow';
28064
+
28065
+ $: defaultSchema = {
28066
+ '@context': 'https://schema.org',
28067
+ '@type': 'WebSite',
28068
+ name: SITE_NAME,
28069
+ url: SITE_URL,
28070
+ };
28071
+
28072
+ $: jsonLd = schema ? [defaultSchema, schema] : [defaultSchema];
27280
28073
  </script>
27281
28074
 
27282
28075
  <svelte:head>
28076
+ <!-- Primary Meta Tags -->
27283
28077
  <title>{fullTitle}</title>
28078
+ <meta name="title" content={fullTitle} />
27284
28079
  <meta name="description" content={description} />
28080
+ <meta name="robots" content={robotsContent} />
27285
28081
  <link rel="canonical" href={canonicalUrl} />
27286
-
28082
+
28083
+ <!-- Open Graph / Facebook -->
27287
28084
  <meta property="og:type" content={type} />
27288
28085
  <meta property="og:url" content={canonicalUrl} />
27289
28086
  <meta property="og:title" content={fullTitle} />
27290
28087
  <meta property="og:description" content={description} />
27291
- <meta property="og:image" content={image} />
27292
- <meta property="og:site_name" content="${siteName}" />
27293
-
28088
+ <meta property="og:image" content={imageUrl} />
28089
+ <meta property="og:image:width" content="1200" />
28090
+ <meta property="og:image:height" content="630" />
28091
+ <meta property="og:site_name" content={SITE_NAME} />
28092
+
28093
+ {#if type === 'article'}
28094
+ {#if publishedTime}
28095
+ <meta property="article:published_time" content={publishedTime} />
28096
+ {/if}
28097
+ {#if modifiedTime}
28098
+ <meta property="article:modified_time" content={modifiedTime} />
28099
+ {/if}
28100
+ {#if author}
28101
+ <meta property="article:author" content={author} />
28102
+ {/if}
28103
+ {#each tags as tag}
28104
+ <meta property="article:tag" content={tag} />
28105
+ {/each}
28106
+ {/if}
28107
+
28108
+ <!-- Twitter -->
27294
28109
  <meta name="twitter:card" content="summary_large_image" />
27295
28110
  <meta name="twitter:title" content={fullTitle} />
27296
28111
  <meta name="twitter:description" content={description} />
27297
- <meta name="twitter:image" content={image} />
28112
+ <meta name="twitter:image" content={imageUrl} />
28113
+ {#if TWITTER_HANDLE}
28114
+ <meta name="twitter:site" content={TWITTER_HANDLE} />
28115
+ <meta name="twitter:creator" content={TWITTER_HANDLE} />
28116
+ {/if}
28117
+
28118
+ <!-- JSON-LD Structured Data -->
28119
+ {@html \`<script type="application/ld+json">\${JSON.stringify(jsonLd)}</script>\`}
27298
28120
  </svelte:head>`,
27299
- explanation: "SvelteKit SEO component using svelte:head. Import and use in +page.svelte files."
28121
+ explanation: `SvelteKit comprehensive SEO component with:
28122
+ \u2022 Reactive props
28123
+ \u2022 Full Open Graph with article support
28124
+ \u2022 Twitter Cards
28125
+ \u2022 JSON-LD structured data
28126
+ \u2022 Robots directives
28127
+
28128
+ Usage: <SEOHead title="Page" description="..." />`,
28129
+ additionalFiles: [
28130
+ {
28131
+ file: "src/routes/+layout.svelte",
28132
+ code: `<script lang="ts">
28133
+ import '../app.css';
28134
+ </script>
28135
+
28136
+ <slot />`,
28137
+ explanation: "Root layout."
28138
+ },
28139
+ {
28140
+ file: "static/robots.txt",
28141
+ code: `User-agent: *
28142
+ Allow: /
28143
+ Disallow: /api/
28144
+
28145
+ User-agent: GPTBot
28146
+ Allow: /
28147
+
28148
+ Sitemap: ${siteUrl}/sitemap.xml`,
28149
+ explanation: "Robots.txt with AI crawler support."
28150
+ }
28151
+ ]
27300
28152
  };
27301
28153
  }
27302
28154
  function generateAngularSEOService(options) {
27303
- const { siteName, siteUrl, title, description, image } = options;
28155
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
27304
28156
  return {
27305
- file: "src/app/services/seo.service.ts",
27306
- code: `import { Injectable } from '@angular/core';
28157
+ file: "src/app/core/services/seo.service.ts",
28158
+ code: `import { Injectable, Inject } from '@angular/core';
27307
28159
  import { Meta, Title } from '@angular/platform-browser';
27308
- import { Router } from '@angular/router';
28160
+ import { Router, NavigationEnd } from '@angular/router';
28161
+ import { DOCUMENT } from '@angular/common';
28162
+ import { filter } from 'rxjs/operators';
27309
28163
 
27310
28164
  interface SEOConfig {
27311
28165
  title?: string;
27312
28166
  description?: string;
27313
28167
  image?: string;
27314
28168
  type?: 'website' | 'article';
28169
+ publishedTime?: string;
28170
+ modifiedTime?: string;
28171
+ author?: string;
28172
+ tags?: string[];
28173
+ noIndex?: boolean;
28174
+ schema?: Record<string, unknown>;
27315
28175
  }
27316
28176
 
27317
28177
  @Injectable({
27318
28178
  providedIn: 'root'
27319
28179
  })
27320
28180
  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`}';
28181
+ private readonly siteName = '${siteName}';
28182
+ private readonly siteUrl = '${siteUrl}';
28183
+ private readonly defaultDescription = '${description || `${siteName} - A compelling description.`}';
28184
+ private readonly defaultImage = '${image || `${siteUrl}/og-image.png`}';
28185
+ private readonly twitterHandle = '${twitterHandle || ""}';
27325
28186
 
27326
28187
  constructor(
27327
28188
  private meta: Meta,
27328
28189
  private titleService: Title,
27329
- private router: Router
27330
- ) {}
28190
+ private router: Router,
28191
+ @Inject(DOCUMENT) private document: Document
28192
+ ) {
28193
+ // Update canonical URL on route change
28194
+ this.router.events.pipe(
28195
+ filter(event => event instanceof NavigationEnd)
28196
+ ).subscribe(() => {
28197
+ this.updateCanonical();
28198
+ });
28199
+ }
27331
28200
 
28201
+ /**
28202
+ * Update all SEO meta tags
28203
+ */
27332
28204
  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';
28205
+ const {
28206
+ title,
28207
+ description = this.defaultDescription,
28208
+ image = this.defaultImage,
28209
+ type = 'website',
28210
+ publishedTime,
28211
+ modifiedTime,
28212
+ author,
28213
+ tags,
28214
+ noIndex = false,
28215
+ schema,
28216
+ } = config;
28217
+
28218
+ const fullTitle = title
28219
+ ? (title.includes(this.siteName) ? title : \`\${title} | \${this.siteName}\`)
28220
+ : this.siteName;
28221
+ const pageUrl = this.siteUrl + this.router.url;
28222
+ const imageUrl = image.startsWith('http') ? image : \`\${this.siteUrl}\${image}\`;
28223
+ const robotsContent = noIndex ? 'noindex, nofollow' : 'index, follow';
27339
28224
 
27340
28225
  // Title
27341
28226
  this.titleService.setTitle(fullTitle);
27342
28227
 
27343
- // Primary Meta
27344
- this.meta.updateTag({ name: 'description', content: description });
27345
- this.meta.updateTag({ rel: 'canonical', href: url });
28228
+ // Primary Meta Tags
28229
+ this.setMetaTag('description', description);
28230
+ this.setMetaTag('robots', robotsContent);
27346
28231
 
27347
28232
  // 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 });
28233
+ this.setMetaProperty('og:type', type);
28234
+ this.setMetaProperty('og:url', pageUrl);
28235
+ this.setMetaProperty('og:title', fullTitle);
28236
+ this.setMetaProperty('og:description', description);
28237
+ this.setMetaProperty('og:image', imageUrl);
28238
+ this.setMetaProperty('og:image:width', '1200');
28239
+ this.setMetaProperty('og:image:height', '630');
28240
+ this.setMetaProperty('og:site_name', this.siteName);
28241
+
28242
+ // Article-specific
28243
+ if (type === 'article') {
28244
+ if (publishedTime) this.setMetaProperty('article:published_time', publishedTime);
28245
+ if (modifiedTime) this.setMetaProperty('article:modified_time', modifiedTime);
28246
+ if (author) this.setMetaProperty('article:author', author);
28247
+ tags?.forEach(tag => this.setMetaProperty('article:tag', tag));
28248
+ }
27354
28249
 
27355
28250
  // 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 });
28251
+ this.setMetaTag('twitter:card', 'summary_large_image');
28252
+ this.setMetaTag('twitter:title', fullTitle);
28253
+ this.setMetaTag('twitter:description', description);
28254
+ this.setMetaTag('twitter:image', imageUrl);
28255
+ if (this.twitterHandle) {
28256
+ this.setMetaTag('twitter:site', this.twitterHandle);
28257
+ this.setMetaTag('twitter:creator', this.twitterHandle);
28258
+ }
28259
+
28260
+ // Update canonical
28261
+ this.updateCanonical(pageUrl);
28262
+
28263
+ // Update JSON-LD
28264
+ this.updateJsonLd(schema);
28265
+ }
28266
+
28267
+ private setMetaTag(name: string, content: string): void {
28268
+ this.meta.updateTag({ name, content });
28269
+ }
28270
+
28271
+ private setMetaProperty(property: string, content: string): void {
28272
+ this.meta.updateTag({ property, content });
28273
+ }
28274
+
28275
+ private updateCanonical(url?: string): void {
28276
+ const canonicalUrl = url || this.siteUrl + this.router.url;
28277
+ let link = this.document.querySelector('link[rel="canonical"]') as HTMLLinkElement;
28278
+
28279
+ if (!link) {
28280
+ link = this.document.createElement('link');
28281
+ link.setAttribute('rel', 'canonical');
28282
+ this.document.head.appendChild(link);
28283
+ }
28284
+
28285
+ link.setAttribute('href', canonicalUrl);
28286
+ }
28287
+
28288
+ private updateJsonLd(schema?: Record<string, unknown>): void {
28289
+ // Remove existing JSON-LD
28290
+ const existing = this.document.querySelector('script[type="application/ld+json"]');
28291
+ if (existing) existing.remove();
28292
+
28293
+ // Add new JSON-LD
28294
+ const defaultSchema = {
28295
+ '@context': 'https://schema.org',
28296
+ '@type': 'WebSite',
28297
+ name: this.siteName,
28298
+ url: this.siteUrl,
28299
+ };
28300
+
28301
+ const jsonLd = schema ? [defaultSchema, schema] : [defaultSchema];
28302
+
28303
+ const script = this.document.createElement('script');
28304
+ script.type = 'application/ld+json';
28305
+ script.text = JSON.stringify(jsonLd);
28306
+ this.document.head.appendChild(script);
28307
+ }
28308
+
28309
+ /**
28310
+ * Generate Article schema
28311
+ */
28312
+ articleSchema(data: {
28313
+ headline: string;
28314
+ description: string;
28315
+ image: string;
28316
+ datePublished: string;
28317
+ dateModified?: string;
28318
+ author: { name: string; url?: string };
28319
+ }): Record<string, unknown> {
28320
+ return {
28321
+ '@context': 'https://schema.org',
28322
+ '@type': 'Article',
28323
+ headline: data.headline,
28324
+ description: data.description,
28325
+ image: data.image,
28326
+ datePublished: data.datePublished,
28327
+ dateModified: data.dateModified || data.datePublished,
28328
+ author: { '@type': 'Person', ...data.author },
28329
+ publisher: {
28330
+ '@type': 'Organization',
28331
+ name: this.siteName,
28332
+ url: this.siteUrl,
28333
+ },
28334
+ };
28335
+ }
28336
+
28337
+ /**
28338
+ * Generate Product schema
28339
+ */
28340
+ productSchema(data: {
28341
+ name: string;
28342
+ description: string;
28343
+ image: string;
28344
+ price: number;
28345
+ currency?: string;
28346
+ }): Record<string, unknown> {
28347
+ return {
28348
+ '@context': 'https://schema.org',
28349
+ '@type': 'Product',
28350
+ name: data.name,
28351
+ description: data.description,
28352
+ image: data.image,
28353
+ offers: {
28354
+ '@type': 'Offer',
28355
+ price: data.price,
28356
+ priceCurrency: data.currency || 'USD',
28357
+ availability: 'https://schema.org/InStock',
28358
+ },
28359
+ };
28360
+ }
28361
+
28362
+ /**
28363
+ * Generate FAQ schema
28364
+ */
28365
+ faqSchema(items: { question: string; answer: string }[]): Record<string, unknown> {
28366
+ return {
28367
+ '@context': 'https://schema.org',
28368
+ '@type': 'FAQPage',
28369
+ mainEntity: items.map(item => ({
28370
+ '@type': 'Question',
28371
+ name: item.question,
28372
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
28373
+ })),
28374
+ };
27360
28375
  }
27361
28376
  }`,
27362
- explanation: "Angular SEO service using Meta and Title services. Inject and call updateMeta() in components."
28377
+ explanation: `Angular comprehensive SEO service with:
28378
+ \u2022 Meta and Title service integration
28379
+ \u2022 Dynamic canonical URL updates
28380
+ \u2022 Full Open Graph with article support
28381
+ \u2022 Twitter Cards
28382
+ \u2022 JSON-LD schema generators
28383
+ \u2022 Automatic route change handling
28384
+
28385
+ Usage: Inject SEOService and call updateMeta()`
27363
28386
  };
27364
28387
  }
27365
28388
  function getFrameworkSpecificFix(framework, options) {
@@ -27388,6 +28411,102 @@ function getFrameworkSpecificFix(framework, options) {
27388
28411
  }
27389
28412
  return generateReactSEOHead(options);
27390
28413
  }
28414
+ function generateNextJsPagesRouterHead(options) {
28415
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
28416
+ return {
28417
+ file: "components/SEOHead.tsx",
28418
+ code: `import Head from 'next/head';
28419
+ import { useRouter } from 'next/router';
28420
+
28421
+ interface SEOHeadProps {
28422
+ title?: string;
28423
+ description?: string;
28424
+ image?: string;
28425
+ type?: 'website' | 'article';
28426
+ publishedTime?: string;
28427
+ modifiedTime?: string;
28428
+ noIndex?: boolean;
28429
+ schema?: Record<string, unknown>;
28430
+ }
28431
+
28432
+ const SITE_NAME = '${siteName}';
28433
+ const SITE_URL = '${siteUrl}';
28434
+ const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
28435
+ const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
28436
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
28437
+
28438
+ export function SEOHead({
28439
+ title,
28440
+ description = DEFAULT_DESCRIPTION,
28441
+ image = DEFAULT_IMAGE,
28442
+ type = 'website',
28443
+ publishedTime,
28444
+ modifiedTime,
28445
+ noIndex = false,
28446
+ schema,
28447
+ }: SEOHeadProps) {
28448
+ const router = useRouter();
28449
+
28450
+ const fullTitle = title
28451
+ ? (title.includes(SITE_NAME) ? title : \`\${title} | \${SITE_NAME}\`)
28452
+ : SITE_NAME;
28453
+ const pageUrl = \`\${SITE_URL}\${router.asPath}\`;
28454
+ const imageUrl = image.startsWith('http') ? image : \`\${SITE_URL}\${image}\`;
28455
+ const robotsContent = noIndex ? 'noindex, nofollow' : 'index, follow';
28456
+
28457
+ const defaultSchema = {
28458
+ '@context': 'https://schema.org',
28459
+ '@type': 'WebSite',
28460
+ name: SITE_NAME,
28461
+ url: SITE_URL,
28462
+ };
28463
+
28464
+ const jsonLd = schema ? [defaultSchema, schema] : [defaultSchema];
28465
+
28466
+ return (
28467
+ <Head>
28468
+ <title>{fullTitle}</title>
28469
+ <meta name="description" content={description} />
28470
+ <meta name="robots" content={robotsContent} />
28471
+ <link rel="canonical" href={pageUrl} />
28472
+
28473
+ <meta property="og:type" content={type} />
28474
+ <meta property="og:url" content={pageUrl} />
28475
+ <meta property="og:title" content={fullTitle} />
28476
+ <meta property="og:description" content={description} />
28477
+ <meta property="og:image" content={imageUrl} />
28478
+ <meta property="og:site_name" content={SITE_NAME} />
28479
+
28480
+ {type === 'article' && publishedTime && (
28481
+ <meta property="article:published_time" content={publishedTime} />
28482
+ )}
28483
+ {type === 'article' && modifiedTime && (
28484
+ <meta property="article:modified_time" content={modifiedTime} />
28485
+ )}
28486
+
28487
+ <meta name="twitter:card" content="summary_large_image" />
28488
+ <meta name="twitter:title" content={fullTitle} />
28489
+ <meta name="twitter:description" content={description} />
28490
+ <meta name="twitter:image" content={imageUrl} />
28491
+ {TWITTER_HANDLE && <meta name="twitter:site" content={TWITTER_HANDLE} />}
28492
+
28493
+ <script
28494
+ type="application/ld+json"
28495
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
28496
+ />
28497
+ </Head>
28498
+ );
28499
+ }`,
28500
+ explanation: `Next.js Pages Router SEO component with:
28501
+ \u2022 Full Open Graph support
28502
+ \u2022 Twitter Cards
28503
+ \u2022 JSON-LD structured data
28504
+ \u2022 Article metadata
28505
+ \u2022 Canonical URLs
28506
+
28507
+ Usage: <SEOHead title="Page" description="..." />`
28508
+ };
28509
+ }
27391
28510
 
27392
28511
  // src/fixer.ts
27393
28512
  async function generateFixes(issues, options) {
@@ -27924,6 +29043,588 @@ async function applyFixes(fixes, options) {
27924
29043
  return { applied, skipped };
27925
29044
  }
27926
29045
 
29046
+ // src/fixer/schemas.ts
29047
+ function generateOrganizationSchema(data) {
29048
+ return {
29049
+ "@context": "https://schema.org",
29050
+ "@type": "Organization",
29051
+ name: data.name,
29052
+ url: data.url,
29053
+ logo: data.logo,
29054
+ description: data.description,
29055
+ foundingDate: data.foundingDate,
29056
+ founder: data.founders?.map((f) => ({
29057
+ "@type": "Person",
29058
+ name: f.name,
29059
+ url: f.url
29060
+ })),
29061
+ address: data.address ? {
29062
+ "@type": "PostalAddress",
29063
+ streetAddress: data.address.streetAddress,
29064
+ addressLocality: data.address.addressLocality,
29065
+ addressRegion: data.address.addressRegion,
29066
+ postalCode: data.address.postalCode,
29067
+ addressCountry: data.address.addressCountry
29068
+ } : void 0,
29069
+ contactPoint: data.contactPoint?.map((cp) => ({
29070
+ "@type": "ContactPoint",
29071
+ telephone: cp.telephone,
29072
+ contactType: cp.contactType,
29073
+ availableLanguage: cp.availableLanguage,
29074
+ areaServed: cp.areaServed
29075
+ })),
29076
+ sameAs: data.sameAs
29077
+ };
29078
+ }
29079
+ function generateWebSiteSchema(data) {
29080
+ return {
29081
+ "@context": "https://schema.org",
29082
+ "@type": "WebSite",
29083
+ name: data.name,
29084
+ url: data.url,
29085
+ description: data.description,
29086
+ publisher: data.publisher ? {
29087
+ "@type": "Organization",
29088
+ name: data.publisher
29089
+ } : void 0,
29090
+ potentialAction: data.potentialAction ? {
29091
+ "@type": "SearchAction",
29092
+ target: {
29093
+ "@type": "EntryPoint",
29094
+ urlTemplate: data.potentialAction.target
29095
+ },
29096
+ "query-input": data.potentialAction.queryInput
29097
+ } : void 0
29098
+ };
29099
+ }
29100
+ function generateArticleSchema(data) {
29101
+ const authors = Array.isArray(data.author) ? data.author : [data.author];
29102
+ return {
29103
+ "@context": "https://schema.org",
29104
+ "@type": "Article",
29105
+ headline: data.headline,
29106
+ description: data.description,
29107
+ image: data.image,
29108
+ datePublished: data.datePublished,
29109
+ dateModified: data.dateModified || data.datePublished,
29110
+ author: authors.map((a) => ({
29111
+ "@type": "Person",
29112
+ name: a.name,
29113
+ url: a.url,
29114
+ image: a.image,
29115
+ jobTitle: a.jobTitle,
29116
+ sameAs: a.sameAs
29117
+ })),
29118
+ publisher: {
29119
+ "@type": "Organization",
29120
+ name: data.publisher.name,
29121
+ logo: {
29122
+ "@type": "ImageObject",
29123
+ url: data.publisher.logo
29124
+ }
29125
+ },
29126
+ mainEntityOfPage: data.mainEntityOfPage ? {
29127
+ "@type": "WebPage",
29128
+ "@id": data.mainEntityOfPage
29129
+ } : void 0,
29130
+ wordCount: data.wordCount,
29131
+ articleSection: data.articleSection,
29132
+ keywords: data.keywords?.join(", ")
29133
+ };
29134
+ }
29135
+ function generateBlogPostingSchema(data) {
29136
+ const schema = generateArticleSchema(data);
29137
+ return { ...schema, "@type": "BlogPosting" };
29138
+ }
29139
+ function generateNewsArticleSchema(data) {
29140
+ const schema = generateArticleSchema(data);
29141
+ return { ...schema, "@type": "NewsArticle", dateline: data.dateline };
29142
+ }
29143
+ function generateProductSchema(data) {
29144
+ const offers = Array.isArray(data.offers) ? data.offers : [data.offers];
29145
+ return {
29146
+ "@context": "https://schema.org",
29147
+ "@type": "Product",
29148
+ name: data.name,
29149
+ description: data.description,
29150
+ image: data.image,
29151
+ sku: data.sku,
29152
+ mpn: data.mpn,
29153
+ gtin: data.gtin,
29154
+ brand: data.brand ? {
29155
+ "@type": "Brand",
29156
+ name: data.brand
29157
+ } : void 0,
29158
+ category: data.category,
29159
+ offers: offers.map((o) => ({
29160
+ "@type": "Offer",
29161
+ price: o.price,
29162
+ priceCurrency: o.priceCurrency || "USD",
29163
+ availability: `https://schema.org/${o.availability || "InStock"}`,
29164
+ priceValidUntil: o.priceValidUntil,
29165
+ url: o.url,
29166
+ seller: o.seller ? {
29167
+ "@type": "Organization",
29168
+ name: o.seller
29169
+ } : void 0,
29170
+ itemCondition: o.itemCondition ? `https://schema.org/${o.itemCondition}` : void 0
29171
+ })),
29172
+ aggregateRating: data.aggregateRating ? {
29173
+ "@type": "AggregateRating",
29174
+ ratingValue: data.aggregateRating.ratingValue,
29175
+ reviewCount: data.aggregateRating.reviewCount,
29176
+ ratingCount: data.aggregateRating.ratingCount,
29177
+ bestRating: data.aggregateRating.bestRating || 5,
29178
+ worstRating: data.aggregateRating.worstRating || 1
29179
+ } : void 0,
29180
+ review: data.review?.map((r) => ({
29181
+ "@type": "Review",
29182
+ author: {
29183
+ "@type": "Person",
29184
+ name: r.author
29185
+ },
29186
+ datePublished: r.datePublished,
29187
+ reviewBody: r.reviewBody,
29188
+ reviewRating: {
29189
+ "@type": "Rating",
29190
+ ratingValue: r.reviewRating.ratingValue,
29191
+ bestRating: r.reviewRating.bestRating || 5
29192
+ }
29193
+ }))
29194
+ };
29195
+ }
29196
+ function generateSoftwareApplicationSchema(data) {
29197
+ const offers = Array.isArray(data.offers) ? data.offers : [data.offers];
29198
+ return {
29199
+ "@context": "https://schema.org",
29200
+ "@type": "SoftwareApplication",
29201
+ name: data.name,
29202
+ description: data.description,
29203
+ applicationCategory: data.applicationCategory,
29204
+ operatingSystem: data.operatingSystem || "Web",
29205
+ offers: offers.map((o) => ({
29206
+ "@type": "Offer",
29207
+ price: o.price,
29208
+ priceCurrency: o.priceCurrency || "USD"
29209
+ })),
29210
+ aggregateRating: data.aggregateRating ? {
29211
+ "@type": "AggregateRating",
29212
+ ratingValue: data.aggregateRating.ratingValue,
29213
+ reviewCount: data.aggregateRating.reviewCount,
29214
+ bestRating: data.aggregateRating.bestRating || 5
29215
+ } : void 0,
29216
+ screenshot: data.screenshot,
29217
+ featureList: data.featureList
29218
+ };
29219
+ }
29220
+ function generateFAQSchema(data) {
29221
+ return {
29222
+ "@context": "https://schema.org",
29223
+ "@type": "FAQPage",
29224
+ mainEntity: data.questions.map((q) => ({
29225
+ "@type": "Question",
29226
+ name: q.question,
29227
+ acceptedAnswer: {
29228
+ "@type": "Answer",
29229
+ text: q.answer
29230
+ }
29231
+ }))
29232
+ };
29233
+ }
29234
+ function generateHowToSchema(data) {
29235
+ return {
29236
+ "@context": "https://schema.org",
29237
+ "@type": "HowTo",
29238
+ name: data.name,
29239
+ description: data.description,
29240
+ image: data.image,
29241
+ totalTime: data.totalTime,
29242
+ estimatedCost: data.estimatedCost ? {
29243
+ "@type": "MonetaryAmount",
29244
+ currency: data.estimatedCost.currency,
29245
+ value: data.estimatedCost.value
29246
+ } : void 0,
29247
+ supply: data.supply?.map((s) => ({
29248
+ "@type": "HowToSupply",
29249
+ name: s
29250
+ })),
29251
+ tool: data.tool?.map((t) => ({
29252
+ "@type": "HowToTool",
29253
+ name: t
29254
+ })),
29255
+ step: data.steps.map((step, i) => ({
29256
+ "@type": "HowToStep",
29257
+ position: i + 1,
29258
+ name: step.name,
29259
+ text: step.text,
29260
+ image: step.image,
29261
+ url: step.url
29262
+ }))
29263
+ };
29264
+ }
29265
+ function generateVideoSchema(data) {
29266
+ const stats = data.interactionStatistic;
29267
+ return {
29268
+ "@context": "https://schema.org",
29269
+ "@type": "VideoObject",
29270
+ name: data.name,
29271
+ description: data.description,
29272
+ thumbnailUrl: data.thumbnailUrl,
29273
+ uploadDate: data.uploadDate,
29274
+ duration: data.duration,
29275
+ contentUrl: data.contentUrl,
29276
+ embedUrl: data.embedUrl,
29277
+ interactionStatistic: stats ? [
29278
+ stats.watchCount ? {
29279
+ "@type": "InteractionCounter",
29280
+ interactionType: { "@type": "WatchAction" },
29281
+ userInteractionCount: stats.watchCount
29282
+ } : null,
29283
+ stats.likeCount ? {
29284
+ "@type": "InteractionCounter",
29285
+ interactionType: { "@type": "LikeAction" },
29286
+ userInteractionCount: stats.likeCount
29287
+ } : null,
29288
+ stats.commentCount ? {
29289
+ "@type": "InteractionCounter",
29290
+ interactionType: { "@type": "CommentAction" },
29291
+ userInteractionCount: stats.commentCount
29292
+ } : null
29293
+ ].filter(Boolean) : void 0,
29294
+ publication: data.publication ? {
29295
+ "@type": "BroadcastEvent",
29296
+ isLiveBroadcast: data.publication.isLiveBroadcast,
29297
+ startDate: data.publication.startDate,
29298
+ endDate: data.publication.endDate
29299
+ } : void 0
29300
+ };
29301
+ }
29302
+ function generateEventSchema(data) {
29303
+ return {
29304
+ "@context": "https://schema.org",
29305
+ "@type": "Event",
29306
+ name: data.name,
29307
+ description: data.description,
29308
+ startDate: data.startDate,
29309
+ endDate: data.endDate,
29310
+ image: data.image,
29311
+ eventStatus: data.eventStatus ? `https://schema.org/${data.eventStatus}` : void 0,
29312
+ eventAttendanceMode: data.eventAttendanceMode ? `https://schema.org/${data.eventAttendanceMode}` : void 0,
29313
+ location: data.location.type === "VirtualLocation" ? {
29314
+ "@type": "VirtualLocation",
29315
+ url: data.location.url
29316
+ } : {
29317
+ "@type": "Place",
29318
+ name: data.location.name,
29319
+ address: data.location.address ? {
29320
+ "@type": "PostalAddress",
29321
+ ...data.location.address
29322
+ } : void 0
29323
+ },
29324
+ performer: data.performer?.map((p) => ({
29325
+ "@type": "Person",
29326
+ name: p.name,
29327
+ url: p.url
29328
+ })),
29329
+ organizer: data.organizer ? {
29330
+ "@type": "Organization",
29331
+ name: data.organizer.name,
29332
+ url: data.organizer.url
29333
+ } : void 0,
29334
+ offers: data.offers?.map((o) => ({
29335
+ "@type": "Offer",
29336
+ price: o.price,
29337
+ priceCurrency: o.priceCurrency || "USD",
29338
+ availability: `https://schema.org/${o.availability || "InStock"}`,
29339
+ url: o.url,
29340
+ validFrom: o.priceValidUntil
29341
+ }))
29342
+ };
29343
+ }
29344
+ function generateLocalBusinessSchema(data) {
29345
+ return {
29346
+ "@context": "https://schema.org",
29347
+ "@type": "LocalBusiness",
29348
+ name: data.name,
29349
+ description: data.description,
29350
+ url: data.url,
29351
+ telephone: data.telephone,
29352
+ image: data.image,
29353
+ priceRange: data.priceRange,
29354
+ servesCuisine: data.servesCuisine,
29355
+ menu: data.menu,
29356
+ address: {
29357
+ "@type": "PostalAddress",
29358
+ streetAddress: data.address.streetAddress,
29359
+ addressLocality: data.address.addressLocality,
29360
+ addressRegion: data.address.addressRegion,
29361
+ postalCode: data.address.postalCode,
29362
+ addressCountry: data.address.addressCountry
29363
+ },
29364
+ geo: data.geo ? {
29365
+ "@type": "GeoCoordinates",
29366
+ latitude: data.geo.latitude,
29367
+ longitude: data.geo.longitude
29368
+ } : void 0,
29369
+ openingHoursSpecification: data.openingHours?.map(parseOpeningHours).filter(Boolean),
29370
+ aggregateRating: data.aggregateRating ? {
29371
+ "@type": "AggregateRating",
29372
+ ratingValue: data.aggregateRating.ratingValue,
29373
+ reviewCount: data.aggregateRating.reviewCount,
29374
+ bestRating: data.aggregateRating.bestRating || 5
29375
+ } : void 0,
29376
+ review: data.review?.map((r) => ({
29377
+ "@type": "Review",
29378
+ author: { "@type": "Person", name: r.author },
29379
+ datePublished: r.datePublished,
29380
+ reviewBody: r.reviewBody,
29381
+ reviewRating: {
29382
+ "@type": "Rating",
29383
+ ratingValue: r.reviewRating.ratingValue
29384
+ }
29385
+ }))
29386
+ };
29387
+ }
29388
+ function parseOpeningHours(hours) {
29389
+ const match = hours.match(/^([A-Za-z,-]+)\s+(\d{2}:\d{2})-(\d{2}:\d{2})$/);
29390
+ if (!match) return null;
29391
+ const dayMap = {
29392
+ "Mo": "Monday",
29393
+ "Tu": "Tuesday",
29394
+ "We": "Wednesday",
29395
+ "Th": "Thursday",
29396
+ "Fr": "Friday",
29397
+ "Sa": "Saturday",
29398
+ "Su": "Sunday"
29399
+ };
29400
+ const dayPart = match[1];
29401
+ const opens = match[2];
29402
+ const closes = match[3];
29403
+ if (dayPart.includes("-")) {
29404
+ const [start, end] = dayPart.split("-");
29405
+ const days = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
29406
+ const startIdx = days.indexOf(start);
29407
+ const endIdx = days.indexOf(end);
29408
+ const dayOfWeek = days.slice(startIdx, endIdx + 1).map((d) => dayMap[d]);
29409
+ return { "@type": "OpeningHoursSpecification", dayOfWeek, opens, closes };
29410
+ }
29411
+ return {
29412
+ "@type": "OpeningHoursSpecification",
29413
+ dayOfWeek: dayPart.split(",").map((d) => dayMap[d.trim()]),
29414
+ opens,
29415
+ closes
29416
+ };
29417
+ }
29418
+ function generateBreadcrumbSchema(data) {
29419
+ return {
29420
+ "@context": "https://schema.org",
29421
+ "@type": "BreadcrumbList",
29422
+ itemListElement: data.items.map((item, index) => ({
29423
+ "@type": "ListItem",
29424
+ position: index + 1,
29425
+ name: item.name,
29426
+ item: item.url
29427
+ }))
29428
+ };
29429
+ }
29430
+ function generateJobPostingSchema(data) {
29431
+ return {
29432
+ "@context": "https://schema.org",
29433
+ "@type": "JobPosting",
29434
+ title: data.title,
29435
+ description: data.description,
29436
+ datePosted: data.datePosted,
29437
+ validThrough: data.validThrough,
29438
+ employmentType: data.employmentType,
29439
+ hiringOrganization: {
29440
+ "@type": "Organization",
29441
+ name: data.hiringOrganization.name,
29442
+ sameAs: data.hiringOrganization.url,
29443
+ logo: data.hiringOrganization.logo
29444
+ },
29445
+ jobLocation: data.jobLocation ? {
29446
+ "@type": "Place",
29447
+ address: {
29448
+ "@type": "PostalAddress",
29449
+ ...data.jobLocation.address
29450
+ }
29451
+ } : void 0,
29452
+ jobLocationType: data.jobLocationType,
29453
+ baseSalary: data.baseSalary ? {
29454
+ "@type": "MonetaryAmount",
29455
+ currency: data.baseSalary.currency,
29456
+ value: typeof data.baseSalary.value === "number" ? {
29457
+ "@type": "QuantitativeValue",
29458
+ value: data.baseSalary.value,
29459
+ unitText: data.baseSalary.unitText
29460
+ } : {
29461
+ "@type": "QuantitativeValue",
29462
+ minValue: data.baseSalary.value.minValue,
29463
+ maxValue: data.baseSalary.value.maxValue,
29464
+ unitText: data.baseSalary.unitText
29465
+ }
29466
+ } : void 0,
29467
+ experienceRequirements: data.experienceRequirements,
29468
+ educationRequirements: data.educationRequirements ? {
29469
+ "@type": "EducationalOccupationalCredential",
29470
+ credentialCategory: data.educationRequirements
29471
+ } : void 0,
29472
+ skills: data.skills
29473
+ };
29474
+ }
29475
+ function generateCourseSchema(data) {
29476
+ return {
29477
+ "@context": "https://schema.org",
29478
+ "@type": "Course",
29479
+ name: data.name,
29480
+ description: data.description,
29481
+ provider: {
29482
+ "@type": "Organization",
29483
+ name: data.provider.name,
29484
+ sameAs: data.provider.url
29485
+ },
29486
+ offers: data.offers ? {
29487
+ "@type": "Offer",
29488
+ price: data.offers.price,
29489
+ priceCurrency: data.offers.priceCurrency || "USD",
29490
+ availability: `https://schema.org/${data.offers.availability || "InStock"}`
29491
+ } : void 0,
29492
+ coursePrerequisites: data.coursePrerequisites,
29493
+ educationalLevel: data.educationalLevel,
29494
+ timeRequired: data.timeRequired,
29495
+ image: data.image,
29496
+ aggregateRating: data.aggregateRating ? {
29497
+ "@type": "AggregateRating",
29498
+ ratingValue: data.aggregateRating.ratingValue,
29499
+ reviewCount: data.aggregateRating.reviewCount
29500
+ } : void 0
29501
+ };
29502
+ }
29503
+ function generateRecipeSchema(data) {
29504
+ return {
29505
+ "@context": "https://schema.org",
29506
+ "@type": "Recipe",
29507
+ name: data.name,
29508
+ description: data.description,
29509
+ image: data.image,
29510
+ author: {
29511
+ "@type": "Person",
29512
+ name: data.author.name,
29513
+ url: data.author.url
29514
+ },
29515
+ datePublished: data.datePublished,
29516
+ prepTime: data.prepTime,
29517
+ cookTime: data.cookTime,
29518
+ totalTime: data.totalTime,
29519
+ recipeYield: data.recipeYield,
29520
+ recipeCategory: data.recipeCategory,
29521
+ recipeCuisine: data.recipeCuisine,
29522
+ recipeIngredient: data.recipeIngredient,
29523
+ recipeInstructions: data.recipeInstructions.map((step, i) => ({
29524
+ "@type": "HowToStep",
29525
+ position: i + 1,
29526
+ name: step.name,
29527
+ text: step.text
29528
+ })),
29529
+ nutrition: data.nutrition ? {
29530
+ "@type": "NutritionInformation",
29531
+ calories: data.nutrition.calories,
29532
+ fatContent: data.nutrition.fatContent,
29533
+ proteinContent: data.nutrition.proteinContent,
29534
+ carbohydrateContent: data.nutrition.carbohydrateContent
29535
+ } : void 0,
29536
+ aggregateRating: data.aggregateRating ? {
29537
+ "@type": "AggregateRating",
29538
+ ratingValue: data.aggregateRating.ratingValue,
29539
+ reviewCount: data.aggregateRating.reviewCount
29540
+ } : void 0,
29541
+ video: data.video ? generateVideoSchema(data.video) : void 0
29542
+ };
29543
+ }
29544
+ function generateBookSchema(data) {
29545
+ const authors = Array.isArray(data.author) ? data.author : [data.author];
29546
+ return {
29547
+ "@context": "https://schema.org",
29548
+ "@type": "Book",
29549
+ name: data.name,
29550
+ description: data.description,
29551
+ author: authors.map((a) => ({
29552
+ "@type": "Person",
29553
+ name: a.name,
29554
+ url: a.url
29555
+ })),
29556
+ isbn: data.isbn,
29557
+ numberOfPages: data.numberOfPages,
29558
+ bookFormat: data.bookFormat ? `https://schema.org/${data.bookFormat}` : void 0,
29559
+ publisher: data.publisher ? {
29560
+ "@type": "Organization",
29561
+ name: data.publisher
29562
+ } : void 0,
29563
+ datePublished: data.datePublished,
29564
+ image: data.image,
29565
+ inLanguage: data.inLanguage,
29566
+ aggregateRating: data.aggregateRating ? {
29567
+ "@type": "AggregateRating",
29568
+ ratingValue: data.aggregateRating.ratingValue,
29569
+ reviewCount: data.aggregateRating.reviewCount
29570
+ } : void 0,
29571
+ offers: data.offers ? {
29572
+ "@type": "Offer",
29573
+ price: data.offers.price,
29574
+ priceCurrency: data.offers.priceCurrency || "USD",
29575
+ availability: `https://schema.org/${data.offers.availability || "InStock"}`
29576
+ } : void 0
29577
+ };
29578
+ }
29579
+ function generateSpeakableSchema(data) {
29580
+ return {
29581
+ "@context": "https://schema.org",
29582
+ "@type": "WebPage",
29583
+ speakable: {
29584
+ "@type": "SpeakableSpecification",
29585
+ cssSelector: data.cssSelector,
29586
+ xpath: data.xpath
29587
+ },
29588
+ url: data.url
29589
+ };
29590
+ }
29591
+ function generateSitelinksSearchboxSchema(data) {
29592
+ return {
29593
+ "@context": "https://schema.org",
29594
+ "@type": "WebSite",
29595
+ url: data.url,
29596
+ potentialAction: {
29597
+ "@type": "SearchAction",
29598
+ target: {
29599
+ "@type": "EntryPoint",
29600
+ urlTemplate: data.searchUrl
29601
+ },
29602
+ "query-input": data.queryInput || "required name=search_term_string"
29603
+ }
29604
+ };
29605
+ }
29606
+ var Schemas = {
29607
+ organization: generateOrganizationSchema,
29608
+ webSite: generateWebSiteSchema,
29609
+ article: generateArticleSchema,
29610
+ blogPosting: generateBlogPostingSchema,
29611
+ newsArticle: generateNewsArticleSchema,
29612
+ product: generateProductSchema,
29613
+ softwareApplication: generateSoftwareApplicationSchema,
29614
+ faq: generateFAQSchema,
29615
+ howTo: generateHowToSchema,
29616
+ video: generateVideoSchema,
29617
+ event: generateEventSchema,
29618
+ localBusiness: generateLocalBusinessSchema,
29619
+ breadcrumb: generateBreadcrumbSchema,
29620
+ jobPosting: generateJobPostingSchema,
29621
+ course: generateCourseSchema,
29622
+ recipe: generateRecipeSchema,
29623
+ book: generateBookSchema,
29624
+ speakable: generateSpeakableSchema,
29625
+ sitelinksSearchbox: generateSitelinksSearchboxSchema
29626
+ };
29627
+
27927
29628
  // src/git/commit-helper.ts
27928
29629
  var import_child_process2 = require("child_process");
27929
29630
  var import_util2 = require("util");
@@ -29561,7 +31262,7 @@ function generateKeyFacts(facts, options = {}) {
29561
31262
  return formatted.join("\n");
29562
31263
  }
29563
31264
  }
29564
- function generateFAQSchema(faqs, options = {}) {
31265
+ function generateFAQSchema2(faqs, options = {}) {
29565
31266
  const format = options.format || "json-ld";
29566
31267
  if (format === "markdown") {
29567
31268
  const lines = ["## FAQ", ""];
@@ -29644,7 +31345,7 @@ function generateAICitableContent(config) {
29644
31345
  sections.push("");
29645
31346
  }
29646
31347
  if (config.faqs && config.faqs.length > 0) {
29647
- sections.push(generateFAQSchema(config.faqs, { format: "markdown" }));
31348
+ sections.push(generateFAQSchema2(config.faqs, { format: "markdown" }));
29648
31349
  sections.push("");
29649
31350
  }
29650
31351
  return sections.join("\n").trim();
@@ -30487,6 +32188,7 @@ if (typeof globalThis !== "undefined") {
30487
32188
  PRIORITY_WEIGHTS,
30488
32189
  SEO_SCOPES,
30489
32190
  SITE_PROFILE_QUESTIONS,
32191
+ Schemas,
30490
32192
  addTrackingResult,
30491
32193
  analyzeAnchorText,
30492
32194
  analyzeCanonicalAdvanced,
@@ -30624,7 +32326,6 @@ if (typeof globalThis !== "undefined") {
30624
32326
  generateAllFixes,
30625
32327
  generateAngularSEOService,
30626
32328
  generateAstroBaseHead,
30627
- generateAstroLayout,
30628
32329
  generateAstroMeta,
30629
32330
  generateBlogPost,
30630
32331
  generateBranchName,
@@ -30650,17 +32351,11 @@ if (typeof globalThis !== "undefined") {
30650
32351
  generateMarkdownReport,
30651
32352
  generateNextAppMetadata,
30652
32353
  generateNextJsAppRouterMetadata,
30653
- generateNextJsDynamicMetadata,
30654
- generateNextJsPageMetadata,
30655
32354
  generateNextJsPagesRouterHead,
30656
- generateNextJsRobots,
30657
- generateNextJsSitemap,
30658
32355
  generateNextPagesHead,
30659
- generateNuxtPageExample,
30660
32356
  generateNuxtSEOHead,
30661
32357
  generatePDFReport,
30662
32358
  generatePRDescription,
30663
- generateReactAppWrapper,
30664
32359
  generateReactHelmetSocialMeta,
30665
32360
  generateReactSEOHead,
30666
32361
  generateRecommendationQueries,