@rankcli/agent-runtime 0.0.8 → 0.0.11

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.
Files changed (49) hide show
  1. package/README.md +90 -196
  2. package/dist/analyzer-GMURJADU.mjs +7 -0
  3. package/dist/chunk-2JADKV3Z.mjs +244 -0
  4. package/dist/chunk-3ZSCLNTW.mjs +557 -0
  5. package/dist/chunk-4E4MQOSP.mjs +374 -0
  6. package/dist/chunk-6BWS3CLP.mjs +16 -0
  7. package/dist/chunk-AK2IC22C.mjs +206 -0
  8. package/dist/chunk-K6VSXDD6.mjs +293 -0
  9. package/dist/chunk-M27NQCWW.mjs +303 -0
  10. package/dist/{chunk-YNZYHEYM.mjs → chunk-PJLNXOLN.mjs} +0 -14
  11. package/dist/chunk-VSQD74I7.mjs +474 -0
  12. package/dist/core-web-vitals-analyzer-TE6LQJMS.mjs +7 -0
  13. package/dist/geo-analyzer-D47LTMMA.mjs +25 -0
  14. package/dist/image-optimization-analyzer-XP4OQGRP.mjs +9 -0
  15. package/dist/index.d.mts +1523 -17
  16. package/dist/index.d.ts +1523 -17
  17. package/dist/index.js +9582 -2664
  18. package/dist/index.mjs +4812 -380
  19. package/dist/internal-linking-analyzer-MRMBV7NM.mjs +9 -0
  20. package/dist/mobile-seo-analyzer-67HNQ7IO.mjs +7 -0
  21. package/dist/security-headers-analyzer-3ZUQARS5.mjs +9 -0
  22. package/dist/structured-data-analyzer-2I4NQAUP.mjs +9 -0
  23. package/package.json +2 -2
  24. package/src/analyzers/core-web-vitals-analyzer.test.ts +236 -0
  25. package/src/analyzers/core-web-vitals-analyzer.ts +557 -0
  26. package/src/analyzers/geo-analyzer.test.ts +310 -0
  27. package/src/analyzers/geo-analyzer.ts +814 -0
  28. package/src/analyzers/image-optimization-analyzer.test.ts +145 -0
  29. package/src/analyzers/image-optimization-analyzer.ts +348 -0
  30. package/src/analyzers/index.ts +233 -0
  31. package/src/analyzers/internal-linking-analyzer.test.ts +141 -0
  32. package/src/analyzers/internal-linking-analyzer.ts +419 -0
  33. package/src/analyzers/mobile-seo-analyzer.test.ts +140 -0
  34. package/src/analyzers/mobile-seo-analyzer.ts +455 -0
  35. package/src/analyzers/security-headers-analyzer.test.ts +115 -0
  36. package/src/analyzers/security-headers-analyzer.ts +318 -0
  37. package/src/analyzers/structured-data-analyzer.test.ts +210 -0
  38. package/src/analyzers/structured-data-analyzer.ts +590 -0
  39. package/src/audit/engine.ts +3 -3
  40. package/src/audit/types.ts +3 -2
  41. package/src/fixer/framework-fixes.test.ts +489 -0
  42. package/src/fixer/framework-fixes.ts +3418 -0
  43. package/src/fixer/index.ts +1 -0
  44. package/src/fixer/schemas.ts +971 -0
  45. package/src/frameworks/detector.ts +642 -114
  46. package/src/frameworks/suggestion-engine.ts +38 -1
  47. package/src/index.ts +6 -0
  48. package/src/types.ts +15 -1
  49. package/dist/analyzer-2CSWIQGD.mjs +0 -6
@@ -1867,3 +1867,3421 @@ export function SEOHead({
1867
1867
  Usage: <SEOHead title="Page" description="..." />`,
1868
1868
  };
1869
1869
  }
1870
+
1871
+ // ============================================================================
1872
+ // RUBY ON RAILS - Comprehensive meta-tags gem integration
1873
+ // ============================================================================
1874
+
1875
+ export function generateRailsSEOHelper(options: MetaFixOptions): GeneratedCode {
1876
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
1877
+
1878
+ return {
1879
+ file: 'app/helpers/seo_helper.rb',
1880
+ code: `# frozen_string_literal: true
1881
+
1882
+ # Comprehensive SEO Helper for Rails
1883
+ #
1884
+ # Features:
1885
+ # - Full Open Graph support
1886
+ # - Twitter Cards
1887
+ # - JSON-LD structured data
1888
+ # - Canonical URLs
1889
+ # - Dynamic meta tags
1890
+ #
1891
+ # Installation: gem 'meta-tags'
1892
+ # Include in ApplicationHelper or use directly
1893
+ module SeoHelper
1894
+ SITE_NAME = '${siteName}'.freeze
1895
+ SITE_URL = '${siteUrl}'.freeze
1896
+ DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}'.freeze
1897
+ DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}'.freeze
1898
+ TWITTER_HANDLE = '${twitterHandle || ''}'.freeze
1899
+
1900
+ # Set comprehensive page SEO
1901
+ #
1902
+ # @param title [String] Page title
1903
+ # @param description [String] Meta description
1904
+ # @param image [String] OG/Twitter image URL
1905
+ # @param type [Symbol] :website, :article, :product
1906
+ # @param options [Hash] Additional options
1907
+ #
1908
+ # @example
1909
+ # set_page_seo(
1910
+ # title: 'Product Name',
1911
+ # description: 'Product description',
1912
+ # type: :product,
1913
+ # schema: product_schema(@product)
1914
+ # )
1915
+ def set_page_seo(title: nil, description: nil, image: nil, type: :website, **options)
1916
+ page_title = title || SITE_NAME
1917
+ page_description = description || DEFAULT_DESCRIPTION
1918
+ page_image = image || DEFAULT_IMAGE
1919
+ page_url = request.original_url
1920
+
1921
+ set_meta_tags(
1922
+ title: page_title,
1923
+ site: SITE_NAME,
1924
+ description: page_description,
1925
+ canonical: page_url,
1926
+ robots: options[:noindex] ? 'noindex, nofollow' : 'index, follow',
1927
+
1928
+ # Open Graph
1929
+ og: {
1930
+ title: page_title,
1931
+ description: page_description,
1932
+ type: type.to_s,
1933
+ url: page_url,
1934
+ image: {
1935
+ _: page_image,
1936
+ width: 1200,
1937
+ height: 630,
1938
+ alt: page_title
1939
+ },
1940
+ site_name: SITE_NAME,
1941
+ locale: options[:locale] || 'en_US'
1942
+ },
1943
+
1944
+ # Twitter Cards
1945
+ twitter: {
1946
+ card: 'summary_large_image',
1947
+ title: page_title,
1948
+ description: page_description,
1949
+ image: page_image,
1950
+ site: TWITTER_HANDLE.presence,
1951
+ creator: TWITTER_HANDLE.presence
1952
+ }.compact,
1953
+
1954
+ # Article-specific
1955
+ article: if type == :article
1956
+ {
1957
+ published_time: options[:published_at]&.iso8601,
1958
+ modified_time: options[:updated_at]&.iso8601,
1959
+ author: options[:author],
1960
+ section: options[:section],
1961
+ tag: options[:tags]
1962
+ }.compact
1963
+ end
1964
+ )
1965
+
1966
+ # Store schema for JSON-LD rendering
1967
+ @json_ld_schemas = [default_website_schema]
1968
+ @json_ld_schemas << options[:schema] if options[:schema]
1969
+ end
1970
+
1971
+ # Render JSON-LD structured data
1972
+ def json_ld_tags
1973
+ return unless @json_ld_schemas&.any?
1974
+
1975
+ safe_join(@json_ld_schemas.map do |schema|
1976
+ content_tag(:script, schema.to_json.html_safe, type: 'application/ld+json')
1977
+ end)
1978
+ end
1979
+
1980
+ # Schema generators
1981
+ def default_website_schema
1982
+ {
1983
+ '@context': 'https://schema.org',
1984
+ '@type': 'WebSite',
1985
+ name: SITE_NAME,
1986
+ url: SITE_URL
1987
+ }
1988
+ end
1989
+
1990
+ def organization_schema(name: SITE_NAME, url: SITE_URL, logo: nil, social: [])
1991
+ {
1992
+ '@context': 'https://schema.org',
1993
+ '@type': 'Organization',
1994
+ name: name,
1995
+ url: url,
1996
+ logo: logo,
1997
+ sameAs: social
1998
+ }.compact
1999
+ end
2000
+
2001
+ def article_schema(article)
2002
+ {
2003
+ '@context': 'https://schema.org',
2004
+ '@type': 'Article',
2005
+ headline: article.title,
2006
+ description: article.excerpt || truncate(strip_tags(article.content), length: 160),
2007
+ image: article.image_url || DEFAULT_IMAGE,
2008
+ datePublished: article.published_at&.iso8601,
2009
+ dateModified: article.updated_at&.iso8601,
2010
+ author: {
2011
+ '@type': 'Person',
2012
+ name: article.author_name
2013
+ },
2014
+ publisher: {
2015
+ '@type': 'Organization',
2016
+ name: SITE_NAME,
2017
+ logo: { '@type': 'ImageObject', url: "\#{SITE_URL}/logo.png" }
2018
+ }
2019
+ }
2020
+ end
2021
+
2022
+ def product_schema(product)
2023
+ {
2024
+ '@context': 'https://schema.org',
2025
+ '@type': 'Product',
2026
+ name: product.name,
2027
+ description: product.description,
2028
+ image: product.image_url,
2029
+ brand: product.brand.present? ? { '@type': 'Brand', name: product.brand } : nil,
2030
+ sku: product.sku,
2031
+ offers: {
2032
+ '@type': 'Offer',
2033
+ price: product.price.to_s,
2034
+ priceCurrency: product.currency || 'USD',
2035
+ availability: "https://schema.org/\#{product.in_stock? ? 'InStock' : 'OutOfStock'}"
2036
+ },
2037
+ aggregateRating: product.reviews_count.positive? ? {
2038
+ '@type': 'AggregateRating',
2039
+ ratingValue: product.average_rating,
2040
+ reviewCount: product.reviews_count
2041
+ } : nil
2042
+ }.compact
2043
+ end
2044
+
2045
+ def faq_schema(items)
2046
+ {
2047
+ '@context': 'https://schema.org',
2048
+ '@type': 'FAQPage',
2049
+ mainEntity: items.map do |item|
2050
+ {
2051
+ '@type': 'Question',
2052
+ name: item[:question],
2053
+ acceptedAnswer: { '@type': 'Answer', text: item[:answer] }
2054
+ }
2055
+ end
2056
+ }
2057
+ end
2058
+
2059
+ def breadcrumb_schema(items)
2060
+ {
2061
+ '@context': 'https://schema.org',
2062
+ '@type': 'BreadcrumbList',
2063
+ itemListElement: items.each_with_index.map do |item, index|
2064
+ {
2065
+ '@type': 'ListItem',
2066
+ position: index + 1,
2067
+ name: item[:name],
2068
+ item: item[:url]
2069
+ }
2070
+ end
2071
+ }
2072
+ end
2073
+
2074
+ def local_business_schema(business)
2075
+ {
2076
+ '@context': 'https://schema.org',
2077
+ '@type': 'LocalBusiness',
2078
+ name: business[:name],
2079
+ description: business[:description],
2080
+ url: business[:url],
2081
+ telephone: business[:phone],
2082
+ address: {
2083
+ '@type': 'PostalAddress',
2084
+ streetAddress: business.dig(:address, :street),
2085
+ addressLocality: business.dig(:address, :city),
2086
+ addressRegion: business.dig(:address, :state),
2087
+ postalCode: business.dig(:address, :zip),
2088
+ addressCountry: business.dig(:address, :country)
2089
+ },
2090
+ geo: business[:coordinates].present? ? {
2091
+ '@type': 'GeoCoordinates',
2092
+ latitude: business.dig(:coordinates, :lat),
2093
+ longitude: business.dig(:coordinates, :lng)
2094
+ } : nil,
2095
+ openingHours: business[:hours],
2096
+ priceRange: business[:price_range]
2097
+ }.compact
2098
+ end
2099
+ end`,
2100
+ explanation: `Ruby on Rails comprehensive SEO helper with:
2101
+ • meta-tags gem integration
2102
+ • Full Open Graph with article support
2103
+ • Twitter Cards
2104
+ • JSON-LD schema generators for all common types
2105
+ • Canonical URLs and robots directives
2106
+
2107
+ Setup:
2108
+ 1. Add to Gemfile: gem 'meta-tags'
2109
+ 2. Include helper in ApplicationHelper
2110
+ 3. Call set_page_seo in controllers
2111
+ 4. Add <%= display_meta_tags %> and <%= json_ld_tags %> to layout`,
2112
+ installCommands: ['bundle add meta-tags'],
2113
+ additionalFiles: [
2114
+ {
2115
+ file: 'app/views/layouts/application.html.erb',
2116
+ code: `<!DOCTYPE html>
2117
+ <html lang="<%= I18n.locale %>">
2118
+ <head>
2119
+ <meta charset="utf-8">
2120
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2121
+
2122
+ <%= display_meta_tags %>
2123
+ <%= csrf_meta_tags %>
2124
+ <%= csp_meta_tag %>
2125
+
2126
+ <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
2127
+ <%= javascript_importmap_tags %>
2128
+
2129
+ <!-- Favicon -->
2130
+ <%= favicon_link_tag 'favicon.ico' %>
2131
+ <%= favicon_link_tag 'apple-touch-icon.png', rel: 'apple-touch-icon', type: 'image/png', sizes: '180x180' %>
2132
+
2133
+ <!-- JSON-LD Structured Data -->
2134
+ <%= json_ld_tags %>
2135
+ </head>
2136
+ <body>
2137
+ <%= yield %>
2138
+ </body>
2139
+ </html>`,
2140
+ explanation: 'Application layout with SEO meta tags and JSON-LD.',
2141
+ },
2142
+ {
2143
+ file: 'config/initializers/meta_tags.rb',
2144
+ code: `# frozen_string_literal: true
2145
+
2146
+ MetaTags.configure do |config|
2147
+ # Use title template
2148
+ config.title_limit = 70
2149
+ config.description_limit = 160
2150
+ config.keywords_limit = 255
2151
+
2152
+ # Truncate long strings
2153
+ config.truncate_on_bytesize = true
2154
+
2155
+ # Separate site name with |
2156
+ config.title_separator = ' | '
2157
+ end`,
2158
+ explanation: 'meta-tags gem configuration.',
2159
+ },
2160
+ {
2161
+ file: 'public/robots.txt',
2162
+ code: `User-agent: *
2163
+ Allow: /
2164
+ Disallow: /admin/
2165
+ Disallow: /api/
2166
+
2167
+ User-agent: GPTBot
2168
+ Allow: /
2169
+
2170
+ Sitemap: ${siteUrl}/sitemap.xml`,
2171
+ explanation: 'Robots.txt with AI crawler support.',
2172
+ },
2173
+ ],
2174
+ };
2175
+ }
2176
+
2177
+ // ============================================================================
2178
+ // DJANGO - Comprehensive SEO with django-meta
2179
+ // ============================================================================
2180
+
2181
+ export function generateDjangoSEOHelper(options: MetaFixOptions): GeneratedCode {
2182
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
2183
+
2184
+ return {
2185
+ file: 'core/seo.py',
2186
+ code: `"""
2187
+ Comprehensive SEO utilities for Django
2188
+
2189
+ Features:
2190
+ - Full Open Graph support
2191
+ - Twitter Cards
2192
+ - JSON-LD structured data
2193
+ - Canonical URLs
2194
+ - Template tags and context processors
2195
+
2196
+ Installation: pip install django-meta
2197
+ """
2198
+
2199
+ from django.conf import settings
2200
+ from django.utils.safestring import mark_safe
2201
+ import json
2202
+ from typing import Any, Optional
2203
+ from dataclasses import dataclass, field
2204
+
2205
+
2206
+ # Configuration
2207
+ SITE_NAME = '${siteName}'
2208
+ SITE_URL = '${siteUrl}'
2209
+ DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}'
2210
+ DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}'
2211
+ TWITTER_HANDLE = '${twitterHandle || ''}'
2212
+
2213
+
2214
+ @dataclass
2215
+ class SEOMeta:
2216
+ """SEO metadata container"""
2217
+ title: Optional[str] = None
2218
+ description: str = DEFAULT_DESCRIPTION
2219
+ image: str = DEFAULT_IMAGE
2220
+ url: Optional[str] = None
2221
+ type: str = 'website'
2222
+
2223
+ # Article specific
2224
+ published_time: Optional[str] = None
2225
+ modified_time: Optional[str] = None
2226
+ author: Optional[str] = None
2227
+ tags: list = field(default_factory=list)
2228
+
2229
+ # Robots
2230
+ noindex: bool = False
2231
+ nofollow: bool = False
2232
+
2233
+ # Structured data
2234
+ schema: Optional[dict] = None
2235
+
2236
+ def get_full_title(self) -> str:
2237
+ if not self.title:
2238
+ return SITE_NAME
2239
+ if SITE_NAME in self.title:
2240
+ return self.title
2241
+ return f"{self.title} | {SITE_NAME}"
2242
+
2243
+ def get_image_url(self) -> str:
2244
+ if self.image.startswith('http'):
2245
+ return self.image
2246
+ return f"{SITE_URL}{self.image}"
2247
+
2248
+ def get_robots(self) -> str:
2249
+ index = 'noindex' if self.noindex else 'index'
2250
+ follow = 'nofollow' if self.nofollow else 'follow'
2251
+ return f"{index}, {follow}"
2252
+
2253
+
2254
+ class SEOContextMixin:
2255
+ """
2256
+ Mixin for class-based views to add SEO context
2257
+
2258
+ Usage:
2259
+ class ArticleDetailView(SEOContextMixin, DetailView):
2260
+ model = Article
2261
+
2262
+ def get_seo_meta(self):
2263
+ return SEOMeta(
2264
+ title=self.object.title,
2265
+ description=self.object.excerpt,
2266
+ image=self.object.image.url if self.object.image else None,
2267
+ type='article',
2268
+ published_time=self.object.published_at.isoformat(),
2269
+ schema=article_schema(self.object)
2270
+ )
2271
+ """
2272
+
2273
+ def get_seo_meta(self) -> SEOMeta:
2274
+ return SEOMeta()
2275
+
2276
+ def get_context_data(self, **kwargs):
2277
+ context = super().get_context_data(**kwargs)
2278
+ context['seo'] = self.get_seo_meta()
2279
+ return context
2280
+
2281
+
2282
+ def seo_context_processor(request):
2283
+ """
2284
+ Context processor to add default SEO values
2285
+
2286
+ Add to settings.TEMPLATES[0]['OPTIONS']['context_processors']
2287
+ """
2288
+ return {
2289
+ 'site_name': SITE_NAME,
2290
+ 'site_url': SITE_URL,
2291
+ 'default_og_image': DEFAULT_IMAGE,
2292
+ }
2293
+
2294
+
2295
+ # JSON-LD Schema Generators
2296
+
2297
+ def website_schema() -> dict:
2298
+ return {
2299
+ '@context': 'https://schema.org',
2300
+ '@type': 'WebSite',
2301
+ 'name': SITE_NAME,
2302
+ 'url': SITE_URL,
2303
+ }
2304
+
2305
+
2306
+ def organization_schema(
2307
+ name: str = SITE_NAME,
2308
+ url: str = SITE_URL,
2309
+ logo: Optional[str] = None,
2310
+ social_profiles: list = None
2311
+ ) -> dict:
2312
+ schema = {
2313
+ '@context': 'https://schema.org',
2314
+ '@type': 'Organization',
2315
+ 'name': name,
2316
+ 'url': url,
2317
+ }
2318
+ if logo:
2319
+ schema['logo'] = logo
2320
+ if social_profiles:
2321
+ schema['sameAs'] = social_profiles
2322
+ return schema
2323
+
2324
+
2325
+ def article_schema(article) -> dict:
2326
+ """Generate Article schema from model instance"""
2327
+ return {
2328
+ '@context': 'https://schema.org',
2329
+ '@type': 'Article',
2330
+ 'headline': article.title,
2331
+ 'description': getattr(article, 'excerpt', '') or article.content[:160],
2332
+ 'image': article.image.url if hasattr(article, 'image') and article.image else DEFAULT_IMAGE,
2333
+ 'datePublished': article.published_at.isoformat() if hasattr(article, 'published_at') else None,
2334
+ 'dateModified': article.updated_at.isoformat() if hasattr(article, 'updated_at') else None,
2335
+ 'author': {
2336
+ '@type': 'Person',
2337
+ 'name': str(article.author) if hasattr(article, 'author') else 'Unknown',
2338
+ },
2339
+ 'publisher': {
2340
+ '@type': 'Organization',
2341
+ 'name': SITE_NAME,
2342
+ 'logo': {'@type': 'ImageObject', 'url': f'{SITE_URL}/static/logo.png'},
2343
+ },
2344
+ }
2345
+
2346
+
2347
+ def product_schema(product) -> dict:
2348
+ """Generate Product schema from model instance"""
2349
+ schema = {
2350
+ '@context': 'https://schema.org',
2351
+ '@type': 'Product',
2352
+ 'name': product.name,
2353
+ 'description': product.description,
2354
+ 'image': product.image.url if hasattr(product, 'image') and product.image else DEFAULT_IMAGE,
2355
+ 'offers': {
2356
+ '@type': 'Offer',
2357
+ 'price': str(product.price),
2358
+ 'priceCurrency': getattr(product, 'currency', 'USD'),
2359
+ 'availability': f"https://schema.org/{'InStock' if product.in_stock else 'OutOfStock'}",
2360
+ },
2361
+ }
2362
+
2363
+ if hasattr(product, 'brand') and product.brand:
2364
+ schema['brand'] = {'@type': 'Brand', 'name': product.brand}
2365
+
2366
+ if hasattr(product, 'sku') and product.sku:
2367
+ schema['sku'] = product.sku
2368
+
2369
+ if hasattr(product, 'average_rating') and hasattr(product, 'review_count'):
2370
+ if product.review_count > 0:
2371
+ schema['aggregateRating'] = {
2372
+ '@type': 'AggregateRating',
2373
+ 'ratingValue': product.average_rating,
2374
+ 'reviewCount': product.review_count,
2375
+ }
2376
+
2377
+ return schema
2378
+
2379
+
2380
+ def faq_schema(items: list[dict]) -> dict:
2381
+ """Generate FAQ schema from list of {question, answer} dicts"""
2382
+ return {
2383
+ '@context': 'https://schema.org',
2384
+ '@type': 'FAQPage',
2385
+ 'mainEntity': [
2386
+ {
2387
+ '@type': 'Question',
2388
+ 'name': item['question'],
2389
+ 'acceptedAnswer': {'@type': 'Answer', 'text': item['answer']},
2390
+ }
2391
+ for item in items
2392
+ ],
2393
+ }
2394
+
2395
+
2396
+ def breadcrumb_schema(items: list[dict]) -> dict:
2397
+ """Generate Breadcrumb schema from list of {name, url} dicts"""
2398
+ return {
2399
+ '@context': 'https://schema.org',
2400
+ '@type': 'BreadcrumbList',
2401
+ 'itemListElement': [
2402
+ {
2403
+ '@type': 'ListItem',
2404
+ 'position': i + 1,
2405
+ 'name': item['name'],
2406
+ 'item': item['url'],
2407
+ }
2408
+ for i, item in enumerate(items)
2409
+ ],
2410
+ }
2411
+
2412
+
2413
+ def local_business_schema(business: dict) -> dict:
2414
+ """Generate LocalBusiness schema"""
2415
+ schema = {
2416
+ '@context': 'https://schema.org',
2417
+ '@type': 'LocalBusiness',
2418
+ 'name': business['name'],
2419
+ 'description': business.get('description', ''),
2420
+ 'url': business.get('url', SITE_URL),
2421
+ 'telephone': business.get('phone'),
2422
+ 'address': {
2423
+ '@type': 'PostalAddress',
2424
+ 'streetAddress': business.get('address', {}).get('street'),
2425
+ 'addressLocality': business.get('address', {}).get('city'),
2426
+ 'addressRegion': business.get('address', {}).get('state'),
2427
+ 'postalCode': business.get('address', {}).get('zip'),
2428
+ 'addressCountry': business.get('address', {}).get('country'),
2429
+ },
2430
+ }
2431
+
2432
+ if business.get('coordinates'):
2433
+ schema['geo'] = {
2434
+ '@type': 'GeoCoordinates',
2435
+ 'latitude': business['coordinates']['lat'],
2436
+ 'longitude': business['coordinates']['lng'],
2437
+ }
2438
+
2439
+ if business.get('hours'):
2440
+ schema['openingHours'] = business['hours']
2441
+
2442
+ if business.get('price_range'):
2443
+ schema['priceRange'] = business['price_range']
2444
+
2445
+ return schema
2446
+
2447
+
2448
+ def render_json_ld(*schemas) -> str:
2449
+ """Render JSON-LD script tags for templates"""
2450
+ all_schemas = [website_schema()] + list(schemas)
2451
+ scripts = '\\n'.join(
2452
+ f'<script type="application/ld+json">{json.dumps(s)}</script>'
2453
+ for s in all_schemas if s
2454
+ )
2455
+ return mark_safe(scripts)`,
2456
+ explanation: `Django comprehensive SEO module with:
2457
+ • SEOMeta dataclass for structured metadata
2458
+ • SEOContextMixin for class-based views
2459
+ • Context processor for global SEO values
2460
+ • JSON-LD schema generators
2461
+ • Template-ready helpers
2462
+
2463
+ Setup:
2464
+ 1. pip install django-meta
2465
+ 2. Add context processor to settings
2466
+ 3. Use SEOContextMixin in views
2467
+ 4. Include seo template tags in base.html`,
2468
+ installCommands: ['pip install django-meta'],
2469
+ additionalFiles: [
2470
+ {
2471
+ file: 'templates/base.html',
2472
+ code: `<!DOCTYPE html>
2473
+ <html lang="{{ LANGUAGE_CODE }}">
2474
+ <head>
2475
+ <meta charset="utf-8">
2476
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2477
+
2478
+ {% if seo %}
2479
+ <!-- Primary Meta Tags -->
2480
+ <title>{{ seo.get_full_title }}</title>
2481
+ <meta name="title" content="{{ seo.get_full_title }}">
2482
+ <meta name="description" content="{{ seo.description }}">
2483
+ <meta name="robots" content="{{ seo.get_robots }}">
2484
+ <link rel="canonical" href="{{ seo.url|default:request.build_absolute_uri }}">
2485
+
2486
+ <!-- Open Graph / Facebook -->
2487
+ <meta property="og:type" content="{{ seo.type }}">
2488
+ <meta property="og:url" content="{{ seo.url|default:request.build_absolute_uri }}">
2489
+ <meta property="og:title" content="{{ seo.get_full_title }}">
2490
+ <meta property="og:description" content="{{ seo.description }}">
2491
+ <meta property="og:image" content="{{ seo.get_image_url }}">
2492
+ <meta property="og:image:width" content="1200">
2493
+ <meta property="og:image:height" content="630">
2494
+ <meta property="og:site_name" content="{{ site_name }}">
2495
+
2496
+ {% if seo.type == 'article' %}
2497
+ {% if seo.published_time %}<meta property="article:published_time" content="{{ seo.published_time }}">{% endif %}
2498
+ {% if seo.modified_time %}<meta property="article:modified_time" content="{{ seo.modified_time }}">{% endif %}
2499
+ {% if seo.author %}<meta property="article:author" content="{{ seo.author }}">{% endif %}
2500
+ {% for tag in seo.tags %}<meta property="article:tag" content="{{ tag }}">{% endfor %}
2501
+ {% endif %}
2502
+
2503
+ <!-- Twitter -->
2504
+ <meta name="twitter:card" content="summary_large_image">
2505
+ <meta name="twitter:title" content="{{ seo.get_full_title }}">
2506
+ <meta name="twitter:description" content="{{ seo.description }}">
2507
+ <meta name="twitter:image" content="{{ seo.get_image_url }}">
2508
+ {% else %}
2509
+ <title>{{ site_name }}</title>
2510
+ {% endif %}
2511
+
2512
+ {% load static %}
2513
+ <link rel="stylesheet" href="{% static 'css/styles.css' %}">
2514
+
2515
+ <!-- JSON-LD Structured Data -->
2516
+ {% block json_ld %}{% endblock %}
2517
+ </head>
2518
+ <body>
2519
+ {% block content %}{% endblock %}
2520
+ </body>
2521
+ </html>`,
2522
+ explanation: 'Django base template with comprehensive SEO meta tags.',
2523
+ },
2524
+ {
2525
+ file: 'robots.txt',
2526
+ code: `User-agent: *
2527
+ Allow: /
2528
+ Disallow: /admin/
2529
+ Disallow: /api/
2530
+
2531
+ User-agent: GPTBot
2532
+ Allow: /
2533
+
2534
+ Sitemap: ${siteUrl}/sitemap.xml`,
2535
+ explanation: 'Robots.txt with AI crawler support.',
2536
+ },
2537
+ ],
2538
+ };
2539
+ }
2540
+
2541
+ // ============================================================================
2542
+ // LARAVEL - Comprehensive SEO with Blade components
2543
+ // ============================================================================
2544
+
2545
+ export function generateLaravelSEOHelper(options: MetaFixOptions): GeneratedCode {
2546
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
2547
+
2548
+ return {
2549
+ file: 'app/Services/SEOService.php',
2550
+ code: `<?php
2551
+
2552
+ namespace App\\Services;
2553
+
2554
+ use Illuminate\\Support\\Facades\\View;
2555
+ use Illuminate\\Support\\HtmlString;
2556
+
2557
+ /**
2558
+ * Comprehensive SEO Service for Laravel
2559
+ *
2560
+ * Features:
2561
+ * - Full Open Graph support
2562
+ * - Twitter Cards
2563
+ * - JSON-LD structured data
2564
+ * - Canonical URLs
2565
+ * - Blade integration
2566
+ */
2567
+ class SEOService
2568
+ {
2569
+ public const SITE_NAME = '${siteName}';
2570
+ public const SITE_URL = '${siteUrl}';
2571
+ public const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
2572
+ public const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
2573
+ public const TWITTER_HANDLE = '${twitterHandle || ''}';
2574
+
2575
+ protected array $meta = [];
2576
+ protected array $schemas = [];
2577
+
2578
+ /**
2579
+ * Set page SEO metadata
2580
+ */
2581
+ public function setMeta(array $options): self
2582
+ {
2583
+ $this->meta = array_merge([
2584
+ 'title' => null,
2585
+ 'description' => self::DEFAULT_DESCRIPTION,
2586
+ 'image' => self::DEFAULT_IMAGE,
2587
+ 'url' => request()->url(),
2588
+ 'type' => 'website',
2589
+ 'published_time' => null,
2590
+ 'modified_time' => null,
2591
+ 'author' => null,
2592
+ 'tags' => [],
2593
+ 'noindex' => false,
2594
+ 'nofollow' => false,
2595
+ ], $options);
2596
+
2597
+ return $this;
2598
+ }
2599
+
2600
+ /**
2601
+ * Add JSON-LD schema
2602
+ */
2603
+ public function addSchema(array $schema): self
2604
+ {
2605
+ $this->schemas[] = $schema;
2606
+ return $this;
2607
+ }
2608
+
2609
+ /**
2610
+ * Get full page title
2611
+ */
2612
+ public function getFullTitle(): string
2613
+ {
2614
+ $title = $this->meta['title'] ?? null;
2615
+
2616
+ if (!$title) {
2617
+ return self::SITE_NAME;
2618
+ }
2619
+
2620
+ if (str_contains($title, self::SITE_NAME)) {
2621
+ return $title;
2622
+ }
2623
+
2624
+ return "{$title} | " . self::SITE_NAME;
2625
+ }
2626
+
2627
+ /**
2628
+ * Get absolute image URL
2629
+ */
2630
+ public function getImageUrl(): string
2631
+ {
2632
+ $image = $this->meta['image'] ?? self::DEFAULT_IMAGE;
2633
+
2634
+ if (str_starts_with($image, 'http')) {
2635
+ return $image;
2636
+ }
2637
+
2638
+ return self::SITE_URL . $image;
2639
+ }
2640
+
2641
+ /**
2642
+ * Get robots directive
2643
+ */
2644
+ public function getRobots(): string
2645
+ {
2646
+ $index = ($this->meta['noindex'] ?? false) ? 'noindex' : 'index';
2647
+ $follow = ($this->meta['nofollow'] ?? false) ? 'nofollow' : 'follow';
2648
+ return "{$index}, {$follow}";
2649
+ }
2650
+
2651
+ /**
2652
+ * Render all meta tags
2653
+ */
2654
+ public function render(): HtmlString
2655
+ {
2656
+ $html = view('components.seo-meta', [
2657
+ 'seo' => $this,
2658
+ 'meta' => $this->meta,
2659
+ ])->render();
2660
+
2661
+ return new HtmlString($html);
2662
+ }
2663
+
2664
+ /**
2665
+ * Render JSON-LD scripts
2666
+ */
2667
+ public function renderJsonLd(): HtmlString
2668
+ {
2669
+ $allSchemas = array_merge(
2670
+ [$this->websiteSchema()],
2671
+ $this->schemas
2672
+ );
2673
+
2674
+ $scripts = collect($allSchemas)
2675
+ ->filter()
2676
+ ->map(fn($schema) => '<script type="application/ld+json">' . json_encode($schema) . '</script>')
2677
+ ->implode("\\n");
2678
+
2679
+ return new HtmlString($scripts);
2680
+ }
2681
+
2682
+ // Schema Generators
2683
+
2684
+ public function websiteSchema(): array
2685
+ {
2686
+ return [
2687
+ '@context' => 'https://schema.org',
2688
+ '@type' => 'WebSite',
2689
+ 'name' => self::SITE_NAME,
2690
+ 'url' => self::SITE_URL,
2691
+ ];
2692
+ }
2693
+
2694
+ public function organizationSchema(
2695
+ ?string $name = null,
2696
+ ?string $url = null,
2697
+ ?string $logo = null,
2698
+ array $socialProfiles = []
2699
+ ): array {
2700
+ return array_filter([
2701
+ '@context' => 'https://schema.org',
2702
+ '@type' => 'Organization',
2703
+ 'name' => $name ?? self::SITE_NAME,
2704
+ 'url' => $url ?? self::SITE_URL,
2705
+ 'logo' => $logo,
2706
+ 'sameAs' => $socialProfiles ?: null,
2707
+ ]);
2708
+ }
2709
+
2710
+ public function articleSchema($article): array
2711
+ {
2712
+ return [
2713
+ '@context' => 'https://schema.org',
2714
+ '@type' => 'Article',
2715
+ 'headline' => $article->title,
2716
+ 'description' => $article->excerpt ?? substr(strip_tags($article->content), 0, 160),
2717
+ 'image' => $article->image_url ?? self::DEFAULT_IMAGE,
2718
+ 'datePublished' => optional($article->published_at)->toIso8601String(),
2719
+ 'dateModified' => optional($article->updated_at)->toIso8601String(),
2720
+ 'author' => [
2721
+ '@type' => 'Person',
2722
+ 'name' => $article->author?->name ?? 'Unknown',
2723
+ ],
2724
+ 'publisher' => [
2725
+ '@type' => 'Organization',
2726
+ 'name' => self::SITE_NAME,
2727
+ 'logo' => [
2728
+ '@type' => 'ImageObject',
2729
+ 'url' => self::SITE_URL . '/logo.png',
2730
+ ],
2731
+ ],
2732
+ ];
2733
+ }
2734
+
2735
+ public function productSchema($product): array
2736
+ {
2737
+ $schema = [
2738
+ '@context' => 'https://schema.org',
2739
+ '@type' => 'Product',
2740
+ 'name' => $product->name,
2741
+ 'description' => $product->description,
2742
+ 'image' => $product->image_url ?? self::DEFAULT_IMAGE,
2743
+ 'offers' => [
2744
+ '@type' => 'Offer',
2745
+ 'price' => (string) $product->price,
2746
+ 'priceCurrency' => $product->currency ?? 'USD',
2747
+ 'availability' => 'https://schema.org/' . ($product->in_stock ? 'InStock' : 'OutOfStock'),
2748
+ ],
2749
+ ];
2750
+
2751
+ if ($product->brand) {
2752
+ $schema['brand'] = ['@type' => 'Brand', 'name' => $product->brand];
2753
+ }
2754
+
2755
+ if ($product->sku) {
2756
+ $schema['sku'] = $product->sku;
2757
+ }
2758
+
2759
+ if ($product->reviews_count > 0) {
2760
+ $schema['aggregateRating'] = [
2761
+ '@type' => 'AggregateRating',
2762
+ 'ratingValue' => $product->average_rating,
2763
+ 'reviewCount' => $product->reviews_count,
2764
+ ];
2765
+ }
2766
+
2767
+ return $schema;
2768
+ }
2769
+
2770
+ public function faqSchema(array $items): array
2771
+ {
2772
+ return [
2773
+ '@context' => 'https://schema.org',
2774
+ '@type' => 'FAQPage',
2775
+ 'mainEntity' => collect($items)->map(fn($item) => [
2776
+ '@type' => 'Question',
2777
+ 'name' => $item['question'],
2778
+ 'acceptedAnswer' => ['@type' => 'Answer', 'text' => $item['answer']],
2779
+ ])->all(),
2780
+ ];
2781
+ }
2782
+
2783
+ public function breadcrumbSchema(array $items): array
2784
+ {
2785
+ return [
2786
+ '@context' => 'https://schema.org',
2787
+ '@type' => 'BreadcrumbList',
2788
+ 'itemListElement' => collect($items)->map(fn($item, $index) => [
2789
+ '@type' => 'ListItem',
2790
+ 'position' => $index + 1,
2791
+ 'name' => $item['name'],
2792
+ 'item' => $item['url'],
2793
+ ])->all(),
2794
+ ];
2795
+ }
2796
+
2797
+ public function localBusinessSchema(array $business): array
2798
+ {
2799
+ return array_filter([
2800
+ '@context' => 'https://schema.org',
2801
+ '@type' => 'LocalBusiness',
2802
+ 'name' => $business['name'],
2803
+ 'description' => $business['description'] ?? null,
2804
+ 'url' => $business['url'] ?? self::SITE_URL,
2805
+ 'telephone' => $business['phone'] ?? null,
2806
+ 'address' => [
2807
+ '@type' => 'PostalAddress',
2808
+ 'streetAddress' => $business['address']['street'] ?? null,
2809
+ 'addressLocality' => $business['address']['city'] ?? null,
2810
+ 'addressRegion' => $business['address']['state'] ?? null,
2811
+ 'postalCode' => $business['address']['zip'] ?? null,
2812
+ 'addressCountry' => $business['address']['country'] ?? null,
2813
+ ],
2814
+ 'geo' => isset($business['coordinates']) ? [
2815
+ '@type' => 'GeoCoordinates',
2816
+ 'latitude' => $business['coordinates']['lat'],
2817
+ 'longitude' => $business['coordinates']['lng'],
2818
+ ] : null,
2819
+ 'openingHours' => $business['hours'] ?? null,
2820
+ 'priceRange' => $business['price_range'] ?? null,
2821
+ ]);
2822
+ }
2823
+ }`,
2824
+ explanation: `Laravel comprehensive SEO service with:
2825
+ • Fluent API for setting metadata
2826
+ • Full Open Graph with article support
2827
+ • Twitter Cards
2828
+ • JSON-LD schema generators
2829
+ • Blade component integration
2830
+
2831
+ Setup:
2832
+ 1. Register SEOService in AppServiceProvider
2833
+ 2. Use @seo directive in Blade templates
2834
+ 3. Call SEO()->setMeta() in controllers`,
2835
+ additionalFiles: [
2836
+ {
2837
+ file: 'resources/views/components/seo-meta.blade.php',
2838
+ code: `{{-- Primary Meta Tags --}}
2839
+ <title>{{ $seo->getFullTitle() }}</title>
2840
+ <meta name="title" content="{{ $seo->getFullTitle() }}">
2841
+ <meta name="description" content="{{ $meta['description'] }}">
2842
+ <meta name="robots" content="{{ $seo->getRobots() }}">
2843
+ <link rel="canonical" href="{{ $meta['url'] }}">
2844
+
2845
+ {{-- Open Graph / Facebook --}}
2846
+ <meta property="og:type" content="{{ $meta['type'] }}">
2847
+ <meta property="og:url" content="{{ $meta['url'] }}">
2848
+ <meta property="og:title" content="{{ $seo->getFullTitle() }}">
2849
+ <meta property="og:description" content="{{ $meta['description'] }}">
2850
+ <meta property="og:image" content="{{ $seo->getImageUrl() }}">
2851
+ <meta property="og:image:width" content="1200">
2852
+ <meta property="og:image:height" content="630">
2853
+ <meta property="og:site_name" content="{{ $seo::SITE_NAME }}">
2854
+
2855
+ @if($meta['type'] === 'article')
2856
+ @if($meta['published_time'])
2857
+ <meta property="article:published_time" content="{{ $meta['published_time'] }}">
2858
+ @endif
2859
+ @if($meta['modified_time'])
2860
+ <meta property="article:modified_time" content="{{ $meta['modified_time'] }}">
2861
+ @endif
2862
+ @if($meta['author'])
2863
+ <meta property="article:author" content="{{ $meta['author'] }}">
2864
+ @endif
2865
+ @foreach($meta['tags'] as $tag)
2866
+ <meta property="article:tag" content="{{ $tag }}">
2867
+ @endforeach
2868
+ @endif
2869
+
2870
+ {{-- Twitter --}}
2871
+ <meta name="twitter:card" content="summary_large_image">
2872
+ <meta name="twitter:title" content="{{ $seo->getFullTitle() }}">
2873
+ <meta name="twitter:description" content="{{ $meta['description'] }}">
2874
+ <meta name="twitter:image" content="{{ $seo->getImageUrl() }}">
2875
+ @if($seo::TWITTER_HANDLE)
2876
+ <meta name="twitter:site" content="{{ $seo::TWITTER_HANDLE }}">
2877
+ <meta name="twitter:creator" content="{{ $seo::TWITTER_HANDLE }}">
2878
+ @endif`,
2879
+ explanation: 'Blade component for rendering SEO meta tags.',
2880
+ },
2881
+ {
2882
+ file: 'resources/views/layouts/app.blade.php',
2883
+ code: `<!DOCTYPE html>
2884
+ <html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
2885
+ <head>
2886
+ <meta charset="utf-8">
2887
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2888
+ <meta name="csrf-token" content="{{ csrf_token() }}">
2889
+
2890
+ {{-- SEO Meta Tags --}}
2891
+ {!! SEO()->render() !!}
2892
+
2893
+ {{-- Favicon --}}
2894
+ <link rel="icon" href="/favicon.ico">
2895
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
2896
+
2897
+ {{-- Styles --}}
2898
+ @vite(['resources/css/app.css', 'resources/js/app.js'])
2899
+
2900
+ {{-- JSON-LD --}}
2901
+ {!! SEO()->renderJsonLd() !!}
2902
+ </head>
2903
+ <body>
2904
+ @yield('content')
2905
+ </body>
2906
+ </html>`,
2907
+ explanation: 'Laravel layout with SEO integration.',
2908
+ },
2909
+ {
2910
+ file: 'app/Providers/AppServiceProvider.php',
2911
+ code: `<?php
2912
+
2913
+ namespace App\\Providers;
2914
+
2915
+ use App\\Services\\SEOService;
2916
+ use Illuminate\\Support\\ServiceProvider;
2917
+
2918
+ class AppServiceProvider extends ServiceProvider
2919
+ {
2920
+ public function register(): void
2921
+ {
2922
+ $this->app->singleton(SEOService::class);
2923
+ }
2924
+
2925
+ public function boot(): void
2926
+ {
2927
+ // Global SEO helper
2928
+ if (!function_exists('SEO')) {
2929
+ function SEO(): SEOService {
2930
+ return app(SEOService::class);
2931
+ }
2932
+ }
2933
+ }
2934
+ }`,
2935
+ explanation: 'Service provider registration.',
2936
+ },
2937
+ {
2938
+ file: 'public/robots.txt',
2939
+ code: `User-agent: *
2940
+ Allow: /
2941
+ Disallow: /admin/
2942
+ Disallow: /api/
2943
+
2944
+ User-agent: GPTBot
2945
+ Allow: /
2946
+
2947
+ Sitemap: ${siteUrl}/sitemap.xml`,
2948
+ explanation: 'Robots.txt with AI crawler support.',
2949
+ },
2950
+ ],
2951
+ };
2952
+ }
2953
+
2954
+ // ============================================================================
2955
+ // SPRING BOOT (Java) - Thymeleaf SEO
2956
+ // ============================================================================
2957
+
2958
+ export function generateSpringBootSEO(options: MetaFixOptions): GeneratedCode {
2959
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
2960
+
2961
+ return {
2962
+ file: 'src/main/java/com/example/seo/SEOService.java',
2963
+ code: `package com.example.seo;
2964
+
2965
+ import org.springframework.stereotype.Service;
2966
+ import java.util.*;
2967
+
2968
+ /**
2969
+ * Comprehensive SEO Service for Spring Boot
2970
+ *
2971
+ * Features:
2972
+ * - Full Open Graph support
2973
+ * - Twitter Cards
2974
+ * - JSON-LD structured data
2975
+ * - Thymeleaf integration
2976
+ */
2977
+ @Service
2978
+ public class SEOService {
2979
+
2980
+ public static final String SITE_NAME = "${siteName}";
2981
+ public static final String SITE_URL = "${siteUrl}";
2982
+ public static final String DEFAULT_IMAGE = "${image || `${siteUrl}/og-image.png`}";
2983
+ public static final String DEFAULT_DESCRIPTION = "${description || `${siteName} - A compelling description.`}";
2984
+ public static final String TWITTER_HANDLE = "${twitterHandle || ''}";
2985
+
2986
+ /**
2987
+ * Build SEO metadata for a page
2988
+ */
2989
+ public SEOMeta buildMeta(String title, String description, String image, String type) {
2990
+ return SEOMeta.builder()
2991
+ .title(title)
2992
+ .description(description != null ? description : DEFAULT_DESCRIPTION)
2993
+ .image(image != null ? image : DEFAULT_IMAGE)
2994
+ .type(type != null ? type : "website")
2995
+ .build();
2996
+ }
2997
+
2998
+ /**
2999
+ * Get full page title with site name
3000
+ */
3001
+ public String getFullTitle(String title) {
3002
+ if (title == null || title.isEmpty()) {
3003
+ return SITE_NAME;
3004
+ }
3005
+ if (title.contains(SITE_NAME)) {
3006
+ return title;
3007
+ }
3008
+ return title + " | " + SITE_NAME;
3009
+ }
3010
+
3011
+ /**
3012
+ * Get absolute image URL
3013
+ */
3014
+ public String getAbsoluteImageUrl(String image) {
3015
+ if (image == null) {
3016
+ return DEFAULT_IMAGE;
3017
+ }
3018
+ if (image.startsWith("http")) {
3019
+ return image;
3020
+ }
3021
+ return SITE_URL + image;
3022
+ }
3023
+
3024
+ // JSON-LD Schema Generators
3025
+
3026
+ public Map<String, Object> websiteSchema() {
3027
+ Map<String, Object> schema = new LinkedHashMap<>();
3028
+ schema.put("@context", "https://schema.org");
3029
+ schema.put("@type", "WebSite");
3030
+ schema.put("name", SITE_NAME);
3031
+ schema.put("url", SITE_URL);
3032
+ return schema;
3033
+ }
3034
+
3035
+ public Map<String, Object> organizationSchema(String name, String url, String logo, List<String> socialProfiles) {
3036
+ Map<String, Object> schema = new LinkedHashMap<>();
3037
+ schema.put("@context", "https://schema.org");
3038
+ schema.put("@type", "Organization");
3039
+ schema.put("name", name != null ? name : SITE_NAME);
3040
+ schema.put("url", url != null ? url : SITE_URL);
3041
+ if (logo != null) schema.put("logo", logo);
3042
+ if (socialProfiles != null && !socialProfiles.isEmpty()) {
3043
+ schema.put("sameAs", socialProfiles);
3044
+ }
3045
+ return schema;
3046
+ }
3047
+
3048
+ public Map<String, Object> articleSchema(
3049
+ String headline,
3050
+ String description,
3051
+ String image,
3052
+ String datePublished,
3053
+ String dateModified,
3054
+ String authorName
3055
+ ) {
3056
+ Map<String, Object> schema = new LinkedHashMap<>();
3057
+ schema.put("@context", "https://schema.org");
3058
+ schema.put("@type", "Article");
3059
+ schema.put("headline", headline);
3060
+ schema.put("description", description);
3061
+ schema.put("image", image != null ? image : DEFAULT_IMAGE);
3062
+ schema.put("datePublished", datePublished);
3063
+ schema.put("dateModified", dateModified != null ? dateModified : datePublished);
3064
+
3065
+ Map<String, Object> author = new LinkedHashMap<>();
3066
+ author.put("@type", "Person");
3067
+ author.put("name", authorName);
3068
+ schema.put("author", author);
3069
+
3070
+ Map<String, Object> publisher = new LinkedHashMap<>();
3071
+ publisher.put("@type", "Organization");
3072
+ publisher.put("name", SITE_NAME);
3073
+ Map<String, Object> logo = new LinkedHashMap<>();
3074
+ logo.put("@type", "ImageObject");
3075
+ logo.put("url", SITE_URL + "/logo.png");
3076
+ publisher.put("logo", logo);
3077
+ schema.put("publisher", publisher);
3078
+
3079
+ return schema;
3080
+ }
3081
+
3082
+ public Map<String, Object> productSchema(
3083
+ String name,
3084
+ String description,
3085
+ String image,
3086
+ String price,
3087
+ String currency,
3088
+ boolean inStock
3089
+ ) {
3090
+ Map<String, Object> schema = new LinkedHashMap<>();
3091
+ schema.put("@context", "https://schema.org");
3092
+ schema.put("@type", "Product");
3093
+ schema.put("name", name);
3094
+ schema.put("description", description);
3095
+ schema.put("image", image);
3096
+
3097
+ Map<String, Object> offers = new LinkedHashMap<>();
3098
+ offers.put("@type", "Offer");
3099
+ offers.put("price", price);
3100
+ offers.put("priceCurrency", currency != null ? currency : "USD");
3101
+ offers.put("availability", "https://schema.org/" + (inStock ? "InStock" : "OutOfStock"));
3102
+ schema.put("offers", offers);
3103
+
3104
+ return schema;
3105
+ }
3106
+
3107
+ public Map<String, Object> faqSchema(List<Map<String, String>> items) {
3108
+ Map<String, Object> schema = new LinkedHashMap<>();
3109
+ schema.put("@context", "https://schema.org");
3110
+ schema.put("@type", "FAQPage");
3111
+
3112
+ List<Map<String, Object>> mainEntity = new ArrayList<>();
3113
+ for (Map<String, String> item : items) {
3114
+ Map<String, Object> question = new LinkedHashMap<>();
3115
+ question.put("@type", "Question");
3116
+ question.put("name", item.get("question"));
3117
+
3118
+ Map<String, Object> answer = new LinkedHashMap<>();
3119
+ answer.put("@type", "Answer");
3120
+ answer.put("text", item.get("answer"));
3121
+ question.put("acceptedAnswer", answer);
3122
+
3123
+ mainEntity.add(question);
3124
+ }
3125
+ schema.put("mainEntity", mainEntity);
3126
+
3127
+ return schema;
3128
+ }
3129
+
3130
+ public Map<String, Object> breadcrumbSchema(List<Map<String, String>> items) {
3131
+ Map<String, Object> schema = new LinkedHashMap<>();
3132
+ schema.put("@context", "https://schema.org");
3133
+ schema.put("@type", "BreadcrumbList");
3134
+
3135
+ List<Map<String, Object>> itemList = new ArrayList<>();
3136
+ for (int i = 0; i < items.size(); i++) {
3137
+ Map<String, String> item = items.get(i);
3138
+ Map<String, Object> listItem = new LinkedHashMap<>();
3139
+ listItem.put("@type", "ListItem");
3140
+ listItem.put("position", i + 1);
3141
+ listItem.put("name", item.get("name"));
3142
+ listItem.put("item", item.get("url"));
3143
+ itemList.add(listItem);
3144
+ }
3145
+ schema.put("itemListElement", itemList);
3146
+
3147
+ return schema;
3148
+ }
3149
+ }`,
3150
+ explanation: `Spring Boot comprehensive SEO service with:
3151
+ • Full Open Graph support
3152
+ • Twitter Cards
3153
+ • JSON-LD schema generators
3154
+ • Thymeleaf integration
3155
+
3156
+ Setup:
3157
+ 1. Add SEOService to your controllers
3158
+ 2. Pass SEOMeta to model
3159
+ 3. Use Thymeleaf fragments in templates`,
3160
+ additionalFiles: [
3161
+ {
3162
+ file: 'src/main/java/com/example/seo/SEOMeta.java',
3163
+ code: `package com.example.seo;
3164
+
3165
+ import lombok.Builder;
3166
+ import lombok.Data;
3167
+ import java.util.List;
3168
+
3169
+ @Data
3170
+ @Builder
3171
+ public class SEOMeta {
3172
+ private String title;
3173
+ private String description;
3174
+ private String image;
3175
+ private String url;
3176
+ private String type;
3177
+
3178
+ // Article specific
3179
+ private String publishedTime;
3180
+ private String modifiedTime;
3181
+ private String author;
3182
+ private List<String> tags;
3183
+
3184
+ // Robots
3185
+ @Builder.Default
3186
+ private boolean noindex = false;
3187
+ @Builder.Default
3188
+ private boolean nofollow = false;
3189
+
3190
+ public String getFullTitle() {
3191
+ if (title == null || title.isEmpty()) {
3192
+ return SEOService.SITE_NAME;
3193
+ }
3194
+ if (title.contains(SEOService.SITE_NAME)) {
3195
+ return title;
3196
+ }
3197
+ return title + " | " + SEOService.SITE_NAME;
3198
+ }
3199
+
3200
+ public String getImageUrl() {
3201
+ if (image == null) {
3202
+ return SEOService.DEFAULT_IMAGE;
3203
+ }
3204
+ if (image.startsWith("http")) {
3205
+ return image;
3206
+ }
3207
+ return SEOService.SITE_URL + image;
3208
+ }
3209
+
3210
+ public String getRobots() {
3211
+ String index = noindex ? "noindex" : "index";
3212
+ String follow = nofollow ? "nofollow" : "follow";
3213
+ return index + ", " + follow;
3214
+ }
3215
+ }`,
3216
+ explanation: 'SEO metadata DTO with Lombok.',
3217
+ },
3218
+ {
3219
+ file: 'src/main/resources/templates/fragments/seo.html',
3220
+ code: `<!DOCTYPE html>
3221
+ <html xmlns:th="http://www.thymeleaf.org">
3222
+ <head th:fragment="meta(seo)">
3223
+ <!-- Primary Meta Tags -->
3224
+ <title th:text="\${seo.getFullTitle()}">Page Title</title>
3225
+ <meta name="title" th:content="\${seo.getFullTitle()}">
3226
+ <meta name="description" th:content="\${seo.description}">
3227
+ <meta name="robots" th:content="\${seo.getRobots()}">
3228
+ <link rel="canonical" th:href="\${seo.url}">
3229
+
3230
+ <!-- Open Graph / Facebook -->
3231
+ <meta property="og:type" th:content="\${seo.type}">
3232
+ <meta property="og:url" th:content="\${seo.url}">
3233
+ <meta property="og:title" th:content="\${seo.getFullTitle()}">
3234
+ <meta property="og:description" th:content="\${seo.description}">
3235
+ <meta property="og:image" th:content="\${seo.getImageUrl()}">
3236
+ <meta property="og:image:width" content="1200">
3237
+ <meta property="og:image:height" content="630">
3238
+ <meta property="og:site_name" th:content="@{${siteName}}">
3239
+
3240
+ <th:block th:if="\${seo.type == 'article'}">
3241
+ <meta th:if="\${seo.publishedTime}" property="article:published_time" th:content="\${seo.publishedTime}">
3242
+ <meta th:if="\${seo.modifiedTime}" property="article:modified_time" th:content="\${seo.modifiedTime}">
3243
+ <meta th:if="\${seo.author}" property="article:author" th:content="\${seo.author}">
3244
+ <meta th:each="tag : \${seo.tags}" property="article:tag" th:content="\${tag}">
3245
+ </th:block>
3246
+
3247
+ <!-- Twitter -->
3248
+ <meta name="twitter:card" content="summary_large_image">
3249
+ <meta name="twitter:title" th:content="\${seo.getFullTitle()}">
3250
+ <meta name="twitter:description" th:content="\${seo.description}">
3251
+ <meta name="twitter:image" th:content="\${seo.getImageUrl()}">
3252
+ <meta th:if="${twitterHandle}" name="twitter:site" content="${twitterHandle}">
3253
+ </head>
3254
+ </html>`,
3255
+ explanation: 'Thymeleaf fragment for SEO meta tags.',
3256
+ },
3257
+ {
3258
+ file: 'src/main/resources/templates/layout/base.html',
3259
+ code: `<!DOCTYPE html>
3260
+ <html xmlns:th="http://www.thymeleaf.org" th:lang="\${#locale.language}">
3261
+ <head>
3262
+ <meta charset="UTF-8">
3263
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
3264
+
3265
+ <!-- SEO Meta Tags -->
3266
+ <th:block th:replace="~{fragments/seo :: meta(seo=\${seo})}"></th:block>
3267
+
3268
+ <!-- Favicon -->
3269
+ <link rel="icon" type="image/x-icon" href="/favicon.ico">
3270
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
3271
+
3272
+ <!-- Styles -->
3273
+ <link rel="stylesheet" th:href="@{/css/styles.css}">
3274
+
3275
+ <!-- JSON-LD -->
3276
+ <script th:if="\${jsonLd}" type="application/ld+json" th:utext="\${jsonLd}"></script>
3277
+ </head>
3278
+ <body>
3279
+ <div th:replace="~{:: content}"></div>
3280
+ </body>
3281
+ </html>`,
3282
+ explanation: 'Base layout with SEO integration.',
3283
+ },
3284
+ {
3285
+ file: 'src/main/resources/static/robots.txt',
3286
+ code: `User-agent: *
3287
+ Allow: /
3288
+ Disallow: /admin/
3289
+ Disallow: /api/
3290
+
3291
+ User-agent: GPTBot
3292
+ Allow: /
3293
+
3294
+ Sitemap: ${siteUrl}/sitemap.xml`,
3295
+ explanation: 'Robots.txt with AI crawler support.',
3296
+ },
3297
+ ],
3298
+ };
3299
+ }
3300
+
3301
+ // ============================================================================
3302
+ // PHOENIX (Elixir) - HEEx SEO
3303
+ // ============================================================================
3304
+
3305
+ export function generatePhoenixSEO(options: MetaFixOptions): GeneratedCode {
3306
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
3307
+
3308
+ return {
3309
+ file: 'lib/my_app_web/components/seo.ex',
3310
+ code: `defmodule MyAppWeb.Components.SEO do
3311
+ @moduledoc """
3312
+ Comprehensive SEO components for Phoenix
3313
+
3314
+ Features:
3315
+ - Full Open Graph support
3316
+ - Twitter Cards
3317
+ - JSON-LD structured data
3318
+ - HEEx integration
3319
+ """
3320
+
3321
+ use Phoenix.Component
3322
+
3323
+ @site_name "${siteName}"
3324
+ @site_url "${siteUrl}"
3325
+ @default_image "${image || `${siteUrl}/og-image.png`}"
3326
+ @default_description "${description || `${siteName} - A compelling description.`}"
3327
+ @twitter_handle "${twitterHandle || ''}"
3328
+
3329
+ def site_name, do: @site_name
3330
+ def site_url, do: @site_url
3331
+
3332
+ @doc """
3333
+ SEO meta tags component
3334
+
3335
+ ## Examples
3336
+
3337
+ <.seo_meta
3338
+ title="Page Title"
3339
+ description="Page description"
3340
+ type="article"
3341
+ published_time={@article.published_at}
3342
+ />
3343
+ """
3344
+ attr :title, :string, default: nil
3345
+ attr :description, :string, default: @default_description
3346
+ attr :image, :string, default: @default_image
3347
+ attr :url, :string, default: nil
3348
+ attr :type, :string, default: "website"
3349
+ attr :published_time, :any, default: nil
3350
+ attr :modified_time, :any, default: nil
3351
+ attr :author, :string, default: nil
3352
+ attr :tags, :list, default: []
3353
+ attr :noindex, :boolean, default: false
3354
+ attr :nofollow, :boolean, default: false
3355
+
3356
+ def seo_meta(assigns) do
3357
+ assigns =
3358
+ assigns
3359
+ |> assign(:full_title, get_full_title(assigns.title))
3360
+ |> assign(:image_url, get_absolute_url(assigns.image))
3361
+ |> assign(:canonical_url, assigns.url || @site_url)
3362
+ |> assign(:robots, get_robots(assigns.noindex, assigns.nofollow))
3363
+
3364
+ ~H\"\"\"
3365
+ <!-- Primary Meta Tags -->
3366
+ <title><%= @full_title %></title>
3367
+ <meta name="title" content={@full_title}>
3368
+ <meta name="description" content={@description}>
3369
+ <meta name="robots" content={@robots}>
3370
+ <link rel="canonical" href={@canonical_url}>
3371
+
3372
+ <!-- Open Graph / Facebook -->
3373
+ <meta property="og:type" content={@type}>
3374
+ <meta property="og:url" content={@canonical_url}>
3375
+ <meta property="og:title" content={@full_title}>
3376
+ <meta property="og:description" content={@description}>
3377
+ <meta property="og:image" content={@image_url}>
3378
+ <meta property="og:image:width" content="1200">
3379
+ <meta property="og:image:height" content="630">
3380
+ <meta property="og:site_name" content={@site_name}>
3381
+
3382
+ <%= if @type == "article" do %>
3383
+ <%= if @published_time do %>
3384
+ <meta property="article:published_time" content={format_datetime(@published_time)}>
3385
+ <% end %>
3386
+ <%= if @modified_time do %>
3387
+ <meta property="article:modified_time" content={format_datetime(@modified_time)}>
3388
+ <% end %>
3389
+ <%= if @author do %>
3390
+ <meta property="article:author" content={@author}>
3391
+ <% end %>
3392
+ <%= for tag <- @tags do %>
3393
+ <meta property="article:tag" content={tag}>
3394
+ <% end %>
3395
+ <% end %>
3396
+
3397
+ <!-- Twitter -->
3398
+ <meta name="twitter:card" content="summary_large_image">
3399
+ <meta name="twitter:title" content={@full_title}>
3400
+ <meta name="twitter:description" content={@description}>
3401
+ <meta name="twitter:image" content={@image_url}>
3402
+ <%= if @twitter_handle != "" do %>
3403
+ <meta name="twitter:site" content={@twitter_handle}>
3404
+ <meta name="twitter:creator" content={@twitter_handle}>
3405
+ <% end %>
3406
+ \"\"\"
3407
+ end
3408
+
3409
+ @doc """
3410
+ JSON-LD structured data component
3411
+ """
3412
+ attr :schemas, :list, required: true
3413
+
3414
+ def json_ld(assigns) do
3415
+ all_schemas = [website_schema() | assigns.schemas]
3416
+
3417
+ ~H\"\"\"
3418
+ <%= for schema <- @all_schemas do %>
3419
+ <script type="application/ld+json">
3420
+ <%= raw(Jason.encode!(schema)) %>
3421
+ </script>
3422
+ <% end %>
3423
+ \"\"\"
3424
+ end
3425
+
3426
+ # Schema generators
3427
+
3428
+ def website_schema do
3429
+ %{
3430
+ "@context" => "https://schema.org",
3431
+ "@type" => "WebSite",
3432
+ "name" => @site_name,
3433
+ "url" => @site_url
3434
+ }
3435
+ end
3436
+
3437
+ def organization_schema(opts \\\\ []) do
3438
+ %{
3439
+ "@context" => "https://schema.org",
3440
+ "@type" => "Organization",
3441
+ "name" => opts[:name] || @site_name,
3442
+ "url" => opts[:url] || @site_url
3443
+ }
3444
+ |> maybe_put("logo", opts[:logo])
3445
+ |> maybe_put("sameAs", opts[:social_profiles])
3446
+ end
3447
+
3448
+ def article_schema(article) do
3449
+ %{
3450
+ "@context" => "https://schema.org",
3451
+ "@type" => "Article",
3452
+ "headline" => article.title,
3453
+ "description" => article.excerpt || String.slice(article.content, 0, 160),
3454
+ "image" => article.image_url || @default_image,
3455
+ "datePublished" => format_datetime(article.published_at),
3456
+ "dateModified" => format_datetime(article.updated_at || article.published_at),
3457
+ "author" => %{
3458
+ "@type" => "Person",
3459
+ "name" => article.author_name || "Unknown"
3460
+ },
3461
+ "publisher" => %{
3462
+ "@type" => "Organization",
3463
+ "name" => @site_name,
3464
+ "logo" => %{"@type" => "ImageObject", "url" => "\#{@site_url}/logo.png"}
3465
+ }
3466
+ }
3467
+ end
3468
+
3469
+ def product_schema(product) do
3470
+ %{
3471
+ "@context" => "https://schema.org",
3472
+ "@type" => "Product",
3473
+ "name" => product.name,
3474
+ "description" => product.description,
3475
+ "image" => product.image_url || @default_image,
3476
+ "offers" => %{
3477
+ "@type" => "Offer",
3478
+ "price" => to_string(product.price),
3479
+ "priceCurrency" => product.currency || "USD",
3480
+ "availability" => "https://schema.org/\#{if product.in_stock, do: "InStock", else: "OutOfStock"}"
3481
+ }
3482
+ }
3483
+ |> maybe_put("brand", product.brand && %{"@type" => "Brand", "name" => product.brand})
3484
+ |> maybe_put("sku", product.sku)
3485
+ end
3486
+
3487
+ def faq_schema(items) do
3488
+ %{
3489
+ "@context" => "https://schema.org",
3490
+ "@type" => "FAQPage",
3491
+ "mainEntity" =>
3492
+ Enum.map(items, fn item ->
3493
+ %{
3494
+ "@type" => "Question",
3495
+ "name" => item.question,
3496
+ "acceptedAnswer" => %{"@type" => "Answer", "text" => item.answer}
3497
+ }
3498
+ end)
3499
+ }
3500
+ end
3501
+
3502
+ def breadcrumb_schema(items) do
3503
+ %{
3504
+ "@context" => "https://schema.org",
3505
+ "@type" => "BreadcrumbList",
3506
+ "itemListElement" =>
3507
+ items
3508
+ |> Enum.with_index(1)
3509
+ |> Enum.map(fn {item, position} ->
3510
+ %{
3511
+ "@type" => "ListItem",
3512
+ "position" => position,
3513
+ "name" => item.name,
3514
+ "item" => item.url
3515
+ }
3516
+ end)
3517
+ }
3518
+ end
3519
+
3520
+ # Helpers
3521
+
3522
+ defp get_full_title(nil), do: @site_name
3523
+ defp get_full_title(title) when is_binary(title) do
3524
+ if String.contains?(title, @site_name), do: title, else: "\#{title} | \#{@site_name}"
3525
+ end
3526
+
3527
+ defp get_absolute_url(nil), do: @default_image
3528
+ defp get_absolute_url(url) when is_binary(url) do
3529
+ if String.starts_with?(url, "http"), do: url, else: @site_url <> url
3530
+ end
3531
+
3532
+ defp get_robots(noindex, nofollow) do
3533
+ index = if noindex, do: "noindex", else: "index"
3534
+ follow = if nofollow, do: "nofollow", else: "follow"
3535
+ "\#{index}, \#{follow}"
3536
+ end
3537
+
3538
+ defp format_datetime(nil), do: nil
3539
+ defp format_datetime(%DateTime{} = dt), do: DateTime.to_iso8601(dt)
3540
+ defp format_datetime(%NaiveDateTime{} = dt), do: NaiveDateTime.to_iso8601(dt)
3541
+ defp format_datetime(dt) when is_binary(dt), do: dt
3542
+
3543
+ defp maybe_put(map, _key, nil), do: map
3544
+ defp maybe_put(map, _key, []), do: map
3545
+ defp maybe_put(map, key, value), do: Map.put(map, key, value)
3546
+ end`,
3547
+ explanation: `Phoenix/Elixir comprehensive SEO module with:
3548
+ • HEEx function components
3549
+ • Full Open Graph with article support
3550
+ • Twitter Cards
3551
+ • JSON-LD schema generators
3552
+ • Elixir-idiomatic API
3553
+
3554
+ Setup:
3555
+ 1. Import in your components module
3556
+ 2. Use <.seo_meta> in layouts
3557
+ 3. Add <.json_ld schemas={[@article_schema]} />`,
3558
+ additionalFiles: [
3559
+ {
3560
+ file: 'lib/my_app_web/components/layouts/root.html.heex',
3561
+ code: `<!DOCTYPE html>
3562
+ <html lang={@locale || "en"}>
3563
+ <head>
3564
+ <meta charset="utf-8">
3565
+ <meta name="viewport" content="width=device-width, initial-scale=1">
3566
+ <meta name="csrf-token" content={get_csrf_token()}>
3567
+
3568
+ <.seo_meta
3569
+ title={assigns[:page_title]}
3570
+ description={assigns[:page_description]}
3571
+ image={assigns[:page_image]}
3572
+ type={assigns[:page_type] || "website"}
3573
+ />
3574
+
3575
+ <link rel="icon" href="/favicon.ico">
3576
+ <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"}>
3577
+ <script defer phx-track-static src={~p"/assets/app.js"}></script>
3578
+
3579
+ <%= if assigns[:json_ld_schemas] do %>
3580
+ <.json_ld schemas={@json_ld_schemas} />
3581
+ <% end %>
3582
+ </head>
3583
+ <body>
3584
+ <%= @inner_content %>
3585
+ </body>
3586
+ </html>`,
3587
+ explanation: 'Phoenix root layout with SEO components.',
3588
+ },
3589
+ {
3590
+ file: 'priv/static/robots.txt',
3591
+ code: `User-agent: *
3592
+ Allow: /
3593
+ Disallow: /admin/
3594
+ Disallow: /api/
3595
+
3596
+ User-agent: GPTBot
3597
+ Allow: /
3598
+
3599
+ Sitemap: ${siteUrl}/sitemap.xml`,
3600
+ explanation: 'Robots.txt with AI crawler support.',
3601
+ },
3602
+ ],
3603
+ };
3604
+ }
3605
+
3606
+ // ============================================================================
3607
+ // GO (Gin/Echo/Fiber) - Template SEO
3608
+ // ============================================================================
3609
+
3610
+ export function generateGoSEO(options: MetaFixOptions): GeneratedCode {
3611
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
3612
+
3613
+ return {
3614
+ file: 'internal/seo/seo.go',
3615
+ code: `package seo
3616
+
3617
+ import (
3618
+ "encoding/json"
3619
+ "html/template"
3620
+ "strings"
3621
+ "time"
3622
+ )
3623
+
3624
+ // Configuration
3625
+ const (
3626
+ SiteName = "${siteName}"
3627
+ SiteURL = "${siteUrl}"
3628
+ DefaultImage = "${image || `${siteUrl}/og-image.png`}"
3629
+ DefaultDescription = "${description || `${siteName} - A compelling description.`}"
3630
+ TwitterHandle = "${twitterHandle || ''}"
3631
+ )
3632
+
3633
+ // Meta contains SEO metadata for a page
3634
+ type Meta struct {
3635
+ Title string
3636
+ Description string
3637
+ Image string
3638
+ URL string
3639
+ Type string // website, article, product
3640
+ PublishedTime *time.Time
3641
+ ModifiedTime *time.Time
3642
+ Author string
3643
+ Tags []string
3644
+ NoIndex bool
3645
+ NoFollow bool
3646
+ }
3647
+
3648
+ // NewMeta creates a Meta with defaults
3649
+ func NewMeta() *Meta {
3650
+ return &Meta{
3651
+ Description: DefaultDescription,
3652
+ Image: DefaultImage,
3653
+ Type: "website",
3654
+ }
3655
+ }
3656
+
3657
+ // FullTitle returns the title with site name
3658
+ func (m *Meta) FullTitle() string {
3659
+ if m.Title == "" {
3660
+ return SiteName
3661
+ }
3662
+ if strings.Contains(m.Title, SiteName) {
3663
+ return m.Title
3664
+ }
3665
+ return m.Title + " | " + SiteName
3666
+ }
3667
+
3668
+ // AbsoluteImageURL returns the absolute image URL
3669
+ func (m *Meta) AbsoluteImageURL() string {
3670
+ if m.Image == "" {
3671
+ return DefaultImage
3672
+ }
3673
+ if strings.HasPrefix(m.Image, "http") {
3674
+ return m.Image
3675
+ }
3676
+ return SiteURL + m.Image
3677
+ }
3678
+
3679
+ // Robots returns the robots meta content
3680
+ func (m *Meta) Robots() string {
3681
+ index := "index"
3682
+ if m.NoIndex {
3683
+ index = "noindex"
3684
+ }
3685
+ follow := "follow"
3686
+ if m.NoFollow {
3687
+ follow = "nofollow"
3688
+ }
3689
+ return index + ", " + follow
3690
+ }
3691
+
3692
+ // Schema types for JSON-LD
3693
+
3694
+ type Schema map[string]interface{}
3695
+
3696
+ // WebsiteSchema returns the default website schema
3697
+ func WebsiteSchema() Schema {
3698
+ return Schema{
3699
+ "@context": "https://schema.org",
3700
+ "@type": "WebSite",
3701
+ "name": SiteName,
3702
+ "url": SiteURL,
3703
+ }
3704
+ }
3705
+
3706
+ // OrganizationSchema creates an organization schema
3707
+ func OrganizationSchema(name, url, logo string, socialProfiles []string) Schema {
3708
+ schema := Schema{
3709
+ "@context": "https://schema.org",
3710
+ "@type": "Organization",
3711
+ "name": name,
3712
+ "url": url,
3713
+ }
3714
+ if logo != "" {
3715
+ schema["logo"] = logo
3716
+ }
3717
+ if len(socialProfiles) > 0 {
3718
+ schema["sameAs"] = socialProfiles
3719
+ }
3720
+ return schema
3721
+ }
3722
+
3723
+ // ArticleSchema creates an article schema
3724
+ func ArticleSchema(headline, description, image string, datePublished, dateModified time.Time, authorName string) Schema {
3725
+ return Schema{
3726
+ "@context": "https://schema.org",
3727
+ "@type": "Article",
3728
+ "headline": headline,
3729
+ "description": description,
3730
+ "image": image,
3731
+ "datePublished": datePublished.Format(time.RFC3339),
3732
+ "dateModified": dateModified.Format(time.RFC3339),
3733
+ "author": Schema{
3734
+ "@type": "Person",
3735
+ "name": authorName,
3736
+ },
3737
+ "publisher": Schema{
3738
+ "@type": "Organization",
3739
+ "name": SiteName,
3740
+ "logo": Schema{
3741
+ "@type": "ImageObject",
3742
+ "url": SiteURL + "/logo.png",
3743
+ },
3744
+ },
3745
+ }
3746
+ }
3747
+
3748
+ // ProductSchema creates a product schema
3749
+ func ProductSchema(name, description, image, price, currency string, inStock bool) Schema {
3750
+ availability := "https://schema.org/InStock"
3751
+ if !inStock {
3752
+ availability = "https://schema.org/OutOfStock"
3753
+ }
3754
+
3755
+ return Schema{
3756
+ "@context": "https://schema.org",
3757
+ "@type": "Product",
3758
+ "name": name,
3759
+ "description": description,
3760
+ "image": image,
3761
+ "offers": Schema{
3762
+ "@type": "Offer",
3763
+ "price": price,
3764
+ "priceCurrency": currency,
3765
+ "availability": availability,
3766
+ },
3767
+ }
3768
+ }
3769
+
3770
+ // FAQItem represents a single FAQ entry
3771
+ type FAQItem struct {
3772
+ Question string
3773
+ Answer string
3774
+ }
3775
+
3776
+ // FAQSchema creates a FAQ schema
3777
+ func FAQSchema(items []FAQItem) Schema {
3778
+ mainEntity := make([]Schema, len(items))
3779
+ for i, item := range items {
3780
+ mainEntity[i] = Schema{
3781
+ "@type": "Question",
3782
+ "name": item.Question,
3783
+ "acceptedAnswer": Schema{
3784
+ "@type": "Answer",
3785
+ "text": item.Answer,
3786
+ },
3787
+ }
3788
+ }
3789
+ return Schema{
3790
+ "@context": "https://schema.org",
3791
+ "@type": "FAQPage",
3792
+ "mainEntity": mainEntity,
3793
+ }
3794
+ }
3795
+
3796
+ // BreadcrumbItem represents a breadcrumb entry
3797
+ type BreadcrumbItem struct {
3798
+ Name string
3799
+ URL string
3800
+ }
3801
+
3802
+ // BreadcrumbSchema creates a breadcrumb schema
3803
+ func BreadcrumbSchema(items []BreadcrumbItem) Schema {
3804
+ itemList := make([]Schema, len(items))
3805
+ for i, item := range items {
3806
+ itemList[i] = Schema{
3807
+ "@type": "ListItem",
3808
+ "position": i + 1,
3809
+ "name": item.Name,
3810
+ "item": item.URL,
3811
+ }
3812
+ }
3813
+ return Schema{
3814
+ "@context": "https://schema.org",
3815
+ "@type": "BreadcrumbList",
3816
+ "itemListElement": itemList,
3817
+ }
3818
+ }
3819
+
3820
+ // ToJSON converts a schema to JSON string
3821
+ func (s Schema) ToJSON() template.JS {
3822
+ b, err := json.Marshal(s)
3823
+ if err != nil {
3824
+ return ""
3825
+ }
3826
+ return template.JS(b)
3827
+ }
3828
+
3829
+ // RenderJSONLD renders multiple schemas as script tags
3830
+ func RenderJSONLD(schemas ...Schema) template.HTML {
3831
+ all := append([]Schema{WebsiteSchema()}, schemas...)
3832
+ var sb strings.Builder
3833
+ for _, schema := range all {
3834
+ b, err := json.Marshal(schema)
3835
+ if err != nil {
3836
+ continue
3837
+ }
3838
+ sb.WriteString(\`<script type="application/ld+json">\`)
3839
+ sb.Write(b)
3840
+ sb.WriteString(\`</script>\\n\`)
3841
+ }
3842
+ return template.HTML(sb.String())
3843
+ }`,
3844
+ explanation: `Go comprehensive SEO package with:
3845
+ • Meta struct for page metadata
3846
+ • JSON-LD schema generators
3847
+ • Template-friendly helpers
3848
+ • Works with Gin, Echo, Fiber, or stdlib
3849
+
3850
+ Setup:
3851
+ 1. Import in your handlers
3852
+ 2. Pass Meta to templates
3853
+ 3. Use schema helpers for JSON-LD`,
3854
+ additionalFiles: [
3855
+ {
3856
+ file: 'templates/partials/seo.html',
3857
+ code: `{{define "seo"}}
3858
+ <!-- Primary Meta Tags -->
3859
+ <title>{{.SEO.FullTitle}}</title>
3860
+ <meta name="title" content="{{.SEO.FullTitle}}">
3861
+ <meta name="description" content="{{.SEO.Description}}">
3862
+ <meta name="robots" content="{{.SEO.Robots}}">
3863
+ <link rel="canonical" href="{{.SEO.URL}}">
3864
+
3865
+ <!-- Open Graph / Facebook -->
3866
+ <meta property="og:type" content="{{.SEO.Type}}">
3867
+ <meta property="og:url" content="{{.SEO.URL}}">
3868
+ <meta property="og:title" content="{{.SEO.FullTitle}}">
3869
+ <meta property="og:description" content="{{.SEO.Description}}">
3870
+ <meta property="og:image" content="{{.SEO.AbsoluteImageURL}}">
3871
+ <meta property="og:image:width" content="1200">
3872
+ <meta property="og:image:height" content="630">
3873
+ <meta property="og:site_name" content="${siteName}">
3874
+
3875
+ {{if eq .SEO.Type "article"}}
3876
+ {{if .SEO.PublishedTime}}
3877
+ <meta property="article:published_time" content="{{.SEO.PublishedTime.Format "2006-01-02T15:04:05Z07:00"}}">
3878
+ {{end}}
3879
+ {{if .SEO.ModifiedTime}}
3880
+ <meta property="article:modified_time" content="{{.SEO.ModifiedTime.Format "2006-01-02T15:04:05Z07:00"}}">
3881
+ {{end}}
3882
+ {{if .SEO.Author}}
3883
+ <meta property="article:author" content="{{.SEO.Author}}">
3884
+ {{end}}
3885
+ {{range .SEO.Tags}}
3886
+ <meta property="article:tag" content="{{.}}">
3887
+ {{end}}
3888
+ {{end}}
3889
+
3890
+ <!-- Twitter -->
3891
+ <meta name="twitter:card" content="summary_large_image">
3892
+ <meta name="twitter:title" content="{{.SEO.FullTitle}}">
3893
+ <meta name="twitter:description" content="{{.SEO.Description}}">
3894
+ <meta name="twitter:image" content="{{.SEO.AbsoluteImageURL}}">
3895
+ {{if ne "${twitterHandle}" ""}}
3896
+ <meta name="twitter:site" content="${twitterHandle}">
3897
+ <meta name="twitter:creator" content="${twitterHandle}">
3898
+ {{end}}
3899
+ {{end}}`,
3900
+ explanation: 'Go html/template partial for SEO meta tags.',
3901
+ },
3902
+ {
3903
+ file: 'templates/layouts/base.html',
3904
+ code: `<!DOCTYPE html>
3905
+ <html lang="{{.Lang}}">
3906
+ <head>
3907
+ <meta charset="utf-8">
3908
+ <meta name="viewport" content="width=device-width, initial-scale=1">
3909
+
3910
+ {{template "seo" .}}
3911
+
3912
+ <link rel="icon" href="/static/favicon.ico">
3913
+ <link rel="stylesheet" href="/static/css/styles.css">
3914
+
3915
+ {{if .JSONLDSchemas}}
3916
+ {{.JSONLDSchemas}}
3917
+ {{end}}
3918
+ </head>
3919
+ <body>
3920
+ {{template "content" .}}
3921
+ </body>
3922
+ </html>`,
3923
+ explanation: 'Go base template layout.',
3924
+ },
3925
+ {
3926
+ file: 'static/robots.txt',
3927
+ code: `User-agent: *
3928
+ Allow: /
3929
+ Disallow: /admin/
3930
+ Disallow: /api/
3931
+
3932
+ User-agent: GPTBot
3933
+ Allow: /
3934
+
3935
+ Sitemap: ${siteUrl}/sitemap.xml`,
3936
+ explanation: 'Robots.txt with AI crawler support.',
3937
+ },
3938
+ ],
3939
+ };
3940
+ }
3941
+
3942
+ // ============================================================================
3943
+ // ASP.NET CORE (C#) - Razor SEO
3944
+ // ============================================================================
3945
+
3946
+ export function generateAspNetCoreSEO(options: MetaFixOptions): GeneratedCode {
3947
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
3948
+
3949
+ return {
3950
+ file: 'Services/SEOService.cs',
3951
+ code: `using System.Text.Json;
3952
+
3953
+ namespace MyApp.Services;
3954
+
3955
+ /// <summary>
3956
+ /// Comprehensive SEO Service for ASP.NET Core
3957
+ ///
3958
+ /// Features:
3959
+ /// - Full Open Graph support
3960
+ /// - Twitter Cards
3961
+ /// - JSON-LD structured data
3962
+ /// - Razor integration
3963
+ /// </summary>
3964
+ public class SEOService
3965
+ {
3966
+ public const string SiteName = "${siteName}";
3967
+ public const string SiteUrl = "${siteUrl}";
3968
+ public const string DefaultImage = "${image || `${siteUrl}/og-image.png`}";
3969
+ public const string DefaultDescription = "${description || `${siteName} - A compelling description.`}";
3970
+ public const string TwitterHandle = "${twitterHandle || ''}";
3971
+
3972
+ /// <summary>
3973
+ /// Create SEO metadata for a page
3974
+ /// </summary>
3975
+ public SEOMeta CreateMeta(
3976
+ string? title = null,
3977
+ string? description = null,
3978
+ string? image = null,
3979
+ string? type = "website")
3980
+ {
3981
+ return new SEOMeta
3982
+ {
3983
+ Title = title,
3984
+ Description = description ?? DefaultDescription,
3985
+ Image = image ?? DefaultImage,
3986
+ Type = type ?? "website"
3987
+ };
3988
+ }
3989
+
3990
+ // JSON-LD Schema Generators
3991
+
3992
+ public object WebsiteSchema() => new
3993
+ {
3994
+ @context = "https://schema.org",
3995
+ @type = "WebSite",
3996
+ name = SiteName,
3997
+ url = SiteUrl
3998
+ };
3999
+
4000
+ public object OrganizationSchema(
4001
+ string? name = null,
4002
+ string? url = null,
4003
+ string? logo = null,
4004
+ string[]? socialProfiles = null) => new
4005
+ {
4006
+ @context = "https://schema.org",
4007
+ @type = "Organization",
4008
+ name = name ?? SiteName,
4009
+ url = url ?? SiteUrl,
4010
+ logo,
4011
+ sameAs = socialProfiles
4012
+ };
4013
+
4014
+ public object ArticleSchema(
4015
+ string headline,
4016
+ string description,
4017
+ string? image,
4018
+ DateTime datePublished,
4019
+ DateTime? dateModified,
4020
+ string authorName) => new
4021
+ {
4022
+ @context = "https://schema.org",
4023
+ @type = "Article",
4024
+ headline,
4025
+ description,
4026
+ image = image ?? DefaultImage,
4027
+ datePublished = datePublished.ToString("O"),
4028
+ dateModified = (dateModified ?? datePublished).ToString("O"),
4029
+ author = new { @type = "Person", name = authorName },
4030
+ publisher = new
4031
+ {
4032
+ @type = "Organization",
4033
+ name = SiteName,
4034
+ logo = new { @type = "ImageObject", url = SiteUrl + "/logo.png" }
4035
+ }
4036
+ };
4037
+
4038
+ public object ProductSchema(
4039
+ string name,
4040
+ string description,
4041
+ string? image,
4042
+ decimal price,
4043
+ string currency = "USD",
4044
+ bool inStock = true) => new
4045
+ {
4046
+ @context = "https://schema.org",
4047
+ @type = "Product",
4048
+ name,
4049
+ description,
4050
+ image = image ?? DefaultImage,
4051
+ offers = new
4052
+ {
4053
+ @type = "Offer",
4054
+ price = price.ToString("F2"),
4055
+ priceCurrency = currency,
4056
+ availability = $"https://schema.org/{(inStock ? "InStock" : "OutOfStock")}"
4057
+ }
4058
+ };
4059
+
4060
+ public object FAQSchema(IEnumerable<(string Question, string Answer)> items) => new
4061
+ {
4062
+ @context = "https://schema.org",
4063
+ @type = "FAQPage",
4064
+ mainEntity = items.Select(item => new
4065
+ {
4066
+ @type = "Question",
4067
+ name = item.Question,
4068
+ acceptedAnswer = new { @type = "Answer", text = item.Answer }
4069
+ })
4070
+ };
4071
+
4072
+ public object BreadcrumbSchema(IEnumerable<(string Name, string Url)> items) => new
4073
+ {
4074
+ @context = "https://schema.org",
4075
+ @type = "BreadcrumbList",
4076
+ itemListElement = items.Select((item, index) => new
4077
+ {
4078
+ @type = "ListItem",
4079
+ position = index + 1,
4080
+ name = item.Name,
4081
+ item = item.Url
4082
+ })
4083
+ };
4084
+
4085
+ /// <summary>
4086
+ /// Serialize schema to JSON for embedding in HTML
4087
+ /// </summary>
4088
+ public string ToJson(object schema)
4089
+ {
4090
+ return JsonSerializer.Serialize(schema, new JsonSerializerOptions
4091
+ {
4092
+ WriteIndented = false,
4093
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
4094
+ });
4095
+ }
4096
+ }
4097
+
4098
+ public class SEOMeta
4099
+ {
4100
+ public string? Title { get; set; }
4101
+ public string Description { get; set; } = SEOService.DefaultDescription;
4102
+ public string Image { get; set; } = SEOService.DefaultImage;
4103
+ public string? Url { get; set; }
4104
+ public string Type { get; set; } = "website";
4105
+ public DateTime? PublishedTime { get; set; }
4106
+ public DateTime? ModifiedTime { get; set; }
4107
+ public string? Author { get; set; }
4108
+ public List<string> Tags { get; set; } = new();
4109
+ public bool NoIndex { get; set; }
4110
+ public bool NoFollow { get; set; }
4111
+
4112
+ public string FullTitle => string.IsNullOrEmpty(Title)
4113
+ ? SEOService.SiteName
4114
+ : Title.Contains(SEOService.SiteName)
4115
+ ? Title
4116
+ : $"{Title} | {SEOService.SiteName}";
4117
+
4118
+ public string AbsoluteImageUrl => Image.StartsWith("http")
4119
+ ? Image
4120
+ : SEOService.SiteUrl + Image;
4121
+
4122
+ public string Robots
4123
+ {
4124
+ get
4125
+ {
4126
+ var index = NoIndex ? "noindex" : "index";
4127
+ var follow = NoFollow ? "nofollow" : "follow";
4128
+ return $"{index}, {follow}";
4129
+ }
4130
+ }
4131
+ }`,
4132
+ explanation: `ASP.NET Core comprehensive SEO service with:
4133
+ • SEOMeta class for page metadata
4134
+ • JSON-LD schema generators
4135
+ • Razor integration
4136
+ • Fluent API
4137
+
4138
+ Setup:
4139
+ 1. Register SEOService as singleton
4140
+ 2. Inject in controllers/pages
4141
+ 3. Use in Razor views`,
4142
+ additionalFiles: [
4143
+ {
4144
+ file: 'Pages/Shared/_SEOMeta.cshtml',
4145
+ code: `@model SEOMeta
4146
+
4147
+ <!-- Primary Meta Tags -->
4148
+ <title>@Model.FullTitle</title>
4149
+ <meta name="title" content="@Model.FullTitle">
4150
+ <meta name="description" content="@Model.Description">
4151
+ <meta name="robots" content="@Model.Robots">
4152
+ <link rel="canonical" href="@Model.Url">
4153
+
4154
+ <!-- Open Graph / Facebook -->
4155
+ <meta property="og:type" content="@Model.Type">
4156
+ <meta property="og:url" content="@Model.Url">
4157
+ <meta property="og:title" content="@Model.FullTitle">
4158
+ <meta property="og:description" content="@Model.Description">
4159
+ <meta property="og:image" content="@Model.AbsoluteImageUrl">
4160
+ <meta property="og:image:width" content="1200">
4161
+ <meta property="og:image:height" content="630">
4162
+ <meta property="og:site_name" content="${siteName}">
4163
+
4164
+ @if (Model.Type == "article")
4165
+ {
4166
+ if (Model.PublishedTime.HasValue)
4167
+ {
4168
+ <meta property="article:published_time" content="@Model.PublishedTime.Value.ToString("O")">
4169
+ }
4170
+ if (Model.ModifiedTime.HasValue)
4171
+ {
4172
+ <meta property="article:modified_time" content="@Model.ModifiedTime.Value.ToString("O")">
4173
+ }
4174
+ if (!string.IsNullOrEmpty(Model.Author))
4175
+ {
4176
+ <meta property="article:author" content="@Model.Author">
4177
+ }
4178
+ foreach (var tag in Model.Tags)
4179
+ {
4180
+ <meta property="article:tag" content="@tag">
4181
+ }
4182
+ }
4183
+
4184
+ <!-- Twitter -->
4185
+ <meta name="twitter:card" content="summary_large_image">
4186
+ <meta name="twitter:title" content="@Model.FullTitle">
4187
+ <meta name="twitter:description" content="@Model.Description">
4188
+ <meta name="twitter:image" content="@Model.AbsoluteImageUrl">
4189
+ @if (!string.IsNullOrEmpty("${twitterHandle}"))
4190
+ {
4191
+ <meta name="twitter:site" content="${twitterHandle}">
4192
+ <meta name="twitter:creator" content="${twitterHandle}">
4193
+ }`,
4194
+ explanation: 'Razor partial view for SEO meta tags.',
4195
+ },
4196
+ {
4197
+ file: 'Pages/Shared/_Layout.cshtml',
4198
+ code: `<!DOCTYPE html>
4199
+ <html lang="en">
4200
+ <head>
4201
+ <meta charset="utf-8">
4202
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
4203
+
4204
+ @if (ViewData["SEO"] is SEOMeta seo)
4205
+ {
4206
+ <partial name="_SEOMeta" model="seo" />
4207
+ }
4208
+ else
4209
+ {
4210
+ <title>${siteName}</title>
4211
+ }
4212
+
4213
+ <link rel="icon" type="image/x-icon" href="~/favicon.ico">
4214
+ <link rel="stylesheet" href="~/css/site.css" asp-append-version="true">
4215
+
4216
+ @if (ViewData["JsonLd"] is string jsonLd)
4217
+ {
4218
+ <script type="application/ld+json">@Html.Raw(jsonLd)</script>
4219
+ }
4220
+ </head>
4221
+ <body>
4222
+ @RenderBody()
4223
+
4224
+ <script src="~/js/site.js" asp-append-version="true"></script>
4225
+ @await RenderSectionAsync("Scripts", required: false)
4226
+ </body>
4227
+ </html>`,
4228
+ explanation: 'Razor layout with SEO integration.',
4229
+ },
4230
+ {
4231
+ file: 'wwwroot/robots.txt',
4232
+ code: `User-agent: *
4233
+ Allow: /
4234
+ Disallow: /admin/
4235
+ Disallow: /api/
4236
+
4237
+ User-agent: GPTBot
4238
+ Allow: /
4239
+
4240
+ Sitemap: ${siteUrl}/sitemap.xml`,
4241
+ explanation: 'Robots.txt with AI crawler support.',
4242
+ },
4243
+ ],
4244
+ };
4245
+ }
4246
+
4247
+ // ============================================================================
4248
+ // HTMX - Server-rendered SEO (framework-agnostic)
4249
+ // ============================================================================
4250
+
4251
+ export function generateHTMXSEO(options: MetaFixOptions): GeneratedCode {
4252
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
4253
+
4254
+ return {
4255
+ file: 'templates/seo_head.html',
4256
+ code: `<!--
4257
+ HTMX SEO Template
4258
+
4259
+ This template provides comprehensive SEO for HTMX applications.
4260
+ HTMX apps are server-rendered, so SEO is straightforward - just
4261
+ include proper meta tags in your initial HTML response.
4262
+
4263
+ Features:
4264
+ - Full Open Graph support
4265
+ - Twitter Cards
4266
+ - JSON-LD structured data
4267
+ - Works with any backend (Python, Ruby, Go, PHP, etc.)
4268
+
4269
+ Usage:
4270
+ Include this template in your base layout, passing the required variables.
4271
+ -->
4272
+
4273
+ <!-- Primary Meta Tags -->
4274
+ <title>{{ full_title or '${siteName}' }}</title>
4275
+ <meta name="title" content="{{ full_title or '${siteName}' }}">
4276
+ <meta name="description" content="{{ description or '${description || `${siteName} - A compelling description.`}' }}">
4277
+ <meta name="robots" content="{{ 'noindex, nofollow' if noindex else 'index, follow' }}">
4278
+ <link rel="canonical" href="{{ canonical_url }}">
4279
+
4280
+ <!-- Open Graph / Facebook -->
4281
+ <meta property="og:type" content="{{ og_type or 'website' }}">
4282
+ <meta property="og:url" content="{{ canonical_url }}">
4283
+ <meta property="og:title" content="{{ full_title or '${siteName}' }}">
4284
+ <meta property="og:description" content="{{ description or '${description || `${siteName} - A compelling description.`}' }}">
4285
+ <meta property="og:image" content="{{ image_url or '${image || `${siteUrl}/og-image.png`}' }}">
4286
+ <meta property="og:image:width" content="1200">
4287
+ <meta property="og:image:height" content="630">
4288
+ <meta property="og:site_name" content="${siteName}">
4289
+ <meta property="og:locale" content="{{ locale or 'en_US' }}">
4290
+
4291
+ {% if og_type == 'article' %}
4292
+ {% if published_time %}
4293
+ <meta property="article:published_time" content="{{ published_time }}">
4294
+ {% endif %}
4295
+ {% if modified_time %}
4296
+ <meta property="article:modified_time" content="{{ modified_time }}">
4297
+ {% endif %}
4298
+ {% if author %}
4299
+ <meta property="article:author" content="{{ author }}">
4300
+ {% endif %}
4301
+ {% for tag in tags or [] %}
4302
+ <meta property="article:tag" content="{{ tag }}">
4303
+ {% endfor %}
4304
+ {% endif %}
4305
+
4306
+ <!-- Twitter -->
4307
+ <meta name="twitter:card" content="summary_large_image">
4308
+ <meta name="twitter:title" content="{{ full_title or '${siteName}' }}">
4309
+ <meta name="twitter:description" content="{{ description or '${description || `${siteName} - A compelling description.`}' }}">
4310
+ <meta name="twitter:image" content="{{ image_url or '${image || `${siteUrl}/og-image.png`}' }}">
4311
+ {% if '${twitterHandle}' %}
4312
+ <meta name="twitter:site" content="${twitterHandle}">
4313
+ <meta name="twitter:creator" content="${twitterHandle}">
4314
+ {% endif %}
4315
+
4316
+ <!-- JSON-LD Structured Data -->
4317
+ <script type="application/ld+json">
4318
+ {
4319
+ "@context": "https://schema.org",
4320
+ "@type": "WebSite",
4321
+ "name": "${siteName}",
4322
+ "url": "${siteUrl}"
4323
+ }
4324
+ </script>
4325
+ {% if schema_json %}
4326
+ <script type="application/ld+json">
4327
+ {{ schema_json | safe }}
4328
+ </script>
4329
+ {% endif %}`,
4330
+ explanation: `HTMX SEO template (framework-agnostic) with:
4331
+ • Full Open Graph support
4332
+ • Twitter Cards
4333
+ • JSON-LD structured data
4334
+ • Works with any template engine (Jinja2, Liquid, Go templates, etc.)
4335
+
4336
+ HTMX apps are server-rendered, so SEO is built-in!
4337
+ Just include proper meta tags in your initial HTML response.
4338
+
4339
+ Usage:
4340
+ 1. Include in your base layout
4341
+ 2. Pass SEO variables from your backend
4342
+ 3. JSON-LD schemas can be generated server-side`,
4343
+ additionalFiles: [
4344
+ {
4345
+ file: 'templates/base.html',
4346
+ code: `<!DOCTYPE html>
4347
+ <html lang="{{ lang or 'en' }}">
4348
+ <head>
4349
+ <meta charset="utf-8">
4350
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4351
+
4352
+ {% include 'seo_head.html' %}
4353
+
4354
+ <!-- HTMX -->
4355
+ <script src="https://unpkg.com/htmx.org@1.9.10"></script>
4356
+
4357
+ <!-- Styles -->
4358
+ <link rel="stylesheet" href="/static/css/styles.css">
4359
+
4360
+ <!-- Favicon -->
4361
+ <link rel="icon" href="/static/favicon.ico">
4362
+ </head>
4363
+ <body hx-boost="true">
4364
+ <main id="content">
4365
+ {% block content %}{% endblock %}
4366
+ </main>
4367
+ </body>
4368
+ </html>`,
4369
+ explanation: 'Base HTMX layout with SEO integration.',
4370
+ },
4371
+ {
4372
+ file: 'static/robots.txt',
4373
+ code: `User-agent: *
4374
+ Allow: /
4375
+ Disallow: /admin/
4376
+ Disallow: /api/
4377
+
4378
+ User-agent: GPTBot
4379
+ Allow: /
4380
+
4381
+ Sitemap: ${siteUrl}/sitemap.xml`,
4382
+ explanation: 'Robots.txt with AI crawler support.',
4383
+ },
4384
+ ],
4385
+ };
4386
+ }
4387
+
4388
+ // ============================================================================
4389
+ // HUGO (Static Site Generator)
4390
+ // ============================================================================
4391
+
4392
+ export function generateHugoSEO(options: MetaFixOptions): GeneratedCode {
4393
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
4394
+
4395
+ return {
4396
+ file: 'layouts/partials/seo.html',
4397
+ code: `{{- /* Comprehensive SEO partial for Hugo */ -}}
4398
+ {{- $title := .Title | default site.Title -}}
4399
+ {{- $description := .Description | default .Summary | default site.Params.description | default "${description || `${siteName} - A compelling description.`}" -}}
4400
+ {{- $image := .Params.image | default site.Params.defaultImage | default "${image || '/og-image.png'}" -}}
4401
+ {{- $imageURL := $image | absURL -}}
4402
+ {{- $canonical := .Permalink -}}
4403
+ {{- $type := cond (eq .Kind "page") "article" "website" -}}
4404
+
4405
+ {{- /* Full title with site name */ -}}
4406
+ {{- $fullTitle := $title -}}
4407
+ {{- if and (ne $title site.Title) (not (in $title site.Title)) -}}
4408
+ {{- $fullTitle = printf "%s | %s" $title site.Title -}}
4409
+ {{- end -}}
4410
+
4411
+ <!-- Primary Meta Tags -->
4412
+ <title>{{ $fullTitle }}</title>
4413
+ <meta name="title" content="{{ $fullTitle }}">
4414
+ <meta name="description" content="{{ $description | truncate 160 }}">
4415
+ <meta name="robots" content="{{ if .Params.noindex }}noindex, nofollow{{ else }}index, follow{{ end }}">
4416
+ <link rel="canonical" href="{{ $canonical }}">
4417
+
4418
+ <!-- Open Graph / Facebook -->
4419
+ <meta property="og:type" content="{{ $type }}">
4420
+ <meta property="og:url" content="{{ $canonical }}">
4421
+ <meta property="og:title" content="{{ $fullTitle }}">
4422
+ <meta property="og:description" content="{{ $description | truncate 160 }}">
4423
+ <meta property="og:image" content="{{ $imageURL }}">
4424
+ <meta property="og:image:width" content="1200">
4425
+ <meta property="og:image:height" content="630">
4426
+ <meta property="og:site_name" content="{{ site.Title }}">
4427
+ <meta property="og:locale" content="{{ site.Language.Lang | default "en" }}_{{ site.Language.Lang | default "US" | upper }}">
4428
+
4429
+ {{- if eq $type "article" }}
4430
+ {{- with .PublishDate }}
4431
+ <meta property="article:published_time" content="{{ .Format "2006-01-02T15:04:05Z07:00" }}">
4432
+ {{- end }}
4433
+ {{- with .Lastmod }}
4434
+ <meta property="article:modified_time" content="{{ .Format "2006-01-02T15:04:05Z07:00" }}">
4435
+ {{- end }}
4436
+ {{- with .Params.author }}
4437
+ <meta property="article:author" content="{{ . }}">
4438
+ {{- end }}
4439
+ {{- range .Params.tags }}
4440
+ <meta property="article:tag" content="{{ . }}">
4441
+ {{- end }}
4442
+ {{- end }}
4443
+
4444
+ <!-- Twitter -->
4445
+ <meta name="twitter:card" content="summary_large_image">
4446
+ <meta name="twitter:title" content="{{ $fullTitle }}">
4447
+ <meta name="twitter:description" content="{{ $description | truncate 160 }}">
4448
+ <meta name="twitter:image" content="{{ $imageURL }}">
4449
+ {{- with site.Params.twitterHandle }}
4450
+ <meta name="twitter:site" content="{{ . }}">
4451
+ <meta name="twitter:creator" content="{{ . }}">
4452
+ {{- end }}
4453
+
4454
+ <!-- JSON-LD: WebSite -->
4455
+ <script type="application/ld+json">
4456
+ {
4457
+ "@context": "https://schema.org",
4458
+ "@type": "WebSite",
4459
+ "name": "{{ site.Title }}",
4460
+ "url": "{{ site.BaseURL }}"
4461
+ }
4462
+ </script>
4463
+
4464
+ {{- /* Article Schema */ -}}
4465
+ {{- if eq $type "article" }}
4466
+ <script type="application/ld+json">
4467
+ {
4468
+ "@context": "https://schema.org",
4469
+ "@type": "Article",
4470
+ "headline": "{{ $title }}",
4471
+ "description": "{{ $description | truncate 160 }}",
4472
+ "image": "{{ $imageURL }}",
4473
+ "datePublished": "{{ .PublishDate.Format "2006-01-02T15:04:05Z07:00" }}",
4474
+ "dateModified": "{{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" }}",
4475
+ "author": {
4476
+ "@type": "Person",
4477
+ "name": "{{ .Params.author | default site.Params.author | default "Author" }}"
4478
+ },
4479
+ "publisher": {
4480
+ "@type": "Organization",
4481
+ "name": "{{ site.Title }}",
4482
+ "logo": {
4483
+ "@type": "ImageObject",
4484
+ "url": "{{ "logo.png" | absURL }}"
4485
+ }
4486
+ }
4487
+ }
4488
+ </script>
4489
+ {{- end }}
4490
+
4491
+ {{- /* Breadcrumbs */ -}}
4492
+ {{- if .Parent }}
4493
+ <script type="application/ld+json">
4494
+ {
4495
+ "@context": "https://schema.org",
4496
+ "@type": "BreadcrumbList",
4497
+ "itemListElement": [
4498
+ {{- $breadcrumbs := slice -}}
4499
+ {{- range .Ancestors.Reverse }}
4500
+ {{- $breadcrumbs = $breadcrumbs | append (dict "name" .Title "url" .Permalink) -}}
4501
+ {{- end }}
4502
+ {{- $breadcrumbs = $breadcrumbs | append (dict "name" .Title "url" .Permalink) -}}
4503
+ {{- range $i, $item := $breadcrumbs }}
4504
+ {{- if $i }},{{ end }}
4505
+ {
4506
+ "@type": "ListItem",
4507
+ "position": {{ add $i 1 }},
4508
+ "name": "{{ $item.name }}",
4509
+ "item": "{{ $item.url }}"
4510
+ }
4511
+ {{- end }}
4512
+ ]
4513
+ }
4514
+ </script>
4515
+ {{- end }}`,
4516
+ explanation: `Hugo comprehensive SEO partial with:
4517
+ • Automatic title handling
4518
+ • Full Open Graph with article support
4519
+ • Twitter Cards
4520
+ • JSON-LD schemas (WebSite, Article, Breadcrumbs)
4521
+ • Content summaries for descriptions
4522
+
4523
+ Usage: {{ partial "seo.html" . }}`,
4524
+ additionalFiles: [
4525
+ {
4526
+ file: 'layouts/_default/baseof.html',
4527
+ code: `<!DOCTYPE html>
4528
+ <html lang="{{ site.Language.Lang | default "en" }}">
4529
+ <head>
4530
+ <meta charset="utf-8">
4531
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4532
+
4533
+ {{ partial "seo.html" . }}
4534
+
4535
+ <!-- Favicon -->
4536
+ <link rel="icon" href="{{ "favicon.ico" | relURL }}">
4537
+ <link rel="apple-touch-icon" sizes="180x180" href="{{ "apple-touch-icon.png" | relURL }}">
4538
+
4539
+ <!-- Styles -->
4540
+ {{ $styles := resources.Get "css/styles.css" | minify | fingerprint }}
4541
+ <link rel="stylesheet" href="{{ $styles.Permalink }}">
4542
+ </head>
4543
+ <body>
4544
+ {{ block "main" . }}{{ end }}
4545
+ </body>
4546
+ </html>`,
4547
+ explanation: 'Hugo base template with SEO partial.',
4548
+ },
4549
+ {
4550
+ file: 'config.toml',
4551
+ code: `baseURL = '${siteUrl}/'
4552
+ languageCode = 'en-us'
4553
+ title = '${siteName}'
4554
+
4555
+ [params]
4556
+ description = '${description || `${siteName} - A compelling description.`}'
4557
+ defaultImage = '${image || '/og-image.png'}'
4558
+ twitterHandle = '${twitterHandle || ''}'
4559
+ author = 'Your Name'
4560
+
4561
+ [sitemap]
4562
+ changefreq = 'weekly'
4563
+ filename = 'sitemap.xml'
4564
+ priority = 0.5
4565
+
4566
+ [outputs]
4567
+ home = ['HTML', 'RSS', 'JSON']
4568
+
4569
+ # Robots.txt
4570
+ enableRobotsTXT = true`,
4571
+ explanation: 'Hugo configuration with SEO settings.',
4572
+ },
4573
+ {
4574
+ file: 'layouts/robots.txt',
4575
+ code: `User-agent: *
4576
+ Allow: /
4577
+ Disallow: /admin/
4578
+
4579
+ User-agent: GPTBot
4580
+ Allow: /
4581
+
4582
+ Sitemap: {{ .Site.BaseURL }}sitemap.xml`,
4583
+ explanation: 'Hugo robots.txt template.',
4584
+ },
4585
+ ],
4586
+ };
4587
+ }
4588
+
4589
+ // ============================================================================
4590
+ // JEKYLL (Ruby Static Site Generator)
4591
+ // ============================================================================
4592
+
4593
+ export function generateJekyllSEO(options: MetaFixOptions): GeneratedCode {
4594
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
4595
+
4596
+ return {
4597
+ file: '_includes/seo.html',
4598
+ code: `{% comment %}
4599
+ Comprehensive SEO include for Jekyll
4600
+
4601
+ Features:
4602
+ - Full Open Graph support
4603
+ - Twitter Cards
4604
+ - JSON-LD structured data
4605
+ - jekyll-seo-tag compatible
4606
+
4607
+ Usage: {% include seo.html %}
4608
+ {% endcomment %}
4609
+
4610
+ {%- assign seo_title = page.title | default: site.title -%}
4611
+ {%- if page.title and site.title and page.title != site.title -%}
4612
+ {%- assign seo_title = page.title | append: " | " | append: site.title -%}
4613
+ {%- endif -%}
4614
+
4615
+ {%- assign seo_description = page.description | default: page.excerpt | default: site.description | default: "${description || `${siteName} - A compelling description.`}" | strip_html | truncate: 160 -%}
4616
+
4617
+ {%- assign seo_image = page.image | default: site.og_image | default: "${image || '/og-image.png'}" -%}
4618
+ {%- unless seo_image contains "://" -%}
4619
+ {%- assign seo_image = seo_image | absolute_url -%}
4620
+ {%- endunless -%}
4621
+
4622
+ {%- assign seo_type = "website" -%}
4623
+ {%- if page.layout == "post" -%}
4624
+ {%- assign seo_type = "article" -%}
4625
+ {%- endif -%}
4626
+
4627
+ <!-- Primary Meta Tags -->
4628
+ <title>{{ seo_title }}</title>
4629
+ <meta name="title" content="{{ seo_title }}">
4630
+ <meta name="description" content="{{ seo_description }}">
4631
+ <meta name="robots" content="{% if page.noindex %}noindex, nofollow{% else %}index, follow{% endif %}">
4632
+ <link rel="canonical" href="{{ page.url | absolute_url }}">
4633
+
4634
+ <!-- Open Graph / Facebook -->
4635
+ <meta property="og:type" content="{{ seo_type }}">
4636
+ <meta property="og:url" content="{{ page.url | absolute_url }}">
4637
+ <meta property="og:title" content="{{ seo_title }}">
4638
+ <meta property="og:description" content="{{ seo_description }}">
4639
+ <meta property="og:image" content="{{ seo_image }}">
4640
+ <meta property="og:image:width" content="1200">
4641
+ <meta property="og:image:height" content="630">
4642
+ <meta property="og:site_name" content="{{ site.title }}">
4643
+ <meta property="og:locale" content="{{ site.locale | default: "en_US" }}">
4644
+
4645
+ {% if seo_type == "article" %}
4646
+ {% if page.date %}
4647
+ <meta property="article:published_time" content="{{ page.date | date_to_xmlschema }}">
4648
+ {% endif %}
4649
+ {% if page.last_modified_at %}
4650
+ <meta property="article:modified_time" content="{{ page.last_modified_at | date_to_xmlschema }}">
4651
+ {% endif %}
4652
+ {% if page.author %}
4653
+ <meta property="article:author" content="{{ page.author }}">
4654
+ {% endif %}
4655
+ {% for tag in page.tags %}
4656
+ <meta property="article:tag" content="{{ tag }}">
4657
+ {% endfor %}
4658
+ {% endif %}
4659
+
4660
+ <!-- Twitter -->
4661
+ <meta name="twitter:card" content="summary_large_image">
4662
+ <meta name="twitter:title" content="{{ seo_title }}">
4663
+ <meta name="twitter:description" content="{{ seo_description }}">
4664
+ <meta name="twitter:image" content="{{ seo_image }}">
4665
+ {% if site.twitter_handle %}
4666
+ <meta name="twitter:site" content="{{ site.twitter_handle }}">
4667
+ <meta name="twitter:creator" content="{{ site.twitter_handle }}">
4668
+ {% endif %}
4669
+
4670
+ <!-- JSON-LD: WebSite -->
4671
+ <script type="application/ld+json">
4672
+ {
4673
+ "@context": "https://schema.org",
4674
+ "@type": "WebSite",
4675
+ "name": "{{ site.title }}",
4676
+ "url": "{{ site.url }}"
4677
+ }
4678
+ </script>
4679
+
4680
+ {% if seo_type == "article" %}
4681
+ <!-- JSON-LD: Article -->
4682
+ <script type="application/ld+json">
4683
+ {
4684
+ "@context": "https://schema.org",
4685
+ "@type": "Article",
4686
+ "headline": "{{ page.title | escape }}",
4687
+ "description": "{{ seo_description | escape }}",
4688
+ "image": "{{ seo_image }}",
4689
+ "datePublished": "{{ page.date | date_to_xmlschema }}",
4690
+ "dateModified": "{{ page.last_modified_at | default: page.date | date_to_xmlschema }}",
4691
+ "author": {
4692
+ "@type": "Person",
4693
+ "name": "{{ page.author | default: site.author | default: "Author" }}"
4694
+ },
4695
+ "publisher": {
4696
+ "@type": "Organization",
4697
+ "name": "{{ site.title }}",
4698
+ "logo": {
4699
+ "@type": "ImageObject",
4700
+ "url": "{{ '/logo.png' | absolute_url }}"
4701
+ }
4702
+ }
4703
+ }
4704
+ </script>
4705
+ {% endif %}`,
4706
+ explanation: `Jekyll comprehensive SEO include with:
4707
+ • Full Open Graph with article support
4708
+ • Twitter Cards
4709
+ • JSON-LD schemas
4710
+ • Liquid template integration
4711
+
4712
+ Usage: {% include seo.html %}`,
4713
+ additionalFiles: [
4714
+ {
4715
+ file: '_layouts/default.html',
4716
+ code: `<!DOCTYPE html>
4717
+ <html lang="{{ site.locale | default: "en" | slice: 0, 2 }}">
4718
+ <head>
4719
+ <meta charset="utf-8">
4720
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4721
+
4722
+ {% include seo.html %}
4723
+
4724
+ <link rel="icon" href="{{ '/favicon.ico' | relative_url }}">
4725
+ <link rel="apple-touch-icon" sizes="180x180" href="{{ '/apple-touch-icon.png' | relative_url }}">
4726
+
4727
+ <link rel="stylesheet" href="{{ '/assets/css/styles.css' | relative_url }}">
4728
+ </head>
4729
+ <body>
4730
+ {{ content }}
4731
+ </body>
4732
+ </html>`,
4733
+ explanation: 'Jekyll default layout with SEO include.',
4734
+ },
4735
+ {
4736
+ file: '_config.yml',
4737
+ code: `title: ${siteName}
4738
+ description: "${description || `${siteName} - A compelling description.`}"
4739
+ url: "${siteUrl}"
4740
+ baseurl: ""
4741
+
4742
+ # SEO
4743
+ og_image: "${image || '/og-image.png'}"
4744
+ twitter_handle: "${twitterHandle || ''}"
4745
+ locale: "en_US"
4746
+ author: "Your Name"
4747
+
4748
+ # Plugins
4749
+ plugins:
4750
+ - jekyll-sitemap
4751
+ - jekyll-feed
4752
+
4753
+ # Defaults
4754
+ defaults:
4755
+ - scope:
4756
+ path: ""
4757
+ type: "posts"
4758
+ values:
4759
+ layout: "post"
4760
+ - scope:
4761
+ path: ""
4762
+ values:
4763
+ layout: "default"`,
4764
+ explanation: 'Jekyll configuration with SEO settings.',
4765
+ },
4766
+ {
4767
+ file: 'robots.txt',
4768
+ code: `---
4769
+ ---
4770
+ User-agent: *
4771
+ Allow: /
4772
+ Disallow: /admin/
4773
+
4774
+ User-agent: GPTBot
4775
+ Allow: /
4776
+
4777
+ Sitemap: {{ site.url }}/sitemap.xml`,
4778
+ explanation: 'Jekyll robots.txt with front matter.',
4779
+ },
4780
+ ],
4781
+ };
4782
+ }
4783
+
4784
+ // ============================================================================
4785
+ // ELEVENTY (11ty)
4786
+ // ============================================================================
4787
+
4788
+ export function generateEleventySEO(options: MetaFixOptions): GeneratedCode {
4789
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
4790
+
4791
+ return {
4792
+ file: 'src/_includes/seo.njk',
4793
+ code: `{#
4794
+ Comprehensive SEO partial for Eleventy (11ty)
4795
+
4796
+ Features:
4797
+ - Full Open Graph support
4798
+ - Twitter Cards
4799
+ - JSON-LD structured data
4800
+ - Nunjucks template
4801
+
4802
+ Usage: {% include "seo.njk" %}
4803
+ #}
4804
+
4805
+ {%- set seoTitle = title or metadata.title -%}
4806
+ {%- set fullTitle = seoTitle -%}
4807
+ {%- if seoTitle and metadata.title and seoTitle != metadata.title -%}
4808
+ {%- set fullTitle = seoTitle + " | " + metadata.title -%}
4809
+ {%- endif -%}
4810
+
4811
+ {%- set seoDescription = description or excerpt or metadata.description or "${description || `${siteName} - A compelling description.`}" -%}
4812
+ {%- set seoImage = image or metadata.defaultImage or "${image || '/og-image.png'}" -%}
4813
+ {%- if not seoImage.startsWith("http") -%}
4814
+ {%- set seoImage = metadata.url + seoImage -%}
4815
+ {%- endif -%}
4816
+
4817
+ {%- set pageUrl = page.url | url | absoluteUrl(metadata.url) -%}
4818
+ {%- set seoType = "article" if layout == "post" else "website" -%}
4819
+
4820
+ <!-- Primary Meta Tags -->
4821
+ <title>{{ fullTitle }}</title>
4822
+ <meta name="title" content="{{ fullTitle }}">
4823
+ <meta name="description" content="{{ seoDescription | truncate(160) }}">
4824
+ <meta name="robots" content="{{ 'noindex, nofollow' if noindex else 'index, follow' }}">
4825
+ <link rel="canonical" href="{{ pageUrl }}">
4826
+
4827
+ <!-- Open Graph / Facebook -->
4828
+ <meta property="og:type" content="{{ seoType }}">
4829
+ <meta property="og:url" content="{{ pageUrl }}">
4830
+ <meta property="og:title" content="{{ fullTitle }}">
4831
+ <meta property="og:description" content="{{ seoDescription | truncate(160) }}">
4832
+ <meta property="og:image" content="{{ seoImage }}">
4833
+ <meta property="og:image:width" content="1200">
4834
+ <meta property="og:image:height" content="630">
4835
+ <meta property="og:site_name" content="{{ metadata.title }}">
4836
+ <meta property="og:locale" content="{{ metadata.locale or 'en_US' }}">
4837
+
4838
+ {% if seoType == "article" %}
4839
+ {% if date %}
4840
+ <meta property="article:published_time" content="{{ date | dateToISO }}">
4841
+ {% endif %}
4842
+ {% if lastModified %}
4843
+ <meta property="article:modified_time" content="{{ lastModified | dateToISO }}">
4844
+ {% endif %}
4845
+ {% if author %}
4846
+ <meta property="article:author" content="{{ author }}">
4847
+ {% endif %}
4848
+ {% for tag in tags %}
4849
+ {% if tag != "posts" and tag != "all" %}
4850
+ <meta property="article:tag" content="{{ tag }}">
4851
+ {% endif %}
4852
+ {% endfor %}
4853
+ {% endif %}
4854
+
4855
+ <!-- Twitter -->
4856
+ <meta name="twitter:card" content="summary_large_image">
4857
+ <meta name="twitter:title" content="{{ fullTitle }}">
4858
+ <meta name="twitter:description" content="{{ seoDescription | truncate(160) }}">
4859
+ <meta name="twitter:image" content="{{ seoImage }}">
4860
+ {% if metadata.twitterHandle %}
4861
+ <meta name="twitter:site" content="{{ metadata.twitterHandle }}">
4862
+ <meta name="twitter:creator" content="{{ metadata.twitterHandle }}">
4863
+ {% endif %}
4864
+
4865
+ <!-- JSON-LD: WebSite -->
4866
+ <script type="application/ld+json">
4867
+ {
4868
+ "@context": "https://schema.org",
4869
+ "@type": "WebSite",
4870
+ "name": "{{ metadata.title }}",
4871
+ "url": "{{ metadata.url }}"
4872
+ }
4873
+ </script>
4874
+
4875
+ {% if seoType == "article" %}
4876
+ <!-- JSON-LD: Article -->
4877
+ <script type="application/ld+json">
4878
+ {
4879
+ "@context": "https://schema.org",
4880
+ "@type": "Article",
4881
+ "headline": "{{ title }}",
4882
+ "description": "{{ seoDescription | truncate(160) | escape }}",
4883
+ "image": "{{ seoImage }}",
4884
+ "datePublished": "{{ date | dateToISO }}",
4885
+ "dateModified": "{{ (lastModified or date) | dateToISO }}",
4886
+ "author": {
4887
+ "@type": "Person",
4888
+ "name": "{{ author or metadata.author or 'Author' }}"
4889
+ },
4890
+ "publisher": {
4891
+ "@type": "Organization",
4892
+ "name": "{{ metadata.title }}",
4893
+ "logo": {
4894
+ "@type": "ImageObject",
4895
+ "url": "{{ metadata.url }}/logo.png"
4896
+ }
4897
+ }
4898
+ }
4899
+ </script>
4900
+ {% endif %}`,
4901
+ explanation: `Eleventy comprehensive SEO partial with:
4902
+ • Full Open Graph with article support
4903
+ • Twitter Cards
4904
+ • JSON-LD schemas
4905
+ • Nunjucks templating
4906
+
4907
+ Usage: {% include "seo.njk" %}`,
4908
+ additionalFiles: [
4909
+ {
4910
+ file: 'src/_includes/base.njk',
4911
+ code: `<!DOCTYPE html>
4912
+ <html lang="{{ metadata.locale | default('en') | truncate(2, true, '') }}">
4913
+ <head>
4914
+ <meta charset="utf-8">
4915
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4916
+
4917
+ {% include "seo.njk" %}
4918
+
4919
+ <link rel="icon" href="/favicon.ico">
4920
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
4921
+
4922
+ <link rel="stylesheet" href="/css/styles.css">
4923
+ </head>
4924
+ <body>
4925
+ {{ content | safe }}
4926
+ </body>
4927
+ </html>`,
4928
+ explanation: 'Eleventy base layout with SEO include.',
4929
+ },
4930
+ {
4931
+ file: 'src/_data/metadata.json',
4932
+ code: `{
4933
+ "title": "${siteName}",
4934
+ "url": "${siteUrl}",
4935
+ "description": "${description || `${siteName} - A compelling description.`}",
4936
+ "defaultImage": "${image || '/og-image.png'}",
4937
+ "twitterHandle": "${twitterHandle || ''}",
4938
+ "locale": "en_US",
4939
+ "author": "Your Name"
4940
+ }`,
4941
+ explanation: 'Eleventy global data file with SEO metadata.',
4942
+ },
4943
+ {
4944
+ file: 'src/robots.njk',
4945
+ code: `---
4946
+ permalink: /robots.txt
4947
+ eleventyExcludeFromCollections: true
4948
+ ---
4949
+ User-agent: *
4950
+ Allow: /
4951
+ Disallow: /admin/
4952
+
4953
+ User-agent: GPTBot
4954
+ Allow: /
4955
+
4956
+ Sitemap: {{ metadata.url }}/sitemap.xml`,
4957
+ explanation: 'Eleventy robots.txt template.',
4958
+ },
4959
+ ],
4960
+ };
4961
+ }
4962
+
4963
+ // ============================================================================
4964
+ // REMIX
4965
+ // ============================================================================
4966
+
4967
+ export function generateRemixSEO(options: MetaFixOptions): GeneratedCode {
4968
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
4969
+
4970
+ return {
4971
+ file: 'app/utils/seo.ts',
4972
+ code: `import type { V2_MetaFunction, V2_MetaDescriptor } from '@remix-run/node';
4973
+
4974
+ /**
4975
+ * Comprehensive SEO utilities for Remix
4976
+ *
4977
+ * Features:
4978
+ * - Full Open Graph support
4979
+ * - Twitter Cards
4980
+ * - JSON-LD structured data
4981
+ * - Type-safe meta function helpers
4982
+ */
4983
+
4984
+ const SITE_NAME = '${siteName}';
4985
+ const SITE_URL = '${siteUrl}';
4986
+ const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
4987
+ const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
4988
+ const TWITTER_HANDLE = '${twitterHandle || ''}';
4989
+
4990
+ interface SEOConfig {
4991
+ title?: string;
4992
+ description?: string;
4993
+ image?: string;
4994
+ url?: string;
4995
+ type?: 'website' | 'article';
4996
+ publishedTime?: string;
4997
+ modifiedTime?: string;
4998
+ author?: string;
4999
+ tags?: string[];
5000
+ noIndex?: boolean;
5001
+ }
5002
+
5003
+ /**
5004
+ * Generate meta tags for a page
5005
+ */
5006
+ export function generateMeta(config: SEOConfig = {}): V2_MetaDescriptor[] {
5007
+ const {
5008
+ title,
5009
+ description = DEFAULT_DESCRIPTION,
5010
+ image = DEFAULT_IMAGE,
5011
+ url = SITE_URL,
5012
+ type = 'website',
5013
+ publishedTime,
5014
+ modifiedTime,
5015
+ author,
5016
+ tags = [],
5017
+ noIndex = false,
5018
+ } = config;
5019
+
5020
+ const fullTitle = title
5021
+ ? title.includes(SITE_NAME)
5022
+ ? title
5023
+ : \`\${title} | \${SITE_NAME}\`
5024
+ : SITE_NAME;
5025
+
5026
+ const imageUrl = image.startsWith('http') ? image : \`\${SITE_URL}\${image}\`;
5027
+ const robotsContent = noIndex ? 'noindex, nofollow' : 'index, follow';
5028
+
5029
+ const meta: V2_MetaDescriptor[] = [
5030
+ { title: fullTitle },
5031
+ { name: 'description', content: description },
5032
+ { name: 'robots', content: robotsContent },
5033
+
5034
+ // Open Graph
5035
+ { property: 'og:type', content: type },
5036
+ { property: 'og:url', content: url },
5037
+ { property: 'og:title', content: fullTitle },
5038
+ { property: 'og:description', content: description },
5039
+ { property: 'og:image', content: imageUrl },
5040
+ { property: 'og:image:width', content: '1200' },
5041
+ { property: 'og:image:height', content: '630' },
5042
+ { property: 'og:site_name', content: SITE_NAME },
5043
+
5044
+ // Twitter
5045
+ { name: 'twitter:card', content: 'summary_large_image' },
5046
+ { name: 'twitter:title', content: fullTitle },
5047
+ { name: 'twitter:description', content: description },
5048
+ { name: 'twitter:image', content: imageUrl },
5049
+ ];
5050
+
5051
+ if (TWITTER_HANDLE) {
5052
+ meta.push(
5053
+ { name: 'twitter:site', content: TWITTER_HANDLE },
5054
+ { name: 'twitter:creator', content: TWITTER_HANDLE }
5055
+ );
5056
+ }
5057
+
5058
+ // Article-specific meta
5059
+ if (type === 'article') {
5060
+ if (publishedTime) {
5061
+ meta.push({ property: 'article:published_time', content: publishedTime });
5062
+ }
5063
+ if (modifiedTime) {
5064
+ meta.push({ property: 'article:modified_time', content: modifiedTime });
5065
+ }
5066
+ if (author) {
5067
+ meta.push({ property: 'article:author', content: author });
5068
+ }
5069
+ tags.forEach((tag) => {
5070
+ meta.push({ property: 'article:tag', content: tag });
5071
+ });
5072
+ }
5073
+
5074
+ return meta;
5075
+ }
5076
+
5077
+ /**
5078
+ * Generate canonical link
5079
+ */
5080
+ export function generateLinks(url: string) {
5081
+ return [{ rel: 'canonical', href: url }];
5082
+ }
5083
+
5084
+ // JSON-LD Schema Generators
5085
+
5086
+ export function websiteSchema() {
5087
+ return {
5088
+ '@context': 'https://schema.org',
5089
+ '@type': 'WebSite',
5090
+ name: SITE_NAME,
5091
+ url: SITE_URL,
5092
+ };
5093
+ }
5094
+
5095
+ export function articleSchema(data: {
5096
+ headline: string;
5097
+ description: string;
5098
+ image?: string;
5099
+ datePublished: string;
5100
+ dateModified?: string;
5101
+ author: { name: string; url?: string };
5102
+ }) {
5103
+ return {
5104
+ '@context': 'https://schema.org',
5105
+ '@type': 'Article',
5106
+ headline: data.headline,
5107
+ description: data.description,
5108
+ image: data.image || DEFAULT_IMAGE,
5109
+ datePublished: data.datePublished,
5110
+ dateModified: data.dateModified || data.datePublished,
5111
+ author: { '@type': 'Person', ...data.author },
5112
+ publisher: {
5113
+ '@type': 'Organization',
5114
+ name: SITE_NAME,
5115
+ logo: { '@type': 'ImageObject', url: \`\${SITE_URL}/logo.png\` },
5116
+ },
5117
+ };
5118
+ }
5119
+
5120
+ export function productSchema(data: {
5121
+ name: string;
5122
+ description: string;
5123
+ image?: string;
5124
+ price: number;
5125
+ currency?: string;
5126
+ inStock?: boolean;
5127
+ }) {
5128
+ return {
5129
+ '@context': 'https://schema.org',
5130
+ '@type': 'Product',
5131
+ name: data.name,
5132
+ description: data.description,
5133
+ image: data.image || DEFAULT_IMAGE,
5134
+ offers: {
5135
+ '@type': 'Offer',
5136
+ price: data.price.toString(),
5137
+ priceCurrency: data.currency || 'USD',
5138
+ availability: \`https://schema.org/\${data.inStock !== false ? 'InStock' : 'OutOfStock'}\`,
5139
+ },
5140
+ };
5141
+ }
5142
+
5143
+ export function faqSchema(items: { question: string; answer: string }[]) {
5144
+ return {
5145
+ '@context': 'https://schema.org',
5146
+ '@type': 'FAQPage',
5147
+ mainEntity: items.map((item) => ({
5148
+ '@type': 'Question',
5149
+ name: item.question,
5150
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
5151
+ })),
5152
+ };
5153
+ }
5154
+
5155
+ export function breadcrumbSchema(items: { name: string; url: string }[]) {
5156
+ return {
5157
+ '@context': 'https://schema.org',
5158
+ '@type': 'BreadcrumbList',
5159
+ itemListElement: items.map((item, i) => ({
5160
+ '@type': 'ListItem',
5161
+ position: i + 1,
5162
+ name: item.name,
5163
+ item: item.url,
5164
+ })),
5165
+ };
5166
+ }
5167
+
5168
+ /**
5169
+ * JSON-LD Script component helper
5170
+ */
5171
+ export function JsonLd({ schema }: { schema: Record<string, unknown> }) {
5172
+ return (
5173
+ <script
5174
+ type="application/ld+json"
5175
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
5176
+ />
5177
+ );
5178
+ }`,
5179
+ explanation: `Remix comprehensive SEO utilities with:
5180
+ • Type-safe meta function helpers
5181
+ • Full Open Graph with article support
5182
+ • Twitter Cards
5183
+ • JSON-LD schema generators
5184
+ • Remix V2 meta conventions
5185
+
5186
+ Usage in route:
5187
+ export const meta: V2_MetaFunction = () => generateMeta({ title: "Page" });`,
5188
+ additionalFiles: [
5189
+ {
5190
+ file: 'app/root.tsx',
5191
+ code: `import {
5192
+ Links,
5193
+ LiveReload,
5194
+ Meta,
5195
+ Outlet,
5196
+ Scripts,
5197
+ ScrollRestoration,
5198
+ } from '@remix-run/react';
5199
+ import { JsonLd, websiteSchema } from '~/utils/seo';
5200
+
5201
+ export default function App() {
5202
+ return (
5203
+ <html lang="en">
5204
+ <head>
5205
+ <meta charSet="utf-8" />
5206
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
5207
+ <Meta />
5208
+ <Links />
5209
+ <JsonLd schema={websiteSchema()} />
5210
+ </head>
5211
+ <body>
5212
+ <Outlet />
5213
+ <ScrollRestoration />
5214
+ <Scripts />
5215
+ <LiveReload />
5216
+ </body>
5217
+ </html>
5218
+ );
5219
+ }`,
5220
+ explanation: 'Remix root with SEO integration.',
5221
+ },
5222
+ {
5223
+ file: 'public/robots.txt',
5224
+ code: `User-agent: *
5225
+ Allow: /
5226
+ Disallow: /admin/
5227
+ Disallow: /api/
5228
+
5229
+ User-agent: GPTBot
5230
+ Allow: /
5231
+
5232
+ Sitemap: ${siteUrl}/sitemap.xml`,
5233
+ explanation: 'Robots.txt with AI crawler support.',
5234
+ },
5235
+ ],
5236
+ };
5237
+ }
5238
+
5239
+ // ============================================================================
5240
+ // Updated getFrameworkSpecificFix to include all new frameworks
5241
+ // ============================================================================
5242
+
5243
+ export function getFrameworkSpecificFixExtended(
5244
+ framework: FrameworkInfo,
5245
+ options: MetaFixOptions
5246
+ ): GeneratedCode {
5247
+ const name = framework.name.toLowerCase();
5248
+
5249
+ // JavaScript frameworks (existing)
5250
+ if (name.includes('next')) {
5251
+ return framework.router === 'app'
5252
+ ? generateNextJsAppRouterMetadata(options)
5253
+ : generateNextJsPagesRouterHead(options);
5254
+ }
5255
+ if (name.includes('nuxt')) return generateNuxtSEOHead(options);
5256
+ if (name.includes('vue')) return generateVueSEOHead(options);
5257
+ if (name.includes('astro')) return generateAstroBaseHead(options);
5258
+ if (name.includes('svelte')) return generateSvelteKitSEOHead(options);
5259
+ if (name.includes('angular')) return generateAngularSEOService(options);
5260
+ if (name.includes('remix')) return generateRemixSEO(options);
5261
+
5262
+ // Backend frameworks (new)
5263
+ if (name.includes('rails') || name.includes('ruby')) return generateRailsSEOHelper(options);
5264
+ if (name.includes('django')) return generateDjangoSEOHelper(options);
5265
+ if (name.includes('laravel') || name.includes('php')) return generateLaravelSEOHelper(options);
5266
+ if (name.includes('spring') || name.includes('java')) return generateSpringBootSEO(options);
5267
+ if (name.includes('phoenix') || name.includes('elixir')) return generatePhoenixSEO(options);
5268
+ if (name.includes('asp') || name.includes('.net') || name.includes('csharp') || name.includes('c#')) {
5269
+ return generateAspNetCoreSEO(options);
5270
+ }
5271
+ if (name.includes('go') || name.includes('gin') || name.includes('echo') || name.includes('fiber')) {
5272
+ return generateGoSEO(options);
5273
+ }
5274
+
5275
+ // HTMX and hypermedia
5276
+ if (name.includes('htmx') || name.includes('hotwire') || name.includes('turbo')) {
5277
+ return generateHTMXSEO(options);
5278
+ }
5279
+
5280
+ // Static site generators
5281
+ if (name.includes('hugo')) return generateHugoSEO(options);
5282
+ if (name.includes('jekyll')) return generateJekyllSEO(options);
5283
+ if (name.includes('eleventy') || name.includes('11ty')) return generateEleventySEO(options);
5284
+
5285
+ // Default: React with react-helmet-async
5286
+ return generateReactSEOHead(options);
5287
+ }