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