@rankcli/agent-runtime 0.0.9 → 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 (47) 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 +612 -17
  16. package/dist/index.d.ts +612 -17
  17. package/dist/index.js +9020 -2686
  18. package/dist/index.mjs +4177 -328
  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/frameworks/detector.ts +642 -114
  44. package/src/frameworks/suggestion-engine.ts +38 -1
  45. package/src/index.ts +3 -0
  46. package/src/types.ts +15 -1
  47. package/dist/analyzer-2CSWIQGD.mjs +0 -6
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
- __export,
3
- __require,
2
+ analyzeMobileSEO
3
+ } from "./chunk-M27NQCWW.mjs";
4
+ import {
4
5
  analyzeHeadings,
5
6
  analyzeUrl,
6
7
  checkRobots,
@@ -13,7 +14,41 @@ import {
13
14
  httpGet,
14
15
  httpHead,
15
16
  httpPost
16
- } from "./chunk-YNZYHEYM.mjs";
17
+ } from "./chunk-PJLNXOLN.mjs";
18
+ import {
19
+ AI_CRAWLERS_INFO,
20
+ analyzeCitationReadiness,
21
+ analyzeContentStructure,
22
+ analyzeEntityExtraction,
23
+ analyzeGEO,
24
+ analyzeRobotsTxtForAI,
25
+ calculateLLMSignals,
26
+ detectRenderingMode,
27
+ generateAIFriendlyRobotsTxt
28
+ } from "./chunk-3ZSCLNTW.mjs";
29
+ import {
30
+ analyzeCoreWebVitals
31
+ } from "./chunk-4E4MQOSP.mjs";
32
+ import {
33
+ analyzeSecurityHeaders,
34
+ generateSecurityHeaders
35
+ } from "./chunk-AK2IC22C.mjs";
36
+ import {
37
+ analyzeStructuredData,
38
+ generateSchemaTemplate
39
+ } from "./chunk-VSQD74I7.mjs";
40
+ import {
41
+ analyzeImages,
42
+ generateResponsiveImage
43
+ } from "./chunk-2JADKV3Z.mjs";
44
+ import {
45
+ analyzeInternalLinking,
46
+ suggestInternalLinks
47
+ } from "./chunk-K6VSXDD6.mjs";
48
+ import {
49
+ __export,
50
+ __require
51
+ } from "./chunk-6BWS3CLP.mjs";
17
52
 
18
53
  // src/audit/types.ts
19
54
  var ISSUE_DEFINITIONS = {
@@ -2356,7 +2391,7 @@ async function analyzeLinks(html, baseUrl, checkBroken = false) {
2356
2391
 
2357
2392
  // src/audit/checks/images.ts
2358
2393
  import * as cheerio3 from "cheerio";
2359
- async function analyzeImages(html, baseUrl, checkBroken = false) {
2394
+ async function analyzeImages2(html, baseUrl, checkBroken = false) {
2360
2395
  const issues = [];
2361
2396
  const $ = cheerio3.load(html);
2362
2397
  const missingAlt = [];
@@ -2714,7 +2749,7 @@ var SCHEMA_RECOMMENDED_PROPERTIES = {
2714
2749
  "SoftwareApplication": ["applicationCategory", "offers", "aggregateRating"],
2715
2750
  "VideoObject": ["duration", "embedUrl"]
2716
2751
  };
2717
- function analyzeStructuredData(html, url) {
2752
+ function analyzeStructuredData2(html, url) {
2718
2753
  const issues = [];
2719
2754
  const $ = cheerio5.load(html);
2720
2755
  const schemas = [];
@@ -4508,7 +4543,7 @@ function parseHSTS(header) {
4508
4543
  result.preload = /preload/i.test(header);
4509
4544
  return result;
4510
4545
  }
4511
- async function analyzeSecurityHeaders(url) {
4546
+ async function analyzeSecurityHeaders2(url) {
4512
4547
  const issues = [];
4513
4548
  try {
4514
4549
  const response = await httpGet(url, {
@@ -16645,7 +16680,7 @@ async function runFullAudit(options) {
16645
16680
  const headers = fetchResult.headers;
16646
16681
  console.log(`\u{1F4DD} Phase 2: Running synchronous HTML checks (tier: ${tier}, limit: ${checksLimit})...`);
16647
16682
  const onPageResult = analyzeOnPage(html, url);
16648
- const structuredDataResult = analyzeStructuredData(html, url);
16683
+ const structuredDataResult = analyzeStructuredData2(html, url);
16649
16684
  const mobileResult = analyzeMobile(html, url);
16650
16685
  const mobileResourceIssues = checkMobileResources(html, url);
16651
16686
  const socialResult = analyzeSocialMeta(html, url);
@@ -16780,7 +16815,7 @@ async function runFullAudit(options) {
16780
16815
  });
16781
16816
  const asyncChecks = [];
16782
16817
  asyncChecks.push(safeAsync("Links", () => analyzeLinks(html, url, checkBrokenLinks)));
16783
- asyncChecks.push(safeAsync("Images", () => analyzeImages(html, url, checkBrokenLinks)));
16818
+ asyncChecks.push(safeAsync("Images", () => analyzeImages2(html, url, checkBrokenLinks)));
16784
16819
  asyncChecks.push(performancePromise);
16785
16820
  asyncChecks.push(safeAsync("Security", () => analyzeSecurity(html, url, headers)));
16786
16821
  asyncChecks.push(safeAsyncIssues("Certificate", async () => (await checkCertificate(url)).issues));
@@ -16790,7 +16825,7 @@ async function runFullAudit(options) {
16790
16825
  if (runExtendedChecks) {
16791
16826
  asyncChecks.push(safeAsync("Hreflang", () => analyzeHreflang(html, url, { validateUrls: checkHreflangUrls })));
16792
16827
  asyncChecks.push(safeAsync("Canonical Advanced", () => analyzeCanonicalAdvanced(html, url, { checkChain: checkCanonicalChain })));
16793
- asyncChecks.push(safeAsync("Security Headers", () => analyzeSecurityHeaders(url)));
16828
+ asyncChecks.push(safeAsync("Security Headers", () => analyzeSecurityHeaders2(url)));
16794
16829
  asyncChecks.push(safeAsync("Caching Headers", () => analyzeCachingHeaders(url, headers)));
16795
16830
  asyncChecks.push(safeAsync("HTML Compliance", () => analyzeHtmlCompliance(html, url, headers)));
16796
16831
  asyncChecks.push(safeAsync("Redirect Chain", () => analyzeRedirectChain(url)));
@@ -16890,7 +16925,7 @@ function createReport(url, domain, issues, pages) {
16890
16925
  };
16891
16926
  }
16892
16927
  function calculateHealthScore(issues) {
16893
- const weights = { error: 10, warning: 3, notice: 1 };
16928
+ const weights = { critical: 15, error: 10, warning: 3, info: 1, notice: 1 };
16894
16929
  const categoryDeductions = {
16895
16930
  crawlability: 0,
16896
16931
  indexability: 0,
@@ -17010,8 +17045,8 @@ function groupIssuesByCategory(issues) {
17010
17045
  }
17011
17046
  for (const category of Object.keys(grouped)) {
17012
17047
  grouped[category].sort((a, b) => {
17013
- const order = { error: 0, warning: 1, notice: 2 };
17014
- return order[a.severity] - order[b.severity];
17048
+ const order = { critical: 0, error: 1, warning: 2, info: 3, notice: 4 };
17049
+ return (order[a.severity] ?? 5) - (order[b.severity] ?? 5);
17015
17050
  });
17016
17051
  }
17017
17052
  return grouped;
@@ -25639,7 +25674,7 @@ async function executeAgent(agent, options = {}) {
25639
25674
  }
25640
25675
  }
25641
25676
  async function runDirectAnalysis(url) {
25642
- const { analyzeUrl: analyzeUrl2 } = await import("./analyzer-2CSWIQGD.mjs");
25677
+ const { analyzeUrl: analyzeUrl2 } = await import("./analyzer-GMURJADU.mjs");
25643
25678
  const result = await analyzeUrl2({ url });
25644
25679
  if (!result.success || !result.data) {
25645
25680
  throw new Error(result.error || "Analysis failed");
@@ -27425,213 +27460,3539 @@ export function SEOHead({
27425
27460
  Usage: <SEOHead title="Page" description="..." />`
27426
27461
  };
27427
27462
  }
27463
+ function generateRailsSEOHelper(options) {
27464
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
27465
+ return {
27466
+ file: "app/helpers/seo_helper.rb",
27467
+ code: `# frozen_string_literal: true
27468
+
27469
+ # Comprehensive SEO Helper for Rails
27470
+ #
27471
+ # Features:
27472
+ # - Full Open Graph support
27473
+ # - Twitter Cards
27474
+ # - JSON-LD structured data
27475
+ # - Canonical URLs
27476
+ # - Dynamic meta tags
27477
+ #
27478
+ # Installation: gem 'meta-tags'
27479
+ # Include in ApplicationHelper or use directly
27480
+ module SeoHelper
27481
+ SITE_NAME = '${siteName}'.freeze
27482
+ SITE_URL = '${siteUrl}'.freeze
27483
+ DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}'.freeze
27484
+ DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}'.freeze
27485
+ TWITTER_HANDLE = '${twitterHandle || ""}'.freeze
27486
+
27487
+ # Set comprehensive page SEO
27488
+ #
27489
+ # @param title [String] Page title
27490
+ # @param description [String] Meta description
27491
+ # @param image [String] OG/Twitter image URL
27492
+ # @param type [Symbol] :website, :article, :product
27493
+ # @param options [Hash] Additional options
27494
+ #
27495
+ # @example
27496
+ # set_page_seo(
27497
+ # title: 'Product Name',
27498
+ # description: 'Product description',
27499
+ # type: :product,
27500
+ # schema: product_schema(@product)
27501
+ # )
27502
+ def set_page_seo(title: nil, description: nil, image: nil, type: :website, **options)
27503
+ page_title = title || SITE_NAME
27504
+ page_description = description || DEFAULT_DESCRIPTION
27505
+ page_image = image || DEFAULT_IMAGE
27506
+ page_url = request.original_url
27507
+
27508
+ set_meta_tags(
27509
+ title: page_title,
27510
+ site: SITE_NAME,
27511
+ description: page_description,
27512
+ canonical: page_url,
27513
+ robots: options[:noindex] ? 'noindex, nofollow' : 'index, follow',
27514
+
27515
+ # Open Graph
27516
+ og: {
27517
+ title: page_title,
27518
+ description: page_description,
27519
+ type: type.to_s,
27520
+ url: page_url,
27521
+ image: {
27522
+ _: page_image,
27523
+ width: 1200,
27524
+ height: 630,
27525
+ alt: page_title
27526
+ },
27527
+ site_name: SITE_NAME,
27528
+ locale: options[:locale] || 'en_US'
27529
+ },
27428
27530
 
27429
- // src/fixer.ts
27430
- async function generateFixes(issues, options) {
27431
- const { cwd, url } = options;
27432
- let framework = options.framework;
27433
- if (!framework) {
27434
- const result = await detectFramework3({ cwd });
27435
- framework = result.success ? result.data : { name: "Unknown", metaPattern: "html-head" };
27436
- }
27437
- const fixes = [];
27438
- const htmlEntry = await findHtmlEntry({ cwd });
27439
- const htmlPath = htmlEntry.success ? htmlEntry.data.path : "index.html";
27440
- const htmlContent = htmlEntry.success ? htmlEntry.data.content : "";
27441
- for (const issue of issues) {
27442
- const fix = await generateFixForIssue(issue, {
27443
- cwd,
27444
- url,
27445
- framework,
27446
- htmlPath,
27447
- htmlContent
27448
- });
27449
- if (fix) {
27450
- fixes.push(fix);
27531
+ # Twitter Cards
27532
+ twitter: {
27533
+ card: 'summary_large_image',
27534
+ title: page_title,
27535
+ description: page_description,
27536
+ image: page_image,
27537
+ site: TWITTER_HANDLE.presence,
27538
+ creator: TWITTER_HANDLE.presence
27539
+ }.compact,
27540
+
27541
+ # Article-specific
27542
+ article: if type == :article
27543
+ {
27544
+ published_time: options[:published_at]&.iso8601,
27545
+ modified_time: options[:updated_at]&.iso8601,
27546
+ author: options[:author],
27547
+ section: options[:section],
27548
+ tag: options[:tags]
27549
+ }.compact
27550
+ end
27551
+ )
27552
+
27553
+ # Store schema for JSON-LD rendering
27554
+ @json_ld_schemas = [default_website_schema]
27555
+ @json_ld_schemas << options[:schema] if options[:schema]
27556
+ end
27557
+
27558
+ # Render JSON-LD structured data
27559
+ def json_ld_tags
27560
+ return unless @json_ld_schemas&.any?
27561
+
27562
+ safe_join(@json_ld_schemas.map do |schema|
27563
+ content_tag(:script, schema.to_json.html_safe, type: 'application/ld+json')
27564
+ end)
27565
+ end
27566
+
27567
+ # Schema generators
27568
+ def default_website_schema
27569
+ {
27570
+ '@context': 'https://schema.org',
27571
+ '@type': 'WebSite',
27572
+ name: SITE_NAME,
27573
+ url: SITE_URL
27451
27574
  }
27452
- }
27453
- return fixes;
27454
- }
27455
- async function generateFixForIssue(issue, context) {
27456
- const { cwd, url, framework, htmlPath, htmlContent } = context;
27457
- const fullUrl = url || "https://example.com";
27458
- const siteName = new URL(fullUrl).hostname.replace("www.", "");
27459
- switch (issue.code) {
27460
- // Title issues
27461
- case "MISSING_TITLE":
27462
- case "TITLE_MISSING":
27463
- // Full audit code
27464
- case "TITLE_KEYWORD_MISMATCH":
27465
- case "TITLE_H1_KEYWORD_MISMATCH":
27466
- case "OUTDATED_YEAR_IN_TITLE":
27467
- return generateTitleFix(context, siteName);
27468
- // Meta description issues
27469
- case "MISSING_META_DESC":
27470
- case "META_DESC_MISSING":
27471
- return generateMetaDescFix(context, siteName);
27472
- // Canonical issues
27473
- case "MISSING_CANONICAL":
27474
- case "CANONICAL_MISSING":
27475
- // Full audit code
27476
- case "CANONICAL_NO_HTTPS_REDIRECT":
27477
- return generateCanonicalFix(context, fullUrl);
27478
- // Viewport issues
27479
- case "MISSING_VIEWPORT":
27480
- case "HTML_NO_VIEWPORT":
27481
- case "RESPONSIVE_NO_VIEWPORT":
27482
- case "VIEWPORT_MISSING":
27483
- return generateViewportFix(context);
27484
- // Open Graph issues
27485
- case "MISSING_OG_TAGS":
27486
- case "OG_TITLE_MISSING":
27487
- case "OG_DESC_MISSING":
27488
- case "OG_DESCRIPTION_MISSING":
27489
- // Full audit code
27490
- case "OG_IMAGE_MISSING":
27491
- case "OG_URL_MISSING":
27492
- case "OG_TYPE_MISSING":
27493
- return generateOGFix(context, siteName, fullUrl);
27494
- // Twitter Card issues
27495
- case "MISSING_TWITTER_CARD":
27496
- case "TWITTER_CARD_MISSING":
27497
- case "TWITTER_TITLE_MISSING":
27498
- case "TWITTER_DESC_MISSING":
27499
- case "TWITTER_DESCRIPTION_MISSING":
27500
- // Full audit code
27501
- case "TWITTER_IMAGE_MISSING":
27502
- return generateTwitterFix(context, siteName);
27503
- // Schema/structured data issues
27504
- case "MISSING_SCHEMA":
27505
- case "SCHEMA_MISSING":
27506
- // Full audit code
27507
- case "SCHEMA_ORG_MISSING":
27508
- case "NO_ORGANIZATION_SCHEMA":
27509
- case "NO_ENTITY_SCHEMA":
27510
- case "FAQ_SCHEMA_MISSING":
27511
- return generateSchemaFix(context, siteName, fullUrl);
27512
- // Robots.txt issues
27513
- case "MISSING_ROBOTS":
27514
- case "ROBOTS_TXT_MISSING":
27515
- // Full audit code
27516
- case "ROBOTS_TXT_WARNINGS":
27517
- case "ROBOTS_TXT_INVALID_SYNTAX":
27518
- return generateRobotsFix(context, fullUrl);
27519
- // Sitemap issues
27520
- case "MISSING_SITEMAP":
27521
- case "SITEMAP_MISSING":
27522
- // Full audit code
27523
- case "BING_SITEMAP_MISSING":
27524
- return generateSitemapFix(context, fullUrl);
27525
- // H1 issues
27526
- case "MISSING_H1":
27527
- case "H1_MISSING":
27528
- // Full audit code
27529
- case "NO_VISIBLE_HEADLINE":
27530
- case "H1_MISSING_KEYWORD":
27531
- return await generateH1Fix({ cwd });
27532
- // SPA-specific: add meta management library recommendation
27533
- case "SPA_NO_META_MANAGEMENT":
27534
- case "SPA_WITHOUT_SSR":
27535
- case "CLIENT_SIDE_RENDERING":
27536
- return generateSPAMetaFix(context, framework);
27537
- // Internal links - suggest adding links
27538
- case "NO_INTERNAL_LINKS":
27539
- case "LINKS_NO_INTERNAL":
27540
- case "NO_CONTENT_INTERNAL_LINKS":
27541
- return {
27542
- issue: { code: issue.code, message: "No internal links found", severity: "warning" },
27543
- file: "src/components/Footer.tsx",
27544
- before: null,
27545
- after: `// Add internal navigation links to your footer or content
27546
- // Example: <Link to="/about">About</Link>`,
27547
- explanation: "Add internal links to help search engines discover your content",
27548
- skipped: true,
27549
- skipReason: "Requires manual content updates - add links to your navigation and content"
27550
- };
27551
- // Preconnect issues
27552
- case "GOOGLE_FONTS_NO_PRECONNECT":
27553
- case "MISSING_PRECONNECT":
27554
- return generatePreconnectFix(context);
27555
- // Favicon/icons
27556
- case "HTML_NO_FAVICON":
27557
- case "HTML_NO_APPLE_TOUCH_ICON":
27558
- case "FAVICON_MISSING":
27559
- return generateFaviconFix(context);
27560
- // Charset/lang
27561
- case "HTML_NO_CHARSET":
27562
- case "HTML_NOT_UTF8":
27563
- return generateCharsetFix(context);
27564
- case "HTML_NO_LANG":
27565
- case "LANG_ATTR_MISSING":
27566
- return generateLangFix(context);
27567
- // AI/LLMs.txt
27568
- case "AI_NO_LLMS_TXT":
27569
- return generateLlmsTxtFix(context, siteName, fullUrl);
27570
- default:
27571
- return null;
27572
- }
27573
- }
27574
- function generateTitleFix(context, siteName) {
27575
- const { htmlPath, htmlContent, framework } = context;
27576
- if (framework.metaPattern === "html-head") {
27577
- const titleTag = `<title>${siteName} - Your Product Tagline</title>`;
27578
- if (htmlContent.includes("<title>")) {
27579
- const before = htmlContent.match(/<title>.*?<\/title>/)?.[0] || "";
27580
- return {
27581
- issue: { code: "MISSING_TITLE", message: "Missing or empty title tag", severity: "critical" },
27582
- file: htmlPath,
27583
- before,
27584
- after: titleTag,
27585
- explanation: "Updated the title tag with a descriptive title"
27586
- };
27587
- } else {
27588
- return {
27589
- issue: { code: "MISSING_TITLE", message: "Missing title tag", severity: "critical" },
27590
- file: htmlPath,
27591
- before: "<head>",
27592
- after: `<head>
27593
- ${titleTag}`,
27594
- explanation: "Added a title tag to the document head"
27595
- };
27575
+ end
27576
+
27577
+ def organization_schema(name: SITE_NAME, url: SITE_URL, logo: nil, social: [])
27578
+ {
27579
+ '@context': 'https://schema.org',
27580
+ '@type': 'Organization',
27581
+ name: name,
27582
+ url: url,
27583
+ logo: logo,
27584
+ sameAs: social
27585
+ }.compact
27586
+ end
27587
+
27588
+ def article_schema(article)
27589
+ {
27590
+ '@context': 'https://schema.org',
27591
+ '@type': 'Article',
27592
+ headline: article.title,
27593
+ description: article.excerpt || truncate(strip_tags(article.content), length: 160),
27594
+ image: article.image_url || DEFAULT_IMAGE,
27595
+ datePublished: article.published_at&.iso8601,
27596
+ dateModified: article.updated_at&.iso8601,
27597
+ author: {
27598
+ '@type': 'Person',
27599
+ name: article.author_name
27600
+ },
27601
+ publisher: {
27602
+ '@type': 'Organization',
27603
+ name: SITE_NAME,
27604
+ logo: { '@type': 'ImageObject', url: "#{SITE_URL}/logo.png" }
27605
+ }
27596
27606
  }
27597
- }
27598
- return {
27599
- issue: { code: "MISSING_TITLE", message: "Missing title tag", severity: "critical" },
27600
- file: htmlPath,
27601
- before: null,
27602
- after: `<title>${siteName} - Your Product Tagline</title>`,
27603
- explanation: `Add title using ${framework.name} patterns`
27604
- };
27605
- }
27606
- function generateMetaDescFix(context, siteName) {
27607
- const { htmlPath, htmlContent, framework } = context;
27608
- const description = `${siteName} - A brief, compelling description of your product or service. Include key features and benefits.`;
27609
- if (framework.metaPattern === "html-head") {
27610
- const metaTag = `<meta name="description" content="${description}" />`;
27611
- if (htmlContent.includes("<title>")) {
27612
- return {
27613
- issue: { code: "MISSING_META_DESC", message: "Missing meta description", severity: "critical" },
27614
- file: htmlPath,
27615
- before: "</title>",
27616
- after: `</title>
27617
- ${metaTag}`,
27618
- explanation: "Added meta description after the title tag"
27619
- };
27620
- } else {
27621
- return {
27622
- issue: { code: "MISSING_META_DESC", message: "Missing meta description", severity: "critical" },
27623
- file: htmlPath,
27624
- before: "<head>",
27625
- after: `<head>
27626
- ${metaTag}`,
27627
- explanation: "Added meta description to the document head"
27628
- };
27607
+ end
27608
+
27609
+ def product_schema(product)
27610
+ {
27611
+ '@context': 'https://schema.org',
27612
+ '@type': 'Product',
27613
+ name: product.name,
27614
+ description: product.description,
27615
+ image: product.image_url,
27616
+ brand: product.brand.present? ? { '@type': 'Brand', name: product.brand } : nil,
27617
+ sku: product.sku,
27618
+ offers: {
27619
+ '@type': 'Offer',
27620
+ price: product.price.to_s,
27621
+ priceCurrency: product.currency || 'USD',
27622
+ availability: "https://schema.org/#{product.in_stock? ? 'InStock' : 'OutOfStock'}"
27623
+ },
27624
+ aggregateRating: product.reviews_count.positive? ? {
27625
+ '@type': 'AggregateRating',
27626
+ ratingValue: product.average_rating,
27627
+ reviewCount: product.reviews_count
27628
+ } : nil
27629
+ }.compact
27630
+ end
27631
+
27632
+ def faq_schema(items)
27633
+ {
27634
+ '@context': 'https://schema.org',
27635
+ '@type': 'FAQPage',
27636
+ mainEntity: items.map do |item|
27637
+ {
27638
+ '@type': 'Question',
27639
+ name: item[:question],
27640
+ acceptedAnswer: { '@type': 'Answer', text: item[:answer] }
27641
+ }
27642
+ end
27629
27643
  }
27630
- }
27631
- return {
27632
- issue: { code: "MISSING_META_DESC", message: "Missing meta description", severity: "critical" },
27633
- file: htmlPath,
27634
- before: null,
27644
+ end
27645
+
27646
+ def breadcrumb_schema(items)
27647
+ {
27648
+ '@context': 'https://schema.org',
27649
+ '@type': 'BreadcrumbList',
27650
+ itemListElement: items.each_with_index.map do |item, index|
27651
+ {
27652
+ '@type': 'ListItem',
27653
+ position: index + 1,
27654
+ name: item[:name],
27655
+ item: item[:url]
27656
+ }
27657
+ end
27658
+ }
27659
+ end
27660
+
27661
+ def local_business_schema(business)
27662
+ {
27663
+ '@context': 'https://schema.org',
27664
+ '@type': 'LocalBusiness',
27665
+ name: business[:name],
27666
+ description: business[:description],
27667
+ url: business[:url],
27668
+ telephone: business[:phone],
27669
+ address: {
27670
+ '@type': 'PostalAddress',
27671
+ streetAddress: business.dig(:address, :street),
27672
+ addressLocality: business.dig(:address, :city),
27673
+ addressRegion: business.dig(:address, :state),
27674
+ postalCode: business.dig(:address, :zip),
27675
+ addressCountry: business.dig(:address, :country)
27676
+ },
27677
+ geo: business[:coordinates].present? ? {
27678
+ '@type': 'GeoCoordinates',
27679
+ latitude: business.dig(:coordinates, :lat),
27680
+ longitude: business.dig(:coordinates, :lng)
27681
+ } : nil,
27682
+ openingHours: business[:hours],
27683
+ priceRange: business[:price_range]
27684
+ }.compact
27685
+ end
27686
+ end`,
27687
+ explanation: `Ruby on Rails comprehensive SEO helper with:
27688
+ \u2022 meta-tags gem integration
27689
+ \u2022 Full Open Graph with article support
27690
+ \u2022 Twitter Cards
27691
+ \u2022 JSON-LD schema generators for all common types
27692
+ \u2022 Canonical URLs and robots directives
27693
+
27694
+ Setup:
27695
+ 1. Add to Gemfile: gem 'meta-tags'
27696
+ 2. Include helper in ApplicationHelper
27697
+ 3. Call set_page_seo in controllers
27698
+ 4. Add <%= display_meta_tags %> and <%= json_ld_tags %> to layout`,
27699
+ installCommands: ["bundle add meta-tags"],
27700
+ additionalFiles: [
27701
+ {
27702
+ file: "app/views/layouts/application.html.erb",
27703
+ code: `<!DOCTYPE html>
27704
+ <html lang="<%= I18n.locale %>">
27705
+ <head>
27706
+ <meta charset="utf-8">
27707
+ <meta name="viewport" content="width=device-width, initial-scale=1">
27708
+
27709
+ <%= display_meta_tags %>
27710
+ <%= csrf_meta_tags %>
27711
+ <%= csp_meta_tag %>
27712
+
27713
+ <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
27714
+ <%= javascript_importmap_tags %>
27715
+
27716
+ <!-- Favicon -->
27717
+ <%= favicon_link_tag 'favicon.ico' %>
27718
+ <%= favicon_link_tag 'apple-touch-icon.png', rel: 'apple-touch-icon', type: 'image/png', sizes: '180x180' %>
27719
+
27720
+ <!-- JSON-LD Structured Data -->
27721
+ <%= json_ld_tags %>
27722
+ </head>
27723
+ <body>
27724
+ <%= yield %>
27725
+ </body>
27726
+ </html>`,
27727
+ explanation: "Application layout with SEO meta tags and JSON-LD."
27728
+ },
27729
+ {
27730
+ file: "config/initializers/meta_tags.rb",
27731
+ code: `# frozen_string_literal: true
27732
+
27733
+ MetaTags.configure do |config|
27734
+ # Use title template
27735
+ config.title_limit = 70
27736
+ config.description_limit = 160
27737
+ config.keywords_limit = 255
27738
+
27739
+ # Truncate long strings
27740
+ config.truncate_on_bytesize = true
27741
+
27742
+ # Separate site name with |
27743
+ config.title_separator = ' | '
27744
+ end`,
27745
+ explanation: "meta-tags gem configuration."
27746
+ },
27747
+ {
27748
+ file: "public/robots.txt",
27749
+ code: `User-agent: *
27750
+ Allow: /
27751
+ Disallow: /admin/
27752
+ Disallow: /api/
27753
+
27754
+ User-agent: GPTBot
27755
+ Allow: /
27756
+
27757
+ Sitemap: ${siteUrl}/sitemap.xml`,
27758
+ explanation: "Robots.txt with AI crawler support."
27759
+ }
27760
+ ]
27761
+ };
27762
+ }
27763
+ function generateDjangoSEOHelper(options) {
27764
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
27765
+ return {
27766
+ file: "core/seo.py",
27767
+ code: `"""
27768
+ Comprehensive SEO utilities for Django
27769
+
27770
+ Features:
27771
+ - Full Open Graph support
27772
+ - Twitter Cards
27773
+ - JSON-LD structured data
27774
+ - Canonical URLs
27775
+ - Template tags and context processors
27776
+
27777
+ Installation: pip install django-meta
27778
+ """
27779
+
27780
+ from django.conf import settings
27781
+ from django.utils.safestring import mark_safe
27782
+ import json
27783
+ from typing import Any, Optional
27784
+ from dataclasses import dataclass, field
27785
+
27786
+
27787
+ # Configuration
27788
+ SITE_NAME = '${siteName}'
27789
+ SITE_URL = '${siteUrl}'
27790
+ DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}'
27791
+ DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}'
27792
+ TWITTER_HANDLE = '${twitterHandle || ""}'
27793
+
27794
+
27795
+ @dataclass
27796
+ class SEOMeta:
27797
+ """SEO metadata container"""
27798
+ title: Optional[str] = None
27799
+ description: str = DEFAULT_DESCRIPTION
27800
+ image: str = DEFAULT_IMAGE
27801
+ url: Optional[str] = None
27802
+ type: str = 'website'
27803
+
27804
+ # Article specific
27805
+ published_time: Optional[str] = None
27806
+ modified_time: Optional[str] = None
27807
+ author: Optional[str] = None
27808
+ tags: list = field(default_factory=list)
27809
+
27810
+ # Robots
27811
+ noindex: bool = False
27812
+ nofollow: bool = False
27813
+
27814
+ # Structured data
27815
+ schema: Optional[dict] = None
27816
+
27817
+ def get_full_title(self) -> str:
27818
+ if not self.title:
27819
+ return SITE_NAME
27820
+ if SITE_NAME in self.title:
27821
+ return self.title
27822
+ return f"{self.title} | {SITE_NAME}"
27823
+
27824
+ def get_image_url(self) -> str:
27825
+ if self.image.startswith('http'):
27826
+ return self.image
27827
+ return f"{SITE_URL}{self.image}"
27828
+
27829
+ def get_robots(self) -> str:
27830
+ index = 'noindex' if self.noindex else 'index'
27831
+ follow = 'nofollow' if self.nofollow else 'follow'
27832
+ return f"{index}, {follow}"
27833
+
27834
+
27835
+ class SEOContextMixin:
27836
+ """
27837
+ Mixin for class-based views to add SEO context
27838
+
27839
+ Usage:
27840
+ class ArticleDetailView(SEOContextMixin, DetailView):
27841
+ model = Article
27842
+
27843
+ def get_seo_meta(self):
27844
+ return SEOMeta(
27845
+ title=self.object.title,
27846
+ description=self.object.excerpt,
27847
+ image=self.object.image.url if self.object.image else None,
27848
+ type='article',
27849
+ published_time=self.object.published_at.isoformat(),
27850
+ schema=article_schema(self.object)
27851
+ )
27852
+ """
27853
+
27854
+ def get_seo_meta(self) -> SEOMeta:
27855
+ return SEOMeta()
27856
+
27857
+ def get_context_data(self, **kwargs):
27858
+ context = super().get_context_data(**kwargs)
27859
+ context['seo'] = self.get_seo_meta()
27860
+ return context
27861
+
27862
+
27863
+ def seo_context_processor(request):
27864
+ """
27865
+ Context processor to add default SEO values
27866
+
27867
+ Add to settings.TEMPLATES[0]['OPTIONS']['context_processors']
27868
+ """
27869
+ return {
27870
+ 'site_name': SITE_NAME,
27871
+ 'site_url': SITE_URL,
27872
+ 'default_og_image': DEFAULT_IMAGE,
27873
+ }
27874
+
27875
+
27876
+ # JSON-LD Schema Generators
27877
+
27878
+ def website_schema() -> dict:
27879
+ return {
27880
+ '@context': 'https://schema.org',
27881
+ '@type': 'WebSite',
27882
+ 'name': SITE_NAME,
27883
+ 'url': SITE_URL,
27884
+ }
27885
+
27886
+
27887
+ def organization_schema(
27888
+ name: str = SITE_NAME,
27889
+ url: str = SITE_URL,
27890
+ logo: Optional[str] = None,
27891
+ social_profiles: list = None
27892
+ ) -> dict:
27893
+ schema = {
27894
+ '@context': 'https://schema.org',
27895
+ '@type': 'Organization',
27896
+ 'name': name,
27897
+ 'url': url,
27898
+ }
27899
+ if logo:
27900
+ schema['logo'] = logo
27901
+ if social_profiles:
27902
+ schema['sameAs'] = social_profiles
27903
+ return schema
27904
+
27905
+
27906
+ def article_schema(article) -> dict:
27907
+ """Generate Article schema from model instance"""
27908
+ return {
27909
+ '@context': 'https://schema.org',
27910
+ '@type': 'Article',
27911
+ 'headline': article.title,
27912
+ 'description': getattr(article, 'excerpt', '') or article.content[:160],
27913
+ 'image': article.image.url if hasattr(article, 'image') and article.image else DEFAULT_IMAGE,
27914
+ 'datePublished': article.published_at.isoformat() if hasattr(article, 'published_at') else None,
27915
+ 'dateModified': article.updated_at.isoformat() if hasattr(article, 'updated_at') else None,
27916
+ 'author': {
27917
+ '@type': 'Person',
27918
+ 'name': str(article.author) if hasattr(article, 'author') else 'Unknown',
27919
+ },
27920
+ 'publisher': {
27921
+ '@type': 'Organization',
27922
+ 'name': SITE_NAME,
27923
+ 'logo': {'@type': 'ImageObject', 'url': f'{SITE_URL}/static/logo.png'},
27924
+ },
27925
+ }
27926
+
27927
+
27928
+ def product_schema(product) -> dict:
27929
+ """Generate Product schema from model instance"""
27930
+ schema = {
27931
+ '@context': 'https://schema.org',
27932
+ '@type': 'Product',
27933
+ 'name': product.name,
27934
+ 'description': product.description,
27935
+ 'image': product.image.url if hasattr(product, 'image') and product.image else DEFAULT_IMAGE,
27936
+ 'offers': {
27937
+ '@type': 'Offer',
27938
+ 'price': str(product.price),
27939
+ 'priceCurrency': getattr(product, 'currency', 'USD'),
27940
+ 'availability': f"https://schema.org/{'InStock' if product.in_stock else 'OutOfStock'}",
27941
+ },
27942
+ }
27943
+
27944
+ if hasattr(product, 'brand') and product.brand:
27945
+ schema['brand'] = {'@type': 'Brand', 'name': product.brand}
27946
+
27947
+ if hasattr(product, 'sku') and product.sku:
27948
+ schema['sku'] = product.sku
27949
+
27950
+ if hasattr(product, 'average_rating') and hasattr(product, 'review_count'):
27951
+ if product.review_count > 0:
27952
+ schema['aggregateRating'] = {
27953
+ '@type': 'AggregateRating',
27954
+ 'ratingValue': product.average_rating,
27955
+ 'reviewCount': product.review_count,
27956
+ }
27957
+
27958
+ return schema
27959
+
27960
+
27961
+ def faq_schema(items: list[dict]) -> dict:
27962
+ """Generate FAQ schema from list of {question, answer} dicts"""
27963
+ return {
27964
+ '@context': 'https://schema.org',
27965
+ '@type': 'FAQPage',
27966
+ 'mainEntity': [
27967
+ {
27968
+ '@type': 'Question',
27969
+ 'name': item['question'],
27970
+ 'acceptedAnswer': {'@type': 'Answer', 'text': item['answer']},
27971
+ }
27972
+ for item in items
27973
+ ],
27974
+ }
27975
+
27976
+
27977
+ def breadcrumb_schema(items: list[dict]) -> dict:
27978
+ """Generate Breadcrumb schema from list of {name, url} dicts"""
27979
+ return {
27980
+ '@context': 'https://schema.org',
27981
+ '@type': 'BreadcrumbList',
27982
+ 'itemListElement': [
27983
+ {
27984
+ '@type': 'ListItem',
27985
+ 'position': i + 1,
27986
+ 'name': item['name'],
27987
+ 'item': item['url'],
27988
+ }
27989
+ for i, item in enumerate(items)
27990
+ ],
27991
+ }
27992
+
27993
+
27994
+ def local_business_schema(business: dict) -> dict:
27995
+ """Generate LocalBusiness schema"""
27996
+ schema = {
27997
+ '@context': 'https://schema.org',
27998
+ '@type': 'LocalBusiness',
27999
+ 'name': business['name'],
28000
+ 'description': business.get('description', ''),
28001
+ 'url': business.get('url', SITE_URL),
28002
+ 'telephone': business.get('phone'),
28003
+ 'address': {
28004
+ '@type': 'PostalAddress',
28005
+ 'streetAddress': business.get('address', {}).get('street'),
28006
+ 'addressLocality': business.get('address', {}).get('city'),
28007
+ 'addressRegion': business.get('address', {}).get('state'),
28008
+ 'postalCode': business.get('address', {}).get('zip'),
28009
+ 'addressCountry': business.get('address', {}).get('country'),
28010
+ },
28011
+ }
28012
+
28013
+ if business.get('coordinates'):
28014
+ schema['geo'] = {
28015
+ '@type': 'GeoCoordinates',
28016
+ 'latitude': business['coordinates']['lat'],
28017
+ 'longitude': business['coordinates']['lng'],
28018
+ }
28019
+
28020
+ if business.get('hours'):
28021
+ schema['openingHours'] = business['hours']
28022
+
28023
+ if business.get('price_range'):
28024
+ schema['priceRange'] = business['price_range']
28025
+
28026
+ return schema
28027
+
28028
+
28029
+ def render_json_ld(*schemas) -> str:
28030
+ """Render JSON-LD script tags for templates"""
28031
+ all_schemas = [website_schema()] + list(schemas)
28032
+ scripts = '\\n'.join(
28033
+ f'<script type="application/ld+json">{json.dumps(s)}</script>'
28034
+ for s in all_schemas if s
28035
+ )
28036
+ return mark_safe(scripts)`,
28037
+ explanation: `Django comprehensive SEO module with:
28038
+ \u2022 SEOMeta dataclass for structured metadata
28039
+ \u2022 SEOContextMixin for class-based views
28040
+ \u2022 Context processor for global SEO values
28041
+ \u2022 JSON-LD schema generators
28042
+ \u2022 Template-ready helpers
28043
+
28044
+ Setup:
28045
+ 1. pip install django-meta
28046
+ 2. Add context processor to settings
28047
+ 3. Use SEOContextMixin in views
28048
+ 4. Include seo template tags in base.html`,
28049
+ installCommands: ["pip install django-meta"],
28050
+ additionalFiles: [
28051
+ {
28052
+ file: "templates/base.html",
28053
+ code: `<!DOCTYPE html>
28054
+ <html lang="{{ LANGUAGE_CODE }}">
28055
+ <head>
28056
+ <meta charset="utf-8">
28057
+ <meta name="viewport" content="width=device-width, initial-scale=1">
28058
+
28059
+ {% if seo %}
28060
+ <!-- Primary Meta Tags -->
28061
+ <title>{{ seo.get_full_title }}</title>
28062
+ <meta name="title" content="{{ seo.get_full_title }}">
28063
+ <meta name="description" content="{{ seo.description }}">
28064
+ <meta name="robots" content="{{ seo.get_robots }}">
28065
+ <link rel="canonical" href="{{ seo.url|default:request.build_absolute_uri }}">
28066
+
28067
+ <!-- Open Graph / Facebook -->
28068
+ <meta property="og:type" content="{{ seo.type }}">
28069
+ <meta property="og:url" content="{{ seo.url|default:request.build_absolute_uri }}">
28070
+ <meta property="og:title" content="{{ seo.get_full_title }}">
28071
+ <meta property="og:description" content="{{ seo.description }}">
28072
+ <meta property="og:image" content="{{ seo.get_image_url }}">
28073
+ <meta property="og:image:width" content="1200">
28074
+ <meta property="og:image:height" content="630">
28075
+ <meta property="og:site_name" content="{{ site_name }}">
28076
+
28077
+ {% if seo.type == 'article' %}
28078
+ {% if seo.published_time %}<meta property="article:published_time" content="{{ seo.published_time }}">{% endif %}
28079
+ {% if seo.modified_time %}<meta property="article:modified_time" content="{{ seo.modified_time }}">{% endif %}
28080
+ {% if seo.author %}<meta property="article:author" content="{{ seo.author }}">{% endif %}
28081
+ {% for tag in seo.tags %}<meta property="article:tag" content="{{ tag }}">{% endfor %}
28082
+ {% endif %}
28083
+
28084
+ <!-- Twitter -->
28085
+ <meta name="twitter:card" content="summary_large_image">
28086
+ <meta name="twitter:title" content="{{ seo.get_full_title }}">
28087
+ <meta name="twitter:description" content="{{ seo.description }}">
28088
+ <meta name="twitter:image" content="{{ seo.get_image_url }}">
28089
+ {% else %}
28090
+ <title>{{ site_name }}</title>
28091
+ {% endif %}
28092
+
28093
+ {% load static %}
28094
+ <link rel="stylesheet" href="{% static 'css/styles.css' %}">
28095
+
28096
+ <!-- JSON-LD Structured Data -->
28097
+ {% block json_ld %}{% endblock %}
28098
+ </head>
28099
+ <body>
28100
+ {% block content %}{% endblock %}
28101
+ </body>
28102
+ </html>`,
28103
+ explanation: "Django base template with comprehensive SEO meta tags."
28104
+ },
28105
+ {
28106
+ file: "robots.txt",
28107
+ code: `User-agent: *
28108
+ Allow: /
28109
+ Disallow: /admin/
28110
+ Disallow: /api/
28111
+
28112
+ User-agent: GPTBot
28113
+ Allow: /
28114
+
28115
+ Sitemap: ${siteUrl}/sitemap.xml`,
28116
+ explanation: "Robots.txt with AI crawler support."
28117
+ }
28118
+ ]
28119
+ };
28120
+ }
28121
+ function generateLaravelSEOHelper(options) {
28122
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
28123
+ return {
28124
+ file: "app/Services/SEOService.php",
28125
+ code: `<?php
28126
+
28127
+ namespace App\\Services;
28128
+
28129
+ use Illuminate\\Support\\Facades\\View;
28130
+ use Illuminate\\Support\\HtmlString;
28131
+
28132
+ /**
28133
+ * Comprehensive SEO Service for Laravel
28134
+ *
28135
+ * Features:
28136
+ * - Full Open Graph support
28137
+ * - Twitter Cards
28138
+ * - JSON-LD structured data
28139
+ * - Canonical URLs
28140
+ * - Blade integration
28141
+ */
28142
+ class SEOService
28143
+ {
28144
+ public const SITE_NAME = '${siteName}';
28145
+ public const SITE_URL = '${siteUrl}';
28146
+ public const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
28147
+ public const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
28148
+ public const TWITTER_HANDLE = '${twitterHandle || ""}';
28149
+
28150
+ protected array $meta = [];
28151
+ protected array $schemas = [];
28152
+
28153
+ /**
28154
+ * Set page SEO metadata
28155
+ */
28156
+ public function setMeta(array $options): self
28157
+ {
28158
+ $this->meta = array_merge([
28159
+ 'title' => null,
28160
+ 'description' => self::DEFAULT_DESCRIPTION,
28161
+ 'image' => self::DEFAULT_IMAGE,
28162
+ 'url' => request()->url(),
28163
+ 'type' => 'website',
28164
+ 'published_time' => null,
28165
+ 'modified_time' => null,
28166
+ 'author' => null,
28167
+ 'tags' => [],
28168
+ 'noindex' => false,
28169
+ 'nofollow' => false,
28170
+ ], $options);
28171
+
28172
+ return $this;
28173
+ }
28174
+
28175
+ /**
28176
+ * Add JSON-LD schema
28177
+ */
28178
+ public function addSchema(array $schema): self
28179
+ {
28180
+ $this->schemas[] = $schema;
28181
+ return $this;
28182
+ }
28183
+
28184
+ /**
28185
+ * Get full page title
28186
+ */
28187
+ public function getFullTitle(): string
28188
+ {
28189
+ $title = $this->meta['title'] ?? null;
28190
+
28191
+ if (!$title) {
28192
+ return self::SITE_NAME;
28193
+ }
28194
+
28195
+ if (str_contains($title, self::SITE_NAME)) {
28196
+ return $title;
28197
+ }
28198
+
28199
+ return "{$title} | " . self::SITE_NAME;
28200
+ }
28201
+
28202
+ /**
28203
+ * Get absolute image URL
28204
+ */
28205
+ public function getImageUrl(): string
28206
+ {
28207
+ $image = $this->meta['image'] ?? self::DEFAULT_IMAGE;
28208
+
28209
+ if (str_starts_with($image, 'http')) {
28210
+ return $image;
28211
+ }
28212
+
28213
+ return self::SITE_URL . $image;
28214
+ }
28215
+
28216
+ /**
28217
+ * Get robots directive
28218
+ */
28219
+ public function getRobots(): string
28220
+ {
28221
+ $index = ($this->meta['noindex'] ?? false) ? 'noindex' : 'index';
28222
+ $follow = ($this->meta['nofollow'] ?? false) ? 'nofollow' : 'follow';
28223
+ return "{$index}, {$follow}";
28224
+ }
28225
+
28226
+ /**
28227
+ * Render all meta tags
28228
+ */
28229
+ public function render(): HtmlString
28230
+ {
28231
+ $html = view('components.seo-meta', [
28232
+ 'seo' => $this,
28233
+ 'meta' => $this->meta,
28234
+ ])->render();
28235
+
28236
+ return new HtmlString($html);
28237
+ }
28238
+
28239
+ /**
28240
+ * Render JSON-LD scripts
28241
+ */
28242
+ public function renderJsonLd(): HtmlString
28243
+ {
28244
+ $allSchemas = array_merge(
28245
+ [$this->websiteSchema()],
28246
+ $this->schemas
28247
+ );
28248
+
28249
+ $scripts = collect($allSchemas)
28250
+ ->filter()
28251
+ ->map(fn($schema) => '<script type="application/ld+json">' . json_encode($schema) . '</script>')
28252
+ ->implode("\\n");
28253
+
28254
+ return new HtmlString($scripts);
28255
+ }
28256
+
28257
+ // Schema Generators
28258
+
28259
+ public function websiteSchema(): array
28260
+ {
28261
+ return [
28262
+ '@context' => 'https://schema.org',
28263
+ '@type' => 'WebSite',
28264
+ 'name' => self::SITE_NAME,
28265
+ 'url' => self::SITE_URL,
28266
+ ];
28267
+ }
28268
+
28269
+ public function organizationSchema(
28270
+ ?string $name = null,
28271
+ ?string $url = null,
28272
+ ?string $logo = null,
28273
+ array $socialProfiles = []
28274
+ ): array {
28275
+ return array_filter([
28276
+ '@context' => 'https://schema.org',
28277
+ '@type' => 'Organization',
28278
+ 'name' => $name ?? self::SITE_NAME,
28279
+ 'url' => $url ?? self::SITE_URL,
28280
+ 'logo' => $logo,
28281
+ 'sameAs' => $socialProfiles ?: null,
28282
+ ]);
28283
+ }
28284
+
28285
+ public function articleSchema($article): array
28286
+ {
28287
+ return [
28288
+ '@context' => 'https://schema.org',
28289
+ '@type' => 'Article',
28290
+ 'headline' => $article->title,
28291
+ 'description' => $article->excerpt ?? substr(strip_tags($article->content), 0, 160),
28292
+ 'image' => $article->image_url ?? self::DEFAULT_IMAGE,
28293
+ 'datePublished' => optional($article->published_at)->toIso8601String(),
28294
+ 'dateModified' => optional($article->updated_at)->toIso8601String(),
28295
+ 'author' => [
28296
+ '@type' => 'Person',
28297
+ 'name' => $article->author?->name ?? 'Unknown',
28298
+ ],
28299
+ 'publisher' => [
28300
+ '@type' => 'Organization',
28301
+ 'name' => self::SITE_NAME,
28302
+ 'logo' => [
28303
+ '@type' => 'ImageObject',
28304
+ 'url' => self::SITE_URL . '/logo.png',
28305
+ ],
28306
+ ],
28307
+ ];
28308
+ }
28309
+
28310
+ public function productSchema($product): array
28311
+ {
28312
+ $schema = [
28313
+ '@context' => 'https://schema.org',
28314
+ '@type' => 'Product',
28315
+ 'name' => $product->name,
28316
+ 'description' => $product->description,
28317
+ 'image' => $product->image_url ?? self::DEFAULT_IMAGE,
28318
+ 'offers' => [
28319
+ '@type' => 'Offer',
28320
+ 'price' => (string) $product->price,
28321
+ 'priceCurrency' => $product->currency ?? 'USD',
28322
+ 'availability' => 'https://schema.org/' . ($product->in_stock ? 'InStock' : 'OutOfStock'),
28323
+ ],
28324
+ ];
28325
+
28326
+ if ($product->brand) {
28327
+ $schema['brand'] = ['@type' => 'Brand', 'name' => $product->brand];
28328
+ }
28329
+
28330
+ if ($product->sku) {
28331
+ $schema['sku'] = $product->sku;
28332
+ }
28333
+
28334
+ if ($product->reviews_count > 0) {
28335
+ $schema['aggregateRating'] = [
28336
+ '@type' => 'AggregateRating',
28337
+ 'ratingValue' => $product->average_rating,
28338
+ 'reviewCount' => $product->reviews_count,
28339
+ ];
28340
+ }
28341
+
28342
+ return $schema;
28343
+ }
28344
+
28345
+ public function faqSchema(array $items): array
28346
+ {
28347
+ return [
28348
+ '@context' => 'https://schema.org',
28349
+ '@type' => 'FAQPage',
28350
+ 'mainEntity' => collect($items)->map(fn($item) => [
28351
+ '@type' => 'Question',
28352
+ 'name' => $item['question'],
28353
+ 'acceptedAnswer' => ['@type' => 'Answer', 'text' => $item['answer']],
28354
+ ])->all(),
28355
+ ];
28356
+ }
28357
+
28358
+ public function breadcrumbSchema(array $items): array
28359
+ {
28360
+ return [
28361
+ '@context' => 'https://schema.org',
28362
+ '@type' => 'BreadcrumbList',
28363
+ 'itemListElement' => collect($items)->map(fn($item, $index) => [
28364
+ '@type' => 'ListItem',
28365
+ 'position' => $index + 1,
28366
+ 'name' => $item['name'],
28367
+ 'item' => $item['url'],
28368
+ ])->all(),
28369
+ ];
28370
+ }
28371
+
28372
+ public function localBusinessSchema(array $business): array
28373
+ {
28374
+ return array_filter([
28375
+ '@context' => 'https://schema.org',
28376
+ '@type' => 'LocalBusiness',
28377
+ 'name' => $business['name'],
28378
+ 'description' => $business['description'] ?? null,
28379
+ 'url' => $business['url'] ?? self::SITE_URL,
28380
+ 'telephone' => $business['phone'] ?? null,
28381
+ 'address' => [
28382
+ '@type' => 'PostalAddress',
28383
+ 'streetAddress' => $business['address']['street'] ?? null,
28384
+ 'addressLocality' => $business['address']['city'] ?? null,
28385
+ 'addressRegion' => $business['address']['state'] ?? null,
28386
+ 'postalCode' => $business['address']['zip'] ?? null,
28387
+ 'addressCountry' => $business['address']['country'] ?? null,
28388
+ ],
28389
+ 'geo' => isset($business['coordinates']) ? [
28390
+ '@type' => 'GeoCoordinates',
28391
+ 'latitude' => $business['coordinates']['lat'],
28392
+ 'longitude' => $business['coordinates']['lng'],
28393
+ ] : null,
28394
+ 'openingHours' => $business['hours'] ?? null,
28395
+ 'priceRange' => $business['price_range'] ?? null,
28396
+ ]);
28397
+ }
28398
+ }`,
28399
+ explanation: `Laravel comprehensive SEO service with:
28400
+ \u2022 Fluent API for setting metadata
28401
+ \u2022 Full Open Graph with article support
28402
+ \u2022 Twitter Cards
28403
+ \u2022 JSON-LD schema generators
28404
+ \u2022 Blade component integration
28405
+
28406
+ Setup:
28407
+ 1. Register SEOService in AppServiceProvider
28408
+ 2. Use @seo directive in Blade templates
28409
+ 3. Call SEO()->setMeta() in controllers`,
28410
+ additionalFiles: [
28411
+ {
28412
+ file: "resources/views/components/seo-meta.blade.php",
28413
+ code: `{{-- Primary Meta Tags --}}
28414
+ <title>{{ $seo->getFullTitle() }}</title>
28415
+ <meta name="title" content="{{ $seo->getFullTitle() }}">
28416
+ <meta name="description" content="{{ $meta['description'] }}">
28417
+ <meta name="robots" content="{{ $seo->getRobots() }}">
28418
+ <link rel="canonical" href="{{ $meta['url'] }}">
28419
+
28420
+ {{-- Open Graph / Facebook --}}
28421
+ <meta property="og:type" content="{{ $meta['type'] }}">
28422
+ <meta property="og:url" content="{{ $meta['url'] }}">
28423
+ <meta property="og:title" content="{{ $seo->getFullTitle() }}">
28424
+ <meta property="og:description" content="{{ $meta['description'] }}">
28425
+ <meta property="og:image" content="{{ $seo->getImageUrl() }}">
28426
+ <meta property="og:image:width" content="1200">
28427
+ <meta property="og:image:height" content="630">
28428
+ <meta property="og:site_name" content="{{ $seo::SITE_NAME }}">
28429
+
28430
+ @if($meta['type'] === 'article')
28431
+ @if($meta['published_time'])
28432
+ <meta property="article:published_time" content="{{ $meta['published_time'] }}">
28433
+ @endif
28434
+ @if($meta['modified_time'])
28435
+ <meta property="article:modified_time" content="{{ $meta['modified_time'] }}">
28436
+ @endif
28437
+ @if($meta['author'])
28438
+ <meta property="article:author" content="{{ $meta['author'] }}">
28439
+ @endif
28440
+ @foreach($meta['tags'] as $tag)
28441
+ <meta property="article:tag" content="{{ $tag }}">
28442
+ @endforeach
28443
+ @endif
28444
+
28445
+ {{-- Twitter --}}
28446
+ <meta name="twitter:card" content="summary_large_image">
28447
+ <meta name="twitter:title" content="{{ $seo->getFullTitle() }}">
28448
+ <meta name="twitter:description" content="{{ $meta['description'] }}">
28449
+ <meta name="twitter:image" content="{{ $seo->getImageUrl() }}">
28450
+ @if($seo::TWITTER_HANDLE)
28451
+ <meta name="twitter:site" content="{{ $seo::TWITTER_HANDLE }}">
28452
+ <meta name="twitter:creator" content="{{ $seo::TWITTER_HANDLE }}">
28453
+ @endif`,
28454
+ explanation: "Blade component for rendering SEO meta tags."
28455
+ },
28456
+ {
28457
+ file: "resources/views/layouts/app.blade.php",
28458
+ code: `<!DOCTYPE html>
28459
+ <html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
28460
+ <head>
28461
+ <meta charset="utf-8">
28462
+ <meta name="viewport" content="width=device-width, initial-scale=1">
28463
+ <meta name="csrf-token" content="{{ csrf_token() }}">
28464
+
28465
+ {{-- SEO Meta Tags --}}
28466
+ {!! SEO()->render() !!}
28467
+
28468
+ {{-- Favicon --}}
28469
+ <link rel="icon" href="/favicon.ico">
28470
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
28471
+
28472
+ {{-- Styles --}}
28473
+ @vite(['resources/css/app.css', 'resources/js/app.js'])
28474
+
28475
+ {{-- JSON-LD --}}
28476
+ {!! SEO()->renderJsonLd() !!}
28477
+ </head>
28478
+ <body>
28479
+ @yield('content')
28480
+ </body>
28481
+ </html>`,
28482
+ explanation: "Laravel layout with SEO integration."
28483
+ },
28484
+ {
28485
+ file: "app/Providers/AppServiceProvider.php",
28486
+ code: `<?php
28487
+
28488
+ namespace App\\Providers;
28489
+
28490
+ use App\\Services\\SEOService;
28491
+ use Illuminate\\Support\\ServiceProvider;
28492
+
28493
+ class AppServiceProvider extends ServiceProvider
28494
+ {
28495
+ public function register(): void
28496
+ {
28497
+ $this->app->singleton(SEOService::class);
28498
+ }
28499
+
28500
+ public function boot(): void
28501
+ {
28502
+ // Global SEO helper
28503
+ if (!function_exists('SEO')) {
28504
+ function SEO(): SEOService {
28505
+ return app(SEOService::class);
28506
+ }
28507
+ }
28508
+ }
28509
+ }`,
28510
+ explanation: "Service provider registration."
28511
+ },
28512
+ {
28513
+ file: "public/robots.txt",
28514
+ code: `User-agent: *
28515
+ Allow: /
28516
+ Disallow: /admin/
28517
+ Disallow: /api/
28518
+
28519
+ User-agent: GPTBot
28520
+ Allow: /
28521
+
28522
+ Sitemap: ${siteUrl}/sitemap.xml`,
28523
+ explanation: "Robots.txt with AI crawler support."
28524
+ }
28525
+ ]
28526
+ };
28527
+ }
28528
+ function generateSpringBootSEO(options) {
28529
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
28530
+ return {
28531
+ file: "src/main/java/com/example/seo/SEOService.java",
28532
+ code: `package com.example.seo;
28533
+
28534
+ import org.springframework.stereotype.Service;
28535
+ import java.util.*;
28536
+
28537
+ /**
28538
+ * Comprehensive SEO Service for Spring Boot
28539
+ *
28540
+ * Features:
28541
+ * - Full Open Graph support
28542
+ * - Twitter Cards
28543
+ * - JSON-LD structured data
28544
+ * - Thymeleaf integration
28545
+ */
28546
+ @Service
28547
+ public class SEOService {
28548
+
28549
+ public static final String SITE_NAME = "${siteName}";
28550
+ public static final String SITE_URL = "${siteUrl}";
28551
+ public static final String DEFAULT_IMAGE = "${image || `${siteUrl}/og-image.png`}";
28552
+ public static final String DEFAULT_DESCRIPTION = "${description || `${siteName} - A compelling description.`}";
28553
+ public static final String TWITTER_HANDLE = "${twitterHandle || ""}";
28554
+
28555
+ /**
28556
+ * Build SEO metadata for a page
28557
+ */
28558
+ public SEOMeta buildMeta(String title, String description, String image, String type) {
28559
+ return SEOMeta.builder()
28560
+ .title(title)
28561
+ .description(description != null ? description : DEFAULT_DESCRIPTION)
28562
+ .image(image != null ? image : DEFAULT_IMAGE)
28563
+ .type(type != null ? type : "website")
28564
+ .build();
28565
+ }
28566
+
28567
+ /**
28568
+ * Get full page title with site name
28569
+ */
28570
+ public String getFullTitle(String title) {
28571
+ if (title == null || title.isEmpty()) {
28572
+ return SITE_NAME;
28573
+ }
28574
+ if (title.contains(SITE_NAME)) {
28575
+ return title;
28576
+ }
28577
+ return title + " | " + SITE_NAME;
28578
+ }
28579
+
28580
+ /**
28581
+ * Get absolute image URL
28582
+ */
28583
+ public String getAbsoluteImageUrl(String image) {
28584
+ if (image == null) {
28585
+ return DEFAULT_IMAGE;
28586
+ }
28587
+ if (image.startsWith("http")) {
28588
+ return image;
28589
+ }
28590
+ return SITE_URL + image;
28591
+ }
28592
+
28593
+ // JSON-LD Schema Generators
28594
+
28595
+ public Map<String, Object> websiteSchema() {
28596
+ Map<String, Object> schema = new LinkedHashMap<>();
28597
+ schema.put("@context", "https://schema.org");
28598
+ schema.put("@type", "WebSite");
28599
+ schema.put("name", SITE_NAME);
28600
+ schema.put("url", SITE_URL);
28601
+ return schema;
28602
+ }
28603
+
28604
+ public Map<String, Object> organizationSchema(String name, String url, String logo, List<String> socialProfiles) {
28605
+ Map<String, Object> schema = new LinkedHashMap<>();
28606
+ schema.put("@context", "https://schema.org");
28607
+ schema.put("@type", "Organization");
28608
+ schema.put("name", name != null ? name : SITE_NAME);
28609
+ schema.put("url", url != null ? url : SITE_URL);
28610
+ if (logo != null) schema.put("logo", logo);
28611
+ if (socialProfiles != null && !socialProfiles.isEmpty()) {
28612
+ schema.put("sameAs", socialProfiles);
28613
+ }
28614
+ return schema;
28615
+ }
28616
+
28617
+ public Map<String, Object> articleSchema(
28618
+ String headline,
28619
+ String description,
28620
+ String image,
28621
+ String datePublished,
28622
+ String dateModified,
28623
+ String authorName
28624
+ ) {
28625
+ Map<String, Object> schema = new LinkedHashMap<>();
28626
+ schema.put("@context", "https://schema.org");
28627
+ schema.put("@type", "Article");
28628
+ schema.put("headline", headline);
28629
+ schema.put("description", description);
28630
+ schema.put("image", image != null ? image : DEFAULT_IMAGE);
28631
+ schema.put("datePublished", datePublished);
28632
+ schema.put("dateModified", dateModified != null ? dateModified : datePublished);
28633
+
28634
+ Map<String, Object> author = new LinkedHashMap<>();
28635
+ author.put("@type", "Person");
28636
+ author.put("name", authorName);
28637
+ schema.put("author", author);
28638
+
28639
+ Map<String, Object> publisher = new LinkedHashMap<>();
28640
+ publisher.put("@type", "Organization");
28641
+ publisher.put("name", SITE_NAME);
28642
+ Map<String, Object> logo = new LinkedHashMap<>();
28643
+ logo.put("@type", "ImageObject");
28644
+ logo.put("url", SITE_URL + "/logo.png");
28645
+ publisher.put("logo", logo);
28646
+ schema.put("publisher", publisher);
28647
+
28648
+ return schema;
28649
+ }
28650
+
28651
+ public Map<String, Object> productSchema(
28652
+ String name,
28653
+ String description,
28654
+ String image,
28655
+ String price,
28656
+ String currency,
28657
+ boolean inStock
28658
+ ) {
28659
+ Map<String, Object> schema = new LinkedHashMap<>();
28660
+ schema.put("@context", "https://schema.org");
28661
+ schema.put("@type", "Product");
28662
+ schema.put("name", name);
28663
+ schema.put("description", description);
28664
+ schema.put("image", image);
28665
+
28666
+ Map<String, Object> offers = new LinkedHashMap<>();
28667
+ offers.put("@type", "Offer");
28668
+ offers.put("price", price);
28669
+ offers.put("priceCurrency", currency != null ? currency : "USD");
28670
+ offers.put("availability", "https://schema.org/" + (inStock ? "InStock" : "OutOfStock"));
28671
+ schema.put("offers", offers);
28672
+
28673
+ return schema;
28674
+ }
28675
+
28676
+ public Map<String, Object> faqSchema(List<Map<String, String>> items) {
28677
+ Map<String, Object> schema = new LinkedHashMap<>();
28678
+ schema.put("@context", "https://schema.org");
28679
+ schema.put("@type", "FAQPage");
28680
+
28681
+ List<Map<String, Object>> mainEntity = new ArrayList<>();
28682
+ for (Map<String, String> item : items) {
28683
+ Map<String, Object> question = new LinkedHashMap<>();
28684
+ question.put("@type", "Question");
28685
+ question.put("name", item.get("question"));
28686
+
28687
+ Map<String, Object> answer = new LinkedHashMap<>();
28688
+ answer.put("@type", "Answer");
28689
+ answer.put("text", item.get("answer"));
28690
+ question.put("acceptedAnswer", answer);
28691
+
28692
+ mainEntity.add(question);
28693
+ }
28694
+ schema.put("mainEntity", mainEntity);
28695
+
28696
+ return schema;
28697
+ }
28698
+
28699
+ public Map<String, Object> breadcrumbSchema(List<Map<String, String>> items) {
28700
+ Map<String, Object> schema = new LinkedHashMap<>();
28701
+ schema.put("@context", "https://schema.org");
28702
+ schema.put("@type", "BreadcrumbList");
28703
+
28704
+ List<Map<String, Object>> itemList = new ArrayList<>();
28705
+ for (int i = 0; i < items.size(); i++) {
28706
+ Map<String, String> item = items.get(i);
28707
+ Map<String, Object> listItem = new LinkedHashMap<>();
28708
+ listItem.put("@type", "ListItem");
28709
+ listItem.put("position", i + 1);
28710
+ listItem.put("name", item.get("name"));
28711
+ listItem.put("item", item.get("url"));
28712
+ itemList.add(listItem);
28713
+ }
28714
+ schema.put("itemListElement", itemList);
28715
+
28716
+ return schema;
28717
+ }
28718
+ }`,
28719
+ explanation: `Spring Boot comprehensive SEO service with:
28720
+ \u2022 Full Open Graph support
28721
+ \u2022 Twitter Cards
28722
+ \u2022 JSON-LD schema generators
28723
+ \u2022 Thymeleaf integration
28724
+
28725
+ Setup:
28726
+ 1. Add SEOService to your controllers
28727
+ 2. Pass SEOMeta to model
28728
+ 3. Use Thymeleaf fragments in templates`,
28729
+ additionalFiles: [
28730
+ {
28731
+ file: "src/main/java/com/example/seo/SEOMeta.java",
28732
+ code: `package com.example.seo;
28733
+
28734
+ import lombok.Builder;
28735
+ import lombok.Data;
28736
+ import java.util.List;
28737
+
28738
+ @Data
28739
+ @Builder
28740
+ public class SEOMeta {
28741
+ private String title;
28742
+ private String description;
28743
+ private String image;
28744
+ private String url;
28745
+ private String type;
28746
+
28747
+ // Article specific
28748
+ private String publishedTime;
28749
+ private String modifiedTime;
28750
+ private String author;
28751
+ private List<String> tags;
28752
+
28753
+ // Robots
28754
+ @Builder.Default
28755
+ private boolean noindex = false;
28756
+ @Builder.Default
28757
+ private boolean nofollow = false;
28758
+
28759
+ public String getFullTitle() {
28760
+ if (title == null || title.isEmpty()) {
28761
+ return SEOService.SITE_NAME;
28762
+ }
28763
+ if (title.contains(SEOService.SITE_NAME)) {
28764
+ return title;
28765
+ }
28766
+ return title + " | " + SEOService.SITE_NAME;
28767
+ }
28768
+
28769
+ public String getImageUrl() {
28770
+ if (image == null) {
28771
+ return SEOService.DEFAULT_IMAGE;
28772
+ }
28773
+ if (image.startsWith("http")) {
28774
+ return image;
28775
+ }
28776
+ return SEOService.SITE_URL + image;
28777
+ }
28778
+
28779
+ public String getRobots() {
28780
+ String index = noindex ? "noindex" : "index";
28781
+ String follow = nofollow ? "nofollow" : "follow";
28782
+ return index + ", " + follow;
28783
+ }
28784
+ }`,
28785
+ explanation: "SEO metadata DTO with Lombok."
28786
+ },
28787
+ {
28788
+ file: "src/main/resources/templates/fragments/seo.html",
28789
+ code: `<!DOCTYPE html>
28790
+ <html xmlns:th="http://www.thymeleaf.org">
28791
+ <head th:fragment="meta(seo)">
28792
+ <!-- Primary Meta Tags -->
28793
+ <title th:text="\${seo.getFullTitle()}">Page Title</title>
28794
+ <meta name="title" th:content="\${seo.getFullTitle()}">
28795
+ <meta name="description" th:content="\${seo.description}">
28796
+ <meta name="robots" th:content="\${seo.getRobots()}">
28797
+ <link rel="canonical" th:href="\${seo.url}">
28798
+
28799
+ <!-- Open Graph / Facebook -->
28800
+ <meta property="og:type" th:content="\${seo.type}">
28801
+ <meta property="og:url" th:content="\${seo.url}">
28802
+ <meta property="og:title" th:content="\${seo.getFullTitle()}">
28803
+ <meta property="og:description" th:content="\${seo.description}">
28804
+ <meta property="og:image" th:content="\${seo.getImageUrl()}">
28805
+ <meta property="og:image:width" content="1200">
28806
+ <meta property="og:image:height" content="630">
28807
+ <meta property="og:site_name" th:content="@{${siteName}}">
28808
+
28809
+ <th:block th:if="\${seo.type == 'article'}">
28810
+ <meta th:if="\${seo.publishedTime}" property="article:published_time" th:content="\${seo.publishedTime}">
28811
+ <meta th:if="\${seo.modifiedTime}" property="article:modified_time" th:content="\${seo.modifiedTime}">
28812
+ <meta th:if="\${seo.author}" property="article:author" th:content="\${seo.author}">
28813
+ <meta th:each="tag : \${seo.tags}" property="article:tag" th:content="\${tag}">
28814
+ </th:block>
28815
+
28816
+ <!-- Twitter -->
28817
+ <meta name="twitter:card" content="summary_large_image">
28818
+ <meta name="twitter:title" th:content="\${seo.getFullTitle()}">
28819
+ <meta name="twitter:description" th:content="\${seo.description}">
28820
+ <meta name="twitter:image" th:content="\${seo.getImageUrl()}">
28821
+ <meta th:if="${twitterHandle}" name="twitter:site" content="${twitterHandle}">
28822
+ </head>
28823
+ </html>`,
28824
+ explanation: "Thymeleaf fragment for SEO meta tags."
28825
+ },
28826
+ {
28827
+ file: "src/main/resources/templates/layout/base.html",
28828
+ code: `<!DOCTYPE html>
28829
+ <html xmlns:th="http://www.thymeleaf.org" th:lang="\${#locale.language}">
28830
+ <head>
28831
+ <meta charset="UTF-8">
28832
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
28833
+
28834
+ <!-- SEO Meta Tags -->
28835
+ <th:block th:replace="~{fragments/seo :: meta(seo=\${seo})}"></th:block>
28836
+
28837
+ <!-- Favicon -->
28838
+ <link rel="icon" type="image/x-icon" href="/favicon.ico">
28839
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
28840
+
28841
+ <!-- Styles -->
28842
+ <link rel="stylesheet" th:href="@{/css/styles.css}">
28843
+
28844
+ <!-- JSON-LD -->
28845
+ <script th:if="\${jsonLd}" type="application/ld+json" th:utext="\${jsonLd}"></script>
28846
+ </head>
28847
+ <body>
28848
+ <div th:replace="~{:: content}"></div>
28849
+ </body>
28850
+ </html>`,
28851
+ explanation: "Base layout with SEO integration."
28852
+ },
28853
+ {
28854
+ file: "src/main/resources/static/robots.txt",
28855
+ code: `User-agent: *
28856
+ Allow: /
28857
+ Disallow: /admin/
28858
+ Disallow: /api/
28859
+
28860
+ User-agent: GPTBot
28861
+ Allow: /
28862
+
28863
+ Sitemap: ${siteUrl}/sitemap.xml`,
28864
+ explanation: "Robots.txt with AI crawler support."
28865
+ }
28866
+ ]
28867
+ };
28868
+ }
28869
+ function generatePhoenixSEO(options) {
28870
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
28871
+ return {
28872
+ file: "lib/my_app_web/components/seo.ex",
28873
+ code: `defmodule MyAppWeb.Components.SEO do
28874
+ @moduledoc """
28875
+ Comprehensive SEO components for Phoenix
28876
+
28877
+ Features:
28878
+ - Full Open Graph support
28879
+ - Twitter Cards
28880
+ - JSON-LD structured data
28881
+ - HEEx integration
28882
+ """
28883
+
28884
+ use Phoenix.Component
28885
+
28886
+ @site_name "${siteName}"
28887
+ @site_url "${siteUrl}"
28888
+ @default_image "${image || `${siteUrl}/og-image.png`}"
28889
+ @default_description "${description || `${siteName} - A compelling description.`}"
28890
+ @twitter_handle "${twitterHandle || ""}"
28891
+
28892
+ def site_name, do: @site_name
28893
+ def site_url, do: @site_url
28894
+
28895
+ @doc """
28896
+ SEO meta tags component
28897
+
28898
+ ## Examples
28899
+
28900
+ <.seo_meta
28901
+ title="Page Title"
28902
+ description="Page description"
28903
+ type="article"
28904
+ published_time={@article.published_at}
28905
+ />
28906
+ """
28907
+ attr :title, :string, default: nil
28908
+ attr :description, :string, default: @default_description
28909
+ attr :image, :string, default: @default_image
28910
+ attr :url, :string, default: nil
28911
+ attr :type, :string, default: "website"
28912
+ attr :published_time, :any, default: nil
28913
+ attr :modified_time, :any, default: nil
28914
+ attr :author, :string, default: nil
28915
+ attr :tags, :list, default: []
28916
+ attr :noindex, :boolean, default: false
28917
+ attr :nofollow, :boolean, default: false
28918
+
28919
+ def seo_meta(assigns) do
28920
+ assigns =
28921
+ assigns
28922
+ |> assign(:full_title, get_full_title(assigns.title))
28923
+ |> assign(:image_url, get_absolute_url(assigns.image))
28924
+ |> assign(:canonical_url, assigns.url || @site_url)
28925
+ |> assign(:robots, get_robots(assigns.noindex, assigns.nofollow))
28926
+
28927
+ ~H"""
28928
+ <!-- Primary Meta Tags -->
28929
+ <title><%= @full_title %></title>
28930
+ <meta name="title" content={@full_title}>
28931
+ <meta name="description" content={@description}>
28932
+ <meta name="robots" content={@robots}>
28933
+ <link rel="canonical" href={@canonical_url}>
28934
+
28935
+ <!-- Open Graph / Facebook -->
28936
+ <meta property="og:type" content={@type}>
28937
+ <meta property="og:url" content={@canonical_url}>
28938
+ <meta property="og:title" content={@full_title}>
28939
+ <meta property="og:description" content={@description}>
28940
+ <meta property="og:image" content={@image_url}>
28941
+ <meta property="og:image:width" content="1200">
28942
+ <meta property="og:image:height" content="630">
28943
+ <meta property="og:site_name" content={@site_name}>
28944
+
28945
+ <%= if @type == "article" do %>
28946
+ <%= if @published_time do %>
28947
+ <meta property="article:published_time" content={format_datetime(@published_time)}>
28948
+ <% end %>
28949
+ <%= if @modified_time do %>
28950
+ <meta property="article:modified_time" content={format_datetime(@modified_time)}>
28951
+ <% end %>
28952
+ <%= if @author do %>
28953
+ <meta property="article:author" content={@author}>
28954
+ <% end %>
28955
+ <%= for tag <- @tags do %>
28956
+ <meta property="article:tag" content={tag}>
28957
+ <% end %>
28958
+ <% end %>
28959
+
28960
+ <!-- Twitter -->
28961
+ <meta name="twitter:card" content="summary_large_image">
28962
+ <meta name="twitter:title" content={@full_title}>
28963
+ <meta name="twitter:description" content={@description}>
28964
+ <meta name="twitter:image" content={@image_url}>
28965
+ <%= if @twitter_handle != "" do %>
28966
+ <meta name="twitter:site" content={@twitter_handle}>
28967
+ <meta name="twitter:creator" content={@twitter_handle}>
28968
+ <% end %>
28969
+ """
28970
+ end
28971
+
28972
+ @doc """
28973
+ JSON-LD structured data component
28974
+ """
28975
+ attr :schemas, :list, required: true
28976
+
28977
+ def json_ld(assigns) do
28978
+ all_schemas = [website_schema() | assigns.schemas]
28979
+
28980
+ ~H"""
28981
+ <%= for schema <- @all_schemas do %>
28982
+ <script type="application/ld+json">
28983
+ <%= raw(Jason.encode!(schema)) %>
28984
+ </script>
28985
+ <% end %>
28986
+ """
28987
+ end
28988
+
28989
+ # Schema generators
28990
+
28991
+ def website_schema do
28992
+ %{
28993
+ "@context" => "https://schema.org",
28994
+ "@type" => "WebSite",
28995
+ "name" => @site_name,
28996
+ "url" => @site_url
28997
+ }
28998
+ end
28999
+
29000
+ def organization_schema(opts \\\\ []) do
29001
+ %{
29002
+ "@context" => "https://schema.org",
29003
+ "@type" => "Organization",
29004
+ "name" => opts[:name] || @site_name,
29005
+ "url" => opts[:url] || @site_url
29006
+ }
29007
+ |> maybe_put("logo", opts[:logo])
29008
+ |> maybe_put("sameAs", opts[:social_profiles])
29009
+ end
29010
+
29011
+ def article_schema(article) do
29012
+ %{
29013
+ "@context" => "https://schema.org",
29014
+ "@type" => "Article",
29015
+ "headline" => article.title,
29016
+ "description" => article.excerpt || String.slice(article.content, 0, 160),
29017
+ "image" => article.image_url || @default_image,
29018
+ "datePublished" => format_datetime(article.published_at),
29019
+ "dateModified" => format_datetime(article.updated_at || article.published_at),
29020
+ "author" => %{
29021
+ "@type" => "Person",
29022
+ "name" => article.author_name || "Unknown"
29023
+ },
29024
+ "publisher" => %{
29025
+ "@type" => "Organization",
29026
+ "name" => @site_name,
29027
+ "logo" => %{"@type" => "ImageObject", "url" => "#{@site_url}/logo.png"}
29028
+ }
29029
+ }
29030
+ end
29031
+
29032
+ def product_schema(product) do
29033
+ %{
29034
+ "@context" => "https://schema.org",
29035
+ "@type" => "Product",
29036
+ "name" => product.name,
29037
+ "description" => product.description,
29038
+ "image" => product.image_url || @default_image,
29039
+ "offers" => %{
29040
+ "@type" => "Offer",
29041
+ "price" => to_string(product.price),
29042
+ "priceCurrency" => product.currency || "USD",
29043
+ "availability" => "https://schema.org/#{if product.in_stock, do: "InStock", else: "OutOfStock"}"
29044
+ }
29045
+ }
29046
+ |> maybe_put("brand", product.brand && %{"@type" => "Brand", "name" => product.brand})
29047
+ |> maybe_put("sku", product.sku)
29048
+ end
29049
+
29050
+ def faq_schema(items) do
29051
+ %{
29052
+ "@context" => "https://schema.org",
29053
+ "@type" => "FAQPage",
29054
+ "mainEntity" =>
29055
+ Enum.map(items, fn item ->
29056
+ %{
29057
+ "@type" => "Question",
29058
+ "name" => item.question,
29059
+ "acceptedAnswer" => %{"@type" => "Answer", "text" => item.answer}
29060
+ }
29061
+ end)
29062
+ }
29063
+ end
29064
+
29065
+ def breadcrumb_schema(items) do
29066
+ %{
29067
+ "@context" => "https://schema.org",
29068
+ "@type" => "BreadcrumbList",
29069
+ "itemListElement" =>
29070
+ items
29071
+ |> Enum.with_index(1)
29072
+ |> Enum.map(fn {item, position} ->
29073
+ %{
29074
+ "@type" => "ListItem",
29075
+ "position" => position,
29076
+ "name" => item.name,
29077
+ "item" => item.url
29078
+ }
29079
+ end)
29080
+ }
29081
+ end
29082
+
29083
+ # Helpers
29084
+
29085
+ defp get_full_title(nil), do: @site_name
29086
+ defp get_full_title(title) when is_binary(title) do
29087
+ if String.contains?(title, @site_name), do: title, else: "#{title} | #{@site_name}"
29088
+ end
29089
+
29090
+ defp get_absolute_url(nil), do: @default_image
29091
+ defp get_absolute_url(url) when is_binary(url) do
29092
+ if String.starts_with?(url, "http"), do: url, else: @site_url <> url
29093
+ end
29094
+
29095
+ defp get_robots(noindex, nofollow) do
29096
+ index = if noindex, do: "noindex", else: "index"
29097
+ follow = if nofollow, do: "nofollow", else: "follow"
29098
+ "#{index}, #{follow}"
29099
+ end
29100
+
29101
+ defp format_datetime(nil), do: nil
29102
+ defp format_datetime(%DateTime{} = dt), do: DateTime.to_iso8601(dt)
29103
+ defp format_datetime(%NaiveDateTime{} = dt), do: NaiveDateTime.to_iso8601(dt)
29104
+ defp format_datetime(dt) when is_binary(dt), do: dt
29105
+
29106
+ defp maybe_put(map, _key, nil), do: map
29107
+ defp maybe_put(map, _key, []), do: map
29108
+ defp maybe_put(map, key, value), do: Map.put(map, key, value)
29109
+ end`,
29110
+ explanation: `Phoenix/Elixir comprehensive SEO module with:
29111
+ \u2022 HEEx function components
29112
+ \u2022 Full Open Graph with article support
29113
+ \u2022 Twitter Cards
29114
+ \u2022 JSON-LD schema generators
29115
+ \u2022 Elixir-idiomatic API
29116
+
29117
+ Setup:
29118
+ 1. Import in your components module
29119
+ 2. Use <.seo_meta> in layouts
29120
+ 3. Add <.json_ld schemas={[@article_schema]} />`,
29121
+ additionalFiles: [
29122
+ {
29123
+ file: "lib/my_app_web/components/layouts/root.html.heex",
29124
+ code: `<!DOCTYPE html>
29125
+ <html lang={@locale || "en"}>
29126
+ <head>
29127
+ <meta charset="utf-8">
29128
+ <meta name="viewport" content="width=device-width, initial-scale=1">
29129
+ <meta name="csrf-token" content={get_csrf_token()}>
29130
+
29131
+ <.seo_meta
29132
+ title={assigns[:page_title]}
29133
+ description={assigns[:page_description]}
29134
+ image={assigns[:page_image]}
29135
+ type={assigns[:page_type] || "website"}
29136
+ />
29137
+
29138
+ <link rel="icon" href="/favicon.ico">
29139
+ <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"}>
29140
+ <script defer phx-track-static src={~p"/assets/app.js"}></script>
29141
+
29142
+ <%= if assigns[:json_ld_schemas] do %>
29143
+ <.json_ld schemas={@json_ld_schemas} />
29144
+ <% end %>
29145
+ </head>
29146
+ <body>
29147
+ <%= @inner_content %>
29148
+ </body>
29149
+ </html>`,
29150
+ explanation: "Phoenix root layout with SEO components."
29151
+ },
29152
+ {
29153
+ file: "priv/static/robots.txt",
29154
+ code: `User-agent: *
29155
+ Allow: /
29156
+ Disallow: /admin/
29157
+ Disallow: /api/
29158
+
29159
+ User-agent: GPTBot
29160
+ Allow: /
29161
+
29162
+ Sitemap: ${siteUrl}/sitemap.xml`,
29163
+ explanation: "Robots.txt with AI crawler support."
29164
+ }
29165
+ ]
29166
+ };
29167
+ }
29168
+ function generateGoSEO(options) {
29169
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
29170
+ return {
29171
+ file: "internal/seo/seo.go",
29172
+ code: `package seo
29173
+
29174
+ import (
29175
+ "encoding/json"
29176
+ "html/template"
29177
+ "strings"
29178
+ "time"
29179
+ )
29180
+
29181
+ // Configuration
29182
+ const (
29183
+ SiteName = "${siteName}"
29184
+ SiteURL = "${siteUrl}"
29185
+ DefaultImage = "${image || `${siteUrl}/og-image.png`}"
29186
+ DefaultDescription = "${description || `${siteName} - A compelling description.`}"
29187
+ TwitterHandle = "${twitterHandle || ""}"
29188
+ )
29189
+
29190
+ // Meta contains SEO metadata for a page
29191
+ type Meta struct {
29192
+ Title string
29193
+ Description string
29194
+ Image string
29195
+ URL string
29196
+ Type string // website, article, product
29197
+ PublishedTime *time.Time
29198
+ ModifiedTime *time.Time
29199
+ Author string
29200
+ Tags []string
29201
+ NoIndex bool
29202
+ NoFollow bool
29203
+ }
29204
+
29205
+ // NewMeta creates a Meta with defaults
29206
+ func NewMeta() *Meta {
29207
+ return &Meta{
29208
+ Description: DefaultDescription,
29209
+ Image: DefaultImage,
29210
+ Type: "website",
29211
+ }
29212
+ }
29213
+
29214
+ // FullTitle returns the title with site name
29215
+ func (m *Meta) FullTitle() string {
29216
+ if m.Title == "" {
29217
+ return SiteName
29218
+ }
29219
+ if strings.Contains(m.Title, SiteName) {
29220
+ return m.Title
29221
+ }
29222
+ return m.Title + " | " + SiteName
29223
+ }
29224
+
29225
+ // AbsoluteImageURL returns the absolute image URL
29226
+ func (m *Meta) AbsoluteImageURL() string {
29227
+ if m.Image == "" {
29228
+ return DefaultImage
29229
+ }
29230
+ if strings.HasPrefix(m.Image, "http") {
29231
+ return m.Image
29232
+ }
29233
+ return SiteURL + m.Image
29234
+ }
29235
+
29236
+ // Robots returns the robots meta content
29237
+ func (m *Meta) Robots() string {
29238
+ index := "index"
29239
+ if m.NoIndex {
29240
+ index = "noindex"
29241
+ }
29242
+ follow := "follow"
29243
+ if m.NoFollow {
29244
+ follow = "nofollow"
29245
+ }
29246
+ return index + ", " + follow
29247
+ }
29248
+
29249
+ // Schema types for JSON-LD
29250
+
29251
+ type Schema map[string]interface{}
29252
+
29253
+ // WebsiteSchema returns the default website schema
29254
+ func WebsiteSchema() Schema {
29255
+ return Schema{
29256
+ "@context": "https://schema.org",
29257
+ "@type": "WebSite",
29258
+ "name": SiteName,
29259
+ "url": SiteURL,
29260
+ }
29261
+ }
29262
+
29263
+ // OrganizationSchema creates an organization schema
29264
+ func OrganizationSchema(name, url, logo string, socialProfiles []string) Schema {
29265
+ schema := Schema{
29266
+ "@context": "https://schema.org",
29267
+ "@type": "Organization",
29268
+ "name": name,
29269
+ "url": url,
29270
+ }
29271
+ if logo != "" {
29272
+ schema["logo"] = logo
29273
+ }
29274
+ if len(socialProfiles) > 0 {
29275
+ schema["sameAs"] = socialProfiles
29276
+ }
29277
+ return schema
29278
+ }
29279
+
29280
+ // ArticleSchema creates an article schema
29281
+ func ArticleSchema(headline, description, image string, datePublished, dateModified time.Time, authorName string) Schema {
29282
+ return Schema{
29283
+ "@context": "https://schema.org",
29284
+ "@type": "Article",
29285
+ "headline": headline,
29286
+ "description": description,
29287
+ "image": image,
29288
+ "datePublished": datePublished.Format(time.RFC3339),
29289
+ "dateModified": dateModified.Format(time.RFC3339),
29290
+ "author": Schema{
29291
+ "@type": "Person",
29292
+ "name": authorName,
29293
+ },
29294
+ "publisher": Schema{
29295
+ "@type": "Organization",
29296
+ "name": SiteName,
29297
+ "logo": Schema{
29298
+ "@type": "ImageObject",
29299
+ "url": SiteURL + "/logo.png",
29300
+ },
29301
+ },
29302
+ }
29303
+ }
29304
+
29305
+ // ProductSchema creates a product schema
29306
+ func ProductSchema(name, description, image, price, currency string, inStock bool) Schema {
29307
+ availability := "https://schema.org/InStock"
29308
+ if !inStock {
29309
+ availability = "https://schema.org/OutOfStock"
29310
+ }
29311
+
29312
+ return Schema{
29313
+ "@context": "https://schema.org",
29314
+ "@type": "Product",
29315
+ "name": name,
29316
+ "description": description,
29317
+ "image": image,
29318
+ "offers": Schema{
29319
+ "@type": "Offer",
29320
+ "price": price,
29321
+ "priceCurrency": currency,
29322
+ "availability": availability,
29323
+ },
29324
+ }
29325
+ }
29326
+
29327
+ // FAQItem represents a single FAQ entry
29328
+ type FAQItem struct {
29329
+ Question string
29330
+ Answer string
29331
+ }
29332
+
29333
+ // FAQSchema creates a FAQ schema
29334
+ func FAQSchema(items []FAQItem) Schema {
29335
+ mainEntity := make([]Schema, len(items))
29336
+ for i, item := range items {
29337
+ mainEntity[i] = Schema{
29338
+ "@type": "Question",
29339
+ "name": item.Question,
29340
+ "acceptedAnswer": Schema{
29341
+ "@type": "Answer",
29342
+ "text": item.Answer,
29343
+ },
29344
+ }
29345
+ }
29346
+ return Schema{
29347
+ "@context": "https://schema.org",
29348
+ "@type": "FAQPage",
29349
+ "mainEntity": mainEntity,
29350
+ }
29351
+ }
29352
+
29353
+ // BreadcrumbItem represents a breadcrumb entry
29354
+ type BreadcrumbItem struct {
29355
+ Name string
29356
+ URL string
29357
+ }
29358
+
29359
+ // BreadcrumbSchema creates a breadcrumb schema
29360
+ func BreadcrumbSchema(items []BreadcrumbItem) Schema {
29361
+ itemList := make([]Schema, len(items))
29362
+ for i, item := range items {
29363
+ itemList[i] = Schema{
29364
+ "@type": "ListItem",
29365
+ "position": i + 1,
29366
+ "name": item.Name,
29367
+ "item": item.URL,
29368
+ }
29369
+ }
29370
+ return Schema{
29371
+ "@context": "https://schema.org",
29372
+ "@type": "BreadcrumbList",
29373
+ "itemListElement": itemList,
29374
+ }
29375
+ }
29376
+
29377
+ // ToJSON converts a schema to JSON string
29378
+ func (s Schema) ToJSON() template.JS {
29379
+ b, err := json.Marshal(s)
29380
+ if err != nil {
29381
+ return ""
29382
+ }
29383
+ return template.JS(b)
29384
+ }
29385
+
29386
+ // RenderJSONLD renders multiple schemas as script tags
29387
+ func RenderJSONLD(schemas ...Schema) template.HTML {
29388
+ all := append([]Schema{WebsiteSchema()}, schemas...)
29389
+ var sb strings.Builder
29390
+ for _, schema := range all {
29391
+ b, err := json.Marshal(schema)
29392
+ if err != nil {
29393
+ continue
29394
+ }
29395
+ sb.WriteString(\`<script type="application/ld+json">\`)
29396
+ sb.Write(b)
29397
+ sb.WriteString(\`</script>\\n\`)
29398
+ }
29399
+ return template.HTML(sb.String())
29400
+ }`,
29401
+ explanation: `Go comprehensive SEO package with:
29402
+ \u2022 Meta struct for page metadata
29403
+ \u2022 JSON-LD schema generators
29404
+ \u2022 Template-friendly helpers
29405
+ \u2022 Works with Gin, Echo, Fiber, or stdlib
29406
+
29407
+ Setup:
29408
+ 1. Import in your handlers
29409
+ 2. Pass Meta to templates
29410
+ 3. Use schema helpers for JSON-LD`,
29411
+ additionalFiles: [
29412
+ {
29413
+ file: "templates/partials/seo.html",
29414
+ code: `{{define "seo"}}
29415
+ <!-- Primary Meta Tags -->
29416
+ <title>{{.SEO.FullTitle}}</title>
29417
+ <meta name="title" content="{{.SEO.FullTitle}}">
29418
+ <meta name="description" content="{{.SEO.Description}}">
29419
+ <meta name="robots" content="{{.SEO.Robots}}">
29420
+ <link rel="canonical" href="{{.SEO.URL}}">
29421
+
29422
+ <!-- Open Graph / Facebook -->
29423
+ <meta property="og:type" content="{{.SEO.Type}}">
29424
+ <meta property="og:url" content="{{.SEO.URL}}">
29425
+ <meta property="og:title" content="{{.SEO.FullTitle}}">
29426
+ <meta property="og:description" content="{{.SEO.Description}}">
29427
+ <meta property="og:image" content="{{.SEO.AbsoluteImageURL}}">
29428
+ <meta property="og:image:width" content="1200">
29429
+ <meta property="og:image:height" content="630">
29430
+ <meta property="og:site_name" content="${siteName}">
29431
+
29432
+ {{if eq .SEO.Type "article"}}
29433
+ {{if .SEO.PublishedTime}}
29434
+ <meta property="article:published_time" content="{{.SEO.PublishedTime.Format "2006-01-02T15:04:05Z07:00"}}">
29435
+ {{end}}
29436
+ {{if .SEO.ModifiedTime}}
29437
+ <meta property="article:modified_time" content="{{.SEO.ModifiedTime.Format "2006-01-02T15:04:05Z07:00"}}">
29438
+ {{end}}
29439
+ {{if .SEO.Author}}
29440
+ <meta property="article:author" content="{{.SEO.Author}}">
29441
+ {{end}}
29442
+ {{range .SEO.Tags}}
29443
+ <meta property="article:tag" content="{{.}}">
29444
+ {{end}}
29445
+ {{end}}
29446
+
29447
+ <!-- Twitter -->
29448
+ <meta name="twitter:card" content="summary_large_image">
29449
+ <meta name="twitter:title" content="{{.SEO.FullTitle}}">
29450
+ <meta name="twitter:description" content="{{.SEO.Description}}">
29451
+ <meta name="twitter:image" content="{{.SEO.AbsoluteImageURL}}">
29452
+ {{if ne "${twitterHandle}" ""}}
29453
+ <meta name="twitter:site" content="${twitterHandle}">
29454
+ <meta name="twitter:creator" content="${twitterHandle}">
29455
+ {{end}}
29456
+ {{end}}`,
29457
+ explanation: "Go html/template partial for SEO meta tags."
29458
+ },
29459
+ {
29460
+ file: "templates/layouts/base.html",
29461
+ code: `<!DOCTYPE html>
29462
+ <html lang="{{.Lang}}">
29463
+ <head>
29464
+ <meta charset="utf-8">
29465
+ <meta name="viewport" content="width=device-width, initial-scale=1">
29466
+
29467
+ {{template "seo" .}}
29468
+
29469
+ <link rel="icon" href="/static/favicon.ico">
29470
+ <link rel="stylesheet" href="/static/css/styles.css">
29471
+
29472
+ {{if .JSONLDSchemas}}
29473
+ {{.JSONLDSchemas}}
29474
+ {{end}}
29475
+ </head>
29476
+ <body>
29477
+ {{template "content" .}}
29478
+ </body>
29479
+ </html>`,
29480
+ explanation: "Go base template layout."
29481
+ },
29482
+ {
29483
+ file: "static/robots.txt",
29484
+ code: `User-agent: *
29485
+ Allow: /
29486
+ Disallow: /admin/
29487
+ Disallow: /api/
29488
+
29489
+ User-agent: GPTBot
29490
+ Allow: /
29491
+
29492
+ Sitemap: ${siteUrl}/sitemap.xml`,
29493
+ explanation: "Robots.txt with AI crawler support."
29494
+ }
29495
+ ]
29496
+ };
29497
+ }
29498
+ function generateAspNetCoreSEO(options) {
29499
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
29500
+ return {
29501
+ file: "Services/SEOService.cs",
29502
+ code: `using System.Text.Json;
29503
+
29504
+ namespace MyApp.Services;
29505
+
29506
+ /// <summary>
29507
+ /// Comprehensive SEO Service for ASP.NET Core
29508
+ ///
29509
+ /// Features:
29510
+ /// - Full Open Graph support
29511
+ /// - Twitter Cards
29512
+ /// - JSON-LD structured data
29513
+ /// - Razor integration
29514
+ /// </summary>
29515
+ public class SEOService
29516
+ {
29517
+ public const string SiteName = "${siteName}";
29518
+ public const string SiteUrl = "${siteUrl}";
29519
+ public const string DefaultImage = "${image || `${siteUrl}/og-image.png`}";
29520
+ public const string DefaultDescription = "${description || `${siteName} - A compelling description.`}";
29521
+ public const string TwitterHandle = "${twitterHandle || ""}";
29522
+
29523
+ /// <summary>
29524
+ /// Create SEO metadata for a page
29525
+ /// </summary>
29526
+ public SEOMeta CreateMeta(
29527
+ string? title = null,
29528
+ string? description = null,
29529
+ string? image = null,
29530
+ string? type = "website")
29531
+ {
29532
+ return new SEOMeta
29533
+ {
29534
+ Title = title,
29535
+ Description = description ?? DefaultDescription,
29536
+ Image = image ?? DefaultImage,
29537
+ Type = type ?? "website"
29538
+ };
29539
+ }
29540
+
29541
+ // JSON-LD Schema Generators
29542
+
29543
+ public object WebsiteSchema() => new
29544
+ {
29545
+ @context = "https://schema.org",
29546
+ @type = "WebSite",
29547
+ name = SiteName,
29548
+ url = SiteUrl
29549
+ };
29550
+
29551
+ public object OrganizationSchema(
29552
+ string? name = null,
29553
+ string? url = null,
29554
+ string? logo = null,
29555
+ string[]? socialProfiles = null) => new
29556
+ {
29557
+ @context = "https://schema.org",
29558
+ @type = "Organization",
29559
+ name = name ?? SiteName,
29560
+ url = url ?? SiteUrl,
29561
+ logo,
29562
+ sameAs = socialProfiles
29563
+ };
29564
+
29565
+ public object ArticleSchema(
29566
+ string headline,
29567
+ string description,
29568
+ string? image,
29569
+ DateTime datePublished,
29570
+ DateTime? dateModified,
29571
+ string authorName) => new
29572
+ {
29573
+ @context = "https://schema.org",
29574
+ @type = "Article",
29575
+ headline,
29576
+ description,
29577
+ image = image ?? DefaultImage,
29578
+ datePublished = datePublished.ToString("O"),
29579
+ dateModified = (dateModified ?? datePublished).ToString("O"),
29580
+ author = new { @type = "Person", name = authorName },
29581
+ publisher = new
29582
+ {
29583
+ @type = "Organization",
29584
+ name = SiteName,
29585
+ logo = new { @type = "ImageObject", url = SiteUrl + "/logo.png" }
29586
+ }
29587
+ };
29588
+
29589
+ public object ProductSchema(
29590
+ string name,
29591
+ string description,
29592
+ string? image,
29593
+ decimal price,
29594
+ string currency = "USD",
29595
+ bool inStock = true) => new
29596
+ {
29597
+ @context = "https://schema.org",
29598
+ @type = "Product",
29599
+ name,
29600
+ description,
29601
+ image = image ?? DefaultImage,
29602
+ offers = new
29603
+ {
29604
+ @type = "Offer",
29605
+ price = price.ToString("F2"),
29606
+ priceCurrency = currency,
29607
+ availability = $"https://schema.org/{(inStock ? "InStock" : "OutOfStock")}"
29608
+ }
29609
+ };
29610
+
29611
+ public object FAQSchema(IEnumerable<(string Question, string Answer)> items) => new
29612
+ {
29613
+ @context = "https://schema.org",
29614
+ @type = "FAQPage",
29615
+ mainEntity = items.Select(item => new
29616
+ {
29617
+ @type = "Question",
29618
+ name = item.Question,
29619
+ acceptedAnswer = new { @type = "Answer", text = item.Answer }
29620
+ })
29621
+ };
29622
+
29623
+ public object BreadcrumbSchema(IEnumerable<(string Name, string Url)> items) => new
29624
+ {
29625
+ @context = "https://schema.org",
29626
+ @type = "BreadcrumbList",
29627
+ itemListElement = items.Select((item, index) => new
29628
+ {
29629
+ @type = "ListItem",
29630
+ position = index + 1,
29631
+ name = item.Name,
29632
+ item = item.Url
29633
+ })
29634
+ };
29635
+
29636
+ /// <summary>
29637
+ /// Serialize schema to JSON for embedding in HTML
29638
+ /// </summary>
29639
+ public string ToJson(object schema)
29640
+ {
29641
+ return JsonSerializer.Serialize(schema, new JsonSerializerOptions
29642
+ {
29643
+ WriteIndented = false,
29644
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
29645
+ });
29646
+ }
29647
+ }
29648
+
29649
+ public class SEOMeta
29650
+ {
29651
+ public string? Title { get; set; }
29652
+ public string Description { get; set; } = SEOService.DefaultDescription;
29653
+ public string Image { get; set; } = SEOService.DefaultImage;
29654
+ public string? Url { get; set; }
29655
+ public string Type { get; set; } = "website";
29656
+ public DateTime? PublishedTime { get; set; }
29657
+ public DateTime? ModifiedTime { get; set; }
29658
+ public string? Author { get; set; }
29659
+ public List<string> Tags { get; set; } = new();
29660
+ public bool NoIndex { get; set; }
29661
+ public bool NoFollow { get; set; }
29662
+
29663
+ public string FullTitle => string.IsNullOrEmpty(Title)
29664
+ ? SEOService.SiteName
29665
+ : Title.Contains(SEOService.SiteName)
29666
+ ? Title
29667
+ : $"{Title} | {SEOService.SiteName}";
29668
+
29669
+ public string AbsoluteImageUrl => Image.StartsWith("http")
29670
+ ? Image
29671
+ : SEOService.SiteUrl + Image;
29672
+
29673
+ public string Robots
29674
+ {
29675
+ get
29676
+ {
29677
+ var index = NoIndex ? "noindex" : "index";
29678
+ var follow = NoFollow ? "nofollow" : "follow";
29679
+ return $"{index}, {follow}";
29680
+ }
29681
+ }
29682
+ }`,
29683
+ explanation: `ASP.NET Core comprehensive SEO service with:
29684
+ \u2022 SEOMeta class for page metadata
29685
+ \u2022 JSON-LD schema generators
29686
+ \u2022 Razor integration
29687
+ \u2022 Fluent API
29688
+
29689
+ Setup:
29690
+ 1. Register SEOService as singleton
29691
+ 2. Inject in controllers/pages
29692
+ 3. Use in Razor views`,
29693
+ additionalFiles: [
29694
+ {
29695
+ file: "Pages/Shared/_SEOMeta.cshtml",
29696
+ code: `@model SEOMeta
29697
+
29698
+ <!-- Primary Meta Tags -->
29699
+ <title>@Model.FullTitle</title>
29700
+ <meta name="title" content="@Model.FullTitle">
29701
+ <meta name="description" content="@Model.Description">
29702
+ <meta name="robots" content="@Model.Robots">
29703
+ <link rel="canonical" href="@Model.Url">
29704
+
29705
+ <!-- Open Graph / Facebook -->
29706
+ <meta property="og:type" content="@Model.Type">
29707
+ <meta property="og:url" content="@Model.Url">
29708
+ <meta property="og:title" content="@Model.FullTitle">
29709
+ <meta property="og:description" content="@Model.Description">
29710
+ <meta property="og:image" content="@Model.AbsoluteImageUrl">
29711
+ <meta property="og:image:width" content="1200">
29712
+ <meta property="og:image:height" content="630">
29713
+ <meta property="og:site_name" content="${siteName}">
29714
+
29715
+ @if (Model.Type == "article")
29716
+ {
29717
+ if (Model.PublishedTime.HasValue)
29718
+ {
29719
+ <meta property="article:published_time" content="@Model.PublishedTime.Value.ToString("O")">
29720
+ }
29721
+ if (Model.ModifiedTime.HasValue)
29722
+ {
29723
+ <meta property="article:modified_time" content="@Model.ModifiedTime.Value.ToString("O")">
29724
+ }
29725
+ if (!string.IsNullOrEmpty(Model.Author))
29726
+ {
29727
+ <meta property="article:author" content="@Model.Author">
29728
+ }
29729
+ foreach (var tag in Model.Tags)
29730
+ {
29731
+ <meta property="article:tag" content="@tag">
29732
+ }
29733
+ }
29734
+
29735
+ <!-- Twitter -->
29736
+ <meta name="twitter:card" content="summary_large_image">
29737
+ <meta name="twitter:title" content="@Model.FullTitle">
29738
+ <meta name="twitter:description" content="@Model.Description">
29739
+ <meta name="twitter:image" content="@Model.AbsoluteImageUrl">
29740
+ @if (!string.IsNullOrEmpty("${twitterHandle}"))
29741
+ {
29742
+ <meta name="twitter:site" content="${twitterHandle}">
29743
+ <meta name="twitter:creator" content="${twitterHandle}">
29744
+ }`,
29745
+ explanation: "Razor partial view for SEO meta tags."
29746
+ },
29747
+ {
29748
+ file: "Pages/Shared/_Layout.cshtml",
29749
+ code: `<!DOCTYPE html>
29750
+ <html lang="en">
29751
+ <head>
29752
+ <meta charset="utf-8">
29753
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
29754
+
29755
+ @if (ViewData["SEO"] is SEOMeta seo)
29756
+ {
29757
+ <partial name="_SEOMeta" model="seo" />
29758
+ }
29759
+ else
29760
+ {
29761
+ <title>${siteName}</title>
29762
+ }
29763
+
29764
+ <link rel="icon" type="image/x-icon" href="~/favicon.ico">
29765
+ <link rel="stylesheet" href="~/css/site.css" asp-append-version="true">
29766
+
29767
+ @if (ViewData["JsonLd"] is string jsonLd)
29768
+ {
29769
+ <script type="application/ld+json">@Html.Raw(jsonLd)</script>
29770
+ }
29771
+ </head>
29772
+ <body>
29773
+ @RenderBody()
29774
+
29775
+ <script src="~/js/site.js" asp-append-version="true"></script>
29776
+ @await RenderSectionAsync("Scripts", required: false)
29777
+ </body>
29778
+ </html>`,
29779
+ explanation: "Razor layout with SEO integration."
29780
+ },
29781
+ {
29782
+ file: "wwwroot/robots.txt",
29783
+ code: `User-agent: *
29784
+ Allow: /
29785
+ Disallow: /admin/
29786
+ Disallow: /api/
29787
+
29788
+ User-agent: GPTBot
29789
+ Allow: /
29790
+
29791
+ Sitemap: ${siteUrl}/sitemap.xml`,
29792
+ explanation: "Robots.txt with AI crawler support."
29793
+ }
29794
+ ]
29795
+ };
29796
+ }
29797
+ function generateHTMXSEO(options) {
29798
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
29799
+ return {
29800
+ file: "templates/seo_head.html",
29801
+ code: `<!--
29802
+ HTMX SEO Template
29803
+
29804
+ This template provides comprehensive SEO for HTMX applications.
29805
+ HTMX apps are server-rendered, so SEO is straightforward - just
29806
+ include proper meta tags in your initial HTML response.
29807
+
29808
+ Features:
29809
+ - Full Open Graph support
29810
+ - Twitter Cards
29811
+ - JSON-LD structured data
29812
+ - Works with any backend (Python, Ruby, Go, PHP, etc.)
29813
+
29814
+ Usage:
29815
+ Include this template in your base layout, passing the required variables.
29816
+ -->
29817
+
29818
+ <!-- Primary Meta Tags -->
29819
+ <title>{{ full_title or '${siteName}' }}</title>
29820
+ <meta name="title" content="{{ full_title or '${siteName}' }}">
29821
+ <meta name="description" content="{{ description or '${description || `${siteName} - A compelling description.`}' }}">
29822
+ <meta name="robots" content="{{ 'noindex, nofollow' if noindex else 'index, follow' }}">
29823
+ <link rel="canonical" href="{{ canonical_url }}">
29824
+
29825
+ <!-- Open Graph / Facebook -->
29826
+ <meta property="og:type" content="{{ og_type or 'website' }}">
29827
+ <meta property="og:url" content="{{ canonical_url }}">
29828
+ <meta property="og:title" content="{{ full_title or '${siteName}' }}">
29829
+ <meta property="og:description" content="{{ description or '${description || `${siteName} - A compelling description.`}' }}">
29830
+ <meta property="og:image" content="{{ image_url or '${image || `${siteUrl}/og-image.png`}' }}">
29831
+ <meta property="og:image:width" content="1200">
29832
+ <meta property="og:image:height" content="630">
29833
+ <meta property="og:site_name" content="${siteName}">
29834
+ <meta property="og:locale" content="{{ locale or 'en_US' }}">
29835
+
29836
+ {% if og_type == 'article' %}
29837
+ {% if published_time %}
29838
+ <meta property="article:published_time" content="{{ published_time }}">
29839
+ {% endif %}
29840
+ {% if modified_time %}
29841
+ <meta property="article:modified_time" content="{{ modified_time }}">
29842
+ {% endif %}
29843
+ {% if author %}
29844
+ <meta property="article:author" content="{{ author }}">
29845
+ {% endif %}
29846
+ {% for tag in tags or [] %}
29847
+ <meta property="article:tag" content="{{ tag }}">
29848
+ {% endfor %}
29849
+ {% endif %}
29850
+
29851
+ <!-- Twitter -->
29852
+ <meta name="twitter:card" content="summary_large_image">
29853
+ <meta name="twitter:title" content="{{ full_title or '${siteName}' }}">
29854
+ <meta name="twitter:description" content="{{ description or '${description || `${siteName} - A compelling description.`}' }}">
29855
+ <meta name="twitter:image" content="{{ image_url or '${image || `${siteUrl}/og-image.png`}' }}">
29856
+ {% if '${twitterHandle}' %}
29857
+ <meta name="twitter:site" content="${twitterHandle}">
29858
+ <meta name="twitter:creator" content="${twitterHandle}">
29859
+ {% endif %}
29860
+
29861
+ <!-- JSON-LD Structured Data -->
29862
+ <script type="application/ld+json">
29863
+ {
29864
+ "@context": "https://schema.org",
29865
+ "@type": "WebSite",
29866
+ "name": "${siteName}",
29867
+ "url": "${siteUrl}"
29868
+ }
29869
+ </script>
29870
+ {% if schema_json %}
29871
+ <script type="application/ld+json">
29872
+ {{ schema_json | safe }}
29873
+ </script>
29874
+ {% endif %}`,
29875
+ explanation: `HTMX SEO template (framework-agnostic) with:
29876
+ \u2022 Full Open Graph support
29877
+ \u2022 Twitter Cards
29878
+ \u2022 JSON-LD structured data
29879
+ \u2022 Works with any template engine (Jinja2, Liquid, Go templates, etc.)
29880
+
29881
+ HTMX apps are server-rendered, so SEO is built-in!
29882
+ Just include proper meta tags in your initial HTML response.
29883
+
29884
+ Usage:
29885
+ 1. Include in your base layout
29886
+ 2. Pass SEO variables from your backend
29887
+ 3. JSON-LD schemas can be generated server-side`,
29888
+ additionalFiles: [
29889
+ {
29890
+ file: "templates/base.html",
29891
+ code: `<!DOCTYPE html>
29892
+ <html lang="{{ lang or 'en' }}">
29893
+ <head>
29894
+ <meta charset="utf-8">
29895
+ <meta name="viewport" content="width=device-width, initial-scale=1">
29896
+
29897
+ {% include 'seo_head.html' %}
29898
+
29899
+ <!-- HTMX -->
29900
+ <script src="https://unpkg.com/htmx.org@1.9.10"></script>
29901
+
29902
+ <!-- Styles -->
29903
+ <link rel="stylesheet" href="/static/css/styles.css">
29904
+
29905
+ <!-- Favicon -->
29906
+ <link rel="icon" href="/static/favicon.ico">
29907
+ </head>
29908
+ <body hx-boost="true">
29909
+ <main id="content">
29910
+ {% block content %}{% endblock %}
29911
+ </main>
29912
+ </body>
29913
+ </html>`,
29914
+ explanation: "Base HTMX layout with SEO integration."
29915
+ },
29916
+ {
29917
+ file: "static/robots.txt",
29918
+ code: `User-agent: *
29919
+ Allow: /
29920
+ Disallow: /admin/
29921
+ Disallow: /api/
29922
+
29923
+ User-agent: GPTBot
29924
+ Allow: /
29925
+
29926
+ Sitemap: ${siteUrl}/sitemap.xml`,
29927
+ explanation: "Robots.txt with AI crawler support."
29928
+ }
29929
+ ]
29930
+ };
29931
+ }
29932
+ function generateHugoSEO(options) {
29933
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
29934
+ return {
29935
+ file: "layouts/partials/seo.html",
29936
+ code: `{{- /* Comprehensive SEO partial for Hugo */ -}}
29937
+ {{- $title := .Title | default site.Title -}}
29938
+ {{- $description := .Description | default .Summary | default site.Params.description | default "${description || `${siteName} - A compelling description.`}" -}}
29939
+ {{- $image := .Params.image | default site.Params.defaultImage | default "${image || "/og-image.png"}" -}}
29940
+ {{- $imageURL := $image | absURL -}}
29941
+ {{- $canonical := .Permalink -}}
29942
+ {{- $type := cond (eq .Kind "page") "article" "website" -}}
29943
+
29944
+ {{- /* Full title with site name */ -}}
29945
+ {{- $fullTitle := $title -}}
29946
+ {{- if and (ne $title site.Title) (not (in $title site.Title)) -}}
29947
+ {{- $fullTitle = printf "%s | %s" $title site.Title -}}
29948
+ {{- end -}}
29949
+
29950
+ <!-- Primary Meta Tags -->
29951
+ <title>{{ $fullTitle }}</title>
29952
+ <meta name="title" content="{{ $fullTitle }}">
29953
+ <meta name="description" content="{{ $description | truncate 160 }}">
29954
+ <meta name="robots" content="{{ if .Params.noindex }}noindex, nofollow{{ else }}index, follow{{ end }}">
29955
+ <link rel="canonical" href="{{ $canonical }}">
29956
+
29957
+ <!-- Open Graph / Facebook -->
29958
+ <meta property="og:type" content="{{ $type }}">
29959
+ <meta property="og:url" content="{{ $canonical }}">
29960
+ <meta property="og:title" content="{{ $fullTitle }}">
29961
+ <meta property="og:description" content="{{ $description | truncate 160 }}">
29962
+ <meta property="og:image" content="{{ $imageURL }}">
29963
+ <meta property="og:image:width" content="1200">
29964
+ <meta property="og:image:height" content="630">
29965
+ <meta property="og:site_name" content="{{ site.Title }}">
29966
+ <meta property="og:locale" content="{{ site.Language.Lang | default "en" }}_{{ site.Language.Lang | default "US" | upper }}">
29967
+
29968
+ {{- if eq $type "article" }}
29969
+ {{- with .PublishDate }}
29970
+ <meta property="article:published_time" content="{{ .Format "2006-01-02T15:04:05Z07:00" }}">
29971
+ {{- end }}
29972
+ {{- with .Lastmod }}
29973
+ <meta property="article:modified_time" content="{{ .Format "2006-01-02T15:04:05Z07:00" }}">
29974
+ {{- end }}
29975
+ {{- with .Params.author }}
29976
+ <meta property="article:author" content="{{ . }}">
29977
+ {{- end }}
29978
+ {{- range .Params.tags }}
29979
+ <meta property="article:tag" content="{{ . }}">
29980
+ {{- end }}
29981
+ {{- end }}
29982
+
29983
+ <!-- Twitter -->
29984
+ <meta name="twitter:card" content="summary_large_image">
29985
+ <meta name="twitter:title" content="{{ $fullTitle }}">
29986
+ <meta name="twitter:description" content="{{ $description | truncate 160 }}">
29987
+ <meta name="twitter:image" content="{{ $imageURL }}">
29988
+ {{- with site.Params.twitterHandle }}
29989
+ <meta name="twitter:site" content="{{ . }}">
29990
+ <meta name="twitter:creator" content="{{ . }}">
29991
+ {{- end }}
29992
+
29993
+ <!-- JSON-LD: WebSite -->
29994
+ <script type="application/ld+json">
29995
+ {
29996
+ "@context": "https://schema.org",
29997
+ "@type": "WebSite",
29998
+ "name": "{{ site.Title }}",
29999
+ "url": "{{ site.BaseURL }}"
30000
+ }
30001
+ </script>
30002
+
30003
+ {{- /* Article Schema */ -}}
30004
+ {{- if eq $type "article" }}
30005
+ <script type="application/ld+json">
30006
+ {
30007
+ "@context": "https://schema.org",
30008
+ "@type": "Article",
30009
+ "headline": "{{ $title }}",
30010
+ "description": "{{ $description | truncate 160 }}",
30011
+ "image": "{{ $imageURL }}",
30012
+ "datePublished": "{{ .PublishDate.Format "2006-01-02T15:04:05Z07:00" }}",
30013
+ "dateModified": "{{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" }}",
30014
+ "author": {
30015
+ "@type": "Person",
30016
+ "name": "{{ .Params.author | default site.Params.author | default "Author" }}"
30017
+ },
30018
+ "publisher": {
30019
+ "@type": "Organization",
30020
+ "name": "{{ site.Title }}",
30021
+ "logo": {
30022
+ "@type": "ImageObject",
30023
+ "url": "{{ "logo.png" | absURL }}"
30024
+ }
30025
+ }
30026
+ }
30027
+ </script>
30028
+ {{- end }}
30029
+
30030
+ {{- /* Breadcrumbs */ -}}
30031
+ {{- if .Parent }}
30032
+ <script type="application/ld+json">
30033
+ {
30034
+ "@context": "https://schema.org",
30035
+ "@type": "BreadcrumbList",
30036
+ "itemListElement": [
30037
+ {{- $breadcrumbs := slice -}}
30038
+ {{- range .Ancestors.Reverse }}
30039
+ {{- $breadcrumbs = $breadcrumbs | append (dict "name" .Title "url" .Permalink) -}}
30040
+ {{- end }}
30041
+ {{- $breadcrumbs = $breadcrumbs | append (dict "name" .Title "url" .Permalink) -}}
30042
+ {{- range $i, $item := $breadcrumbs }}
30043
+ {{- if $i }},{{ end }}
30044
+ {
30045
+ "@type": "ListItem",
30046
+ "position": {{ add $i 1 }},
30047
+ "name": "{{ $item.name }}",
30048
+ "item": "{{ $item.url }}"
30049
+ }
30050
+ {{- end }}
30051
+ ]
30052
+ }
30053
+ </script>
30054
+ {{- end }}`,
30055
+ explanation: `Hugo comprehensive SEO partial with:
30056
+ \u2022 Automatic title handling
30057
+ \u2022 Full Open Graph with article support
30058
+ \u2022 Twitter Cards
30059
+ \u2022 JSON-LD schemas (WebSite, Article, Breadcrumbs)
30060
+ \u2022 Content summaries for descriptions
30061
+
30062
+ Usage: {{ partial "seo.html" . }}`,
30063
+ additionalFiles: [
30064
+ {
30065
+ file: "layouts/_default/baseof.html",
30066
+ code: `<!DOCTYPE html>
30067
+ <html lang="{{ site.Language.Lang | default "en" }}">
30068
+ <head>
30069
+ <meta charset="utf-8">
30070
+ <meta name="viewport" content="width=device-width, initial-scale=1">
30071
+
30072
+ {{ partial "seo.html" . }}
30073
+
30074
+ <!-- Favicon -->
30075
+ <link rel="icon" href="{{ "favicon.ico" | relURL }}">
30076
+ <link rel="apple-touch-icon" sizes="180x180" href="{{ "apple-touch-icon.png" | relURL }}">
30077
+
30078
+ <!-- Styles -->
30079
+ {{ $styles := resources.Get "css/styles.css" | minify | fingerprint }}
30080
+ <link rel="stylesheet" href="{{ $styles.Permalink }}">
30081
+ </head>
30082
+ <body>
30083
+ {{ block "main" . }}{{ end }}
30084
+ </body>
30085
+ </html>`,
30086
+ explanation: "Hugo base template with SEO partial."
30087
+ },
30088
+ {
30089
+ file: "config.toml",
30090
+ code: `baseURL = '${siteUrl}/'
30091
+ languageCode = 'en-us'
30092
+ title = '${siteName}'
30093
+
30094
+ [params]
30095
+ description = '${description || `${siteName} - A compelling description.`}'
30096
+ defaultImage = '${image || "/og-image.png"}'
30097
+ twitterHandle = '${twitterHandle || ""}'
30098
+ author = 'Your Name'
30099
+
30100
+ [sitemap]
30101
+ changefreq = 'weekly'
30102
+ filename = 'sitemap.xml'
30103
+ priority = 0.5
30104
+
30105
+ [outputs]
30106
+ home = ['HTML', 'RSS', 'JSON']
30107
+
30108
+ # Robots.txt
30109
+ enableRobotsTXT = true`,
30110
+ explanation: "Hugo configuration with SEO settings."
30111
+ },
30112
+ {
30113
+ file: "layouts/robots.txt",
30114
+ code: `User-agent: *
30115
+ Allow: /
30116
+ Disallow: /admin/
30117
+
30118
+ User-agent: GPTBot
30119
+ Allow: /
30120
+
30121
+ Sitemap: {{ .Site.BaseURL }}sitemap.xml`,
30122
+ explanation: "Hugo robots.txt template."
30123
+ }
30124
+ ]
30125
+ };
30126
+ }
30127
+ function generateJekyllSEO(options) {
30128
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
30129
+ return {
30130
+ file: "_includes/seo.html",
30131
+ code: `{% comment %}
30132
+ Comprehensive SEO include for Jekyll
30133
+
30134
+ Features:
30135
+ - Full Open Graph support
30136
+ - Twitter Cards
30137
+ - JSON-LD structured data
30138
+ - jekyll-seo-tag compatible
30139
+
30140
+ Usage: {% include seo.html %}
30141
+ {% endcomment %}
30142
+
30143
+ {%- assign seo_title = page.title | default: site.title -%}
30144
+ {%- if page.title and site.title and page.title != site.title -%}
30145
+ {%- assign seo_title = page.title | append: " | " | append: site.title -%}
30146
+ {%- endif -%}
30147
+
30148
+ {%- assign seo_description = page.description | default: page.excerpt | default: site.description | default: "${description || `${siteName} - A compelling description.`}" | strip_html | truncate: 160 -%}
30149
+
30150
+ {%- assign seo_image = page.image | default: site.og_image | default: "${image || "/og-image.png"}" -%}
30151
+ {%- unless seo_image contains "://" -%}
30152
+ {%- assign seo_image = seo_image | absolute_url -%}
30153
+ {%- endunless -%}
30154
+
30155
+ {%- assign seo_type = "website" -%}
30156
+ {%- if page.layout == "post" -%}
30157
+ {%- assign seo_type = "article" -%}
30158
+ {%- endif -%}
30159
+
30160
+ <!-- Primary Meta Tags -->
30161
+ <title>{{ seo_title }}</title>
30162
+ <meta name="title" content="{{ seo_title }}">
30163
+ <meta name="description" content="{{ seo_description }}">
30164
+ <meta name="robots" content="{% if page.noindex %}noindex, nofollow{% else %}index, follow{% endif %}">
30165
+ <link rel="canonical" href="{{ page.url | absolute_url }}">
30166
+
30167
+ <!-- Open Graph / Facebook -->
30168
+ <meta property="og:type" content="{{ seo_type }}">
30169
+ <meta property="og:url" content="{{ page.url | absolute_url }}">
30170
+ <meta property="og:title" content="{{ seo_title }}">
30171
+ <meta property="og:description" content="{{ seo_description }}">
30172
+ <meta property="og:image" content="{{ seo_image }}">
30173
+ <meta property="og:image:width" content="1200">
30174
+ <meta property="og:image:height" content="630">
30175
+ <meta property="og:site_name" content="{{ site.title }}">
30176
+ <meta property="og:locale" content="{{ site.locale | default: "en_US" }}">
30177
+
30178
+ {% if seo_type == "article" %}
30179
+ {% if page.date %}
30180
+ <meta property="article:published_time" content="{{ page.date | date_to_xmlschema }}">
30181
+ {% endif %}
30182
+ {% if page.last_modified_at %}
30183
+ <meta property="article:modified_time" content="{{ page.last_modified_at | date_to_xmlschema }}">
30184
+ {% endif %}
30185
+ {% if page.author %}
30186
+ <meta property="article:author" content="{{ page.author }}">
30187
+ {% endif %}
30188
+ {% for tag in page.tags %}
30189
+ <meta property="article:tag" content="{{ tag }}">
30190
+ {% endfor %}
30191
+ {% endif %}
30192
+
30193
+ <!-- Twitter -->
30194
+ <meta name="twitter:card" content="summary_large_image">
30195
+ <meta name="twitter:title" content="{{ seo_title }}">
30196
+ <meta name="twitter:description" content="{{ seo_description }}">
30197
+ <meta name="twitter:image" content="{{ seo_image }}">
30198
+ {% if site.twitter_handle %}
30199
+ <meta name="twitter:site" content="{{ site.twitter_handle }}">
30200
+ <meta name="twitter:creator" content="{{ site.twitter_handle }}">
30201
+ {% endif %}
30202
+
30203
+ <!-- JSON-LD: WebSite -->
30204
+ <script type="application/ld+json">
30205
+ {
30206
+ "@context": "https://schema.org",
30207
+ "@type": "WebSite",
30208
+ "name": "{{ site.title }}",
30209
+ "url": "{{ site.url }}"
30210
+ }
30211
+ </script>
30212
+
30213
+ {% if seo_type == "article" %}
30214
+ <!-- JSON-LD: Article -->
30215
+ <script type="application/ld+json">
30216
+ {
30217
+ "@context": "https://schema.org",
30218
+ "@type": "Article",
30219
+ "headline": "{{ page.title | escape }}",
30220
+ "description": "{{ seo_description | escape }}",
30221
+ "image": "{{ seo_image }}",
30222
+ "datePublished": "{{ page.date | date_to_xmlschema }}",
30223
+ "dateModified": "{{ page.last_modified_at | default: page.date | date_to_xmlschema }}",
30224
+ "author": {
30225
+ "@type": "Person",
30226
+ "name": "{{ page.author | default: site.author | default: "Author" }}"
30227
+ },
30228
+ "publisher": {
30229
+ "@type": "Organization",
30230
+ "name": "{{ site.title }}",
30231
+ "logo": {
30232
+ "@type": "ImageObject",
30233
+ "url": "{{ '/logo.png' | absolute_url }}"
30234
+ }
30235
+ }
30236
+ }
30237
+ </script>
30238
+ {% endif %}`,
30239
+ explanation: `Jekyll comprehensive SEO include with:
30240
+ \u2022 Full Open Graph with article support
30241
+ \u2022 Twitter Cards
30242
+ \u2022 JSON-LD schemas
30243
+ \u2022 Liquid template integration
30244
+
30245
+ Usage: {% include seo.html %}`,
30246
+ additionalFiles: [
30247
+ {
30248
+ file: "_layouts/default.html",
30249
+ code: `<!DOCTYPE html>
30250
+ <html lang="{{ site.locale | default: "en" | slice: 0, 2 }}">
30251
+ <head>
30252
+ <meta charset="utf-8">
30253
+ <meta name="viewport" content="width=device-width, initial-scale=1">
30254
+
30255
+ {% include seo.html %}
30256
+
30257
+ <link rel="icon" href="{{ '/favicon.ico' | relative_url }}">
30258
+ <link rel="apple-touch-icon" sizes="180x180" href="{{ '/apple-touch-icon.png' | relative_url }}">
30259
+
30260
+ <link rel="stylesheet" href="{{ '/assets/css/styles.css' | relative_url }}">
30261
+ </head>
30262
+ <body>
30263
+ {{ content }}
30264
+ </body>
30265
+ </html>`,
30266
+ explanation: "Jekyll default layout with SEO include."
30267
+ },
30268
+ {
30269
+ file: "_config.yml",
30270
+ code: `title: ${siteName}
30271
+ description: "${description || `${siteName} - A compelling description.`}"
30272
+ url: "${siteUrl}"
30273
+ baseurl: ""
30274
+
30275
+ # SEO
30276
+ og_image: "${image || "/og-image.png"}"
30277
+ twitter_handle: "${twitterHandle || ""}"
30278
+ locale: "en_US"
30279
+ author: "Your Name"
30280
+
30281
+ # Plugins
30282
+ plugins:
30283
+ - jekyll-sitemap
30284
+ - jekyll-feed
30285
+
30286
+ # Defaults
30287
+ defaults:
30288
+ - scope:
30289
+ path: ""
30290
+ type: "posts"
30291
+ values:
30292
+ layout: "post"
30293
+ - scope:
30294
+ path: ""
30295
+ values:
30296
+ layout: "default"`,
30297
+ explanation: "Jekyll configuration with SEO settings."
30298
+ },
30299
+ {
30300
+ file: "robots.txt",
30301
+ code: `---
30302
+ ---
30303
+ User-agent: *
30304
+ Allow: /
30305
+ Disallow: /admin/
30306
+
30307
+ User-agent: GPTBot
30308
+ Allow: /
30309
+
30310
+ Sitemap: {{ site.url }}/sitemap.xml`,
30311
+ explanation: "Jekyll robots.txt with front matter."
30312
+ }
30313
+ ]
30314
+ };
30315
+ }
30316
+ function generateEleventySEO(options) {
30317
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
30318
+ return {
30319
+ file: "src/_includes/seo.njk",
30320
+ code: `{#
30321
+ Comprehensive SEO partial for Eleventy (11ty)
30322
+
30323
+ Features:
30324
+ - Full Open Graph support
30325
+ - Twitter Cards
30326
+ - JSON-LD structured data
30327
+ - Nunjucks template
30328
+
30329
+ Usage: {% include "seo.njk" %}
30330
+ #}
30331
+
30332
+ {%- set seoTitle = title or metadata.title -%}
30333
+ {%- set fullTitle = seoTitle -%}
30334
+ {%- if seoTitle and metadata.title and seoTitle != metadata.title -%}
30335
+ {%- set fullTitle = seoTitle + " | " + metadata.title -%}
30336
+ {%- endif -%}
30337
+
30338
+ {%- set seoDescription = description or excerpt or metadata.description or "${description || `${siteName} - A compelling description.`}" -%}
30339
+ {%- set seoImage = image or metadata.defaultImage or "${image || "/og-image.png"}" -%}
30340
+ {%- if not seoImage.startsWith("http") -%}
30341
+ {%- set seoImage = metadata.url + seoImage -%}
30342
+ {%- endif -%}
30343
+
30344
+ {%- set pageUrl = page.url | url | absoluteUrl(metadata.url) -%}
30345
+ {%- set seoType = "article" if layout == "post" else "website" -%}
30346
+
30347
+ <!-- Primary Meta Tags -->
30348
+ <title>{{ fullTitle }}</title>
30349
+ <meta name="title" content="{{ fullTitle }}">
30350
+ <meta name="description" content="{{ seoDescription | truncate(160) }}">
30351
+ <meta name="robots" content="{{ 'noindex, nofollow' if noindex else 'index, follow' }}">
30352
+ <link rel="canonical" href="{{ pageUrl }}">
30353
+
30354
+ <!-- Open Graph / Facebook -->
30355
+ <meta property="og:type" content="{{ seoType }}">
30356
+ <meta property="og:url" content="{{ pageUrl }}">
30357
+ <meta property="og:title" content="{{ fullTitle }}">
30358
+ <meta property="og:description" content="{{ seoDescription | truncate(160) }}">
30359
+ <meta property="og:image" content="{{ seoImage }}">
30360
+ <meta property="og:image:width" content="1200">
30361
+ <meta property="og:image:height" content="630">
30362
+ <meta property="og:site_name" content="{{ metadata.title }}">
30363
+ <meta property="og:locale" content="{{ metadata.locale or 'en_US' }}">
30364
+
30365
+ {% if seoType == "article" %}
30366
+ {% if date %}
30367
+ <meta property="article:published_time" content="{{ date | dateToISO }}">
30368
+ {% endif %}
30369
+ {% if lastModified %}
30370
+ <meta property="article:modified_time" content="{{ lastModified | dateToISO }}">
30371
+ {% endif %}
30372
+ {% if author %}
30373
+ <meta property="article:author" content="{{ author }}">
30374
+ {% endif %}
30375
+ {% for tag in tags %}
30376
+ {% if tag != "posts" and tag != "all" %}
30377
+ <meta property="article:tag" content="{{ tag }}">
30378
+ {% endif %}
30379
+ {% endfor %}
30380
+ {% endif %}
30381
+
30382
+ <!-- Twitter -->
30383
+ <meta name="twitter:card" content="summary_large_image">
30384
+ <meta name="twitter:title" content="{{ fullTitle }}">
30385
+ <meta name="twitter:description" content="{{ seoDescription | truncate(160) }}">
30386
+ <meta name="twitter:image" content="{{ seoImage }}">
30387
+ {% if metadata.twitterHandle %}
30388
+ <meta name="twitter:site" content="{{ metadata.twitterHandle }}">
30389
+ <meta name="twitter:creator" content="{{ metadata.twitterHandle }}">
30390
+ {% endif %}
30391
+
30392
+ <!-- JSON-LD: WebSite -->
30393
+ <script type="application/ld+json">
30394
+ {
30395
+ "@context": "https://schema.org",
30396
+ "@type": "WebSite",
30397
+ "name": "{{ metadata.title }}",
30398
+ "url": "{{ metadata.url }}"
30399
+ }
30400
+ </script>
30401
+
30402
+ {% if seoType == "article" %}
30403
+ <!-- JSON-LD: Article -->
30404
+ <script type="application/ld+json">
30405
+ {
30406
+ "@context": "https://schema.org",
30407
+ "@type": "Article",
30408
+ "headline": "{{ title }}",
30409
+ "description": "{{ seoDescription | truncate(160) | escape }}",
30410
+ "image": "{{ seoImage }}",
30411
+ "datePublished": "{{ date | dateToISO }}",
30412
+ "dateModified": "{{ (lastModified or date) | dateToISO }}",
30413
+ "author": {
30414
+ "@type": "Person",
30415
+ "name": "{{ author or metadata.author or 'Author' }}"
30416
+ },
30417
+ "publisher": {
30418
+ "@type": "Organization",
30419
+ "name": "{{ metadata.title }}",
30420
+ "logo": {
30421
+ "@type": "ImageObject",
30422
+ "url": "{{ metadata.url }}/logo.png"
30423
+ }
30424
+ }
30425
+ }
30426
+ </script>
30427
+ {% endif %}`,
30428
+ explanation: `Eleventy comprehensive SEO partial with:
30429
+ \u2022 Full Open Graph with article support
30430
+ \u2022 Twitter Cards
30431
+ \u2022 JSON-LD schemas
30432
+ \u2022 Nunjucks templating
30433
+
30434
+ Usage: {% include "seo.njk" %}`,
30435
+ additionalFiles: [
30436
+ {
30437
+ file: "src/_includes/base.njk",
30438
+ code: `<!DOCTYPE html>
30439
+ <html lang="{{ metadata.locale | default('en') | truncate(2, true, '') }}">
30440
+ <head>
30441
+ <meta charset="utf-8">
30442
+ <meta name="viewport" content="width=device-width, initial-scale=1">
30443
+
30444
+ {% include "seo.njk" %}
30445
+
30446
+ <link rel="icon" href="/favicon.ico">
30447
+ <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
30448
+
30449
+ <link rel="stylesheet" href="/css/styles.css">
30450
+ </head>
30451
+ <body>
30452
+ {{ content | safe }}
30453
+ </body>
30454
+ </html>`,
30455
+ explanation: "Eleventy base layout with SEO include."
30456
+ },
30457
+ {
30458
+ file: "src/_data/metadata.json",
30459
+ code: `{
30460
+ "title": "${siteName}",
30461
+ "url": "${siteUrl}",
30462
+ "description": "${description || `${siteName} - A compelling description.`}",
30463
+ "defaultImage": "${image || "/og-image.png"}",
30464
+ "twitterHandle": "${twitterHandle || ""}",
30465
+ "locale": "en_US",
30466
+ "author": "Your Name"
30467
+ }`,
30468
+ explanation: "Eleventy global data file with SEO metadata."
30469
+ },
30470
+ {
30471
+ file: "src/robots.njk",
30472
+ code: `---
30473
+ permalink: /robots.txt
30474
+ eleventyExcludeFromCollections: true
30475
+ ---
30476
+ User-agent: *
30477
+ Allow: /
30478
+ Disallow: /admin/
30479
+
30480
+ User-agent: GPTBot
30481
+ Allow: /
30482
+
30483
+ Sitemap: {{ metadata.url }}/sitemap.xml`,
30484
+ explanation: "Eleventy robots.txt template."
30485
+ }
30486
+ ]
30487
+ };
30488
+ }
30489
+ function generateRemixSEO(options) {
30490
+ const { siteName, siteUrl, description, image, twitterHandle } = options;
30491
+ return {
30492
+ file: "app/utils/seo.ts",
30493
+ code: `import type { V2_MetaFunction, V2_MetaDescriptor } from '@remix-run/node';
30494
+
30495
+ /**
30496
+ * Comprehensive SEO utilities for Remix
30497
+ *
30498
+ * Features:
30499
+ * - Full Open Graph support
30500
+ * - Twitter Cards
30501
+ * - JSON-LD structured data
30502
+ * - Type-safe meta function helpers
30503
+ */
30504
+
30505
+ const SITE_NAME = '${siteName}';
30506
+ const SITE_URL = '${siteUrl}';
30507
+ const DEFAULT_IMAGE = '${image || `${siteUrl}/og-image.png`}';
30508
+ const DEFAULT_DESCRIPTION = '${description || `${siteName} - A compelling description.`}';
30509
+ const TWITTER_HANDLE = '${twitterHandle || ""}';
30510
+
30511
+ interface SEOConfig {
30512
+ title?: string;
30513
+ description?: string;
30514
+ image?: string;
30515
+ url?: string;
30516
+ type?: 'website' | 'article';
30517
+ publishedTime?: string;
30518
+ modifiedTime?: string;
30519
+ author?: string;
30520
+ tags?: string[];
30521
+ noIndex?: boolean;
30522
+ }
30523
+
30524
+ /**
30525
+ * Generate meta tags for a page
30526
+ */
30527
+ export function generateMeta(config: SEOConfig = {}): V2_MetaDescriptor[] {
30528
+ const {
30529
+ title,
30530
+ description = DEFAULT_DESCRIPTION,
30531
+ image = DEFAULT_IMAGE,
30532
+ url = SITE_URL,
30533
+ type = 'website',
30534
+ publishedTime,
30535
+ modifiedTime,
30536
+ author,
30537
+ tags = [],
30538
+ noIndex = false,
30539
+ } = config;
30540
+
30541
+ const fullTitle = title
30542
+ ? title.includes(SITE_NAME)
30543
+ ? title
30544
+ : \`\${title} | \${SITE_NAME}\`
30545
+ : SITE_NAME;
30546
+
30547
+ const imageUrl = image.startsWith('http') ? image : \`\${SITE_URL}\${image}\`;
30548
+ const robotsContent = noIndex ? 'noindex, nofollow' : 'index, follow';
30549
+
30550
+ const meta: V2_MetaDescriptor[] = [
30551
+ { title: fullTitle },
30552
+ { name: 'description', content: description },
30553
+ { name: 'robots', content: robotsContent },
30554
+
30555
+ // Open Graph
30556
+ { property: 'og:type', content: type },
30557
+ { property: 'og:url', content: url },
30558
+ { property: 'og:title', content: fullTitle },
30559
+ { property: 'og:description', content: description },
30560
+ { property: 'og:image', content: imageUrl },
30561
+ { property: 'og:image:width', content: '1200' },
30562
+ { property: 'og:image:height', content: '630' },
30563
+ { property: 'og:site_name', content: SITE_NAME },
30564
+
30565
+ // Twitter
30566
+ { name: 'twitter:card', content: 'summary_large_image' },
30567
+ { name: 'twitter:title', content: fullTitle },
30568
+ { name: 'twitter:description', content: description },
30569
+ { name: 'twitter:image', content: imageUrl },
30570
+ ];
30571
+
30572
+ if (TWITTER_HANDLE) {
30573
+ meta.push(
30574
+ { name: 'twitter:site', content: TWITTER_HANDLE },
30575
+ { name: 'twitter:creator', content: TWITTER_HANDLE }
30576
+ );
30577
+ }
30578
+
30579
+ // Article-specific meta
30580
+ if (type === 'article') {
30581
+ if (publishedTime) {
30582
+ meta.push({ property: 'article:published_time', content: publishedTime });
30583
+ }
30584
+ if (modifiedTime) {
30585
+ meta.push({ property: 'article:modified_time', content: modifiedTime });
30586
+ }
30587
+ if (author) {
30588
+ meta.push({ property: 'article:author', content: author });
30589
+ }
30590
+ tags.forEach((tag) => {
30591
+ meta.push({ property: 'article:tag', content: tag });
30592
+ });
30593
+ }
30594
+
30595
+ return meta;
30596
+ }
30597
+
30598
+ /**
30599
+ * Generate canonical link
30600
+ */
30601
+ export function generateLinks(url: string) {
30602
+ return [{ rel: 'canonical', href: url }];
30603
+ }
30604
+
30605
+ // JSON-LD Schema Generators
30606
+
30607
+ export function websiteSchema() {
30608
+ return {
30609
+ '@context': 'https://schema.org',
30610
+ '@type': 'WebSite',
30611
+ name: SITE_NAME,
30612
+ url: SITE_URL,
30613
+ };
30614
+ }
30615
+
30616
+ export function articleSchema(data: {
30617
+ headline: string;
30618
+ description: string;
30619
+ image?: string;
30620
+ datePublished: string;
30621
+ dateModified?: string;
30622
+ author: { name: string; url?: string };
30623
+ }) {
30624
+ return {
30625
+ '@context': 'https://schema.org',
30626
+ '@type': 'Article',
30627
+ headline: data.headline,
30628
+ description: data.description,
30629
+ image: data.image || DEFAULT_IMAGE,
30630
+ datePublished: data.datePublished,
30631
+ dateModified: data.dateModified || data.datePublished,
30632
+ author: { '@type': 'Person', ...data.author },
30633
+ publisher: {
30634
+ '@type': 'Organization',
30635
+ name: SITE_NAME,
30636
+ logo: { '@type': 'ImageObject', url: \`\${SITE_URL}/logo.png\` },
30637
+ },
30638
+ };
30639
+ }
30640
+
30641
+ export function productSchema(data: {
30642
+ name: string;
30643
+ description: string;
30644
+ image?: string;
30645
+ price: number;
30646
+ currency?: string;
30647
+ inStock?: boolean;
30648
+ }) {
30649
+ return {
30650
+ '@context': 'https://schema.org',
30651
+ '@type': 'Product',
30652
+ name: data.name,
30653
+ description: data.description,
30654
+ image: data.image || DEFAULT_IMAGE,
30655
+ offers: {
30656
+ '@type': 'Offer',
30657
+ price: data.price.toString(),
30658
+ priceCurrency: data.currency || 'USD',
30659
+ availability: \`https://schema.org/\${data.inStock !== false ? 'InStock' : 'OutOfStock'}\`,
30660
+ },
30661
+ };
30662
+ }
30663
+
30664
+ export function faqSchema(items: { question: string; answer: string }[]) {
30665
+ return {
30666
+ '@context': 'https://schema.org',
30667
+ '@type': 'FAQPage',
30668
+ mainEntity: items.map((item) => ({
30669
+ '@type': 'Question',
30670
+ name: item.question,
30671
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
30672
+ })),
30673
+ };
30674
+ }
30675
+
30676
+ export function breadcrumbSchema(items: { name: string; url: string }[]) {
30677
+ return {
30678
+ '@context': 'https://schema.org',
30679
+ '@type': 'BreadcrumbList',
30680
+ itemListElement: items.map((item, i) => ({
30681
+ '@type': 'ListItem',
30682
+ position: i + 1,
30683
+ name: item.name,
30684
+ item: item.url,
30685
+ })),
30686
+ };
30687
+ }
30688
+
30689
+ /**
30690
+ * JSON-LD Script component helper
30691
+ */
30692
+ export function JsonLd({ schema }: { schema: Record<string, unknown> }) {
30693
+ return (
30694
+ <script
30695
+ type="application/ld+json"
30696
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
30697
+ />
30698
+ );
30699
+ }`,
30700
+ explanation: `Remix comprehensive SEO utilities with:
30701
+ \u2022 Type-safe meta function helpers
30702
+ \u2022 Full Open Graph with article support
30703
+ \u2022 Twitter Cards
30704
+ \u2022 JSON-LD schema generators
30705
+ \u2022 Remix V2 meta conventions
30706
+
30707
+ Usage in route:
30708
+ export const meta: V2_MetaFunction = () => generateMeta({ title: "Page" });`,
30709
+ additionalFiles: [
30710
+ {
30711
+ file: "app/root.tsx",
30712
+ code: `import {
30713
+ Links,
30714
+ LiveReload,
30715
+ Meta,
30716
+ Outlet,
30717
+ Scripts,
30718
+ ScrollRestoration,
30719
+ } from '@remix-run/react';
30720
+ import { JsonLd, websiteSchema } from '~/utils/seo';
30721
+
30722
+ export default function App() {
30723
+ return (
30724
+ <html lang="en">
30725
+ <head>
30726
+ <meta charSet="utf-8" />
30727
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
30728
+ <Meta />
30729
+ <Links />
30730
+ <JsonLd schema={websiteSchema()} />
30731
+ </head>
30732
+ <body>
30733
+ <Outlet />
30734
+ <ScrollRestoration />
30735
+ <Scripts />
30736
+ <LiveReload />
30737
+ </body>
30738
+ </html>
30739
+ );
30740
+ }`,
30741
+ explanation: "Remix root with SEO integration."
30742
+ },
30743
+ {
30744
+ file: "public/robots.txt",
30745
+ code: `User-agent: *
30746
+ Allow: /
30747
+ Disallow: /admin/
30748
+ Disallow: /api/
30749
+
30750
+ User-agent: GPTBot
30751
+ Allow: /
30752
+
30753
+ Sitemap: ${siteUrl}/sitemap.xml`,
30754
+ explanation: "Robots.txt with AI crawler support."
30755
+ }
30756
+ ]
30757
+ };
30758
+ }
30759
+ function getFrameworkSpecificFixExtended(framework, options) {
30760
+ const name = framework.name.toLowerCase();
30761
+ if (name.includes("next")) {
30762
+ return framework.router === "app" ? generateNextJsAppRouterMetadata(options) : generateNextJsPagesRouterHead(options);
30763
+ }
30764
+ if (name.includes("nuxt")) return generateNuxtSEOHead(options);
30765
+ if (name.includes("vue")) return generateVueSEOHead(options);
30766
+ if (name.includes("astro")) return generateAstroBaseHead(options);
30767
+ if (name.includes("svelte")) return generateSvelteKitSEOHead(options);
30768
+ if (name.includes("angular")) return generateAngularSEOService(options);
30769
+ if (name.includes("remix")) return generateRemixSEO(options);
30770
+ if (name.includes("rails") || name.includes("ruby")) return generateRailsSEOHelper(options);
30771
+ if (name.includes("django")) return generateDjangoSEOHelper(options);
30772
+ if (name.includes("laravel") || name.includes("php")) return generateLaravelSEOHelper(options);
30773
+ if (name.includes("spring") || name.includes("java")) return generateSpringBootSEO(options);
30774
+ if (name.includes("phoenix") || name.includes("elixir")) return generatePhoenixSEO(options);
30775
+ if (name.includes("asp") || name.includes(".net") || name.includes("csharp") || name.includes("c#")) {
30776
+ return generateAspNetCoreSEO(options);
30777
+ }
30778
+ if (name.includes("go") || name.includes("gin") || name.includes("echo") || name.includes("fiber")) {
30779
+ return generateGoSEO(options);
30780
+ }
30781
+ if (name.includes("htmx") || name.includes("hotwire") || name.includes("turbo")) {
30782
+ return generateHTMXSEO(options);
30783
+ }
30784
+ if (name.includes("hugo")) return generateHugoSEO(options);
30785
+ if (name.includes("jekyll")) return generateJekyllSEO(options);
30786
+ if (name.includes("eleventy") || name.includes("11ty")) return generateEleventySEO(options);
30787
+ return generateReactSEOHead(options);
30788
+ }
30789
+
30790
+ // src/fixer.ts
30791
+ async function generateFixes(issues, options) {
30792
+ const { cwd, url } = options;
30793
+ let framework = options.framework;
30794
+ if (!framework) {
30795
+ const result = await detectFramework3({ cwd });
30796
+ framework = result.success ? result.data : { name: "Unknown", metaPattern: "html-head" };
30797
+ }
30798
+ const fixes = [];
30799
+ const htmlEntry = await findHtmlEntry({ cwd });
30800
+ const htmlPath = htmlEntry.success ? htmlEntry.data.path : "index.html";
30801
+ const htmlContent = htmlEntry.success ? htmlEntry.data.content : "";
30802
+ for (const issue of issues) {
30803
+ const fix = await generateFixForIssue(issue, {
30804
+ cwd,
30805
+ url,
30806
+ framework,
30807
+ htmlPath,
30808
+ htmlContent
30809
+ });
30810
+ if (fix) {
30811
+ fixes.push(fix);
30812
+ }
30813
+ }
30814
+ return fixes;
30815
+ }
30816
+ async function generateFixForIssue(issue, context) {
30817
+ const { cwd, url, framework, htmlPath, htmlContent } = context;
30818
+ const fullUrl = url || "https://example.com";
30819
+ const siteName = new URL(fullUrl).hostname.replace("www.", "");
30820
+ switch (issue.code) {
30821
+ // Title issues
30822
+ case "MISSING_TITLE":
30823
+ case "TITLE_MISSING":
30824
+ // Full audit code
30825
+ case "TITLE_KEYWORD_MISMATCH":
30826
+ case "TITLE_H1_KEYWORD_MISMATCH":
30827
+ case "OUTDATED_YEAR_IN_TITLE":
30828
+ return generateTitleFix(context, siteName);
30829
+ // Meta description issues
30830
+ case "MISSING_META_DESC":
30831
+ case "META_DESC_MISSING":
30832
+ return generateMetaDescFix(context, siteName);
30833
+ // Canonical issues
30834
+ case "MISSING_CANONICAL":
30835
+ case "CANONICAL_MISSING":
30836
+ // Full audit code
30837
+ case "CANONICAL_NO_HTTPS_REDIRECT":
30838
+ return generateCanonicalFix(context, fullUrl);
30839
+ // Viewport issues
30840
+ case "MISSING_VIEWPORT":
30841
+ case "HTML_NO_VIEWPORT":
30842
+ case "RESPONSIVE_NO_VIEWPORT":
30843
+ case "VIEWPORT_MISSING":
30844
+ return generateViewportFix(context);
30845
+ // Open Graph issues
30846
+ case "MISSING_OG_TAGS":
30847
+ case "OG_TITLE_MISSING":
30848
+ case "OG_DESC_MISSING":
30849
+ case "OG_DESCRIPTION_MISSING":
30850
+ // Full audit code
30851
+ case "OG_IMAGE_MISSING":
30852
+ case "OG_URL_MISSING":
30853
+ case "OG_TYPE_MISSING":
30854
+ return generateOGFix(context, siteName, fullUrl);
30855
+ // Twitter Card issues
30856
+ case "MISSING_TWITTER_CARD":
30857
+ case "TWITTER_CARD_MISSING":
30858
+ case "TWITTER_TITLE_MISSING":
30859
+ case "TWITTER_DESC_MISSING":
30860
+ case "TWITTER_DESCRIPTION_MISSING":
30861
+ // Full audit code
30862
+ case "TWITTER_IMAGE_MISSING":
30863
+ return generateTwitterFix(context, siteName);
30864
+ // Schema/structured data issues
30865
+ case "MISSING_SCHEMA":
30866
+ case "SCHEMA_MISSING":
30867
+ // Full audit code
30868
+ case "SCHEMA_ORG_MISSING":
30869
+ case "NO_ORGANIZATION_SCHEMA":
30870
+ case "NO_ENTITY_SCHEMA":
30871
+ case "FAQ_SCHEMA_MISSING":
30872
+ return generateSchemaFix(context, siteName, fullUrl);
30873
+ // Robots.txt issues
30874
+ case "MISSING_ROBOTS":
30875
+ case "ROBOTS_TXT_MISSING":
30876
+ // Full audit code
30877
+ case "ROBOTS_TXT_WARNINGS":
30878
+ case "ROBOTS_TXT_INVALID_SYNTAX":
30879
+ return generateRobotsFix(context, fullUrl);
30880
+ // Sitemap issues
30881
+ case "MISSING_SITEMAP":
30882
+ case "SITEMAP_MISSING":
30883
+ // Full audit code
30884
+ case "BING_SITEMAP_MISSING":
30885
+ return generateSitemapFix(context, fullUrl);
30886
+ // H1 issues
30887
+ case "MISSING_H1":
30888
+ case "H1_MISSING":
30889
+ // Full audit code
30890
+ case "NO_VISIBLE_HEADLINE":
30891
+ case "H1_MISSING_KEYWORD":
30892
+ return await generateH1Fix({ cwd });
30893
+ // SPA-specific: add meta management library recommendation
30894
+ case "SPA_NO_META_MANAGEMENT":
30895
+ case "SPA_WITHOUT_SSR":
30896
+ case "CLIENT_SIDE_RENDERING":
30897
+ return generateSPAMetaFix(context, framework);
30898
+ // Internal links - suggest adding links
30899
+ case "NO_INTERNAL_LINKS":
30900
+ case "LINKS_NO_INTERNAL":
30901
+ case "NO_CONTENT_INTERNAL_LINKS":
30902
+ return {
30903
+ issue: { code: issue.code, message: "No internal links found", severity: "warning" },
30904
+ file: "src/components/Footer.tsx",
30905
+ before: null,
30906
+ after: `// Add internal navigation links to your footer or content
30907
+ // Example: <Link to="/about">About</Link>`,
30908
+ explanation: "Add internal links to help search engines discover your content",
30909
+ skipped: true,
30910
+ skipReason: "Requires manual content updates - add links to your navigation and content"
30911
+ };
30912
+ // Preconnect issues
30913
+ case "GOOGLE_FONTS_NO_PRECONNECT":
30914
+ case "MISSING_PRECONNECT":
30915
+ return generatePreconnectFix(context);
30916
+ // Favicon/icons
30917
+ case "HTML_NO_FAVICON":
30918
+ case "HTML_NO_APPLE_TOUCH_ICON":
30919
+ case "FAVICON_MISSING":
30920
+ return generateFaviconFix(context);
30921
+ // Charset/lang
30922
+ case "HTML_NO_CHARSET":
30923
+ case "HTML_NOT_UTF8":
30924
+ return generateCharsetFix(context);
30925
+ case "HTML_NO_LANG":
30926
+ case "LANG_ATTR_MISSING":
30927
+ return generateLangFix(context);
30928
+ // AI/LLMs.txt
30929
+ case "AI_NO_LLMS_TXT":
30930
+ return generateLlmsTxtFix(context, siteName, fullUrl);
30931
+ default:
30932
+ return null;
30933
+ }
30934
+ }
30935
+ function generateTitleFix(context, siteName) {
30936
+ const { htmlPath, htmlContent, framework } = context;
30937
+ if (framework.metaPattern === "html-head") {
30938
+ const titleTag = `<title>${siteName} - Your Product Tagline</title>`;
30939
+ if (htmlContent.includes("<title>")) {
30940
+ const before = htmlContent.match(/<title>.*?<\/title>/)?.[0] || "";
30941
+ return {
30942
+ issue: { code: "MISSING_TITLE", message: "Missing or empty title tag", severity: "critical" },
30943
+ file: htmlPath,
30944
+ before,
30945
+ after: titleTag,
30946
+ explanation: "Updated the title tag with a descriptive title"
30947
+ };
30948
+ } else {
30949
+ return {
30950
+ issue: { code: "MISSING_TITLE", message: "Missing title tag", severity: "critical" },
30951
+ file: htmlPath,
30952
+ before: "<head>",
30953
+ after: `<head>
30954
+ ${titleTag}`,
30955
+ explanation: "Added a title tag to the document head"
30956
+ };
30957
+ }
30958
+ }
30959
+ return {
30960
+ issue: { code: "MISSING_TITLE", message: "Missing title tag", severity: "critical" },
30961
+ file: htmlPath,
30962
+ before: null,
30963
+ after: `<title>${siteName} - Your Product Tagline</title>`,
30964
+ explanation: `Add title using ${framework.name} patterns`
30965
+ };
30966
+ }
30967
+ function generateMetaDescFix(context, siteName) {
30968
+ const { htmlPath, htmlContent, framework } = context;
30969
+ const description = `${siteName} - A brief, compelling description of your product or service. Include key features and benefits.`;
30970
+ if (framework.metaPattern === "html-head") {
30971
+ const metaTag = `<meta name="description" content="${description}" />`;
30972
+ if (htmlContent.includes("<title>")) {
30973
+ return {
30974
+ issue: { code: "MISSING_META_DESC", message: "Missing meta description", severity: "critical" },
30975
+ file: htmlPath,
30976
+ before: "</title>",
30977
+ after: `</title>
30978
+ ${metaTag}`,
30979
+ explanation: "Added meta description after the title tag"
30980
+ };
30981
+ } else {
30982
+ return {
30983
+ issue: { code: "MISSING_META_DESC", message: "Missing meta description", severity: "critical" },
30984
+ file: htmlPath,
30985
+ before: "<head>",
30986
+ after: `<head>
30987
+ ${metaTag}`,
30988
+ explanation: "Added meta description to the document head"
30989
+ };
30990
+ }
30991
+ }
30992
+ return {
30993
+ issue: { code: "MISSING_META_DESC", message: "Missing meta description", severity: "critical" },
30994
+ file: htmlPath,
30995
+ before: null,
27635
30996
  after: `<meta name="description" content="${description}" />`,
27636
30997
  explanation: `Add meta description using ${framework.name} patterns`
27637
30998
  };
@@ -30394,6 +33755,121 @@ function getAIVisibilitySummary(results) {
30394
33755
  };
30395
33756
  }
30396
33757
 
33758
+ // src/analyzers/index.ts
33759
+ var analyzers_exports = {};
33760
+ __export(analyzers_exports, {
33761
+ AI_CRAWLERS_INFO: () => AI_CRAWLERS_INFO,
33762
+ analyzeCitationReadiness: () => analyzeCitationReadiness,
33763
+ analyzeComprehensive: () => analyzeComprehensive,
33764
+ analyzeContentStructure: () => analyzeContentStructure,
33765
+ analyzeCoreWebVitals: () => analyzeCoreWebVitals,
33766
+ analyzeEntityExtraction: () => analyzeEntityExtraction,
33767
+ analyzeGEO: () => analyzeGEO,
33768
+ analyzeImages: () => analyzeImages,
33769
+ analyzeInternalLinking: () => analyzeInternalLinking,
33770
+ analyzeMobileSEO: () => analyzeMobileSEO,
33771
+ analyzeRobotsTxtForAI: () => analyzeRobotsTxtForAI,
33772
+ analyzeSecurityHeaders: () => analyzeSecurityHeaders,
33773
+ analyzeStructuredData: () => analyzeStructuredData,
33774
+ calculateLLMSignals: () => calculateLLMSignals,
33775
+ detectRenderingMode: () => detectRenderingMode,
33776
+ generateAIFriendlyRobotsTxt: () => generateAIFriendlyRobotsTxt,
33777
+ generateResponsiveImage: () => generateResponsiveImage,
33778
+ generateSchemaTemplate: () => generateSchemaTemplate,
33779
+ generateSecurityHeaders: () => generateSecurityHeaders,
33780
+ suggestInternalLinks: () => suggestInternalLinks
33781
+ });
33782
+ async function analyzeComprehensive(html, url, options = {}) {
33783
+ const { robotsTxt, headers } = options;
33784
+ const { analyzeGEO: analyzeGEO2 } = await import("./geo-analyzer-D47LTMMA.mjs");
33785
+ const { analyzeCoreWebVitals: analyzeCoreWebVitals2 } = await import("./core-web-vitals-analyzer-TE6LQJMS.mjs");
33786
+ const { analyzeSecurityHeaders: analyzeSecurityHeaders3 } = await import("./security-headers-analyzer-3ZUQARS5.mjs");
33787
+ const { analyzeStructuredData: analyzeStructuredData3 } = await import("./structured-data-analyzer-2I4NQAUP.mjs");
33788
+ const { analyzeImages: analyzeImages3 } = await import("./image-optimization-analyzer-XP4OQGRP.mjs");
33789
+ const { analyzeInternalLinking: analyzeInternalLinking2 } = await import("./internal-linking-analyzer-MRMBV7NM.mjs");
33790
+ const { analyzeMobileSEO: analyzeMobileSEO2 } = await import("./mobile-seo-analyzer-67HNQ7IO.mjs");
33791
+ const geo = await analyzeGEO2(html, url, robotsTxt);
33792
+ const coreWebVitals = analyzeCoreWebVitals2(html, url, headers);
33793
+ const security = analyzeSecurityHeaders3(headers || {}, url);
33794
+ const structuredData = analyzeStructuredData3(html, url);
33795
+ const images = analyzeImages3(html, url);
33796
+ const internalLinking = analyzeInternalLinking2(html, url);
33797
+ const mobile = analyzeMobileSEO2(html, url);
33798
+ const allIssues = [
33799
+ ...geo.issues,
33800
+ ...coreWebVitals.issues,
33801
+ ...security.issues,
33802
+ ...structuredData.issues,
33803
+ ...images.issues,
33804
+ ...internalLinking.issues,
33805
+ ...mobile.issues
33806
+ ];
33807
+ const severityOrder = { critical: 0, error: 1, warning: 2, info: 3, notice: 4 };
33808
+ allIssues.sort((a, b) => (severityOrder[a.severity] ?? 5) - (severityOrder[b.severity] ?? 5));
33809
+ const weights = {
33810
+ geo: 0.2,
33811
+ // AI search is crucial
33812
+ cwv: 0.2,
33813
+ // Core Web Vitals
33814
+ security: 0.1,
33815
+ // Security headers
33816
+ schema: 0.15,
33817
+ // Structured data
33818
+ images: 0.1,
33819
+ // Image optimization
33820
+ links: 0.1,
33821
+ // Internal linking
33822
+ mobile: 0.15
33823
+ // Mobile SEO
33824
+ };
33825
+ const securityScore = security.grade === "A+" ? 100 : security.grade === "A" ? 90 : security.grade === "B" ? 75 : security.grade === "C" ? 55 : security.grade === "D" ? 35 : 20;
33826
+ const overallScore = Math.round(
33827
+ geo.score * weights.geo + coreWebVitals.overallScore * weights.cwv + securityScore * weights.security + structuredData.score * weights.schema + images.score * weights.images + internalLinking.score * weights.links + mobile.score * weights.mobile
33828
+ );
33829
+ const prioritizedRecommendations = [];
33830
+ if (geo.aiCrawlerAccess.blockedCrawlers.length > 0) {
33831
+ prioritizedRecommendations.push("\u{1F6A8} URGENT: Unblock AI crawlers (GPTBot, PerplexityBot) in robots.txt");
33832
+ }
33833
+ if (geo.aiCrawlerAccess.jsRenderingRequired && !geo.aiCrawlerAccess.hasPrerendering) {
33834
+ prioritizedRecommendations.push("\u{1F6A8} URGENT: Implement SSR - AI crawlers cannot see your JS-rendered content");
33835
+ }
33836
+ const criticalCWV = coreWebVitals.issues.filter((i) => i.severity === "critical");
33837
+ criticalCWV.forEach((i) => prioritizedRecommendations.push(`\u26A1 ${i.title}`));
33838
+ if (!security.https.enabled) {
33839
+ prioritizedRecommendations.push("\u{1F512} Enable HTTPS immediately");
33840
+ }
33841
+ if (!mobile.viewport.hasViewport) {
33842
+ prioritizedRecommendations.push("\u{1F4F1} Add viewport meta tag for mobile rendering");
33843
+ }
33844
+ geo.recommendations.slice(0, 2).forEach((r) => prioritizedRecommendations.push(r));
33845
+ structuredData.recommendations.slice(0, 2).forEach((r) => prioritizedRecommendations.push(r));
33846
+ images.recommendations.slice(0, 2).forEach((r) => prioritizedRecommendations.push(r));
33847
+ internalLinking.recommendations.slice(0, 2).forEach((r) => prioritizedRecommendations.push(r));
33848
+ return {
33849
+ url,
33850
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
33851
+ overallScore,
33852
+ grades: {
33853
+ geo: geo.score,
33854
+ coreWebVitals: coreWebVitals.overallScore,
33855
+ security: security.grade,
33856
+ structuredData: structuredData.score,
33857
+ images: images.score,
33858
+ internalLinking: internalLinking.score,
33859
+ mobile: mobile.score
33860
+ },
33861
+ geo,
33862
+ coreWebVitals,
33863
+ security,
33864
+ structuredData,
33865
+ images,
33866
+ internalLinking,
33867
+ mobile,
33868
+ allIssues,
33869
+ prioritizedRecommendations
33870
+ };
33871
+ }
33872
+
30397
33873
  // src/frameworks/index.ts
30398
33874
  var frameworks_exports = {};
30399
33875
  __export(frameworks_exports, {
@@ -30417,165 +33893,266 @@ __export(frameworks_exports, {
30417
33893
 
30418
33894
  // src/frameworks/detector.ts
30419
33895
  var HTML_SIGNATURES = [
33896
+ // ============ JavaScript/TypeScript Frameworks ============
30420
33897
  // Next.js
30421
33898
  {
30422
33899
  framework: "nextjs",
30423
- patterns: [
30424
- /__next/,
30425
- /_next\//,
30426
- /data-nscript/,
30427
- /__NEXT_DATA__/,
30428
- /next\/script/
30429
- ],
33900
+ language: "typescript",
33901
+ patterns: [/__next/, /_next\//, /data-nscript/, /__NEXT_DATA__/, /next\/script/],
30430
33902
  confidence: "high"
30431
33903
  },
30432
33904
  // Nuxt
30433
33905
  {
30434
33906
  framework: "nuxt",
30435
- patterns: [
30436
- /__nuxt/,
30437
- /_nuxt\//,
30438
- /data-n-head/,
30439
- /nuxt-link/,
30440
- /__NUXT__/
30441
- ],
33907
+ language: "typescript",
33908
+ patterns: [/__nuxt/, /_nuxt\//, /data-n-head/, /nuxt-link/, /__NUXT__/],
30442
33909
  confidence: "high"
30443
33910
  },
30444
33911
  // Gatsby
30445
33912
  {
30446
33913
  framework: "gatsby",
30447
- patterns: [
30448
- /___gatsby/,
30449
- /gatsby-/,
30450
- /gatsby-image/,
30451
- /data-gatsby/
30452
- ],
33914
+ language: "javascript",
33915
+ patterns: [/___gatsby/, /gatsby-/, /gatsby-image/, /data-gatsby/],
30453
33916
  confidence: "high"
30454
33917
  },
30455
33918
  // Astro
30456
33919
  {
30457
33920
  framework: "astro",
30458
- patterns: [
30459
- /astro-/,
30460
- /data-astro/,
30461
- /_astro\//
30462
- ],
33921
+ language: "typescript",
33922
+ patterns: [/astro-/, /data-astro/, /_astro\//],
30463
33923
  confidence: "high"
30464
33924
  },
30465
33925
  // SvelteKit
30466
33926
  {
30467
33927
  framework: "sveltekit",
30468
- patterns: [
30469
- /__sveltekit/,
30470
- /_app\/immutable/,
30471
- /svelte-kit/
30472
- ],
33928
+ language: "typescript",
33929
+ patterns: [/__sveltekit/, /_app\/immutable/, /svelte-kit/],
30473
33930
  confidence: "high"
30474
33931
  },
30475
33932
  // Svelte (standalone)
30476
33933
  {
30477
33934
  framework: "svelte",
30478
- patterns: [
30479
- /svelte-\w+/,
30480
- /__svelte/
30481
- ],
33935
+ language: "javascript",
33936
+ patterns: [/svelte-\w+/, /__svelte/],
30482
33937
  confidence: "medium"
30483
33938
  },
30484
33939
  // Remix
30485
33940
  {
30486
33941
  framework: "remix",
30487
- patterns: [
30488
- /__remix/,
30489
- /remix-/,
30490
- /data-remix/
30491
- ],
33942
+ language: "typescript",
33943
+ patterns: [/__remix/, /remix-/, /data-remix/],
33944
+ confidence: "high"
33945
+ },
33946
+ // Solid.js
33947
+ {
33948
+ framework: "solidjs",
33949
+ language: "typescript",
33950
+ patterns: [/solid-/, /data-solid/, /_solid/],
33951
+ confidence: "high"
33952
+ },
33953
+ // Qwik
33954
+ {
33955
+ framework: "qwik",
33956
+ language: "typescript",
33957
+ patterns: [/q:/, /qwik/, /q-/],
30492
33958
  confidence: "high"
30493
33959
  },
30494
- // React (generic - lower priority than Next/Gatsby/Remix)
33960
+ // Fresh (Deno)
33961
+ {
33962
+ framework: "fresh",
33963
+ language: "typescript",
33964
+ patterns: [/fresh-/, /_frsh\//, /data-fresh/],
33965
+ confidence: "high"
33966
+ },
33967
+ // React (generic - lower priority)
30495
33968
  {
30496
33969
  framework: "react",
30497
- patterns: [
30498
- /data-reactroot/,
30499
- /data-react-helmet/,
30500
- /_react/,
30501
- /react-app/
30502
- ],
33970
+ language: "javascript",
33971
+ patterns: [/data-reactroot/, /data-react-helmet/, /_react/, /react-app/],
30503
33972
  confidence: "medium"
30504
33973
  },
30505
- // Vue (generic - lower priority than Nuxt)
33974
+ // Vue (generic - lower priority)
30506
33975
  {
30507
33976
  framework: "vue",
30508
- patterns: [
30509
- /data-v-[a-f0-9]+/,
30510
- /v-cloak/,
30511
- /__vue/
30512
- ],
33977
+ language: "javascript",
33978
+ patterns: [/data-v-[a-f0-9]+/, /v-cloak/, /__vue/],
30513
33979
  confidence: "medium"
30514
33980
  },
30515
33981
  // Angular
30516
33982
  {
30517
33983
  framework: "angular",
30518
- patterns: [
30519
- /ng-version/,
30520
- /_ngcontent/,
30521
- /ng-\w+=/,
30522
- /\[ng/
30523
- ],
33984
+ language: "typescript",
33985
+ patterns: [/ng-version/, /_ngcontent/, /ng-\w+=/, /\[ng/],
33986
+ confidence: "high"
33987
+ },
33988
+ // ============ Hypermedia Frameworks ============
33989
+ // HTMX
33990
+ {
33991
+ framework: "htmx",
33992
+ language: "unknown",
33993
+ patterns: [/hx-get/, /hx-post/, /hx-trigger/, /hx-swap/, /htmx\.org/],
33994
+ confidence: "high"
33995
+ },
33996
+ // Hotwire/Turbo
33997
+ {
33998
+ framework: "hotwire",
33999
+ language: "ruby",
34000
+ patterns: [/turbo-frame/, /turbo-stream/, /data-turbo/, /stimulus/],
30524
34001
  confidence: "high"
30525
34002
  },
34003
+ // ============ PHP Frameworks ============
30526
34004
  // WordPress
30527
34005
  {
30528
34006
  framework: "wordpress",
30529
- patterns: [
30530
- /wp-content/,
30531
- /wp-includes/,
30532
- /wp-json/,
30533
- /wordpress/i
30534
- ],
34007
+ language: "php",
34008
+ patterns: [/wp-content/, /wp-includes/, /wp-json/, /wordpress/i],
30535
34009
  confidence: "high"
30536
34010
  },
30537
34011
  // Laravel
30538
34012
  {
30539
34013
  framework: "laravel",
30540
- patterns: [
30541
- /laravel_session/,
30542
- /XSRF-TOKEN/,
30543
- /@csrf/
30544
- ],
34014
+ language: "php",
34015
+ patterns: [/laravel_session/, /XSRF-TOKEN/, /@csrf/, /laravel/i],
34016
+ confidence: "medium"
34017
+ },
34018
+ // Symfony
34019
+ {
34020
+ framework: "symfony",
34021
+ language: "php",
34022
+ patterns: [/symfony/, /sf2/, /_sf_/, /profiler\.css/],
30545
34023
  confidence: "medium"
30546
34024
  },
34025
+ // ============ Ruby Frameworks ============
30547
34026
  // Rails
30548
34027
  {
30549
34028
  framework: "rails",
30550
- patterns: [
30551
- /csrf-token/,
30552
- /turbolinks/,
30553
- /data-turbo/,
30554
- /rails-ujs/
30555
- ],
34029
+ language: "ruby",
34030
+ patterns: [/csrf-token/, /turbolinks/, /data-turbo/, /rails-ujs/, /action-cable/],
30556
34031
  confidence: "medium"
30557
34032
  },
34033
+ // ============ Python Frameworks ============
30558
34034
  // Django
30559
34035
  {
30560
34036
  framework: "django",
30561
- patterns: [
30562
- /csrfmiddlewaretoken/,
30563
- /django/
30564
- ],
34037
+ language: "python",
34038
+ patterns: [/csrfmiddlewaretoken/, /django/, /__debug__\//],
34039
+ confidence: "medium"
34040
+ },
34041
+ // Flask
34042
+ {
34043
+ framework: "flask",
34044
+ language: "python",
34045
+ patterns: [/flask/, /werkzeug/i],
34046
+ confidence: "low"
34047
+ },
34048
+ // FastAPI
34049
+ {
34050
+ framework: "fastapi",
34051
+ language: "python",
34052
+ patterns: [/fastapi/, /openapi\.json/],
34053
+ confidence: "low"
34054
+ },
34055
+ // ============ Java Frameworks ============
34056
+ // Spring Boot
34057
+ {
34058
+ framework: "spring",
34059
+ language: "java",
34060
+ patterns: [/spring/, /thymeleaf/, /th:/, /_csrf/],
34061
+ confidence: "medium"
34062
+ },
34063
+ // ============ C# Frameworks ============
34064
+ // ASP.NET Core
34065
+ {
34066
+ framework: "aspnet",
34067
+ language: "csharp",
34068
+ patterns: [/aspnetcore/, /__RequestVerificationToken/, /blazor/, /_framework\/blazor/],
34069
+ confidence: "medium"
34070
+ },
34071
+ // ============ Elixir Frameworks ============
34072
+ // Phoenix
34073
+ {
34074
+ framework: "phoenix",
34075
+ language: "elixir",
34076
+ patterns: [/phx-/, /phoenix/, /live-socket/, /data-phx/],
34077
+ confidence: "high"
34078
+ },
34079
+ // ============ Static Site Generators ============
34080
+ // Hugo
34081
+ {
34082
+ framework: "hugo",
34083
+ language: "go",
34084
+ patterns: [/hugo/, /generator.*hugo/i],
34085
+ confidence: "medium"
34086
+ },
34087
+ // Jekyll
34088
+ {
34089
+ framework: "jekyll",
34090
+ language: "ruby",
34091
+ patterns: [/jekyll/, /generator.*jekyll/i],
34092
+ confidence: "medium"
34093
+ },
34094
+ // Eleventy
34095
+ {
34096
+ framework: "eleventy",
34097
+ language: "javascript",
34098
+ patterns: [/11ty/, /eleventy/, /generator.*eleventy/i],
34099
+ confidence: "medium"
34100
+ },
34101
+ // Pelican
34102
+ {
34103
+ framework: "pelican",
34104
+ language: "python",
34105
+ patterns: [/pelican/, /generator.*pelican/i],
30565
34106
  confidence: "medium"
30566
34107
  }
30567
34108
  ];
30568
34109
  var PACKAGE_DEPENDENCIES = [
30569
- { framework: "nextjs", packages: ["next"], meta_framework: "react" },
30570
- { framework: "gatsby", packages: ["gatsby"], meta_framework: "react" },
30571
- { framework: "remix", packages: ["@remix-run/react", "@remix-run/node"], meta_framework: "react" },
30572
- { framework: "nuxt", packages: ["nuxt", "nuxt3"], meta_framework: "vue" },
30573
- { framework: "sveltekit", packages: ["@sveltejs/kit"], meta_framework: "svelte" },
30574
- { framework: "astro", packages: ["astro"] },
30575
- { framework: "react", packages: ["react", "react-dom"] },
30576
- { framework: "vue", packages: ["vue"] },
30577
- { framework: "angular", packages: ["@angular/core"] },
30578
- { framework: "svelte", packages: ["svelte"] }
34110
+ // Meta-frameworks (check first - more specific)
34111
+ { framework: "nextjs", language: "typescript", packages: ["next"], meta_framework: "react" },
34112
+ { framework: "gatsby", language: "javascript", packages: ["gatsby"], meta_framework: "react" },
34113
+ { framework: "remix", language: "typescript", packages: ["@remix-run/react", "@remix-run/node"], meta_framework: "react" },
34114
+ { framework: "nuxt", language: "typescript", packages: ["nuxt", "nuxt3"], meta_framework: "vue" },
34115
+ { framework: "sveltekit", language: "typescript", packages: ["@sveltejs/kit"], meta_framework: "svelte" },
34116
+ { framework: "astro", language: "typescript", packages: ["astro"] },
34117
+ { framework: "solidjs", language: "typescript", packages: ["solid-js", "solid-start"] },
34118
+ { framework: "qwik", language: "typescript", packages: ["@builder.io/qwik"] },
34119
+ { framework: "eleventy", language: "javascript", packages: ["@11ty/eleventy"] },
34120
+ // Base frameworks
34121
+ { framework: "react", language: "javascript", packages: ["react", "react-dom"] },
34122
+ { framework: "vue", language: "javascript", packages: ["vue"] },
34123
+ { framework: "angular", language: "typescript", packages: ["@angular/core"] },
34124
+ { framework: "svelte", language: "javascript", packages: ["svelte"] },
34125
+ // HTMX (can be used with any backend)
34126
+ { framework: "htmx", language: "unknown", packages: ["htmx.org"] }
34127
+ ];
34128
+ var GEMFILE_DEPENDENCIES = [
34129
+ { framework: "rails", language: "ruby", packages: ["rails"] },
34130
+ { framework: "hotwire", language: "ruby", packages: ["turbo-rails", "stimulus-rails"], meta_framework: "rails" },
34131
+ { framework: "jekyll", language: "ruby", packages: ["jekyll"] }
34132
+ ];
34133
+ var PYTHON_DEPENDENCIES = [
34134
+ { framework: "django", language: "python", packages: ["django", "Django"] },
34135
+ { framework: "flask", language: "python", packages: ["flask", "Flask"] },
34136
+ { framework: "fastapi", language: "python", packages: ["fastapi", "FastAPI"] },
34137
+ { framework: "pelican", language: "python", packages: ["pelican", "Pelican"] }
34138
+ ];
34139
+ var COMPOSER_DEPENDENCIES = [
34140
+ { framework: "laravel", language: "php", packages: ["laravel/framework"] },
34141
+ { framework: "symfony", language: "php", packages: ["symfony/framework-bundle", "symfony/symfony"] },
34142
+ { framework: "wordpress", language: "php", packages: ["johnpbloch/wordpress-core"] }
34143
+ ];
34144
+ var JAVA_DEPENDENCIES = [
34145
+ { framework: "spring", language: "java", packages: ["spring-boot-starter-web", "org.springframework.boot"] }
34146
+ ];
34147
+ var DOTNET_DEPENDENCIES = [
34148
+ { framework: "aspnet", language: "csharp", packages: ["Microsoft.AspNetCore", "Microsoft.NET.Sdk.Web"] }
34149
+ ];
34150
+ var GO_DEPENDENCIES = [
34151
+ { framework: "hugo", language: "go", packages: ["github.com/gohugoio/hugo"] },
34152
+ { framework: "go", language: "go", packages: ["github.com/gin-gonic/gin", "github.com/labstack/echo", "github.com/gofiber/fiber"] }
34153
+ ];
34154
+ var ELIXIR_DEPENDENCIES = [
34155
+ { framework: "phoenix", language: "elixir", packages: [":phoenix", "phoenix"] }
30579
34156
  ];
30580
34157
  function detectFromHtml(html) {
30581
34158
  for (const signature of HTML_SIGNATURES) {
@@ -30583,6 +34160,7 @@ function detectFromHtml(html) {
30583
34160
  if (pattern.test(html)) {
30584
34161
  return {
30585
34162
  framework: signature.framework,
34163
+ language: signature.language,
30586
34164
  confidence: signature.confidence,
30587
34165
  detected_from: "html"
30588
34166
  };
@@ -30601,6 +34179,7 @@ function detectFromPackageJson(packageJson) {
30601
34179
  if (allDeps[pkg]) {
30602
34180
  return {
30603
34181
  framework: dep.framework,
34182
+ language: dep.language,
30604
34183
  version: allDeps[pkg],
30605
34184
  confidence: "high",
30606
34185
  detected_from: "package.json",
@@ -30611,23 +34190,169 @@ function detectFromPackageJson(packageJson) {
30611
34190
  }
30612
34191
  return null;
30613
34192
  }
34193
+ function detectFromGemfile(gemfileContent) {
34194
+ const gemPattern = /gem\s+['"]([^'"]+)['"]/g;
34195
+ const gems = [];
34196
+ let match;
34197
+ while ((match = gemPattern.exec(gemfileContent)) !== null) {
34198
+ gems.push(match[1]);
34199
+ }
34200
+ for (const dep of GEMFILE_DEPENDENCIES) {
34201
+ for (const pkg of dep.packages) {
34202
+ if (gems.includes(pkg)) {
34203
+ return {
34204
+ framework: dep.framework,
34205
+ language: dep.language,
34206
+ confidence: "high",
34207
+ detected_from: "gemfile",
34208
+ meta_framework: dep.meta_framework
34209
+ };
34210
+ }
34211
+ }
34212
+ }
34213
+ return null;
34214
+ }
34215
+ function detectFromPythonDeps(content) {
34216
+ for (const dep of PYTHON_DEPENDENCIES) {
34217
+ for (const pkg of dep.packages) {
34218
+ const pattern = new RegExp(`(^|\\s)${pkg.toLowerCase()}[\\s=<>~!]`, "im");
34219
+ if (pattern.test(content.toLowerCase())) {
34220
+ return {
34221
+ framework: dep.framework,
34222
+ language: dep.language,
34223
+ confidence: "high",
34224
+ detected_from: "requirements"
34225
+ };
34226
+ }
34227
+ }
34228
+ }
34229
+ return null;
34230
+ }
34231
+ function detectFromComposerJson(composerJson) {
34232
+ const allDeps = {
34233
+ ...composerJson.require,
34234
+ ...composerJson["require-dev"]
34235
+ };
34236
+ for (const dep of COMPOSER_DEPENDENCIES) {
34237
+ for (const pkg of dep.packages) {
34238
+ if (allDeps[pkg]) {
34239
+ return {
34240
+ framework: dep.framework,
34241
+ language: dep.language,
34242
+ version: allDeps[pkg],
34243
+ confidence: "high",
34244
+ detected_from: "composer"
34245
+ };
34246
+ }
34247
+ }
34248
+ }
34249
+ return null;
34250
+ }
34251
+ function detectFromPomXml(pomContent) {
34252
+ for (const dep of JAVA_DEPENDENCIES) {
34253
+ for (const pkg of dep.packages) {
34254
+ if (pomContent.includes(pkg)) {
34255
+ return {
34256
+ framework: dep.framework,
34257
+ language: dep.language,
34258
+ confidence: "high",
34259
+ detected_from: "pom"
34260
+ };
34261
+ }
34262
+ }
34263
+ }
34264
+ return null;
34265
+ }
34266
+ function detectFromCsproj(csprojContent) {
34267
+ for (const dep of DOTNET_DEPENDENCIES) {
34268
+ for (const pkg of dep.packages) {
34269
+ if (csprojContent.includes(pkg)) {
34270
+ return {
34271
+ framework: dep.framework,
34272
+ language: dep.language,
34273
+ confidence: "high",
34274
+ detected_from: "csproj"
34275
+ };
34276
+ }
34277
+ }
34278
+ }
34279
+ return null;
34280
+ }
34281
+ function detectFromGoMod(goModContent) {
34282
+ for (const dep of GO_DEPENDENCIES) {
34283
+ for (const pkg of dep.packages) {
34284
+ if (goModContent.includes(pkg)) {
34285
+ return {
34286
+ framework: dep.framework,
34287
+ language: dep.language,
34288
+ confidence: "high",
34289
+ detected_from: "go.mod"
34290
+ };
34291
+ }
34292
+ }
34293
+ }
34294
+ return null;
34295
+ }
34296
+ function detectFromMixExs(mixContent) {
34297
+ for (const dep of ELIXIR_DEPENDENCIES) {
34298
+ for (const pkg of dep.packages) {
34299
+ if (mixContent.includes(pkg)) {
34300
+ return {
34301
+ framework: dep.framework,
34302
+ language: dep.language,
34303
+ confidence: "high",
34304
+ detected_from: "mix.exs"
34305
+ };
34306
+ }
34307
+ }
34308
+ }
34309
+ return null;
34310
+ }
30614
34311
  function detectFromHeaders(headers) {
30615
34312
  const poweredBy = headers["x-powered-by"]?.toLowerCase() || "";
30616
34313
  const server = headers["server"]?.toLowerCase() || "";
34314
+ const generator = headers["x-generator"]?.toLowerCase() || "";
30617
34315
  if (poweredBy.includes("next.js") || poweredBy.includes("next")) {
30618
- return { framework: "nextjs", confidence: "high", detected_from: "headers" };
34316
+ return { framework: "nextjs", language: "typescript", confidence: "high", detected_from: "headers" };
30619
34317
  }
30620
34318
  if (poweredBy.includes("nuxt")) {
30621
- return { framework: "nuxt", confidence: "high", detected_from: "headers" };
34319
+ return { framework: "nuxt", language: "typescript", confidence: "high", detected_from: "headers" };
30622
34320
  }
30623
- if (poweredBy.includes("express") || poweredBy.includes("koa")) {
34321
+ if (poweredBy.includes("remix")) {
34322
+ return { framework: "remix", language: "typescript", confidence: "high", detected_from: "headers" };
34323
+ }
34324
+ if (poweredBy.includes("phusion") || poweredBy.includes("passenger") || poweredBy.includes("puma") || poweredBy.includes("unicorn")) {
34325
+ return { framework: "rails", language: "ruby", confidence: "medium", detected_from: "headers" };
34326
+ }
34327
+ if (poweredBy.includes("gunicorn") || poweredBy.includes("uvicorn") || poweredBy.includes("daphne")) {
30624
34328
  return null;
30625
34329
  }
30626
- if (poweredBy.includes("php") || server.includes("php")) {
34330
+ if (poweredBy.includes("php")) {
30627
34331
  return null;
30628
34332
  }
30629
- if (poweredBy.includes("phusion") || poweredBy.includes("passenger")) {
30630
- return { framework: "rails", confidence: "medium", detected_from: "headers" };
34333
+ if (server.includes("tomcat") || server.includes("jetty") || poweredBy.includes("spring")) {
34334
+ return { framework: "spring", language: "java", confidence: "medium", detected_from: "headers" };
34335
+ }
34336
+ if (poweredBy.includes("asp.net") || server.includes("kestrel")) {
34337
+ return { framework: "aspnet", language: "csharp", confidence: "high", detected_from: "headers" };
34338
+ }
34339
+ if (poweredBy.includes("phoenix") || poweredBy.includes("cowboy")) {
34340
+ return { framework: "phoenix", language: "elixir", confidence: "medium", detected_from: "headers" };
34341
+ }
34342
+ if (server.includes("gin") || server.includes("echo") || server.includes("fiber")) {
34343
+ return { framework: "go", language: "go", confidence: "medium", detected_from: "headers" };
34344
+ }
34345
+ if (generator.includes("hugo")) {
34346
+ return { framework: "hugo", language: "go", confidence: "high", detected_from: "headers" };
34347
+ }
34348
+ if (generator.includes("jekyll")) {
34349
+ return { framework: "jekyll", language: "ruby", confidence: "high", detected_from: "headers" };
34350
+ }
34351
+ if (generator.includes("eleventy") || generator.includes("11ty")) {
34352
+ return { framework: "eleventy", language: "javascript", confidence: "high", detected_from: "headers" };
34353
+ }
34354
+ if (generator.includes("pelican")) {
34355
+ return { framework: "pelican", language: "python", confidence: "high", detected_from: "headers" };
30631
34356
  }
30632
34357
  return null;
30633
34358
  }
@@ -30636,6 +34361,34 @@ function detectFramework4(options) {
30636
34361
  const result = detectFromPackageJson(options.packageJson);
30637
34362
  if (result) return result;
30638
34363
  }
34364
+ if (options.gemfileContent) {
34365
+ const result = detectFromGemfile(options.gemfileContent);
34366
+ if (result) return result;
34367
+ }
34368
+ if (options.requirementsContent) {
34369
+ const result = detectFromPythonDeps(options.requirementsContent);
34370
+ if (result) return result;
34371
+ }
34372
+ if (options.composerJson) {
34373
+ const result = detectFromComposerJson(options.composerJson);
34374
+ if (result) return result;
34375
+ }
34376
+ if (options.pomXmlContent) {
34377
+ const result = detectFromPomXml(options.pomXmlContent);
34378
+ if (result) return result;
34379
+ }
34380
+ if (options.csprojContent) {
34381
+ const result = detectFromCsproj(options.csprojContent);
34382
+ if (result) return result;
34383
+ }
34384
+ if (options.goModContent) {
34385
+ const result = detectFromGoMod(options.goModContent);
34386
+ if (result) return result;
34387
+ }
34388
+ if (options.mixExsContent) {
34389
+ const result = detectFromMixExs(options.mixExsContent);
34390
+ if (result) return result;
34391
+ }
30639
34392
  if (options.headers) {
30640
34393
  const result = detectFromHeaders(options.headers);
30641
34394
  if (result) return result;
@@ -30646,40 +34399,70 @@ function detectFramework4(options) {
30646
34399
  }
30647
34400
  if (options.files) {
30648
34401
  if (options.files.some((f) => f.includes("next.config"))) {
30649
- return { framework: "nextjs", confidence: "high", detected_from: "files" };
34402
+ return { framework: "nextjs", language: "typescript", confidence: "high", detected_from: "files" };
30650
34403
  }
30651
34404
  if (options.files.some((f) => f.includes("nuxt.config"))) {
30652
- return { framework: "nuxt", confidence: "high", detected_from: "files" };
34405
+ return { framework: "nuxt", language: "typescript", confidence: "high", detected_from: "files" };
30653
34406
  }
30654
34407
  if (options.files.some((f) => f.includes("astro.config"))) {
30655
- return { framework: "astro", confidence: "high", detected_from: "files" };
34408
+ return { framework: "astro", language: "typescript", confidence: "high", detected_from: "files" };
30656
34409
  }
30657
34410
  if (options.files.some((f) => f.includes("svelte.config"))) {
30658
- return { framework: "sveltekit", confidence: "high", detected_from: "files" };
34411
+ return { framework: "sveltekit", language: "typescript", confidence: "high", detected_from: "files" };
30659
34412
  }
30660
34413
  if (options.files.some((f) => f.includes("angular.json"))) {
30661
- return { framework: "angular", confidence: "high", detected_from: "files" };
34414
+ return { framework: "angular", language: "typescript", confidence: "high", detected_from: "files" };
30662
34415
  }
30663
34416
  if (options.files.some((f) => f.includes("gatsby-config"))) {
30664
- return { framework: "gatsby", confidence: "high", detected_from: "files" };
34417
+ return { framework: "gatsby", language: "javascript", confidence: "high", detected_from: "files" };
34418
+ }
34419
+ if (options.files.some((f) => f.includes("remix.config"))) {
34420
+ return { framework: "remix", language: "typescript", confidence: "high", detected_from: "files" };
34421
+ }
34422
+ if (options.files.some((f) => f.includes(".eleventy.js") || f.includes("eleventy.config"))) {
34423
+ return { framework: "eleventy", language: "javascript", confidence: "high", detected_from: "files" };
30665
34424
  }
30666
34425
  if (options.files.some((f) => f === "wp-config.php")) {
30667
- return { framework: "wordpress", confidence: "high", detected_from: "files" };
34426
+ return { framework: "wordpress", language: "php", confidence: "high", detected_from: "files" };
30668
34427
  }
30669
34428
  if (options.files.some((f) => f === "artisan")) {
30670
- return { framework: "laravel", confidence: "high", detected_from: "files" };
34429
+ return { framework: "laravel", language: "php", confidence: "high", detected_from: "files" };
34430
+ }
34431
+ if (options.files.some((f) => f.includes("symfony.lock") || f.includes("config/bundles.php"))) {
34432
+ return { framework: "symfony", language: "php", confidence: "high", detected_from: "files" };
30671
34433
  }
30672
34434
  if (options.files.some((f) => f === "manage.py")) {
30673
- return { framework: "django", confidence: "high", detected_from: "files" };
34435
+ return { framework: "django", language: "python", confidence: "high", detected_from: "files" };
34436
+ }
34437
+ if (options.files.some((f) => f.includes("pelicanconf.py"))) {
34438
+ return { framework: "pelican", language: "python", confidence: "high", detected_from: "files" };
30674
34439
  }
30675
34440
  if (options.files.some((f) => f === "Gemfile") && options.files.some((f) => f.includes("config/routes.rb"))) {
30676
- return { framework: "rails", confidence: "high", detected_from: "files" };
34441
+ return { framework: "rails", language: "ruby", confidence: "high", detected_from: "files" };
34442
+ }
34443
+ if (options.files.some((f) => f === "_config.yml") && options.files.some((f) => f === "Gemfile")) {
34444
+ return { framework: "jekyll", language: "ruby", confidence: "high", detected_from: "files" };
34445
+ }
34446
+ if (options.files.some((f) => f.includes("hugo.toml") || f.includes("hugo.yaml") || f.includes("config.toml"))) {
34447
+ return { framework: "hugo", language: "go", confidence: "high", detected_from: "files" };
34448
+ }
34449
+ if (options.files.some((f) => f.includes("pom.xml") || f.includes("build.gradle"))) {
34450
+ if (options.files.some((f) => f.includes("src/main/resources/application"))) {
34451
+ return { framework: "spring", language: "java", confidence: "high", detected_from: "files" };
34452
+ }
34453
+ }
34454
+ if (options.files.some((f) => f.endsWith(".csproj"))) {
34455
+ return { framework: "aspnet", language: "csharp", confidence: "medium", detected_from: "files" };
34456
+ }
34457
+ if (options.files.some((f) => f === "mix.exs") && options.files.some((f) => f.includes("lib") && f.includes("_web"))) {
34458
+ return { framework: "phoenix", language: "elixir", confidence: "high", detected_from: "files" };
30677
34459
  }
30678
34460
  }
30679
- return { framework: "unknown", confidence: "low", detected_from: "html" };
34461
+ return { framework: "unknown", language: "unknown", confidence: "low", detected_from: "html" };
30680
34462
  }
30681
34463
  function getFrameworkDisplayName(framework) {
30682
34464
  const names = {
34465
+ // JavaScript/TypeScript
30683
34466
  react: "React",
30684
34467
  nextjs: "Next.js",
30685
34468
  vue: "Vue.js",
@@ -30690,10 +34473,35 @@ function getFrameworkDisplayName(framework) {
30690
34473
  astro: "Astro",
30691
34474
  remix: "Remix",
30692
34475
  gatsby: "Gatsby",
34476
+ solidjs: "Solid.js",
34477
+ qwik: "Qwik",
34478
+ fresh: "Fresh (Deno)",
34479
+ // Ruby
30693
34480
  rails: "Ruby on Rails",
30694
- laravel: "Laravel",
34481
+ // Python
30695
34482
  django: "Django",
34483
+ flask: "Flask",
34484
+ fastapi: "FastAPI",
34485
+ // PHP
34486
+ laravel: "Laravel",
34487
+ symfony: "Symfony",
30696
34488
  wordpress: "WordPress",
34489
+ // Java
34490
+ spring: "Spring Boot",
34491
+ // C#
34492
+ aspnet: "ASP.NET Core",
34493
+ // Go
34494
+ go: "Go (Gin/Echo/Fiber)",
34495
+ // Elixir
34496
+ phoenix: "Phoenix",
34497
+ // Hypermedia
34498
+ htmx: "HTMX",
34499
+ hotwire: "Hotwire/Turbo",
34500
+ // Static Site Generators
34501
+ hugo: "Hugo",
34502
+ jekyll: "Jekyll",
34503
+ eleventy: "Eleventy (11ty)",
34504
+ pelican: "Pelican",
30697
34505
  unknown: "Unknown"
30698
34506
  };
30699
34507
  return names[framework];
@@ -30702,6 +34510,7 @@ function getFrameworkDisplayName(framework) {
30702
34510
  // src/frameworks/suggestion-engine.ts
30703
34511
  import { parse as parseYaml2 } from "yaml";
30704
34512
  var RECIPES = {
34513
+ // JavaScript/TypeScript
30705
34514
  react: null,
30706
34515
  nextjs: null,
30707
34516
  vue: null,
@@ -30712,10 +34521,36 @@ var RECIPES = {
30712
34521
  astro: null,
30713
34522
  remix: null,
30714
34523
  gatsby: null,
34524
+ solidjs: null,
34525
+ qwik: null,
34526
+ fresh: null,
34527
+ // Ruby
30715
34528
  rails: null,
30716
- laravel: null,
34529
+ // Python
30717
34530
  django: null,
34531
+ flask: null,
34532
+ fastapi: null,
34533
+ // PHP
34534
+ laravel: null,
34535
+ symfony: null,
30718
34536
  wordpress: null,
34537
+ // Java
34538
+ spring: null,
34539
+ // C#
34540
+ aspnet: null,
34541
+ // Go
34542
+ go: null,
34543
+ // Elixir
34544
+ phoenix: null,
34545
+ // Hypermedia
34546
+ htmx: null,
34547
+ hotwire: null,
34548
+ // Static Site Generators
34549
+ hugo: null,
34550
+ jekyll: null,
34551
+ eleventy: null,
34552
+ pelican: null,
34553
+ // Unknown
30719
34554
  unknown: null
30720
34555
  };
30721
34556
  var RECIPE_CONTENT = {};
@@ -30898,7 +34733,7 @@ function runSyncAudit(url, html, checksLimit) {
30898
34733
  } catch (e) {
30899
34734
  }
30900
34735
  try {
30901
- const structured = analyzeStructuredData(html, url);
34736
+ const structured = analyzeStructuredData2(html, url);
30902
34737
  allIssues.push(...structured.issues);
30903
34738
  } catch (e) {
30904
34739
  }
@@ -31119,7 +34954,7 @@ export {
31119
34954
  analyzeHeadings,
31120
34955
  analyzeHeadline,
31121
34956
  analyzeHreflang,
31122
- analyzeImages,
34957
+ analyzeImages2 as analyzeImages,
31123
34958
  analyzeInteractiveTools,
31124
34959
  analyzeKeywordDensity,
31125
34960
  analyzeKeywordPlacement,
@@ -31139,13 +34974,14 @@ export {
31139
34974
  analyzeResponsiveImages,
31140
34975
  analyzeSERPPreview,
31141
34976
  analyzeSecurity,
31142
- analyzeSecurityHeaders,
34977
+ analyzeSecurityHeaders2 as analyzeSecurityHeaders,
31143
34978
  analyzeSocialMeta,
31144
- analyzeStructuredData,
34979
+ analyzeStructuredData2 as analyzeStructuredData,
31145
34980
  analyzeTopicalClusters,
31146
34981
  analyzeTrackerBloat,
31147
34982
  analyzeUrl,
31148
34983
  analyzeUrlSafety,
34984
+ analyzers_exports as analyzers,
31149
34985
  applyFixes,
31150
34986
  buildGSCApiRequest,
31151
34987
  buildGSCRequest,
@@ -31241,6 +35077,7 @@ export {
31241
35077
  generateAICitableContent,
31242
35078
  generateAllFixes,
31243
35079
  generateAngularSEOService,
35080
+ generateAspNetCoreSEO,
31244
35081
  generateAstroBaseHead,
31245
35082
  generateAstroMeta,
31246
35083
  generateBlogPost,
@@ -31249,7 +35086,9 @@ export {
31249
35086
  generateCommitSummary,
31250
35087
  generateComparisonTable,
31251
35088
  generateCompleteSocialMetaSetup,
35089
+ generateDjangoSEOHelper,
31252
35090
  generateDuplicateIssues,
35091
+ generateEleventySEO,
31253
35092
  generateFAQSchema2 as generateFAQSchema,
31254
35093
  generateFixes,
31255
35094
  generateGA4EnvTemplate,
@@ -31259,11 +35098,16 @@ export {
31259
35098
  generateGEOReport,
31260
35099
  generateGSCVerificationTag,
31261
35100
  generateGitHubActionSetup,
35101
+ generateGoSEO,
31262
35102
  generateHTMLReport,
31263
35103
  generateHTMLSocialMeta,
35104
+ generateHTMXSEO,
31264
35105
  generateHeadlineVariations,
35106
+ generateHugoSEO,
35107
+ generateJekyllSEO,
31265
35108
  generateJsonReport,
31266
35109
  generateKeyFacts,
35110
+ generateLaravelSEOHelper,
31267
35111
  generateMarkdownReport,
31268
35112
  generateNextAppMetadata,
31269
35113
  generateNextJsAppRouterMetadata,
@@ -31272,12 +35116,16 @@ export {
31272
35116
  generateNuxtSEOHead,
31273
35117
  generatePDFReport,
31274
35118
  generatePRDescription,
35119
+ generatePhoenixSEO,
35120
+ generateRailsSEOHelper,
31275
35121
  generateReactHelmetSocialMeta,
31276
35122
  generateReactSEOHead,
31277
35123
  generateRecommendationQueries,
31278
35124
  generateRemixMeta,
35125
+ generateRemixSEO,
31279
35126
  generateSecretsDoc,
31280
35127
  generateSocialMetaFix,
35128
+ generateSpringBootSEO,
31281
35129
  generateSvelteKitMeta,
31282
35130
  generateSvelteKitSEOHead,
31283
35131
  generateUncertaintyQuestions,
@@ -31289,6 +35137,7 @@ export {
31289
35137
  getDateRange,
31290
35138
  getExpandedSuggestions,
31291
35139
  getFrameworkSpecificFix,
35140
+ getFrameworkSpecificFixExtended,
31292
35141
  getGSCSetupInstructions,
31293
35142
  getGitUser,
31294
35143
  getKeywordData,