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