@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.d.mts +44 -1
- package/dist/index.d.ts +44 -1
- package/dist/index.js +1812 -51
- package/dist/index.mjs +1803 -51
- package/package.json +1 -1
- package/src/fixer/framework-fixes.ts +1869 -0
- package/src/fixer/index.ts +5 -0
- package/src/fixer.ts +21 -54
- package/src/index.ts +1 -0
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 {
|
|
27095
|
-
const
|
|
27096
|
-
|
|
27097
|
-
|
|
27098
|
-
|
|
27099
|
-
|
|
27100
|
-
|
|
27101
|
-
|
|
27102
|
-
|
|
27103
|
-
|
|
27104
|
-
|
|
27105
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
27145
|
-
explanation:
|
|
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,
|