@rankcli/agent-runtime 0.0.6 → 0.0.8

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