@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.js
CHANGED
|
@@ -946,7 +946,6 @@ __export(index_exports, {
|
|
|
946
946
|
generateAllFixes: () => generateAllFixes,
|
|
947
947
|
generateAngularSEOService: () => generateAngularSEOService,
|
|
948
948
|
generateAstroBaseHead: () => generateAstroBaseHead,
|
|
949
|
-
generateAstroLayout: () => generateAstroLayout,
|
|
950
949
|
generateAstroMeta: () => generateAstroMeta,
|
|
951
950
|
generateBlogPost: () => generateBlogPost,
|
|
952
951
|
generateBranchName: () => generateBranchName,
|
|
@@ -972,17 +971,11 @@ __export(index_exports, {
|
|
|
972
971
|
generateMarkdownReport: () => generateMarkdownReport,
|
|
973
972
|
generateNextAppMetadata: () => generateNextAppMetadata,
|
|
974
973
|
generateNextJsAppRouterMetadata: () => generateNextJsAppRouterMetadata,
|
|
975
|
-
generateNextJsDynamicMetadata: () => generateNextJsDynamicMetadata,
|
|
976
|
-
generateNextJsPageMetadata: () => generateNextJsPageMetadata,
|
|
977
974
|
generateNextJsPagesRouterHead: () => generateNextJsPagesRouterHead,
|
|
978
|
-
generateNextJsRobots: () => generateNextJsRobots,
|
|
979
|
-
generateNextJsSitemap: () => generateNextJsSitemap,
|
|
980
975
|
generateNextPagesHead: () => generateNextPagesHead,
|
|
981
|
-
generateNuxtPageExample: () => generateNuxtPageExample,
|
|
982
976
|
generateNuxtSEOHead: () => generateNuxtSEOHead,
|
|
983
977
|
generatePDFReport: () => generatePDFReport,
|
|
984
978
|
generatePRDescription: () => generatePRDescription,
|
|
985
|
-
generateReactAppWrapper: () => generateReactAppWrapper,
|
|
986
979
|
generateReactHelmetSocialMeta: () => generateReactHelmetSocialMeta,
|
|
987
980
|
generateReactSEOHead: () => generateReactSEOHead,
|
|
988
981
|
generateRecommendationQueries: () => generateRecommendationQueries,
|
|
@@ -26741,61 +26734,332 @@ var import_path4 = require("path");
|
|
|
26741
26734
|
|
|
26742
26735
|
// src/fixer/framework-fixes.ts
|
|
26743
26736
|
function generateReactSEOHead(options) {
|
|
26744
|
-
const { siteName, siteUrl, title, description, image } = options;
|
|
26737
|
+
const { siteName, siteUrl, title, description, image, twitterHandle, locale } = options;
|
|
26745
26738
|
return {
|
|
26746
26739
|
file: "src/components/SEOHead.tsx",
|
|
26747
26740
|
code: `import { Helmet } from 'react-helmet-async';
|
|
26748
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
|
+
|
|
26749
26765
|
interface SEOHeadProps {
|
|
26766
|
+
// Required
|
|
26750
26767
|
title?: string;
|
|
26751
26768
|
description?: string;
|
|
26752
|
-
|
|
26769
|
+
|
|
26770
|
+
// URLs
|
|
26753
26771
|
url?: string;
|
|
26754
|
-
|
|
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 }[];
|
|
26755
26797
|
}
|
|
26756
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
|
+
|
|
26757
26805
|
export function SEOHead({
|
|
26758
|
-
title
|
|
26806
|
+
title,
|
|
26759
26807
|
description = '${description || `${siteName} - A compelling description of your product or service.`}',
|
|
26760
|
-
|
|
26761
|
-
|
|
26808
|
+
url,
|
|
26809
|
+
canonical,
|
|
26810
|
+
image = DEFAULT_IMAGE,
|
|
26762
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,
|
|
26763
26822
|
}: SEOHeadProps) {
|
|
26764
|
-
const
|
|
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];
|
|
26765
26852
|
|
|
26766
26853
|
return (
|
|
26767
26854
|
<Helmet>
|
|
26768
26855
|
{/* Primary Meta Tags */}
|
|
26769
26856
|
<title>{fullTitle}</title>
|
|
26857
|
+
<meta name="title" content={fullTitle} />
|
|
26770
26858
|
<meta name="description" content={description} />
|
|
26771
|
-
<
|
|
26859
|
+
<meta name="robots" content={robotsContent} />
|
|
26860
|
+
<link rel="canonical" href={canonicalUrl} />
|
|
26772
26861
|
|
|
26773
26862
|
{/* Open Graph / Facebook */}
|
|
26774
26863
|
<meta property="og:type" content={type} />
|
|
26775
|
-
<meta property="og:url" content={
|
|
26864
|
+
<meta property="og:url" content={pageUrl} />
|
|
26776
26865
|
<meta property="og:title" content={fullTitle} />
|
|
26777
26866
|
<meta property="og:description" content={description} />
|
|
26778
|
-
<meta property="og:image" content={
|
|
26779
|
-
<meta property="og:
|
|
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
|
+
))}
|
|
26780
26890
|
|
|
26781
26891
|
{/* Twitter */}
|
|
26782
|
-
<meta name="twitter:card" content=
|
|
26783
|
-
<meta name="twitter:url" content={
|
|
26892
|
+
<meta name="twitter:card" content={twitterCard} />
|
|
26893
|
+
<meta name="twitter:url" content={pageUrl} />
|
|
26784
26894
|
<meta name="twitter:title" content={fullTitle} />
|
|
26785
26895
|
<meta name="twitter:description" content={description} />
|
|
26786
|
-
<meta name="twitter:image" content={
|
|
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>
|
|
26787
26910
|
</Helmet>
|
|
26788
26911
|
);
|
|
26789
|
-
}`,
|
|
26790
|
-
explanation: "React SEO component using react-helmet-async. Wrap your app in <HelmetProvider> and use <SEOHead /> on each page.",
|
|
26791
|
-
installCommands: ["npm install react-helmet-async"],
|
|
26792
|
-
imports: ["import { HelmetProvider } from 'react-helmet-async';"]
|
|
26793
|
-
};
|
|
26794
26912
|
}
|
|
26795
|
-
|
|
26796
|
-
|
|
26797
|
-
|
|
26798
|
-
|
|
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';
|
|
26799
27063
|
import ReactDOM from 'react-dom/client';
|
|
26800
27064
|
import { HelmetProvider } from 'react-helmet-async';
|
|
26801
27065
|
import App from './App';
|
|
@@ -26808,29 +27072,63 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
26808
27072
|
</HelmetProvider>
|
|
26809
27073
|
</React.StrictMode>,
|
|
26810
27074
|
);`,
|
|
26811
|
-
|
|
27075
|
+
explanation: "Updated main.tsx with HelmetProvider wrapper."
|
|
27076
|
+
}
|
|
27077
|
+
]
|
|
26812
27078
|
};
|
|
26813
27079
|
}
|
|
26814
27080
|
function generateNextJsAppRouterMetadata(options) {
|
|
26815
|
-
const { siteName, siteUrl, title, description, image } = options;
|
|
27081
|
+
const { siteName, siteUrl, title, description, image, twitterHandle, locale } = options;
|
|
26816
27082
|
return {
|
|
26817
27083
|
file: "app/layout.tsx",
|
|
26818
|
-
code: `import type { Metadata } from 'next';
|
|
27084
|
+
code: `import type { Metadata, Viewport } from 'next';
|
|
26819
27085
|
import { Inter } from 'next/font/google';
|
|
26820
27086
|
import './globals.css';
|
|
26821
27087
|
|
|
26822
|
-
const inter = Inter({ subsets: ['latin'] });
|
|
27088
|
+
const inter = Inter({ subsets: ['latin'], display: 'swap' });
|
|
26823
27089
|
|
|
27090
|
+
/**
|
|
27091
|
+
* Default metadata for all pages
|
|
27092
|
+
* Individual pages can override with their own metadata export
|
|
27093
|
+
*/
|
|
26824
27094
|
export const metadata: Metadata = {
|
|
26825
27095
|
metadataBase: new URL('${siteUrl}'),
|
|
27096
|
+
|
|
27097
|
+
// Default title with template
|
|
26826
27098
|
title: {
|
|
26827
27099
|
default: '${title || siteName}',
|
|
26828
27100
|
template: \`%s | ${siteName}\`,
|
|
26829
27101
|
},
|
|
27102
|
+
|
|
26830
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
|
|
26831
27129
|
openGraph: {
|
|
26832
27130
|
type: 'website',
|
|
26833
|
-
locale: 'en_US',
|
|
27131
|
+
locale: '${locale || "en_US"}',
|
|
26834
27132
|
url: '${siteUrl}',
|
|
26835
27133
|
siteName: '${siteName}',
|
|
26836
27134
|
title: '${title || siteName}',
|
|
@@ -26844,16 +27142,49 @@ export const metadata: Metadata = {
|
|
|
26844
27142
|
},
|
|
26845
27143
|
],
|
|
26846
27144
|
},
|
|
27145
|
+
|
|
27146
|
+
// Twitter
|
|
26847
27147
|
twitter: {
|
|
26848
27148
|
card: 'summary_large_image',
|
|
26849
27149
|
title: '${title || siteName}',
|
|
26850
27150
|
description: '${description || `${siteName} - A compelling description.`}',
|
|
26851
27151
|
images: ['${image || "/og-image.png"}'],
|
|
27152
|
+
${twitterHandle ? `site: '${twitterHandle}',
|
|
27153
|
+
creator: '${twitterHandle}',` : ""}
|
|
26852
27154
|
},
|
|
26853
|
-
|
|
26854
|
-
|
|
26855
|
-
|
|
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',
|
|
26856
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,
|
|
26857
27188
|
};
|
|
26858
27189
|
|
|
26859
27190
|
export default function RootLayout({
|
|
@@ -26862,143 +27193,104 @@ export default function RootLayout({
|
|
|
26862
27193
|
children: React.ReactNode;
|
|
26863
27194
|
}) {
|
|
26864
27195
|
return (
|
|
26865
|
-
<html lang="
|
|
26866
|
-
<body
|
|
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>
|
|
26867
27219
|
</html>
|
|
26868
27220
|
);
|
|
26869
27221
|
}`,
|
|
26870
|
-
explanation:
|
|
26871
|
-
|
|
26872
|
-
|
|
26873
|
-
|
|
26874
|
-
|
|
26875
|
-
|
|
26876
|
-
|
|
26877
|
-
|
|
26878
|
-
|
|
26879
|
-
|
|
26880
|
-
|
|
26881
|
-
|
|
26882
|
-
};
|
|
26883
|
-
|
|
26884
|
-
export default function ${pageName.charAt(0).toUpperCase() + pageName.slice(1)}Page() {
|
|
26885
|
-
return (
|
|
26886
|
-
<main>
|
|
26887
|
-
<h1>${pageName.charAt(0).toUpperCase() + pageName.slice(1)}</h1>
|
|
26888
|
-
{/* Your content here */}
|
|
26889
|
-
</main>
|
|
26890
|
-
);
|
|
26891
|
-
}`,
|
|
26892
|
-
explanation: `Next.js page with metadata export. The title will be "${pageName} | ${siteName}" using the template.`
|
|
26893
|
-
};
|
|
26894
|
-
}
|
|
26895
|
-
function generateNextJsDynamicMetadata() {
|
|
26896
|
-
return {
|
|
26897
|
-
file: "app/[slug]/page.tsx",
|
|
26898
|
-
code: `import type { Metadata } from 'next';
|
|
26899
|
-
|
|
26900
|
-
interface PageProps {
|
|
26901
|
-
params: { slug: string };
|
|
26902
|
-
}
|
|
26903
|
-
|
|
26904
|
-
// Generate metadata dynamically based on the slug
|
|
26905
|
-
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
26906
|
-
// Fetch data based on slug (replace with your data fetching logic)
|
|
26907
|
-
const data = await fetchPageData(params.slug);
|
|
26908
|
-
|
|
26909
|
-
return {
|
|
26910
|
-
title: data.title,
|
|
26911
|
-
description: data.description,
|
|
26912
|
-
openGraph: {
|
|
26913
|
-
title: data.title,
|
|
26914
|
-
description: data.description,
|
|
26915
|
-
images: data.image ? [{ url: data.image }] : [],
|
|
26916
|
-
},
|
|
26917
|
-
};
|
|
26918
|
-
}
|
|
26919
|
-
|
|
26920
|
-
// Pre-generate static pages for known slugs (improves SEO)
|
|
26921
|
-
export async function generateStaticParams() {
|
|
26922
|
-
const pages = await fetchAllPageSlugs();
|
|
26923
|
-
return pages.map((slug) => ({ slug }));
|
|
26924
|
-
}
|
|
26925
|
-
|
|
26926
|
-
async function fetchPageData(slug: string) {
|
|
26927
|
-
// Replace with your actual data fetching
|
|
26928
|
-
return {
|
|
26929
|
-
title: slug.replace(/-/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()),
|
|
26930
|
-
description: \`Learn about \${slug}\`,
|
|
26931
|
-
image: null,
|
|
26932
|
-
};
|
|
26933
|
-
}
|
|
26934
|
-
|
|
26935
|
-
async function fetchAllPageSlugs() {
|
|
26936
|
-
// Replace with your actual data source
|
|
26937
|
-
return ['about', 'contact', 'pricing'];
|
|
26938
|
-
}
|
|
26939
|
-
|
|
26940
|
-
export default function DynamicPage({ params }: PageProps) {
|
|
26941
|
-
return (
|
|
26942
|
-
<main>
|
|
26943
|
-
<h1>{params.slug}</h1>
|
|
26944
|
-
</main>
|
|
26945
|
-
);
|
|
26946
|
-
}`,
|
|
26947
|
-
explanation: "Next.js dynamic route with generateMetadata and generateStaticParams for SEO-optimized dynamic pages."
|
|
26948
|
-
};
|
|
26949
|
-
}
|
|
26950
|
-
function generateNextJsRobots(siteUrl) {
|
|
26951
|
-
return {
|
|
26952
|
-
file: "app/robots.ts",
|
|
26953
|
-
code: `import type { MetadataRoute } from 'next';
|
|
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';
|
|
26954
27234
|
|
|
26955
27235
|
export default function robots(): MetadataRoute.Robots {
|
|
27236
|
+
const baseUrl = '${siteUrl}';
|
|
27237
|
+
|
|
26956
27238
|
return {
|
|
26957
27239
|
rules: [
|
|
26958
27240
|
{
|
|
26959
27241
|
userAgent: '*',
|
|
26960
27242
|
allow: '/',
|
|
26961
|
-
disallow: ['/api/', '/admin/', '/_next/'],
|
|
27243
|
+
disallow: ['/api/', '/admin/', '/_next/', '/private/'],
|
|
27244
|
+
},
|
|
27245
|
+
{
|
|
27246
|
+
userAgent: 'GPTBot',
|
|
27247
|
+
allow: '/',
|
|
26962
27248
|
},
|
|
26963
27249
|
],
|
|
26964
|
-
sitemap:
|
|
27250
|
+
sitemap: \`\${baseUrl}/sitemap.xml\`,
|
|
26965
27251
|
};
|
|
26966
27252
|
}`,
|
|
26967
|
-
|
|
26968
|
-
|
|
26969
|
-
|
|
26970
|
-
|
|
26971
|
-
|
|
26972
|
-
file: "app/sitemap.ts",
|
|
26973
|
-
code: `import type { MetadataRoute } from 'next';
|
|
27253
|
+
explanation: "Robots.txt with AI crawler support."
|
|
27254
|
+
},
|
|
27255
|
+
{
|
|
27256
|
+
file: "app/sitemap.ts",
|
|
27257
|
+
code: `import type { MetadataRoute } from 'next';
|
|
26974
27258
|
|
|
26975
27259
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
27260
|
+
const baseUrl = '${siteUrl}';
|
|
27261
|
+
|
|
26976
27262
|
// Static pages
|
|
26977
27263
|
const staticPages: MetadataRoute.Sitemap = [
|
|
26978
27264
|
{
|
|
26979
|
-
url:
|
|
27265
|
+
url: baseUrl,
|
|
26980
27266
|
lastModified: new Date(),
|
|
26981
27267
|
changeFrequency: 'daily',
|
|
26982
27268
|
priority: 1,
|
|
26983
27269
|
},
|
|
26984
27270
|
{
|
|
26985
|
-
url:
|
|
27271
|
+
url: \`\${baseUrl}/about\`,
|
|
26986
27272
|
lastModified: new Date(),
|
|
26987
27273
|
changeFrequency: 'monthly',
|
|
26988
27274
|
priority: 0.8,
|
|
26989
27275
|
},
|
|
26990
27276
|
{
|
|
26991
|
-
url:
|
|
27277
|
+
url: \`\${baseUrl}/pricing\`,
|
|
26992
27278
|
lastModified: new Date(),
|
|
26993
27279
|
changeFrequency: 'weekly',
|
|
26994
27280
|
priority: 0.9,
|
|
26995
27281
|
},
|
|
27282
|
+
{
|
|
27283
|
+
url: \`\${baseUrl}/blog\`,
|
|
27284
|
+
lastModified: new Date(),
|
|
27285
|
+
changeFrequency: 'daily',
|
|
27286
|
+
priority: 0.8,
|
|
27287
|
+
},
|
|
26996
27288
|
];
|
|
26997
27289
|
|
|
26998
|
-
// Dynamic pages
|
|
26999
|
-
// const posts = await
|
|
27290
|
+
// Dynamic pages - fetch from your database/CMS
|
|
27291
|
+
// const posts = await db.post.findMany({ select: { slug: true, updatedAt: true } });
|
|
27000
27292
|
// const dynamicPages = posts.map((post) => ({
|
|
27001
|
-
// url:
|
|
27293
|
+
// url: \`\${baseUrl}/blog/\${post.slug}\`,
|
|
27002
27294
|
// lastModified: post.updatedAt,
|
|
27003
27295
|
// changeFrequency: 'weekly' as const,
|
|
27004
27296
|
// priority: 0.7,
|
|
@@ -27006,209 +27298,628 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
|
27006
27298
|
|
|
27007
27299
|
return [...staticPages];
|
|
27008
27300
|
}`,
|
|
27009
|
-
|
|
27010
|
-
|
|
27011
|
-
|
|
27012
|
-
|
|
27013
|
-
|
|
27014
|
-
return {
|
|
27015
|
-
file: "components/SEOHead.tsx",
|
|
27016
|
-
code: `import Head from 'next/head';
|
|
27301
|
+
explanation: "Dynamic sitemap generator."
|
|
27302
|
+
},
|
|
27303
|
+
{
|
|
27304
|
+
file: "lib/seo.ts",
|
|
27305
|
+
code: `import type { Metadata } from 'next';
|
|
27017
27306
|
|
|
27018
|
-
|
|
27019
|
-
|
|
27020
|
-
|
|
27307
|
+
const baseUrl = '${siteUrl}';
|
|
27308
|
+
const siteName = '${siteName}';
|
|
27309
|
+
|
|
27310
|
+
interface PageSEOProps {
|
|
27311
|
+
title: string;
|
|
27312
|
+
description: string;
|
|
27313
|
+
path?: string;
|
|
27021
27314
|
image?: string;
|
|
27022
|
-
url?: string;
|
|
27023
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
|
+
};
|
|
27024
27374
|
}
|
|
27025
27375
|
|
|
27026
|
-
|
|
27027
|
-
|
|
27028
|
-
|
|
27029
|
-
|
|
27030
|
-
|
|
27031
|
-
|
|
27032
|
-
|
|
27033
|
-
|
|
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
|
+
}
|
|
27034
27412
|
|
|
27035
|
-
|
|
27036
|
-
|
|
27037
|
-
|
|
27038
|
-
|
|
27039
|
-
|
|
27040
|
-
|
|
27041
|
-
|
|
27042
|
-
|
|
27043
|
-
|
|
27044
|
-
|
|
27045
|
-
|
|
27046
|
-
|
|
27047
|
-
|
|
27048
|
-
|
|
27049
|
-
|
|
27050
|
-
|
|
27051
|
-
|
|
27052
|
-
|
|
27053
|
-
|
|
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
|
+
};
|
|
27054
27467
|
}`,
|
|
27055
|
-
|
|
27468
|
+
explanation: "SEO utility functions for generating metadata and JSON-LD."
|
|
27469
|
+
}
|
|
27470
|
+
]
|
|
27056
27471
|
};
|
|
27057
27472
|
}
|
|
27058
27473
|
function generateNuxtSEOHead(options) {
|
|
27059
|
-
const { siteName, siteUrl, title, description, image } = options;
|
|
27474
|
+
const { siteName, siteUrl, title, description, image, twitterHandle, locale } = options;
|
|
27060
27475
|
return {
|
|
27061
27476
|
file: "composables/useSEO.ts",
|
|
27062
|
-
code:
|
|
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 {
|
|
27063
27490
|
title?: string;
|
|
27064
27491
|
description?: string;
|
|
27065
27492
|
image?: string;
|
|
27066
27493
|
url?: string;
|
|
27067
|
-
type?: 'website' | 'article';
|
|
27068
|
-
|
|
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 = {}) {
|
|
27069
27518
|
const route = useRoute();
|
|
27070
|
-
const config = useRuntimeConfig();
|
|
27071
27519
|
|
|
27072
|
-
const
|
|
27073
|
-
title
|
|
27074
|
-
description
|
|
27075
|
-
image
|
|
27076
|
-
url
|
|
27077
|
-
type
|
|
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,
|
|
27078
27591
|
};
|
|
27079
27592
|
|
|
27080
|
-
const
|
|
27081
|
-
|
|
27082
|
-
|
|
27083
|
-
|
|
27593
|
+
const jsonLd = schema
|
|
27594
|
+
? Array.isArray(schema)
|
|
27595
|
+
? [defaultSchema, ...schema]
|
|
27596
|
+
: [defaultSchema, schema]
|
|
27597
|
+
: [defaultSchema];
|
|
27084
27598
|
|
|
27085
27599
|
useHead({
|
|
27086
27600
|
title: fullTitle,
|
|
27087
|
-
meta
|
|
27088
|
-
{ name: 'description', content: meta.description },
|
|
27089
|
-
// Open Graph
|
|
27090
|
-
{ property: 'og:type', content: meta.type },
|
|
27091
|
-
{ property: 'og:url', content: meta.url },
|
|
27092
|
-
{ property: 'og:title', content: fullTitle },
|
|
27093
|
-
{ property: 'og:description', content: meta.description },
|
|
27094
|
-
{ property: 'og:image', content: meta.image },
|
|
27095
|
-
{ property: 'og:site_name', content: '${siteName}' },
|
|
27096
|
-
// Twitter
|
|
27097
|
-
{ name: 'twitter:card', content: 'summary_large_image' },
|
|
27098
|
-
{ name: 'twitter:title', content: fullTitle },
|
|
27099
|
-
{ name: 'twitter:description', content: meta.description },
|
|
27100
|
-
{ name: 'twitter:image', content: meta.image },
|
|
27101
|
-
],
|
|
27601
|
+
meta,
|
|
27102
27602
|
link: [
|
|
27103
|
-
{ rel: 'canonical', href:
|
|
27603
|
+
{ rel: 'canonical', href: pageUrl },
|
|
27604
|
+
],
|
|
27605
|
+
script: [
|
|
27606
|
+
{
|
|
27607
|
+
type: 'application/ld+json',
|
|
27608
|
+
innerHTML: JSON.stringify(jsonLd),
|
|
27609
|
+
},
|
|
27104
27610
|
],
|
|
27105
27611
|
});
|
|
27106
|
-
}`,
|
|
27107
|
-
explanation: "Nuxt 3 SEO composable using useHead(). Call useSEO() in any page to set meta tags."
|
|
27108
|
-
};
|
|
27109
27612
|
}
|
|
27110
|
-
function generateNuxtPageExample() {
|
|
27111
|
-
return {
|
|
27112
|
-
file: "pages/about.vue",
|
|
27113
|
-
code: `<script setup lang="ts">
|
|
27114
|
-
useSEO({
|
|
27115
|
-
title: 'About Us',
|
|
27116
|
-
description: 'Learn more about our company and mission.',
|
|
27117
|
-
});
|
|
27118
|
-
</script>
|
|
27119
27613
|
|
|
27120
|
-
|
|
27121
|
-
|
|
27122
|
-
|
|
27123
|
-
|
|
27124
|
-
|
|
27125
|
-
|
|
27126
|
-
|
|
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
|
+
]
|
|
27127
27740
|
};
|
|
27128
27741
|
}
|
|
27129
27742
|
function generateVueSEOHead(options) {
|
|
27130
|
-
const { siteName, siteUrl,
|
|
27743
|
+
const { siteName, siteUrl, description, image, twitterHandle } = options;
|
|
27131
27744
|
return {
|
|
27132
27745
|
file: "src/composables/useSEO.ts",
|
|
27133
|
-
code: `import { useHead } from '@unhead/vue';
|
|
27134
|
-
import { computed,
|
|
27746
|
+
code: `import { useHead, useServerHead } from '@unhead/vue';
|
|
27747
|
+
import { computed, unref, MaybeRef } from 'vue';
|
|
27748
|
+
import { useRoute } from 'vue-router';
|
|
27135
27749
|
|
|
27136
27750
|
interface SEOOptions {
|
|
27137
|
-
title?: string
|
|
27138
|
-
description?: string
|
|
27139
|
-
image?: string
|
|
27140
|
-
url?: string;
|
|
27751
|
+
title?: MaybeRef<string>;
|
|
27752
|
+
description?: MaybeRef<string>;
|
|
27753
|
+
image?: MaybeRef<string>;
|
|
27141
27754
|
type?: 'website' | 'article';
|
|
27755
|
+
noIndex?: boolean;
|
|
27756
|
+
schema?: Record<string, unknown>;
|
|
27142
27757
|
}
|
|
27143
27758
|
|
|
27144
|
-
|
|
27145
|
-
|
|
27146
|
-
|
|
27147
|
-
|
|
27148
|
-
|
|
27149
|
-
url: typeof window !== 'undefined' ? window.location.href : '${siteUrl}',
|
|
27150
|
-
type: 'website' as const,
|
|
27151
|
-
};
|
|
27152
|
-
|
|
27153
|
-
const meta = { ...defaults, ...options };
|
|
27154
|
-
const fullTitle = computed(() =>
|
|
27155
|
-
meta.title.includes('${siteName}') ? meta.title : \`\${meta.title} | ${siteName}\`
|
|
27156
|
-
);
|
|
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 || ""}';
|
|
27157
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
|
+
|
|
27158
27780
|
useHead({
|
|
27159
|
-
title
|
|
27781
|
+
title,
|
|
27160
27782
|
meta: [
|
|
27161
|
-
{ name: 'description', content:
|
|
27162
|
-
{
|
|
27163
|
-
|
|
27164
|
-
|
|
27165
|
-
{ property: 'og:
|
|
27166
|
-
{ property: 'og:
|
|
27167
|
-
{ property: 'og:
|
|
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
|
|
27168
27795
|
{ name: 'twitter:card', content: 'summary_large_image' },
|
|
27169
|
-
{ name: 'twitter:title', content:
|
|
27170
|
-
{ name: 'twitter:description', content:
|
|
27171
|
-
{ name: 'twitter:image', content:
|
|
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
|
+
] : []),
|
|
27172
27803
|
],
|
|
27173
27804
|
link: [
|
|
27174
|
-
{ rel: 'canonical', href:
|
|
27805
|
+
{ rel: 'canonical', href: url },
|
|
27175
27806
|
],
|
|
27807
|
+
script: options.schema ? [
|
|
27808
|
+
{ type: 'application/ld+json', innerHTML: JSON.stringify(options.schema) },
|
|
27809
|
+
] : [],
|
|
27176
27810
|
});
|
|
27177
27811
|
}`,
|
|
27178
|
-
explanation:
|
|
27179
|
-
|
|
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
|
+
]
|
|
27180
27841
|
};
|
|
27181
27842
|
}
|
|
27182
27843
|
function generateAstroBaseHead(options) {
|
|
27183
|
-
const { siteName, siteUrl,
|
|
27844
|
+
const { siteName, siteUrl, description, image, twitterHandle, locale } = options;
|
|
27184
27845
|
return {
|
|
27185
27846
|
file: "src/components/BaseHead.astro",
|
|
27186
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
|
+
|
|
27187
27860
|
interface Props {
|
|
27188
27861
|
title?: string;
|
|
27189
27862
|
description?: string;
|
|
27190
27863
|
image?: string;
|
|
27191
27864
|
type?: 'website' | 'article';
|
|
27865
|
+
publishedTime?: string;
|
|
27866
|
+
modifiedTime?: string;
|
|
27867
|
+
author?: string;
|
|
27868
|
+
tags?: string[];
|
|
27869
|
+
noIndex?: boolean;
|
|
27870
|
+
schema?: Record<string, unknown>;
|
|
27192
27871
|
}
|
|
27193
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
|
+
|
|
27194
27880
|
const {
|
|
27195
|
-
title
|
|
27196
|
-
description =
|
|
27197
|
-
image =
|
|
27881
|
+
title,
|
|
27882
|
+
description = DEFAULT_DESCRIPTION,
|
|
27883
|
+
image = DEFAULT_IMAGE,
|
|
27198
27884
|
type = 'website',
|
|
27885
|
+
publishedTime,
|
|
27886
|
+
modifiedTime,
|
|
27887
|
+
author,
|
|
27888
|
+
tags,
|
|
27889
|
+
noIndex = false,
|
|
27890
|
+
schema,
|
|
27199
27891
|
} = Astro.props;
|
|
27200
27892
|
|
|
27201
|
-
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
|
27202
|
-
const fullTitle = title
|
|
27203
|
-
|
|
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];
|
|
27204
27909
|
---
|
|
27205
27910
|
|
|
27206
27911
|
<!-- Global Metadata -->
|
|
27207
27912
|
<meta charset="utf-8" />
|
|
27208
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
27209
|
-
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
27913
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
27210
27914
|
<meta name="generator" content={Astro.generator} />
|
|
27211
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
|
+
|
|
27212
27923
|
<!-- Canonical URL -->
|
|
27213
27924
|
<link rel="canonical" href={canonicalURL} />
|
|
27214
27925
|
|
|
@@ -27216,6 +27927,11 @@ const imageURL = new URL(image, Astro.site);
|
|
|
27216
27927
|
<title>{fullTitle}</title>
|
|
27217
27928
|
<meta name="title" content={fullTitle} />
|
|
27218
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)" />
|
|
27219
27935
|
|
|
27220
27936
|
<!-- Open Graph / Facebook -->
|
|
27221
27937
|
<meta property="og:type" content={type} />
|
|
@@ -27223,143 +27939,449 @@ const imageURL = new URL(image, Astro.site);
|
|
|
27223
27939
|
<meta property="og:title" content={fullTitle} />
|
|
27224
27940
|
<meta property="og:description" content={description} />
|
|
27225
27941
|
<meta property="og:image" content={imageURL} />
|
|
27226
|
-
<meta property="og:
|
|
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
|
+
))}
|
|
27227
27960
|
|
|
27228
27961
|
<!-- Twitter -->
|
|
27229
27962
|
<meta name="twitter:card" content="summary_large_image" />
|
|
27230
27963
|
<meta name="twitter:url" content={canonicalURL} />
|
|
27231
27964
|
<meta name="twitter:title" content={fullTitle} />
|
|
27232
27965
|
<meta name="twitter:description" content={description} />
|
|
27233
|
-
<meta name="twitter:image" content={imageURL}
|
|
27234
|
-
|
|
27235
|
-
|
|
27236
|
-
}
|
|
27237
|
-
|
|
27238
|
-
|
|
27239
|
-
|
|
27240
|
-
|
|
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: `---
|
|
27241
27990
|
import BaseHead from '../components/BaseHead.astro';
|
|
27242
27991
|
|
|
27243
27992
|
interface Props {
|
|
27244
27993
|
title?: string;
|
|
27245
27994
|
description?: string;
|
|
27246
27995
|
image?: string;
|
|
27996
|
+
type?: 'website' | 'article';
|
|
27997
|
+
schema?: Record<string, unknown>;
|
|
27247
27998
|
}
|
|
27248
27999
|
|
|
27249
|
-
const { title, description, image } = Astro.props;
|
|
28000
|
+
const { title, description, image, type, schema } = Astro.props;
|
|
27250
28001
|
---
|
|
27251
28002
|
|
|
27252
28003
|
<!DOCTYPE html>
|
|
27253
|
-
<html lang="
|
|
28004
|
+
<html lang="${(locale || "en_US").split("_")[0]}">
|
|
27254
28005
|
<head>
|
|
27255
|
-
<BaseHead
|
|
28006
|
+
<BaseHead
|
|
28007
|
+
title={title}
|
|
28008
|
+
description={description}
|
|
28009
|
+
image={image}
|
|
28010
|
+
type={type}
|
|
28011
|
+
schema={schema}
|
|
28012
|
+
/>
|
|
27256
28013
|
</head>
|
|
27257
28014
|
<body>
|
|
27258
|
-
<
|
|
27259
|
-
<slot />
|
|
27260
|
-
</main>
|
|
28015
|
+
<slot />
|
|
27261
28016
|
</body>
|
|
27262
28017
|
</html>`,
|
|
27263
|
-
|
|
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
|
+
]
|
|
27264
28033
|
};
|
|
27265
28034
|
}
|
|
27266
28035
|
function generateSvelteKitSEOHead(options) {
|
|
27267
|
-
const { siteName, siteUrl,
|
|
28036
|
+
const { siteName, siteUrl, description, image, twitterHandle } = options;
|
|
27268
28037
|
return {
|
|
27269
28038
|
file: "src/lib/components/SEOHead.svelte",
|
|
27270
28039
|
code: `<script lang="ts">
|
|
27271
28040
|
import { page } from '$app/stores';
|
|
27272
28041
|
|
|
27273
|
-
export let title
|
|
27274
|
-
export let description = '${description || `${siteName} - A compelling description.`}';
|
|
27275
|
-
export let image = '${image || `${siteUrl}/og-image.png`}';
|
|
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`}';
|
|
27276
28045
|
export let type: 'website' | 'article' = 'website';
|
|
27277
|
-
|
|
27278
|
-
|
|
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;
|
|
27279
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];
|
|
27280
28072
|
</script>
|
|
27281
28073
|
|
|
27282
28074
|
<svelte:head>
|
|
28075
|
+
<!-- Primary Meta Tags -->
|
|
27283
28076
|
<title>{fullTitle}</title>
|
|
28077
|
+
<meta name="title" content={fullTitle} />
|
|
27284
28078
|
<meta name="description" content={description} />
|
|
28079
|
+
<meta name="robots" content={robotsContent} />
|
|
27285
28080
|
<link rel="canonical" href={canonicalUrl} />
|
|
27286
|
-
|
|
28081
|
+
|
|
28082
|
+
<!-- Open Graph / Facebook -->
|
|
27287
28083
|
<meta property="og:type" content={type} />
|
|
27288
28084
|
<meta property="og:url" content={canonicalUrl} />
|
|
27289
28085
|
<meta property="og:title" content={fullTitle} />
|
|
27290
28086
|
<meta property="og:description" content={description} />
|
|
27291
|
-
<meta property="og:image" content={
|
|
27292
|
-
<meta property="og:
|
|
27293
|
-
|
|
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 -->
|
|
27294
28108
|
<meta name="twitter:card" content="summary_large_image" />
|
|
27295
28109
|
<meta name="twitter:title" content={fullTitle} />
|
|
27296
28110
|
<meta name="twitter:description" content={description} />
|
|
27297
|
-
<meta name="twitter:image" content={
|
|
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>\`}
|
|
27298
28119
|
</svelte:head>`,
|
|
27299
|
-
explanation:
|
|
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
|
+
]
|
|
27300
28151
|
};
|
|
27301
28152
|
}
|
|
27302
28153
|
function generateAngularSEOService(options) {
|
|
27303
|
-
const { siteName, siteUrl,
|
|
28154
|
+
const { siteName, siteUrl, description, image, twitterHandle } = options;
|
|
27304
28155
|
return {
|
|
27305
|
-
file: "src/app/services/seo.service.ts",
|
|
27306
|
-
code: `import { Injectable } from '@angular/core';
|
|
28156
|
+
file: "src/app/core/services/seo.service.ts",
|
|
28157
|
+
code: `import { Injectable, Inject } from '@angular/core';
|
|
27307
28158
|
import { Meta, Title } from '@angular/platform-browser';
|
|
27308
|
-
import { Router } from '@angular/router';
|
|
28159
|
+
import { Router, NavigationEnd } from '@angular/router';
|
|
28160
|
+
import { DOCUMENT } from '@angular/common';
|
|
28161
|
+
import { filter } from 'rxjs/operators';
|
|
27309
28162
|
|
|
27310
28163
|
interface SEOConfig {
|
|
27311
28164
|
title?: string;
|
|
27312
28165
|
description?: string;
|
|
27313
28166
|
image?: string;
|
|
27314
28167
|
type?: 'website' | 'article';
|
|
28168
|
+
publishedTime?: string;
|
|
28169
|
+
modifiedTime?: string;
|
|
28170
|
+
author?: string;
|
|
28171
|
+
tags?: string[];
|
|
28172
|
+
noIndex?: boolean;
|
|
28173
|
+
schema?: Record<string, unknown>;
|
|
27315
28174
|
}
|
|
27316
28175
|
|
|
27317
28176
|
@Injectable({
|
|
27318
28177
|
providedIn: 'root'
|
|
27319
28178
|
})
|
|
27320
28179
|
export class SEOService {
|
|
27321
|
-
private siteName = '${siteName}';
|
|
27322
|
-
private siteUrl = '${siteUrl}';
|
|
27323
|
-
private defaultDescription = '${description || `${siteName} - A compelling description.`}';
|
|
27324
|
-
private defaultImage = '${image || `${siteUrl}/og-image.png`}';
|
|
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 || ""}';
|
|
27325
28185
|
|
|
27326
28186
|
constructor(
|
|
27327
28187
|
private meta: Meta,
|
|
27328
28188
|
private titleService: Title,
|
|
27329
|
-
private router: Router
|
|
27330
|
-
|
|
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
|
+
}
|
|
27331
28199
|
|
|
28200
|
+
/**
|
|
28201
|
+
* Update all SEO meta tags
|
|
28202
|
+
*/
|
|
27332
28203
|
updateMeta(config: SEOConfig = {}): void {
|
|
27333
|
-
const
|
|
27334
|
-
|
|
27335
|
-
|
|
27336
|
-
|
|
27337
|
-
|
|
27338
|
-
|
|
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';
|
|
27339
28223
|
|
|
27340
28224
|
// Title
|
|
27341
28225
|
this.titleService.setTitle(fullTitle);
|
|
27342
28226
|
|
|
27343
|
-
// Primary Meta
|
|
27344
|
-
this.
|
|
27345
|
-
this.
|
|
28227
|
+
// Primary Meta Tags
|
|
28228
|
+
this.setMetaTag('description', description);
|
|
28229
|
+
this.setMetaTag('robots', robotsContent);
|
|
27346
28230
|
|
|
27347
28231
|
// Open Graph
|
|
27348
|
-
this.
|
|
27349
|
-
this.
|
|
27350
|
-
this.
|
|
27351
|
-
this.
|
|
27352
|
-
this.
|
|
27353
|
-
this.
|
|
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
|
+
}
|
|
27354
28248
|
|
|
27355
28249
|
// Twitter
|
|
27356
|
-
this.
|
|
27357
|
-
this.
|
|
27358
|
-
this.
|
|
27359
|
-
this.
|
|
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
|
+
};
|
|
27360
28374
|
}
|
|
27361
28375
|
}`,
|
|
27362
|
-
explanation:
|
|
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()`
|
|
27363
28385
|
};
|
|
27364
28386
|
}
|
|
27365
28387
|
function getFrameworkSpecificFix(framework, options) {
|
|
@@ -27388,6 +28410,102 @@ function getFrameworkSpecificFix(framework, options) {
|
|
|
27388
28410
|
}
|
|
27389
28411
|
return generateReactSEOHead(options);
|
|
27390
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
|
+
}
|
|
27391
28509
|
|
|
27392
28510
|
// src/fixer.ts
|
|
27393
28511
|
async function generateFixes(issues, options) {
|
|
@@ -30624,7 +31742,6 @@ if (typeof globalThis !== "undefined") {
|
|
|
30624
31742
|
generateAllFixes,
|
|
30625
31743
|
generateAngularSEOService,
|
|
30626
31744
|
generateAstroBaseHead,
|
|
30627
|
-
generateAstroLayout,
|
|
30628
31745
|
generateAstroMeta,
|
|
30629
31746
|
generateBlogPost,
|
|
30630
31747
|
generateBranchName,
|
|
@@ -30650,17 +31767,11 @@ if (typeof globalThis !== "undefined") {
|
|
|
30650
31767
|
generateMarkdownReport,
|
|
30651
31768
|
generateNextAppMetadata,
|
|
30652
31769
|
generateNextJsAppRouterMetadata,
|
|
30653
|
-
generateNextJsDynamicMetadata,
|
|
30654
|
-
generateNextJsPageMetadata,
|
|
30655
31770
|
generateNextJsPagesRouterHead,
|
|
30656
|
-
generateNextJsRobots,
|
|
30657
|
-
generateNextJsSitemap,
|
|
30658
31771
|
generateNextPagesHead,
|
|
30659
|
-
generateNuxtPageExample,
|
|
30660
31772
|
generateNuxtSEOHead,
|
|
30661
31773
|
generatePDFReport,
|
|
30662
31774
|
generatePRDescription,
|
|
30663
|
-
generateReactAppWrapper,
|
|
30664
31775
|
generateReactHelmetSocialMeta,
|
|
30665
31776
|
generateReactSEOHead,
|
|
30666
31777
|
generateRecommendationQueries,
|