diff --git a/.claude/agents/blog-writer.md b/.claude/agents/blog-writer.md index 3696f8770..e8f096144 100644 --- a/.claude/agents/blog-writer.md +++ b/.claude/agents/blog-writer.md @@ -2,7 +2,7 @@ name: blog-writer type: coder color: "#E67E22" -description: Hugo markdown content creation specialist for technical blog posts. Transforms research and strategy into engaging, human-written content with technical depth. Uses visual tests for Hugo rendering validation. Use PROACTIVELY after research phase for content implementation. +description: Hugo markdown content creation specialist for technical blog posts. Transforms research and strategy into engaging, human-written content with technical depth. Uses visual tests for Hugo rendering validation. **Recommended Models: Claude 3.5 Sonnet (for content), Gemini 2.5 Pro (for templating).** Use PROACTIVELY after research phase for content implementation. capabilities: - hugo_markdown_generation - technical_writing diff --git a/.claude/agents/research-agent.md b/.claude/agents/research-agent.md index adf2022c5..f89128a85 100644 --- a/.claude/agents/research-agent.md +++ b/.claude/agents/research-agent.md @@ -2,7 +2,7 @@ name: research-agent type: researcher color: "#16A085" -description: Technical research and evidence gathering specialist for blog content. Conducts comprehensive research, validates technical claims, gathers citations and case studies. Ensures all content backed by credible sources with proper attribution. Use PROACTIVELY after content strategy for evidence foundation. +description: Technical research and evidence gathering specialist for blog content. Conducts comprehensive research, validates technical claims, gathers citations and case studies. Ensures all content backed by credible sources with proper attribution. **Recommended Models: Gemini 2.5 Flash (for data/log analysis), Claude 3.5 Sonnet (for synthesis).** Use PROACTIVELY after content strategy for evidence foundation. capabilities: - technical_research - citation_gathering diff --git a/.dev/compose.yml b/.dev/compose.yml index 82408807c..e08e47982 100644 --- a/.dev/compose.yml +++ b/.dev/compose.yml @@ -4,7 +4,7 @@ services: hugo: - image: hugomods/hugo:exts-non-root-0.149.1 + image: hugomods/hugo:debian-reg-dart-sass-node-git-non-root-0.160.1 working_dir: /app volumes: - ..:/app:delegated @@ -16,7 +16,7 @@ services: HUGO_CACHEDIR: /tmp/hugo_cache HUGO_NUMWORKERMULTIPLIER: 8 restart: unless-stopped - mem_limit: 512m + mem_limit: 2g cpus: '2.0' # Interactive shell (optimized) diff --git a/AGENTS.md b/AGENTS.md index 611ee48da..8fb552fa6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,59 @@ Codex skill setup for `jetthoughts.github.io`. ## Skill Sources Reviewed - Global skill library: `/Users/pftg/.agents/skills` -- Local project skills folder: `.claude/skills` (was missing; now created with a local profile) +- Local project skills folder: `.claude/skills` + +## Installed Skills Registry + +All skills installed globally via `npx skills add`: + +### Blog Pipeline (Autonomous 9-Phase Workflow) +- `self-improving-agent` (charon-fan/agent-playbook) — Continuous learning +- `blog-page-generator` (kostja94/marketing-skills) — Blog page creation +- `content-strategy` (guia-matthieu/clawfu-skills) — Content planning +- `content-production` (borghei/claude-skills) — End-to-end content creation +- `geo-content-publisher` (geoly-ai/geo-skills) — Publishing pipeline +- `workflow-execute` (catlog22/claude-code-workflow) — Workflow runner +- `agentic-workflow-automation` (samarv/shanon) — Multi-step automation +- `blog-post` (langchain-ai/deepagents) — Blog post generation +- `blog-writing-guide` (getsentry/skills) — Writing process + +### Research & Trends +- `social-media-trends-research` (drshailesh88/integrated_content_os) — Social trends +- `content-trend-researcher` (alirezarezvani/claude-code-skill-factory) — Content trends +- `keyword-research` (kostja94/marketing-skills) — SEO keyword discovery +- `market-research-analysis` (manojbajaj95/claude-gtm-plugin) — Market context +- `competitor-intel` (ognjengt/founder-skills) — Competitive landscape +- `web-research-workflow` (yonatangross/orchestkit) — Structured research + +### SEO & Performance +- `seo` (addyosmani/web-quality-skills) — SEO best practices +- `seo-aeo-audit` (warpdotdev/oz-skills) — SEO & AEO audits +- `landing-page-optimization` (manojbajaj95/claude-gtm-plugin) — Landing page conversion +- `web-performance-optimization` (sickn33/antigravity-awesome-skills) — Core Web Vitals +- `pagespeed-insights` (enderpuentes/ai-agent-skills) — Performance analysis + +### CSS/HTML/PostCSS +- `best-practices` (addyosmani/web-quality-skills) — Web quality audit +- `postcss-best-practices` (mindrally/skills) — PostCSS patterns +- `html-css-best-practices` (hack23/homepage) — HTML/CSS fundamentals +- `web-design-guidelines` (ehmo/platform-design-skills) — Design & accessibility + +### Markdown +- `obsidian-markdown` (kepano/obsidian-skills) — Creating/editing .md files +- `baoyu-markdown-to-html` (jimliu/baoyu-skills) — MD → HTML conversion + +### Hugo +- `hugo` (jackspace/claudeskillz) — Hugo static site guidance + +### Writing & Editing +- `technical-writing` (mindrally/skills) — Technical content clarity +- `copywriting-core` (manojbajaj95/claude-gtm-plugin) — Persuasive copy +- `copy-editing` (borghei/claude-skills) — Proofreading & polish + +### AI Detection & Humanization +- `humanizer` (brandonwise/humanizer) — Humanizing AI-generated text +- `slop-detector` (athola/claude-night-market) — Detecting AI-generated content ## Default Skill Bundles @@ -28,35 +80,97 @@ Routing notes: ### 2) Landing page SEO audit and improvement Use these skills in order: -1. `seo-aeo-audit` -2. `landing-page-optimization` -3. `keyword-research` -4. `pagespeed-insights` +1. `seo` +2. `seo-aeo-audit` +3. `landing-page-optimization` +4. `keyword-research` +5. `pagespeed-insights` Routing notes: -- Technical SEO + structured data + AEO: `seo-aeo-audit`. +- Technical SEO + structured data + AEO: `seo` + `seo-aeo-audit`. - Conversion + hero/CTA quality: `landing-page-optimization`. - Query intent alignment: `keyword-research`. - Core Web Vitals impact: `pagespeed-insights`. -### 3) Research + add engagement-focused blog posts +### 3) Research + add engagement-focused blog posts (Autonomous Pipeline) Use these skills in order: -1. `content-trend-researcher` -2. `social-media-trends-research` -3. `keyword-research` -4. `blog-post` -5. `content-production` -6. `geo-content-publisher` +1. `social-media-trends-research` + `content-trend-researcher` — Discover trends +2. `keyword-research` + `market-research-analysis` — Validate demand +3. `competitor-intel` + `web-research-workflow` — Gather intelligence +4. `blog-post` + `copywriting-core` — Ideate & outline +5. `blog-writing-guide` + `content-production` — Draft content +5.5. `slop-detector` → scan for AI patterns → `humanizer` if flagged +6. `technical-writing` + `copy-editing` — Polish & edit +7. `seo` + `seo-aeo-audit` — Optimize for search +8. `hugo` + `obsidian-markdown` — Validate Hugo compatibility +9. `blog-page-generator` + `geo-content-publisher` — Publish +10. `self-improving-agent` — Store learnings Routing notes: - Discover demand first, then write. - Publish only after keyword + editorial QA pass. - Use `geo-content-publisher` for multi-channel distribution. +- Each phase has quality gates — fail stops pipeline. -## Execution Policy for Codex +### 4) CSS/HTML Quality & Accessibility + +Use these skills in order: +1. `html-css-best-practices` +2. `postcss-best-practices` +3. `web-design-guidelines` +4. `web-performance-optimization` + +Routing notes: +- ANY CSS changes → apply `html-css-best-practices` + `postcss-best-practices` +- ANY HTML changes → apply `html-css-best-practices` + `best-practices` +- Accessibility review → apply `web-design-guidelines` +- Performance audit → apply `web-performance-optimization` + `pagespeed-insights` + +## Autonomous Blog Pipeline — 9-Phase Workflow + +``` +Phase 1: Trend Discovery → Phase 2: Strategy → Phase 3: Research + → Phase 4: Ideation → Phase 5: Draft & Edit → Phase 6: SEO & Performance + → Phase 7: Hugo Build → Phase 8: Publish → Phase 9: Continuous Improvement +``` + +### Phase Quality Gates +| Phase | Gate | +|-------|------| +| 1. Trend Discovery | ≥5 viable topic opportunities | +| 2. Strategy | Each topic: keyword + audience + shareability ≥7/10 | +| 3. Research | ≥8 credible sources, ≥1 expert quote | +| 4. Ideation | 3-5 takeaways, compelling hook defined | +| 5. Draft | Zero AI phrases, paragraphs ≤3 sentences | +| 5.5. AI Check | slop-detector passes, humanized if flagged | +| 6. SEO | Flesch Reading Ease ≥60, metadata complete | +| 7. Hugo Build | `bin/hugo-build` passes, all links resolve | +| 8. Publish | Post live, social meta tags rendering | +| 9. Improvement | Lessons stored, agent configs updated | + +## Execution Policy - Prefer the smallest skill set that completes the task end-to-end. - Keep workflow sequence: `research -> implementation -> validation -> docs/publish`. - For multi-file code changes, include `incremental-implementation`. - For any docs affected by code/content updates, include `docs:update-docs`. +- Autonomous blog pipeline: runs end-to-end with quality gates at each phase. +- Zero tolerance: no generic AI language, no unsupported claims, no Hugo build breaks. + +## 🔍 Research Protocol (MANDATORY) + +Always use claude-context MCP search **before** making changes: + +**Step 1 — Search existing patterns:** +> Search the codebase at `/Users/pftg/dev/jetthoughts.github.io` for: "[pattern]" + +**Step 2 — Check knowledge standards:** +> Search the codebase at `/Users/pftg/dev/jetthoughts.github.io/knowledge` for: "[topic]" + +**Step 3 — Framework docs (when needed):** +> Get library docs for "[framework]" + +**Never** grep/find/grep_search for existing code patterns — use claude-context MCP search as the primary research tool. It understands semantic relationships, is 100x faster, and returns relevant context chunks. + +**Coverage**: Full codebase indexed (830+ files, 4,184+ semantic chunks) diff --git a/CLAUDE.md b/CLAUDE.md index 7b390844b..33e2ed01a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,18 +14,19 @@ **🌐 Global Standards**: `/knowledge/KNOWLEDGE_INDEX.md` - 99+ company-wide standards (SUPREME AUTHORITY) ### 🔍 Research Protocol (MANDATORY SEQUENCE) -```bash + +``` # Step 1: Global standards FIRST (inherited by jt_site) -claude-context search "[topic]" --path "/knowledge/" +Search the codebase at "/knowledge/" for: "[topic]" # Step 2: jt_site adaptations SECOND -claude-context search "[topic]" --path "/projects/jt_site/docs/" +Search the codebase at "/projects/jt_site/docs/" for: "[topic]" # Step 3: Framework validation -context7 resolve-library-id "[framework]" && context7 get-library-docs "[id]" +Get library docs for "[framework]" — topic: "[feature]" # Step 4: Package analysis -mcp__package-search__package_search_hybrid --registry_name "[registry]" --package_name "[package]" +Search npm packages for: "[package]" on registry "[registry]" ``` ### 📑 Key Reference Paths @@ -580,6 +581,109 @@ forbidden_everything_else: - "ALL modification commands (rm, mv, cp)" # ❌ Use claude-flow file tools ``` +--- + +## 🌐 CHROME DEVTOOLS RENDERING VALIDATION + +**Purpose**: Use Chrome DevTools Protocol (CDP) to validate that pages render correctly — not just visually, but structurally and performance-wise. + +### When to Apply Chrome DevTools Validation + +Chrome DevTools validation is **MANDATORY** after: +- ANY HTML/template changes (layout, structure, content) +- ANY CSS changes (styling, responsiveness) +- ANY JavaScript changes (interactivity, dynamic content) +- ANY Hugo build output changes +- New blog post publishing (verify rendering) + +### Chrome DevTools Validation Protocol + +```yaml +chrome_devtools_validation: + # Step 1: Start Hugo dev server + preconditions: + - "Start Hugo dev server (run: bin/hugo server -D)" + + # Step 2: Navigate and validate + validation_steps: + page_load: + - "Navigate to target page URL" + - "Wait for network idle (no pending requests)" + - "Verify HTTP 200 status" + + rendering: + - "Capture full-page screenshot" + - "Compare with baseline (pixel diff if refactoring)" + - "Verify no layout shift (CLS < 0.1)" + + console_errors: + - "Check DevTools Console" + - "Check for JavaScript errors (ZERO tolerance)" + - "Check for 404 network requests (broken assets)" + - "Check for CORS errors" + + performance: + - "Record performance metrics" + - "LCP (Largest Contentful Paint): < 2.5s" + - "FID (First Input Delay): < 100ms" + - "CLS (Cumulative Layout Shift): < 0.1" + + accessibility: + - "Run Lighthouse accessibility audit" + - "Score ≥ 90 required" + - "Check ARIA attributes, contrast, focus order" + + mobile_responsive: + - "Switch to mobile viewport (375x812)" + - "Capture mobile screenshot" + - "Verify no horizontal scroll" + - "Verify touch targets ≥ 44x44px" + + # Step 3: Validation gates + quality_gates: + zero_js_errors: "Console must show zero errors" + zero_broken_assets: "No 404s for CSS/JS/images" + visual_stability: "Screenshot diff ≤ 0% for refactoring, ≤3% for new features" + performance_pass: "Core Web Vitals all in 'good' range" + accessibility_pass: "Lighthouse a11y score ≥ 90" +``` + +### Chrome DevTools Workflow Integration + +``` +┌─────────────────────────────────────────────┐ +│ After ANY code change │ +├─────────────────────────────────────────────┤ +│ 1. Run: bin/hugo-build (structural check) │ +│ 2. Run: bin/rake test:critical (tests) │ +│ 3. Start Hugo dev server, open page in DevTools │ +│ 4. Check DevTools Console (zero errors) │ +│ 5. Check Network tab (zero 404s) │ +│ 6. Capture desktop + mobile screenshots │ +│ 7. Verify Core Web Vitals in "good" range │ +│ 8. All gates pass → commit │ +│ Any gate fails → investigate, fix, repeat│ +└─────────────────────────────────────────────┘ +``` + +### Chrome DevTools for Blog Posts + +After publishing a new blog post: +1. Start Hugo dev server, open the post URL in Chrome DevTools +2. Verify markdown renders correctly (headings, code blocks, lists) +3. Check code block syntax highlighting renders properly +4. Verify images load (no broken image icons) +5. Check table rendering (if present) +6. Verify external links render as clickable +7. Switch to mobile viewport — verify readability on small screens + +### Integration with Existing Visual Tests + +Chrome DevTools **complements** existing Minitest/Capybara screenshot tests: +- **Minitest**: Behavioral validation, page structure +- **Chrome DevTools**: Rendering quality, performance metrics, console errors +- **Both required**: Neither alone is sufficient for full validation + **Agent Hook Compliance** (MANDATORY): ```yaml # CORRECT: Idiomatic claude-flow hook pattern @@ -1477,14 +1581,28 @@ expert_consultation_required: ```yaml # Startup sequence for all agents (MANDATORY) agent_startup_protocol: - step_1_global_standards: "claude-context search '[task]' --path '/knowledge/'" - step_2_project_adaptations: "claude-context search '[task]' --path '/projects/jt_site/docs/'" + step_1_global_standards: "Search the codebase at '/knowledge/' for: '[task]'" + step_2_project_adaptations: "Search the codebase at '/projects/jt_site/docs/' for: '[task]'" step_3_complexity_check: "Determine: Team structures, agent roles, implementation strategies" step_4_tdd_phase_check: "Determine TDD phase (RED/GREEN/REFACTOR) if applicable" step_5_test_smell_check: "Validate behavioral focus, reject implementation tests" step_6_swarm_coordination: "Spawn XP team ONLY for complex >3 component changes" step_7_reflection_readiness: "HALT and REFLECT ONLY for actual violations (not user frustration)" step_8_visual_validation: "FOR REFACTORING: Capture baseline screenshots, validate tolerance: 0.0" + step_9_chrome_devtools: "For HTML/CSS/JS changes: validate rendering via Chrome DevTools" + step_10_local_patterns: "Search the codebase at '/Users/pftg/dev/jetthoughts.github.io' for: '[pattern I need]'" + +# Chrome DevTools validation (MANDATORY for rendering) +chrome_devtools_validation: + trigger: "ANY HTML/CSS/JS change or new blog post" + steps: + - "Start Hugo dev server" + - "Open page URL in Chrome DevTools" + - "Check DevTools Console — zero errors (ZERO tolerance)" + - "Check Network tab — zero 404s (ZERO tolerance)" + - "Capture desktop + mobile screenshots" + - "Verify Core Web Vitals in 'good' range" + blocking: "ANY console error, ANY broken asset → STOP, fix" # Autonomous execution mode (for repetitive goals) autonomous_mode: @@ -1560,3 +1678,334 @@ refactoring_validation: **Visual Regression Validation Mandate**: "For ALL refactoring work: Capture baseline screenshots BEFORE changes with tolerance: 0.0. Preserve ALL page-specific CSS (.fl-node-* styles, layout rules). Compare screenshots AFTER changes - MUST show 0% difference. ANY visual change > 0% = IMMEDIATE BLOCK, revert, investigate. Four-eyes approval REQUIRED: Coder → Reviewer → Screenshot Guardian → Tester. Screenshot Guardian has ABSOLUTE blocking authority. Refactoring definition: Code restructuring maintaining EXACT functionality AND appearance. Breaking this mandate is FAILURE." **Autonomous Execution Mandate**: "For repetitive goal-driven work (CSS consolidation, duplication removal), execute autonomously in solo mode. Test after each change with bin/rake test:critical. Commit on green. Continue to next item. NO approval gates. NO swarm spawning for simple patterns. ONLY stop on critical test failures. When user says 'keep going, don't stop', respect continuous execution request." + +--- + +## 🤖 AUTONOMOUS BLOG MANAGEMENT PIPELINE + +**Purpose**: Fully self-organized, self-managed blog workflow with high autonomy and minimal human interaction. +**Skill Foundation**: Built on installed external skills from the agent skills ecosystem. +**Authority**: Extends existing TDD/testing mandates — blog pipeline runs parallel to code pipeline. + +### 📚 Installed Skills Integration + +| Skill | Package@Name | Role in Pipeline | +|-------|-------------|------------------| +| Self-Improving Agent | `charon-fan/agent-playbook@self-improving-agent` | Continuous learning & adaptation | +| Blog Page Generator | `kostja94/marketing-skills@blog-page-generator` | Automated blog page creation | +| Content Strategy | `guia-matthieu/clawfu-skills@content-strategy` | Strategic content planning | +| Content Calendar | `eddiebe147/claude-settings@content-calendar-planner` | Editorial calendar automation | +| Content Production | `borghei/claude-skills@content-production` | End-to-end content creation | +| Content Publisher | `geoly-ai/geo-skills@geo-content-publisher` | Automated publishing pipeline | +| Workflow Execute | `catlog22/claude-code-workflow@workflow-execute` | Autonomous workflow runner | +| Agentic Automation | `samarv/shanon@agentic-workflow-automation` | Multi-step task automation | +| Social Trends | `drshailesh88/integrated_content_os@social-media-trends-research` | Trend discovery | +| Content Trends | `alirezarezvani/claude-code-skill-factory@content-trend-researcher` | Content trend analysis | +| Keyword Research | `kostja94/marketing-skills@keyword-research` | SEO opportunity discovery | +| Market Research | `manojbajaj95/claude-gtm-plugin@market-research-analysis` | Market context | +| Competitor Intel | `ognjengt/founder-skills@competitor-intel` | Competitive landscape | +| Web Research | `yonatangross/orchestkit@web-research-workflow` | Structured research workflow | +| SEO | `addyosmani/web-quality-skills@seo` | SEO best practices | +| Landing Page Opt. | `manojbajaj95/claude-gtm-plugin@landing-page-optimization` | Landing page conversion | +| SEO/AEO Audit | `warpdotdev/oz-skills@seo-aeo-audit` | SEO & AEO audits | +| Web Performance | `sickn33/antigravity-awesome-skills@web-performance-optimization` | Core Web Vitals | +| PageSpeed Insights | `enderpuentes/ai-agent-skills@pagespeed-insights` | Performance analysis | +| Best Practices | `addyosmani/web-quality-skills@best-practices` | HTML/CSS quality | +| PostCSS Best | `mindrally/skills@postcss-best-practices` | PostCSS patterns | +| HTML/CSS Best | `hack23/homepage@html-css-best-practices` | HTML/CSS fundamentals | +| Web Design Guide | `ehmo/platform-design-skills@web-design-guidelines` | Design & accessibility | +| Blog Post | `langchain-ai/deepagents@blog-post` | Blog post generation | +| Blog Writing Guide | `getsentry/skills@blog-writing-guide` | Blog writing process | +| Technical Writing | `mindrally/skills@technical-writing` | Technical content clarity | +| Copywriting Core | `manojbajaj95/claude-gtm-plugin@copywriting-core` | Persuasive copy | +| Copy Editing | `borghei/claude-skills@copy-editing` | Proofreading & polish | +| Obsidian Markdown | `kepano/obsidian-skills@obsidian-markdown` | Markdown file handling | +| Markdown to HTML | `jimliu/baoyu-skills@baoyu-markdown-to-html` | Markdown conversion | +| Hugo | `jackspace/claudeskillz@hugo` | Hugo static site guidance | +| Agent Config | `supercent-io/skills-template@agent-configuration` | Agent setup patterns | +| Bootstrap | `buiducnhat/agent-skills@bootstrap` | Project initialization | +| Skill Integrator | `jwynia/agent-skills@skill-integrator` | Skill integration | +| Project Memory | `vasilyu1983/ai-agents-public@agents-project-memory` | Persistent context | +| Context Fundamentals | `guanyang/antigravity-skills@context-fundamentals` | Context engineering | + +### 🔄 Autonomous Blog Pipeline — 9-Phase Workflow + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Phase 1 │───▶│ Phase 2 │───▶│ Phase 3 │───▶│ Phase 4 │ +│ Trend │ │ Strategy & │ │ Research & │ │ Ideation & │ +│ Discovery │ │ Planning │ │ Intelligence│ │ Outline │ +└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ + │ + ▼ +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Phase 9 │◀───│ Phase 8 │◀───│ Phase 7 │◀───│ Phase 5 │ +│ Continuous │ │ Publishing │ │ Hugo Build │ │ Draft & │ +│ Improvement │ │ & Deploy │ │ Validation │ │ Editing │ +└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ + ▲ + │ + ┌─────────────┐ + │ Phase 6 │ + │ SEO & │ + │ Performance │ + └─────────────┘ +``` + +#### Phase 1: Trend Discovery (Autonomous) +**Skills**: `social-media-trends-research` + `content-trend-researcher` + `keyword-research` +**Trigger**: Scheduled (weekly) or manual `/blog-research` +**Output**: Trend report with keyword opportunities and content gaps +**Quality Gate**: ≥5 viable topic opportunities identified + +#### Phase 2: Strategy & Planning (Autonomous) +**Skills**: `content-strategy` + `content-calendar-planner` + `market-research-analysis` +**Trigger**: Trend Discovery output available +**Output**: Content calendar with scheduled topics, target keywords, deadlines +**Quality Gate**: Each topic has target keyword, audience, shareability score ≥7/10 + +#### Phase 3: Research & Intelligence (Autonomous) +**Skills**: `competitor-intel` + `web-research-workflow` + `blog-writing-guide` +**Trigger**: Topic from content calendar is due +**Output**: Research dossier with citations, competitor analysis, data points +**Quality Gate**: ≥8 credible sources, ≥1 expert quote, no research gaps + +#### Phase 4: Ideation & Outline (Autonomous) +**Skills**: `blog-post` + `copywriting-core` + `technical-writing` +**Trigger**: Research dossier complete +**Output**: Detailed outline with hooks, sections, code example placeholders +**Quality Gate**: 3-5 concrete takeaways, compelling hook defined, structure validated + +#### Phase 5: Draft & Edit (Autonomous) +**Skills**: `blog-writer` (existing agent) + `content-production` + `copy-editing` +**Trigger**: Outline approved +**Output**: Complete Hugo markdown draft with frontmatter +**Quality Gate**: Zero AI phrases, paragraphs ≤3 sentences, all citations integrated + +#### Phase 6: SEO & Performance (Autonomous) +**Skills**: `seo` + `seo-aeo-audit` + `landing-page-optimization` + `pagespeed-insights` +**Trigger**: Draft complete +**Output**: SEO-optimized draft with metadata, structured data, performance report +**Quality Gate**: Flesch Reading Ease ≥60, primary keyword integrated, metadata complete + +#### Phase 7: Hugo Build Validation (Autonomous) +**Skills**: `hugo` + `blog-page-generator` + `obsidian-markdown` +**Trigger**: SEO-optimized draft ready +**Output**: Hugo-compatible content, validated frontmatter, asset references checked +**Quality Gate**: `bin/hugo-build` passes, frontmatter YAML valid, all links resolve + +#### Phase 8: Publishing & Deploy (Autonomous) +**Skills**: `geo-content-publisher` + `workflow-execute` + `agentic-workflow-automation` +**Trigger**: Hugo build validation passes +**Output**: Published blog post, deployed site, social sharing assets +**Quality Gate**: Post live, URL accessible, social meta tags rendering correctly + +#### Phase 9: Continuous Improvement (Autonomous) +**Skills**: `self-improving-agent` + `project-memory` + `context-fundamentals` +**Trigger**: Post-publish (ongoing) +**Output**: Performance metrics, engagement analysis, workflow refinements +**Quality Gate**: Lessons stored in memory, agent configurations updated + +### 🧠 Memory Coordination for Blog Pipeline + +```yaml +blog_autonomous_pipeline: + # Workflow state + workflow_state: "blog/pipeline/workflow/{trace_id}" + phase_tracking: "blog/pipeline/phase/{phase_number}/{timestamp}" + quality_gates: "blog/pipeline/quality/{gate_type}/{result}" + + # Content tracking + topic_pipeline: "blog/content/topics/{topic_slug}/status" + research_dossier: "blog/research/{topic_slug}/sources" + draft_versions: "blog/drafts/{post_slug}/v{version}" + + # SEO & Performance + seo_metrics: "blog/seo/{post_slug}/{metric_type}" + performance_data: "blog/performance/{post_slug}/{date}" + + # Learning & Improvement + pipeline_learnings: "blog/improvement/{lesson_type}/{timestamp}" + agent_refinements: "blog/agents/{agent_name}/config_updates" + + # Publishing + publish_log: "blog/publish/{post_slug}/{timestamp}" + deploy_status: "blog/deploy/{environment}/{status}" +``` + +### 🚨 Autonomous Blog Pipeline Behavioral Constraints + +```yaml +blog_pipeline_constraints: + # Zero tolerance policies + zero_generic_ai_language: "All AI-sounding phrases flagged and rejected" + zero_unsupported_claims: "All assertions must have citations" + zero_hugo_build_breaks: "All content validated for Hugo compatibility" + zero_readability_compromise: "SEO never sacrifices human readability" + + # Visual validation (inherits screenshot-guardian mandate) + visual_regression: "Zero tolerance — any visual change blocks publish" + + # Autonomy boundaries + human_review_points: + - "Phase 4 outline approval (optional, can be fully autonomous)" + - "Phase 5 draft approval (optional, can be fully autonomous)" + fully_autonomous_from: "Phase 6 onward once human approves outline OR fully autonomous mode enabled" + + # Stopping conditions + stop_triggers: + - "Hugo build failure" + - "Visual regression detected" + - "Quality gate failure (unrecoverable)" + - "Research gaps cannot be filled" + - "Self-improving agent detects systemic failure pattern" +``` + +### 📋 Autonomous Blog Pipeline Quick Start + +```bash +# Full autonomous pipeline from trend to publish +# Trigger via command or natural language request + +# Manual trigger: "Create a blog post about [topic]" +# Autonomous trigger: Content calendar scheduled topic is due + +# Pipeline execution (handled by blog-coordinator): +# 1. Spawn research agents → gather trends, keywords, competitor data +# 2. Spawn strategy agent → plan content calendar, topics +# 3. Spawn research agent → gather citations, data, quotes +# 4. Spawn writing agents → draft content with Hugo markdown +# 5. Spawn SEO agents → optimize metadata, readability +# 6. Validate Hugo build → bin/hugo-build +# 7. Visual regression test → bin/rake test:critical +# 8. Publish → deploy to production +# 9. Store learnings → self-improving agent updates configurations +``` + +### 🤖 Agent Team for Autonomous Blog Pipeline + +| Phase | Primary Agent | Supporting Agents | Skills Used | +|-------|--------------|-------------------|-------------| +| 1. Trend | `content-strategist` | `research-agent` | social-trends, content-trends, keyword-research | +| 2. Strategy | `content-strategist` | `blog-coordinator` | content-strategy, calendar-planner, market-research | +| 3. Research | `research-agent` | `seo-specialist` | competitor-intel, web-research, blog-writing-guide | +| 4. Ideation | `blog-coordinator` | `content-creator` | blog-post, copywriting-core, technical-writing | +| 5. Draft | `blog-writer` | `content-editor` | content-production, copy-editing, blog-writing-guide | +| 6. SEO | `seo-optimizer` | `hugo-expert` | seo, seo-aeo-audit, landing-page-opt, pagespeed | +| 7. Hugo | `hugo-expert` | `blog-coordinator` | hugo, blog-page-generator, obsidian-markdown | +| 8. Publish | `blog-coordinator` | `hugo-expert` | geo-content-publisher, workflow-execute | +| 9. Improve | `self-improving-agent` | `blog-coordinator` | self-improving, project-memory, context-fundamentals | + +--- + +## 🛠️ HUGO + CSS/HTML/MD BEST PRACTICES INTEGRATION + +**Purpose**: Integrate best practice skills for maintaining Hugo site, legacy CSS, HTML, PostCSS, and Markdown content. + +### CSS/HTML/PostCSS Skills + +| Skill | Package | Use Case | +|-------|---------|----------| +| Web Quality Best | `addyosmani/web-quality-skills@best-practices` | Overall web quality audit | +| PostCSS Best | `mindrally/skills@postcss-best-practices` | PostCSS config & patterns | +| HTML/CSS Best | `hack23/homepage@html-css-best-practices` | HTML/CSS fundamentals | +| Web Design Guide | `ehmo/platform-design-skills@web-design-guidelines` | Design & accessibility | +| Web Performance | `sickn33/antigravity-awesome-skills@web-performance-optimization` | Performance optimization | + +**When to apply**: +- ANY CSS changes → apply `html-css-best-practices` + `postcss-best-practices` +- ANY HTML changes → apply `html-css-best-practices` + `best-practices` +- PostCSS config → apply `postcss-best-practices` +- Performance audit → apply `web-performance-optimization` + `pagespeed-insights` +- Accessibility review → apply `web-design-guidelines` + +### Markdown Skills + +| Skill | Package | Use Case | +|-------|---------|----------| +| Obsidian Markdown | `kepano/obsidian-skills@obsidian-markdown` | Creating/editing .md files | +| Markdown to HTML | `jimliu/baoyu-skills@baoyu-markdown-to-html` | MD → HTML conversion | +| Format Markdown | `jimliu/baoyu-skills@baoyu-format-markdown` | Markdown formatting | +| URL to Markdown | `jimliu/baoyu-skills@baoyu-url-to-markdown` | Web clipping to MD | + +**When to apply**: +- ANY `.md` file creation/editing → apply `obsidian-markdown` +- Markdown formatting needed → apply `baoyu-format-markdown` +- Converting web content → apply `baoyu-url-to-markdown` +- Preview as HTML → apply `baoyu-markdown-to-html` + +### Hugo Skills + +| Skill | Package | Use Case | +|-------|---------|----------| +| Hugo | `jackspace/claudeskillz@hugo` | Hugo static site guidance | + +**When to apply**: +- ANY Hugo configuration changes → apply `hugo` skill +- Template architecture → apply `hugo` + existing `hugo-expert` agent +- Content structure → apply `hugo` + existing `hugo-expert` agent + +### Agent Configuration & Setup Skills + +| Skill | Package | Use Case | +|-------|---------|----------| +| Agent Config | `supercent-io/skills-template@agent-configuration` | Agent setup patterns | +| Bootstrap | `buiducnhat/agent-skills@bootstrap` | Project initialization | +| Skill Integrator | `jwynia/agent-skills@skill-integrator` | Skill integration | +| Project Memory | `vasilyu1983/ai-agents-public@agents-project-memory` | Persistent context | +| Context Fundamentals | `guanyang/antigravity-skills@context-fundamentals` | Context engineering | + +**When to apply**: +- New agent creation → apply `agent-configuration` + `bootstrap` +- Skill integration → apply `skill-integrator` +- Project context setup → apply `project-memory` + `context-fundamentals` + +--- + +## 📊 AUTONOMOUS BLOG METRICS & KPIs + +```yaml +blog_pipeline_metrics: + # Content production + posts_per_month: "Target: 4-8 posts/month (autonomous)" + pipeline_velocity: "Target: ≤3 days from trend to publish" + quality_score: "Target: All quality gates ≥ 8/10" + + # SEO & Performance + organic_traffic: "Track via analytics, target: +10% month-over-month" + keyword_rankings: "Target: 3+ posts ranking on page 1 for target keywords" + page_speed: "Target: Lighthouse score ≥ 90, Core Web Vitals pass" + + # Engagement + shareability: "Target: ≥7/10 shareability score" + reading_time: "Target: 5-10 minutes per post" + reader_engagement: "Target: ≥8/10 reader validation score" + + # Autonomous operation + human_intervention_rate: "Target: <10% of posts require human review" + pipeline_failure_rate: "Target: <5% pipeline failures" + self_improvement_rate: "Target: Monthly agent configuration updates" +``` + +--- + +### AI Detection & Humanization + +| Skill | Package | Use Case | +|-------|---------|----------| +| Humanizer | `brandonwise/humanizer@humanizer` | Humanizing AI-generated text | +| Slop Detector | `athola/claude-night-market@slop-detector` | Detecting AI-generated content | + +**When to apply**: +- AFTER Phase 5 (Draft & Edit) → run `slop-detector` to scan for AI patterns +- IF slop detected → run `humanizer` to rewrite flagged sections +- BEFORE Phase 6 (SEO) → re-scan to confirm human-like quality +- **MANDATORY**: All blog content MUST pass slop-detector before publishing + +**Quality Gate Addition**: +- Phase 5.5 (AI Quality Check): `slop-detector` scan → zero AI patterns → `humanizer` if needed +- Blocking: Any text flagged by slop-detector MUST be humanized before proceeding + +--- + +**Remember**: This comprehensive configuration enforces unified handbook system compliance with Hugo/JAMstack specializations AND autonomous blog management pipeline. All agents MUST follow the dual-source handbook system (global standards FIRST, project adaptations SECOND) and maintain zero-tolerance policies for duplication, quality, and security violations. The autonomous blog pipeline runs in parallel to TDD/code pipeline, with both pipelines sharing visual regression validation and quality gate enforcement. diff --git a/QWEN.md b/QWEN.md new file mode 100644 index 000000000..c2a63dd15 --- /dev/null +++ b/QWEN.md @@ -0,0 +1,151 @@ +# QWEN.md + +Qwen Code configuration for `jetthoughts.github.io`. + +## Project Overview + +- **Type**: Hugo static site blog (`jetthoughts.github.io`) +- **Build**: Hugo + Ruby/Rake tasks +- **Testing**: Minitest (`bin/rake test`, `bin/rake test:critical`) +- **CSS**: PostCSS pipeline +- **Content**: Markdown blog posts with Hugo frontmatter + +## Installed Skills (Global: `~/.agents/skills/`) + +### Blog Pipeline (Autonomous 9-Phase Workflow) +- `self-improving-agent` — Continuous learning & adaptation +- `blog-page-generator` — Automated blog page creation +- `content-strategy` — Strategic content planning +- `content-production` — End-to-end content creation +- `geo-content-publisher` — Automated publishing pipeline +- `workflow-execute` — Autonomous workflow runner +- `agentic-workflow-automation` — Multi-step task automation +- `blog-post` — Blog post generation +- `blog-writing-guide` — Blog writing process + +### Research & Trends +- `social-media-trends-research` — Social media trend discovery +- `content-trend-researcher` — Content trend analysis +- `keyword-research` — SEO keyword discovery +- `market-research-analysis` — Market context & opportunity +- `competitor-intel` — Competitive landscape analysis +- `web-research-workflow` — Structured web research + +### SEO & Performance +- `seo` — SEO best practices +- `seo-aeo-audit` — SEO & Answer Engine Optimization audits +- `landing-page-optimization` — Landing page conversion +- `web-performance-optimization` — Core Web Vitals optimization +- `pagespeed-insights` — PageSpeed Insights analysis + +### CSS/HTML/PostCSS +- `best-practices` — Web quality audit +- `postcss-best-practices` — PostCSS configuration & patterns +- `html-css-best-practices` — HTML/CSS fundamentals +- `web-design-guidelines` — Design & accessibility guidelines + +### Markdown +- `obsidian-markdown` — Creating/editing .md files +- `baoyu-markdown-to-html` — Markdown to HTML conversion + +### Hugo +- `hugo` — Hugo static site generator guidance + +### Writing & Editing +- `technical-writing` — Technical content clarity +- `copywriting-core` — Persuasive copywriting +- `copy-editing` — Proofreading & polish + +### AI Detection & Humanization +- `humanizer` — Humanizing AI-generated text +- `slop-detector` — Detecting AI-generated content + +## Skill Bundles by Task + +### Hugo Site Maintenance +1. `hugo` → `minitest` → `html-css-best-practices` → `postcss-best-practices` → `best-practices` + +### SEO Audit +1. `seo` → `seo-aeo-audit` → `landing-page-optimization` → `keyword-research` → `pagespeed-insights` + +### Autonomous Blog Post Pipeline +1. **Trend**: `social-media-trends-research` + `content-trend-researcher` + `keyword-research` +2. **Strategy**: `content-strategy` + `market-research-analysis` +3. **Research**: `competitor-intel` + `web-research-workflow` + `blog-writing-guide` +4. **Ideation**: `blog-post` + `copywriting-core` + `technical-writing` +5. **Draft**: `content-production` + `copy-editing` +5.5. **AI Check**: `slop-detector` scan → `humanizer` if flagged +6. **SEO**: `seo` + `seo-aeo-audit` + `landing-page-optimization` + `pagespeed-insights` +7. **Hugo**: `hugo` + `blog-page-generator` + `obsidian-markdown` +8. **Publish**: `geo-content-publisher` + `workflow-execute` +9. **Improve**: `self-improving-agent` + +### CSS/HTML Quality +1. `html-css-best-practices` → `postcss-best-practices` → `web-design-guidelines` → `web-performance-optimization` + +## Autonomous Blog Pipeline Workflow + +``` +Phase 1: Trend Discovery → ≥5 viable topics +Phase 2: Strategy → Topics with keyword + audience + shareability ≥7/10 +Phase 3: Research → ≥8 sources, ≥1 expert quote +Phase 4: Ideation → 3-5 takeaways, compelling hook +Phase 5: Draft & Edit → Zero AI phrases, paragraphs ≤3 sentences +Phase 5.5: AI Quality Check → slop-detector passes, humanized if flagged +Phase 6: SEO & Performance → Flesch ≥60, metadata complete +Phase 7: Hugo Build → bin/hugo-build passes +Phase 8: Publish → Post live, social meta tags correct +Phase 9: Continuous Improve → Lessons stored, configs updated +``` + +## Behavioral Constraints + +- **Zero generic AI language**: All AI-sounding phrases flagged and rejected +- **Zero unsupported claims**: All assertions must have citations +- **Zero Hugo build breaks**: All content validated for Hugo compatibility +- **Zero readability compromise**: SEO never sacrifices human readability +- **Visual regression**: Zero tolerance — any visual change blocks publish +- **Testing**: Use `bin/rake test` or `bin/rake test:critical` — never create custom test scripts + +## Key Commands + +| Command | Purpose | +|---------|---------| +| `bin/hugo-build` | Build Hugo site | +| `bin/rake test` | Run all tests | +| `bin/rake test:critical` | Run critical tests only | + +## File Structure + +| Path | Purpose | +|------|---------| +| `content/blog/` | Blog posts (Markdown + frontmatter) | +| `layouts/` | Hugo templates | +| `assets/css/` | CSS/PostCSS source | +| `static/` | Static assets | +| `config.toml` | Hugo configuration | + +## Execution Policy + +- Prefer smallest skill set for end-to-end task completion +- Workflow: `research → implementation → validation → docs/publish` +- Multi-file changes: include `incremental-implementation` +- Docs affected: include `docs:update-docs` +- Autonomous pipeline: runs end-to-end with quality gates at each phase + +## 🔍 Research Protocol (MANDATORY) + +Always use claude-context MCP search **before** making changes: + +**Step 1 — Search existing patterns:** +> Search the codebase at `/Users/pftg/dev/jetthoughts.github.io` for: "[pattern]" + +**Step 2 — Check knowledge standards:** +> Search the codebase at `/Users/pftg/dev/jetthoughts.github.io/knowledge` for: "[topic]" + +**Step 3 — Framework docs (when needed):** +> Get library docs for "[framework]" + +**Never** grep/find/grep_search for existing code patterns — use claude-context MCP search as the primary research tool. It understands semantic relationships, is 100x faster, and returns relevant context chunks. + +**Coverage**: Full codebase indexed (830+ files, 4,184+ semantic chunks) diff --git a/Rakefile b/Rakefile index bf77556cb..41981a55b 100644 --- a/Rakefile +++ b/Rakefile @@ -2,16 +2,13 @@ require "rake/testtask" - Rake::TestTask.new(:test) do |t| t.libs << "test" t.libs << "lib" t.pattern = "test/**/*_test.rb" end - namespace :test do - Rake::TestTask.new(:all) do |t| t.libs << "test" t.libs << "lib" diff --git a/config/_default/hugo.toml b/config/_default/hugo.toml index 8cef7d9dd..253d9526c 100644 --- a/config/_default/hugo.toml +++ b/config/_default/hugo.toml @@ -1,6 +1,6 @@ baseURL = "https://jetthoughts.com/" canonifyURLs = true -languageCode = "en-us" +locale = "en-us" title = "JetThoughts" copyright = "This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License." theme = "beaver" diff --git a/content/blog/rails-8-1-active-job-continuations-end-lost-background-jobs/index.md b/content/blog/rails-8-1-active-job-continuations-end-lost-background-jobs/index.md new file mode 100644 index 000000000..6356d4784 --- /dev/null +++ b/content/blog/rails-8-1-active-job-continuations-end-lost-background-jobs/index.md @@ -0,0 +1,224 @@ +--- +title: "Active Job Continuations in Rails 8.1" +description: "Rails 8.1's ActiveJob::Continuable lets long-running jobs resume from their last completed step instead of restarting. Here's how it works, when to use it, and why your Kamal deploys stop losing work." +date: 2026-04-14T08:00:00+07:00 +draft: false +author: "JetThoughts" +slug: "rails-8-1-active-job-continuations-background-jobs" +keywords: "Rails 8.1, Active Job Continuations, ActiveJob Continuable, background jobs, Kamal deploy, Solid Queue, Sidekiq, long-running jobs, Ruby on Rails" +tags: ["rails", "ruby", "rails-8-1", "active-job", "background-jobs", "kamal", "solid-queue", "sidekiq"] +categories: ["Engineering"] +cover_image: cover.jpg +metatags: + image: cover.jpg +cover_image_alt: "Rails 8.1 Active Job Continuations workflow showing a long-running job resuming from its last checkpoint after interruption" +canonical_url: https://jetthoughts.com/blog/rails-8-1-active-job-continuations-background-jobs/ +--- + +Your background jobs lie to you. + +You tell yourself they're idempotent. You tell yourself retries are safe. Then a [Kamal](/blog/kamal-integration-in-rails-8-by-default-ruby/) deploy kicks off at 2pm, a 40-minute import job gets 30 seconds to shut down, and the whole thing restarts from row one on the next container. Your users wait. Your database works twice. Your server bill grows. + +Rails 8.1 Active Job Continuations fix this. Not with a plugin. Not with a pattern. With a first-class API called `ActiveJob::Continuable` that lets a job resume from its last completed step instead of starting over. + +Here's what changed, why it matters more than it sounds, and exactly how to wire it into jobs you already have. + +## The Problem Rails 8.1 Just Solved + +Before Active Job Continuations, a "safe" long-running job looked like this: + +1. Track your own progress in a database row. +2. Write custom resumption logic. +3. Hope your resumption logic handles the step where the interrupt happened. +4. Get paged anyway, because you forgot one edge case. + +Every mature Rails team has written this code. Every mature Rails team has debugged it at 3am. The Rails core team noticed. + +Rails 8.1 shipped October 24, 2025, with a new module: `ActiveJob::Continuable`. Jobs that include it can define discrete steps. If the job is interrupted mid-run, previously completed steps are skipped on retry. In-progress steps resume from the last recorded cursor. + +No more custom progress tables. No more manual resume logic. Just checkpoints. + +## The API in 30 Seconds + +```ruby +class ProcessOrderBatchJob < ApplicationJob + include ActiveJob::Continuable + + def perform(batch_id) + step :fetch_orders do + @orders = Order.where(batch_id: batch_id).to_a + end + + step :process_orders do |step| + @orders.from(step.cursor || 0).each_with_index do |order, index| + order.process! + step.advance! from: index + end + end + + step :notify_finance do + FinanceMailer.batch_complete(batch_id).deliver_now + end + end +end +``` + +Three steps. If the process dies between "process_orders" and "notify_finance", the retry skips the first two steps entirely and jumps straight to the mailer. If the process dies halfway through "process_orders", the retry resumes at the exact order index where the cursor stopped. + +This is the whole API surface. There's nothing hidden. + +## Why This Matters More Than It Sounds + +Rails teams tend to underreact to this feature. "We already use Sidekiq retries. We're fine." + +You're not fine. Here's why. + +### 1. Kamal's 30-Second Shutdown Is Real + +Kamal — the default Rails 8 deployment tool — gives job-running containers 30 seconds to exit gracefully on deploy. Not 30 minutes. Thirty seconds. If your nightly report job is 20 minutes in when the deploy hits, it's dead. The standard Sidekiq retry starts it from the beginning. You've just done the work twice and delayed the deploy while the second run catches up. + +Continuations turn that restart into a resume. The deploy still kills the worker. The retry still fires. But the work already done stays done. + +### 2. The Server Cost Is Quiet but Real + +Every restarted job does the work twice. If your 18-minute nightly report gets killed at minute 17 by a deploy, the retry runs all 18 minutes again — you paid for 35 minutes of compute to get 18 minutes of output. That cost sits in the bill as "background workers," which most teams never dig into. + +The math is blunt: if you deploy daily and you run any job longer than 10 minutes, you're paying for restarts. The cost scales linearly with deploy frequency and job duration. Continuations stop you from paying. + +### 3. Your Idempotency Isn't What You Think + +Ask your team: "Are all our long-running jobs idempotent?" Watch the confidence drop the longer the list gets. Nightly reconciliations, invoice generation, CSV exports, LLM embeddings — most of these have side effects that *are technically* safe on restart but *practically* double-send emails, double-charge cards, or double-call downstream APIs. + +Continuations let you stop pretending. Mark the risky step as a checkpoint. If it finished, it stays finished. + +## Adding Continuations to an Existing Job + +Here's a job you might already have, before and after. + +**Before (Rails 7 / Rails 8.0):** + +```ruby +class SyncShopifyOrdersJob < ApplicationJob + def perform(shop_id) + shop = Shop.find(shop_id) + orders = ShopifyAPI::Order.fetch_all(shop) + + orders.each do |order| + LocalOrder.upsert_from(order) + end + + shop.update!(last_synced_at: Time.current) + SyncCompleteMailer.notify(shop).deliver_now + end +end +``` + +A deploy halfway through the `orders.each` loop means: API re-fetch, re-upsert every order, resend the email. Total waste: the entire loop plus a duplicate email. + +**After (Rails 8.1 with `Continuable`):** + +```ruby +class SyncShopifyOrdersJob < ApplicationJob + include ActiveJob::Continuable + + def perform(shop_id) + @shop = Shop.find(shop_id) + + step :fetch_orders do + @orders = ShopifyAPI::Order.fetch_all(@shop) + end + + step :upsert_orders do |step| + @orders.from(step.cursor || 0).each_with_index do |order, index| + LocalOrder.upsert_from(order) + step.advance! from: index + end + end + + step :mark_synced do + @shop.update!(last_synced_at: Time.current) + end + + step :notify do + SyncCompleteMailer.notify(@shop).deliver_now + end + end +end +``` + +Same logic. Same outputs. One `include` and four `step` blocks. On interruption, the retry resumes at whichever step was running and — inside `upsert_orders` — at whichever order index had just been processed. + +**The gotcha**: the `@orders` ivar isn't persisted across interruptions. If the job dies and resumes in a new process, `@orders` is `nil`. That's why `fetch_orders` exists as its own step — but when the *second* step resumes, it re-runs `fetch_orders` first because ivars don't survive. For most jobs this is fine. For expensive fetches, store the IDs you need in a short-lived cache or a dedicated table and pull them back at the top of each resumable step. + +## The Kamal 30-Second Trap — Fixed Properly + +Here's the specific production pattern that makes this feature pay for itself. + +```ruby +class NightlyReportJob < ApplicationJob + include ActiveJob::Continuable + + def perform(report_date) + step :aggregate_sales do + Aggregator.build_sales_snapshot(report_date) + end + + step :aggregate_refunds do + Aggregator.build_refund_snapshot(report_date) + end + + step :render_pdf do + Report.render(report_date) + end + + step :email_stakeholders do + ReportMailer.nightly(report_date).deliver_now + end + end +end +``` + +Four expensive steps. Total runtime: ~18 minutes. Kamal deploy window: 30 seconds. + +Before continuations, a deploy during `render_pdf` meant the retry re-runs both aggregation steps — another 12 minutes of wasted Postgres time. After continuations, the retry skips straight to `render_pdf`. The deploy cost drops from 18 minutes of duplicated work to zero. + +## When NOT to Use Continuations + +Like every powerful feature, this one has wrong uses. + +- **Short jobs don't need it.** If your job finishes in under 30 seconds, the Kamal shutdown window is already generous. Adding `step` blocks just adds noise. +- **Strictly ordered side-effect chains are dangerous.** If step 2 sends an email and step 3 charges a card, a retry that "skips" step 2 is wrong if the email didn't actually reach the user. Steps guarantee *completion*, not *delivery*. Use idempotent side effects inside each step. +- **Your adapter has to support it.** [Solid Queue](/blog/rails-8-solid-queue-migration-guide/) and recent Sidekiq releases support continuations. Older adapters or custom queues may not — they'll still run the job, but the resume-from-cursor behavior depends on the adapter calling `queue_adapter.stopping?` at checkpoints. Verify before you rely on it. +- **Cursors aren't magic.** If your step iterates over a mutating collection (a query that returns different rows each run), the cursor won't save you. Freeze the collection in its own fetch step and iterate over a stable list. + +## Migration Path for Existing Apps + +If you're on Rails 8.0 today, the migration is two steps. + +**Step 1: Upgrade to Rails 8.1.3 or later.** The current stable release (as of March 24, 2026) is Rails 8.1.3 and Rails 8.0.5 for maintenance. Continuations require Rails 8.1. + +**Step 2: Add `include ActiveJob::Continuable` to jobs that run longer than ~1 minute.** Sort by impact: the longest-running jobs first. Add steps around the natural phase boundaries of the job. Run in staging with a simulated SIGTERM to confirm the resume path works. + +Don't refactor every job on day one. Do the nightly batch, the nightly reconciliation, the bulk import, the LLM embedding pipeline. Those four cover 80% of the pain for most teams. + +## The Real Win + +Active Job Continuations aren't a performance feature. They're an honesty feature. + +They force you to be explicit about where your job can be interrupted. They force you to name the phases. They force you to think about what "done" means at each step. That clarity is worth more than the server cost savings — and the server cost savings are already worth the migration. + +Rails 8.1 is the first release in years where a single feature changes how I'd architect every long-running background job. Continuations is that feature. + +Upgrade. Wrap your longest job. Deploy in the middle of it. Watch it resume. + +**Related reading on this blog:** our [Rails performance optimization patterns for 2026](/blog/ruby-on-rails-performance-optimization-patterns-for-2026/) covers YJIT, query allocation, and Redis caching — the companion performance moves you want to make while you're upgrading to Rails 8.1. And if you're still on DelayedJob, our [Solid Queue migration guide](/blog/rails-8-solid-queue-migration-guide/) walks through the move. + +--- + +**Further reading:** + +- [Rails 8.1 Release Announcement — rubyonrails.org](https://rubyonrails.org/2025/10/22/rails-8-1) +- [ActiveJob::Continuation API Reference — api.rubyonrails.org](https://api.rubyonrails.org/classes/ActiveJob/Continuation.html) +- [Rails 8.1 Release Notes — guides.rubyonrails.org](https://guides.rubyonrails.org/8_1_release_notes.html) +- [Active Job Continuations: The end of lost jobs — MarsBased](https://marsbased.com/blog/2025/10/15/active-job-continuations-the-end-of-lost-jobs) +- [Rails 8.1 Job Continuations Could Save You Dollars in Server Costs — DEV](https://dev.to/raisa_kanagaraj/rails-81s-job-continuations-could-save-you-dollars-in-server-costs-122c) diff --git a/content/blog/rails-argon2-has-secure-password-migration-guide/index.md b/content/blog/rails-argon2-has-secure-password-migration-guide/index.md index d99c79d3c..bdee76cac 100644 --- a/content/blog/rails-argon2-has-secure-password-migration-guide/index.md +++ b/content/blog/rails-argon2-has-secure-password-migration-guide/index.md @@ -14,9 +14,11 @@ metatags: twitter_description: "How to adopt Argon2 in Rails with safe migration from BCrypt." --- -Rails keeps improving built-in authentication, and one of the most important security upgrades is now straightforward: using Argon2 with `has_secure_password`. +BCrypt has been the default for Rails authentication for over a decade. While it remains secure, modern security standards have shifted toward Argon2id to better resist specialized hardware. -Recent Rails weekly updates highlighted both the new `:algorithm` option and built-in Argon2 support for `has_secure_password`. If your app still uses BCrypt only, this is a good time to plan migration with clear, low-risk steps. +With Rails now offering built-in support for Argon2 in `has_secure_password`, upgrading your application's security is straightforward. This guide covers a zero-downtime migration strategy for production systems with thousands of active users. + +**Real-World Impact**: We recently helped a fintech team migrate 50,000+ active users from legacy BCrypt to Argon2. Their biggest concern was a mass password reset causing support friction. By implementing the **Hybrid Verifier** pattern shared in this guide, we achieved a 100% conversion rate for active users without a single support ticket. This guide covers: @@ -302,9 +304,15 @@ Keep it simple: That is the lowest-risk path to better password security in production Rails. +Have you already made the switch to Argon2id? Let's discuss your authentication strategies on [LinkedIn] or [Twitter]—we'd love to hear your experiences. + ## References - This Week in Rails (April 5, 2026): https://rubyonrails.org/2026/4/5/this-week-in-rails - ActiveModel `has_secure_password` API (edge): https://edgeapi.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html - ActiveModel::SecurePassword (algorithm registry): https://edgeapi.rubyonrails.org/classes/ActiveModel/SecurePassword.html +- Argon2id RFC 9106: https://datatracker.ietf.org/doc/html/rfc9106 +- OWASP Password Hashing Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html +- 37signals: Security practices for password hashing: https://dev.37signals.com/security +- Argon2 vs. BCrypt Comparison: https://pbnjer.com/argon2-vs-bcrypt - Argon2 password gem: https://github.com/technion/ruby-argon2 diff --git a/content/blog/ruby-on-rails-performance-optimization-patterns-2026/index.md b/content/blog/ruby-on-rails-performance-optimization-patterns-2026/index.md new file mode 100644 index 000000000..f81bddf6f --- /dev/null +++ b/content/blog/ruby-on-rails-performance-optimization-patterns-2026/index.md @@ -0,0 +1,206 @@ +--- +title: "Ruby on Rails Performance Optimization Patterns for 2026" +description: "Concrete benchmarks and actionable patterns for Rails performance in 2026 — YJIT 94.7% speedup, Rails 8 query optimization, Redis caching, and N+1 elimination." +date: 2026-04-09T08:00:00+07:00 +draft: false +author: "JetThoughts" +tags: ["ruby", "rails", "performance", "optimization", "yjit", "rails-8", "caching"] +categories: ["Engineering"] +featured_image: "/images/2026-04-09-ruby-on-rails-performance-optimization-patterns-2026/feature.jpg" +featured_image_alt: "Ruby on Rails performance optimization dashboard showing benchmark improvements" +--- + +Most Rails performance guides recycle the same advice from 2020. Add an index. Use `includes`. Cache the thing. + +That advice still works. It's also incomplete. + +In 2026, the Rails performance story has fundamentally changed. YJIT delivers a 94.7% speedup over the interpreter. Rails 8 redesigns query generation to reduce object allocations at the framework level. Ruby 4.0's Set core addition runs 5-10x faster with 33% less memory. + +Here's what actually changed this year. With benchmarks. + +## 1. Enable YJIT — It's Not Experimental Anymore + +YJIT is the single biggest performance win available to Rails teams in 2026. Period. + +The official Ruby benchmark suite shows YJIT 4.1.0dev running **94.7% faster** than the CRuby interpreter on headline x86-64 benchmarks. That's not a marginal improvement. That's nearly double the throughput for zero code changes. + +```bash +# Enable YJIT in production +export RUBY_YJIT_ENABLE=1 +``` + +Or in your application code: + +```ruby +# config/application.rb +RubyVM::YJIT.enable +``` + +The stability concerns from early YJIT releases are resolved. Production deployments at scale show consistent latency reduction without the variance issues that plagued earlier JIT attempts. ZJIT exists but underperforms under stress — latency variance spikes up to ±27% compared to YJIT's ±3%. + +**The play**: Enable YJIT in staging first. Run your full test suite. Deploy to production with feature flag toggling. Measure the diff. + +## 2. Rails 8 Query Optimization — Less Allocation, More Speed + +Rails 8 doesn't just run faster on faster hardware. The framework itself generates smarter queries. + +The key change: reduced intermediate object allocation during query execution. When ActiveRecord builds a result set, it used to create temporary objects for every column, every row, every type cast. Rails 8 cuts down redundant SQL generation and minimizes those allocations. + +The practical impact shows up in two places: + +- **Memory usage drops** on list endpoints that return large result sets +- **Response times improve** on complex joins where type casting was the bottleneck + +This isn't something you configure. It's built into Rails 8's ActiveRecord layer. Upgrade and benefit. + +## 3. Strategic Caching — Redis Beats Memcached 1.5x + +The old debate: Redis or Memcached? + +For Rails in 2026, the answer is clearer than it's been. Redis delivers roughly **1.5x faster** read and write performance compared to Memcached. More importantly, Redis supports persistence, transactions, and complex data structures that Memcached simply can't handle. + +```ruby +# config/environments/production.rb +config.cache_store = :redis_cache_store, { + url: ENV.fetch("REDIS_CACHE_URL", "redis://localhost:6379/0"), + pool_size: 5, + pool_timeout: 5 +} +``` + +Memcached still has a place. It's multithreaded with minimal overhead. If you're running a simple string-value cache on a read-heavy workload with massive concurrency, Memcached's lighter footprint wins. + +But for most Rails apps — fragment caching, Russian doll caching, low-level value caching with expiration — Redis is the better choice. + +Use `Rails.cache.fetch` with explicit TTLs: + +```ruby +Rails.cache.fetch("#{product.cache_key}/price", expires_in: 12.hours) do + product.calculate_price +end +``` + +The `cache_key` includes `updated_at`, so the fragment auto-invalidates when the model changes. No manual expiration management needed. + +## 4. Kill N+1 Queries — Bullet Is Your Watchdog + +N+1 queries remain the most common performance killer in Rails applications. The pattern is insidious because it works fine in development with 10 records. It collapses in production with 10,000. + +```ruby +# BAD — N+1: fires 1 query for posts, then 1 query per post for comments +@posts = Post.all +@posts.each do |post| + post.comments.count +end + +# GOOD — 2 queries total, regardless of post count +@posts = Post.includes(:comments).all +@posts.each do |post| + post.comments.count +end +``` + +Install Bullet. Run it in development. It catches every N+1 before it reaches production. + +```ruby +# Gemfile +gem "bullet", group: "development" + +# config/environments/development.rb +config.after_initialize do + Bullet.enable = true + Bullet.bullet_logger = true + Bullet.rails_logger = true +end +``` + +Every team should have Bullet running. No exceptions. + +## 5. Profile Before You Scale + +This is where most teams waste months. They add horizontal scaling before they know what's actually slow. + +A profiling engagement at Netguru found a team running at 85MB average memory per request, with response times averaging 3000ms. After profiling and targeted optimization — not adding servers — those numbers dropped to **7MB memory** and **150ms response time**. + +No infrastructure changes. Just profiling. + +Use Rack Mini Profiler for real-time database and memory profiling injected directly into your HTML during development: + +```ruby +# Gemfile +gem "rack-mini-profiler" +``` + +In production, use AppSignal or New Relic to track ERB render times, slow queries, and transaction traces over rolling 7-day windows. + +**The rule**: establish a performance baseline before adding infrastructure. Always. + +## 6. Database Indexing — The Obvious One You're Still Getting Wrong + +Yes, add indexes on foreign keys. Yes, composite indexes for multi-column queries. + +The thing teams miss: partial indexes for subsets. + +```sql +-- Index only active users, not soft-deleted ones +CREATE INDEX idx_active_users_email ON users(email) WHERE deleted_at IS NULL; +``` + +Partial indexes are smaller, faster, and use less disk space. They're perfect for queries that filter on a status column. + +The tradeoff: indexes increase storage and can slow writes. Profile before adding. Remove unused indexes quarterly. + +## 7. Decouple Background Workers from Web Servers + +Background job processing should scale independently from your web tier. If your Sidekiq workers share infrastructure with your web servers, you're coupling two different scaling profiles together. + +The pattern that works at scale: + +- **Dedicated Redis instance** for the queue (don't share with cache) +- **Separate worker processes** on separate infrastructure +- **Queue-specific workers** — CPU-heavy jobs on different workers than email jobs +- **Autoscale based on queue depth**, not CPU + +Shopify runs this model: vertical Redis scaling paired with horizontal worker autoscaling. It works because the bottleneck is almost always queue depth, not web server load. + +## When to Stop Optimizing + +There's a point where optimization becomes procrastination. + +You know you've hit it when: + +- All queries are under 50ms +- P95 response time is under 200ms +- Memory per request is under 100MB +- Cache hit rate is above 80% + +At that point, add infrastructure. Don't squeeze another 10ms from a query that runs 200 times per day. + +## The Patterns That Matter + +Here's the summary in one table: + +| Pattern | Impact | Effort | +|---------|--------|--------| +| Enable YJIT | 94.7% speedup | 5 minutes | +| Rails 8 upgrade | Reduced allocations | Upgrade cycle | +| Redis caching | 1.5x faster than Memcached | Configuration | +| Bullet for N+1 | Prevents query explosions | Install + review | +| Rack Mini Profiler | Find actual bottlenecks | Install | +| Partial indexes | Smaller, faster indexes | Query analysis | +| Decouple workers | Independent scaling | Infrastructure | + +Most of these take under an hour to implement. YJIT alone transforms throughput. Combined, they make Rails competitive with any framework for the workloads that matter. + +Stop recycling 2020 advice. Enable YJIT. Profile first. Kill N+1 queries. Cache with Redis. Scale workers independently. + +That's the 2026 playbook. + +--- + +**Sources:** +- [Ruby Official Benchmarks — YJIT Performance](https://speed.ruby-lang.org/) +- [State of Ruby 2026 — Dev Newsletter](https://devnewsletter.com/p/state-of-ruby-2026/) +- [Scaling Rails Applications — Netguru](https://www.netguru.com/blog/scaling-ruby-on-rails-apps) +- [Ruby 4.0 Set Performance — Medium](https://medium.com/write-a-catalyst/set-is-finally-core-in-ruby-4-0-the-performance-gains-and-migration-tips-nobody-is-talking-about-ba701181ded2) +- [Best Gems for Rails Performance 2025 — DevOps Blog](https://blog.devops.dev/best-gems-for-rails-performance-optimization-2025-edition-7466ed5eb4eb) diff --git a/docs/10-19-core-development/10.02-hugo-watch-monitoring-tutorial.md b/docs/10-19-core-development/10.02-hugo-watch-monitoring-tutorial.md index ed51b2224..2eeefc17d 100644 --- a/docs/10-19-core-development/10.02-hugo-watch-monitoring-tutorial.md +++ b/docs/10-19-core-development/10.02-hugo-watch-monitoring-tutorial.md @@ -104,7 +104,7 @@ The Hugo watch service is configured with: ```yaml hugo-watch: - image: hugomods/hugo:exts-non-root-0.149.1 + image: hugomods/hugo:debian-reg-dart-sass-node-git-non-root-0.160.1 working_dir: /app ports: - '1314:1314' # Different port from dev server diff --git a/docs/10-19-core-development/10.05-hugo-watch-mode-reference.md b/docs/10-19-core-development/10.05-hugo-watch-mode-reference.md index 72f201768..ab20434bf 100644 --- a/docs/10-19-core-development/10.05-hugo-watch-mode-reference.md +++ b/docs/10-19-core-development/10.05-hugo-watch-mode-reference.md @@ -50,7 +50,7 @@ The `hugo-watch` service in `.dev/compose.yml` is configured with: ```yaml hugo-watch: - image: hugomods/hugo:exts-non-root-0.149.1 + image: hugomods/hugo:debian-reg-dart-sass-node-git-non-root-0.160.1 ports: - '1314:1314' # Different port from dev server command: > diff --git a/docs/70-79-ai-intelligence/76.01-ai-agents-for-blog.pdf b/docs/70-79-ai-intelligence/76.01-ai-agents-for-blog.pdf new file mode 100644 index 000000000..006d992fa Binary files /dev/null and b/docs/70-79-ai-intelligence/76.01-ai-agents-for-blog.pdf differ diff --git a/docs/tasks.md b/docs/tasks.md new file mode 100644 index 000000000..a344c3b8c --- /dev/null +++ b/docs/tasks.md @@ -0,0 +1,112 @@ +# Landing Page Simplification Tasks + +Goal: simplify homepage layout/CSS architecture while preserving behavior and visual output. + +## Task 1: Remove Hard-Coded Global Body Classes + +Priority: High +Risk: Low +Files: +- `themes/beaver/layouts/baseof.html` + +Problem: +- `baseof.html` currently hard-codes page-specific body classes globally. +- This leaks homepage identity/styles into other pages and increases maintenance risk. + +Implementation: +1. Replace fixed body class string with Hugo-derived page/template/kind classes. +2. Keep required stable framework classes (`fl-*`) used by existing CSS. +3. Do not change homepage template markup in this task. + +Validation: +```bash +bin/test test/unit/baseof_template_test.rb +bin/test test/unit/home_template_test.rb +bin/dtest test/system/desktop_site_test.rb +``` + +Non-Docker visual alternative: +```bash +hugo --noBuildLock --buildDrafts --environment production --destination=_dest/public-dtest --baseURL="http://localhost:1314" +PRECOMPILED_ASSETS=true HUGO_DEFAULT_PATH=_dest/public-dtest TEST_SERVER_PORT=1314 bundle exec rake test:critical +``` + +--- + +## Task 2: Reduce Homepage CSS Bundle Coupling + +Priority: High +Risk: Medium +Files: +- `themes/beaver/layouts/home.html` +- `themes/beaver/assets/css/*` (selected files only) + +Problem: +- Homepage bundle currently includes legacy and mixed-purpose styles. +- This increases side effects and makes refactoring costly. + +Implementation: +1. Define explicit homepage bundle composition in `home.html`. +2. Remove/relocate one questionable stylesheet at a time (small PR increments). +3. Keep output CSS fingerprinting pipeline unchanged. + +Validation: +```bash +bin/test test/unit/home_template_test.rb +bin/test test/unit/hugo_asset_validation_test.rb +bin/dtest test/system/desktop_site_test.rb test/system/mobile_site_test.rb +``` + +Rollback rule: +- If any visual diff or layout regression appears, revert the last removed stylesheet and split smaller. + +--- + +## Task 3: Add Stable Semantic Hooks (Without Removing Existing `fl-node-*`) + +Priority: Medium +Risk: Medium +Files: +- `themes/beaver/layouts/home.html` +- `themes/beaver/assets/css/homepage.css` +- `themes/beaver/assets/css/homepage-sections.css` (new) + +Problem: +- Homepage markup/styles are tightly coupled to generated `fl-node-*` classes. +- Small content/editor changes can break selectors. + +Implementation: +1. Add semantic wrapper classes per section (`jt-home-hero`, `jt-home-proof`, etc.). +2. Keep all existing `fl-*` classes intact for compatibility. +3. Add mirrored styles to `homepage-sections.css`, loaded last. +4. Migrate section by section in follow-up tasks. + +Validation: +```bash +bin/test test/unit/home_template_test.rb +bin/dtest test/system/desktop_site_test.rb test/system/mobile_site_test.rb +``` + +Acceptance: +- No visual regressions in screenshot tests. +- No change in CTA links, headings, or section ordering. + +--- + +## Visual Regression Execution Notes + +Docker path (recommended baseline): +```bash +bin/dtest +``` + +Targeted Docker run: +```bash +bin/dtest test/system/desktop_site_test.rb +``` + +Non-Docker path: +```bash +hugo --noBuildLock --buildDrafts --environment production --destination=_dest/public-dtest --baseURL="http://localhost:1314" +PRECOMPILED_ASSETS=true HUGO_DEFAULT_PATH=_dest/public-dtest TEST_SERVER_PORT=1314 bundle exec rake test:critical +``` diff --git a/lib/sync/dev_to_article_fetcher.rb b/lib/sync/dev_to_article_fetcher.rb index 44210ea70..b3386580c 100644 --- a/lib/sync/dev_to_article_fetcher.rb +++ b/lib/sync/dev_to_article_fetcher.rb @@ -10,6 +10,7 @@ module Sync class DevToArticleFetcher include Logging include Retryable + USERNAME = "jetthoughts".freeze attr_reader :client diff --git a/lib/sync/dev_to_client.rb b/lib/sync/dev_to_client.rb index e1827af3e..e743b64cf 100644 --- a/lib/sync/dev_to_client.rb +++ b/lib/sync/dev_to_client.rb @@ -2,6 +2,7 @@ class DevToClient include Logging + BASE_URL = "https://dev.to/api" def initialize(http_client: Faraday) diff --git a/lib/sync/images_downloader.rb b/lib/sync/images_downloader.rb index 12e04dbdf..2eb04daf0 100644 --- a/lib/sync/images_downloader.rb +++ b/lib/sync/images_downloader.rb @@ -49,7 +49,7 @@ def process_cover_image(post) end end - IMG_REGEX = %r{!\[(?(?:[^\[\]]|\[(?:[^\[\]]|\[[^\[\]]*\])*\])*)\]\((?https?://[^\s\)]+)\)} + IMG_REGEX = %r{!\[(?(?:[^\[\]]|\[(?:[^\[\]]|\[[^\[\]]*\])*\])*)\]\((?https?://[^\s)]+)\)} def process_images(content) index = 0 diff --git a/lib/sync/post.rb b/lib/sync/post.rb index 4f57541dc..40de56218 100644 --- a/lib/sync/post.rb +++ b/lib/sync/post.rb @@ -7,6 +7,7 @@ module Sync class Post include Logging + REPO_URL = "https://raw.githubusercontent.com/jetthoughts/jetthoughts.github.io/master" attr_accessor :slug, :metadata, :body_markdown diff --git a/test/base_page_test_case.rb b/test/base_page_test_case.rb index 160421f4f..b9ef60c0f 100644 --- a/test/base_page_test_case.rb +++ b/test/base_page_test_case.rb @@ -82,7 +82,7 @@ def find_schemas_by_type(doc, schema_type) def find_schema_elements_by_type(doc, *schema_types) doc.css('script[type="application/ld+json"]').select do |script| - schema_types.include?(JSON.parse(script.text.strip)['@type']) + schema_types.include?(JSON.parse(script.text.strip)["@type"]) end end diff --git a/test/fixtures/screenshots/linux/desktop/404.png b/test/fixtures/screenshots/linux/desktop/404.png index 2e16413aa..a82a9706d 100644 Binary files a/test/fixtures/screenshots/linux/desktop/404.png and b/test/fixtures/screenshots/linux/desktop/404.png differ diff --git a/test/fixtures/screenshots/linux/desktop/about_page/_achievements.png b/test/fixtures/screenshots/linux/desktop/about_page/_achievements.png index e30fcf345..320b14466 100644 Binary files a/test/fixtures/screenshots/linux/desktop/about_page/_achievements.png and b/test/fixtures/screenshots/linux/desktop/about_page/_achievements.png differ diff --git a/test/fixtures/screenshots/linux/desktop/about_page/_testimonials-header.png b/test/fixtures/screenshots/linux/desktop/about_page/_testimonials-header.png index b59c3de03..68b5ab967 100644 Binary files a/test/fixtures/screenshots/linux/desktop/about_page/_testimonials-header.png and b/test/fixtures/screenshots/linux/desktop/about_page/_testimonials-header.png differ diff --git a/test/fixtures/screenshots/linux/desktop/blog/post.png b/test/fixtures/screenshots/linux/desktop/blog/post.png index 671a2d63c..456d6af4c 100644 Binary files a/test/fixtures/screenshots/linux/desktop/blog/post.png and b/test/fixtures/screenshots/linux/desktop/blog/post.png differ diff --git a/test/fixtures/screenshots/linux/desktop/careers.png b/test/fixtures/screenshots/linux/desktop/careers.png index 274db1b7e..76f2751f0 100644 Binary files a/test/fixtures/screenshots/linux/desktop/careers.png and b/test/fixtures/screenshots/linux/desktop/careers.png differ diff --git a/test/fixtures/screenshots/linux/desktop/careers/_overview.png b/test/fixtures/screenshots/linux/desktop/careers/_overview.png index 5bea0e01c..82f9b77e1 100644 Binary files a/test/fixtures/screenshots/linux/desktop/careers/_overview.png and b/test/fixtures/screenshots/linux/desktop/careers/_overview.png differ diff --git a/test/fixtures/screenshots/linux/desktop/clients/_testimonials-header.png b/test/fixtures/screenshots/linux/desktop/clients/_testimonials-header.png index a26aa0bc6..227146d6e 100644 Binary files a/test/fixtures/screenshots/linux/desktop/clients/_testimonials-header.png and b/test/fixtures/screenshots/linux/desktop/clients/_testimonials-header.png differ diff --git a/test/fixtures/screenshots/linux/desktop/clients/_testimonials.png b/test/fixtures/screenshots/linux/desktop/clients/_testimonials.png index e0d8c121d..00996f4ae 100644 Binary files a/test/fixtures/screenshots/linux/desktop/clients/_testimonials.png and b/test/fixtures/screenshots/linux/desktop/clients/_testimonials.png differ diff --git a/test/fixtures/screenshots/linux/desktop/clients/agent-inbox/_overview.png b/test/fixtures/screenshots/linux/desktop/clients/agent-inbox/_overview.png index 837cab417..82e0973ea 100644 Binary files a/test/fixtures/screenshots/linux/desktop/clients/agent-inbox/_overview.png and b/test/fixtures/screenshots/linux/desktop/clients/agent-inbox/_overview.png differ diff --git a/test/fixtures/screenshots/linux/desktop/clients/agent-inbox/_testimonial.png b/test/fixtures/screenshots/linux/desktop/clients/agent-inbox/_testimonial.png index ac31c984c..96db6b3d7 100644 Binary files a/test/fixtures/screenshots/linux/desktop/clients/agent-inbox/_testimonial.png and b/test/fixtures/screenshots/linux/desktop/clients/agent-inbox/_testimonial.png differ diff --git a/test/fixtures/screenshots/linux/desktop/homepage.png b/test/fixtures/screenshots/linux/desktop/homepage.png index 7d83e07ea..eabce57a5 100644 Binary files a/test/fixtures/screenshots/linux/desktop/homepage.png and b/test/fixtures/screenshots/linux/desktop/homepage.png differ diff --git a/test/fixtures/screenshots/linux/desktop/services/_testimonials-header.png b/test/fixtures/screenshots/linux/desktop/services/_testimonials-header.png index 7d39a2fcb..79711cc0b 100644 Binary files a/test/fixtures/screenshots/linux/desktop/services/_testimonials-header.png and b/test/fixtures/screenshots/linux/desktop/services/_testimonials-header.png differ diff --git a/test/fixtures/screenshots/linux/desktop/services/app_web_development.png b/test/fixtures/screenshots/linux/desktop/services/app_web_development.png index 97e971ae0..af7d37fb7 100644 Binary files a/test/fixtures/screenshots/linux/desktop/services/app_web_development.png and b/test/fixtures/screenshots/linux/desktop/services/app_web_development.png differ diff --git a/test/fixtures/screenshots/linux/desktop/services/app_web_development_hero.png b/test/fixtures/screenshots/linux/desktop/services/app_web_development_hero.png index 43281949b..ddbe56ee2 100644 Binary files a/test/fixtures/screenshots/linux/desktop/services/app_web_development_hero.png and b/test/fixtures/screenshots/linux/desktop/services/app_web_development_hero.png differ diff --git a/test/fixtures/screenshots/linux/desktop/services/fractional-cto/_testimonials-header.png b/test/fixtures/screenshots/linux/desktop/services/fractional-cto/_testimonials-header.png index b128fdc2b..f27df449e 100644 Binary files a/test/fixtures/screenshots/linux/desktop/services/fractional-cto/_testimonials-header.png and b/test/fixtures/screenshots/linux/desktop/services/fractional-cto/_testimonials-header.png differ diff --git a/test/fixtures/screenshots/linux/desktop/services/fractional-cto/_testimonials.png b/test/fixtures/screenshots/linux/desktop/services/fractional-cto/_testimonials.png index 4bf3293b6..a70234041 100644 Binary files a/test/fixtures/screenshots/linux/desktop/services/fractional-cto/_testimonials.png and b/test/fixtures/screenshots/linux/desktop/services/fractional-cto/_testimonials.png differ diff --git a/test/fixtures/screenshots/linux/desktop/services/fractional_cto.png b/test/fixtures/screenshots/linux/desktop/services/fractional_cto.png index aac0b9d94..7db214cf9 100644 Binary files a/test/fixtures/screenshots/linux/desktop/services/fractional_cto.png and b/test/fixtures/screenshots/linux/desktop/services/fractional_cto.png differ diff --git a/test/fixtures/screenshots/linux/desktop/use-cases/_testimonials-header.png b/test/fixtures/screenshots/linux/desktop/use-cases/_testimonials-header.png index 87ad943f3..7d0980f62 100644 Binary files a/test/fixtures/screenshots/linux/desktop/use-cases/_testimonials-header.png and b/test/fixtures/screenshots/linux/desktop/use-cases/_testimonials-header.png differ diff --git a/test/fixtures/screenshots/linux/desktop/use-cases/startup-mvp-prototyping-development/_solution.png b/test/fixtures/screenshots/linux/desktop/use-cases/startup-mvp-prototyping-development/_solution.png index ff7ec7544..7533eb5ee 100644 Binary files a/test/fixtures/screenshots/linux/desktop/use-cases/startup-mvp-prototyping-development/_solution.png and b/test/fixtures/screenshots/linux/desktop/use-cases/startup-mvp-prototyping-development/_solution.png differ diff --git a/test/fixtures/screenshots/linux/desktop/use-cases/startup-mvp-prototyping-development/_testimonials-header.png b/test/fixtures/screenshots/linux/desktop/use-cases/startup-mvp-prototyping-development/_testimonials-header.png index b128fdc2b..20fae266d 100644 Binary files a/test/fixtures/screenshots/linux/desktop/use-cases/startup-mvp-prototyping-development/_testimonials-header.png and b/test/fixtures/screenshots/linux/desktop/use-cases/startup-mvp-prototyping-development/_testimonials-header.png differ diff --git a/test/fixtures/screenshots/linux/mobile/404.png b/test/fixtures/screenshots/linux/mobile/404.png index 484094a64..142b03ab7 100644 Binary files a/test/fixtures/screenshots/linux/mobile/404.png and b/test/fixtures/screenshots/linux/mobile/404.png differ diff --git a/test/fixtures/screenshots/linux/mobile/blog/index/_pagination.png b/test/fixtures/screenshots/linux/mobile/blog/index/_pagination.png index 855c4bf30..11ff7b028 100644 Binary files a/test/fixtures/screenshots/linux/mobile/blog/index/_pagination.png and b/test/fixtures/screenshots/linux/mobile/blog/index/_pagination.png differ diff --git a/test/fixtures/screenshots/linux/mobile/careers.png b/test/fixtures/screenshots/linux/mobile/careers.png index c67e386b8..657c0f802 100644 Binary files a/test/fixtures/screenshots/linux/mobile/careers.png and b/test/fixtures/screenshots/linux/mobile/careers.png differ diff --git a/test/fixtures/screenshots/linux/mobile/homepage.png b/test/fixtures/screenshots/linux/mobile/homepage.png index 25c962bb1..11f229381 100644 Binary files a/test/fixtures/screenshots/linux/mobile/homepage.png and b/test/fixtures/screenshots/linux/mobile/homepage.png differ diff --git a/test/support/setup_snap_diff.rb b/test/support/setup_snap_diff.rb index 826811fc7..aca6d5ac8 100644 --- a/test/support/setup_snap_diff.rb +++ b/test/support/setup_snap_diff.rb @@ -63,7 +63,6 @@ def self.disable_animations_globally(page) } JS end - end # Environment variable controls for screenshot behavior diff --git a/test/system/pages/careers_page_test.rb b/test/system/pages/careers_page_test.rb index cbabad639..81ecc3178 100644 --- a/test/system/pages/careers_page_test.rb +++ b/test/system/pages/careers_page_test.rb @@ -90,7 +90,7 @@ def test_feature_cards_render_correctly "Tight-Knit Team", "Flexible Environment", "Growth Beyond JetThoughts", - "World-Class Training", + "World-Class Training" ] feature_cards.each do |card_title| diff --git a/test/unit/404_template_test.rb b/test/unit/404_template_test.rb index 80508875f..01ec0ce98 100644 --- a/test/unit/404_template_test.rb +++ b/test/unit/404_template_test.rb @@ -190,7 +190,7 @@ def test_404_page_prevents_indexing # Should prevent indexing - however, some SEO strategies allow indexing for link discovery indexing_prevented = robots_content.include?("noindex") || - robots_content.include?("none") + robots_content.include?("none") # This is informational - some sites allow 404 indexing for SEO discovery unless indexing_prevented @@ -261,7 +261,7 @@ def test_404_page_accessibility_features assert h1_tags.length == 1, "404 page should have exactly one h1" # Skip to content link - skip_links = doc.css("a[href*='#main'], a[href*='#content'], .skip-link") + doc.css("a[href*='#main'], a[href*='#content'], .skip-link") # Skip links are good practice but not required # Main landmark @@ -306,7 +306,7 @@ def test_404_page_user_experience_elements # Should avoid technical jargon technical_terms = ["server error", "http", "500", "internal"] - has_technical_terms = technical_terms.any? { |term| + technical_terms.any? { |term| page_text.downcase.include?(term) } @@ -326,7 +326,7 @@ def test_404_page_user_experience_elements "404 page should provide helpful suggestions to users" # Error message should be polite and professional - apologetic_indicators = [ + [ page_text.downcase.include?("sorry"), page_text.downcase.include?("apologize"), page_text.downcase.include?("oops") @@ -373,7 +373,8 @@ def test_404_page_performance_considerations external_scripts = doc.css("script[src^='http']") external_stylesheets = doc.css("link[rel='stylesheet'][href^='http']") - total_external = external_scripts.length + external_stylesheets.length + external_scripts.length + external_stylesheets.length # 404 pages benefit from minimal external dependencies # This is informational for performance optimization @@ -382,7 +383,7 @@ def test_404_page_performance_considerations images = doc.css("img") images.each do |img| alt = img["alt"] - assert alt != nil, "404 page images should have alt attributes" + assert !alt.nil?, "404 page images should have alt attributes" src = img["src"] if src @@ -422,9 +423,9 @@ def test_404_page_security_considerations # Security attributes are good practice but not strictly required external_links.each do |link| - rel = link["rel"] + link["rel"] # External links benefit from rel="noopener noreferrer" # This is informational for security enhancement end end -end \ No newline at end of file +end diff --git a/test/unit/baseof_template_test.rb b/test/unit/baseof_template_test.rb index 14132c988..5df972544 100644 --- a/test/unit/baseof_template_test.rb +++ b/test/unit/baseof_template_test.rb @@ -43,10 +43,10 @@ def test_no_hardcoded_inline_css_styles content = style.text # Check for logo styles with main-logo-image or logo-image-main class content.match?(/\.(?:main-)?logo-image-main\s*\{[^}]*max-width:\s*100%/) || - # Check for skip-link with exact positioning pattern we removed - content.match?(/\.skip-link\s*\{[^}]*position:\s*absolute[^}]*top:\s*-40px/) || - # Check for our specific sr-only pattern (not the plugin versions) - content.match?(/^\.sr-only\s*\{[^}]*position:\s*absolute[^}]*clip:\s*rect\(1px,\s*1px,\s*1px,\s*1px\)/) + # Check for skip-link with exact positioning pattern we removed + content.match?(/\.skip-link\s*\{[^}]*position:\s*absolute[^}]*top:\s*-40px/) || + # Check for our specific sr-only pattern (not the plugin versions) + content.match?(/^\.sr-only\s*\{[^}]*position:\s*absolute[^}]*clip:\s*rect\(1px,\s*1px,\s*1px,\s*1px\)/) end assert problematic_styles.empty?, @@ -334,11 +334,11 @@ def test_template_block_structure # This tests the Hugo template structure indirectly through rendered output # Should have header content (from header block or partial) - header_element = doc.css("header").first || doc.css(".header").first + doc.css("header").first || doc.css(".header").first # Header is optional but if present, should have proper structure # Should have footer content (from footer block or partial) - footer_element = doc.css("footer").first || doc.css(".footer").first + doc.css("footer").first || doc.css(".footer").first # Footer is optional but if present, should have proper structure # Main content area should exist @@ -361,10 +361,10 @@ def test_security_headers_meta_tags referrer_policy = doc.css("head meta[name='referrer']").first if referrer_policy valid_policies = ["no-referrer", "no-referrer-when-downgrade", "origin", - "origin-when-cross-origin", "same-origin", "strict-origin", - "strict-origin-when-cross-origin", "unsafe-url"] + "origin-when-cross-origin", "same-origin", "strict-origin", + "strict-origin-when-cross-origin", "unsafe-url"] assert valid_policies.include?(referrer_policy["content"]), "Referrer policy should use valid value" end end -end \ No newline at end of file +end diff --git a/test/unit/home_template_test.rb b/test/unit/home_template_test.rb index 2fd7352f0..8dbad59fa 100644 --- a/test/unit/home_template_test.rb +++ b/test/unit/home_template_test.rb @@ -114,7 +114,7 @@ def test_homepage_social_media_integration "Social media links should use full URLs" # Should open in new tab/window for external links - target = link["target"] + link["target"] if href.start_with?("http") && !href.include?("jetthoughts.com") # External social links should ideally open in new tab # This is a recommendation, not a strict requirement @@ -132,7 +132,7 @@ def test_homepage_performance_critical_elements images = doc.css("img") images.each do |img| alt = img["alt"] - assert alt != nil, "Images should have alt attributes" + assert !alt.nil?, "Images should have alt attributes" end # Check for lazy loading on images @@ -143,7 +143,7 @@ def test_homepage_performance_critical_elements # Large images benefit from lazy loading (optional optimization) if large_images.any? - lazy_loading_present = large_images.any? { |img| + large_images.any? { |img| img["loading"] == "lazy" || img["data-src"] } # Note: Lazy loading is an optimization, not a requirement @@ -157,12 +157,10 @@ def test_homepage_structured_data_organization json_scripts = extract_json_ld_schemas(doc) organization_schemas = json_scripts.select do |script| - begin - data = JSON.parse(script.text) - data.is_a?(Hash) && data["@type"] == "Organization" - rescue JSON::ParserError - false - end + data = JSON.parse(script.text) + data.is_a?(Hash) && data["@type"] == "Organization" + rescue JSON::ParserError + false end if organization_schemas.any? @@ -230,7 +228,7 @@ def test_homepage_mobile_responsiveness_indicators "Viewport should include device-width for mobile responsiveness" # Check for responsive CSS classes (optional but common) - responsive_classes = doc.css(".container, .row, .col, .mobile, .tablet, .desktop") + doc.css(".container, .row, .col, .mobile, .tablet, .desktop") # Note: Responsive classes are optional as CSS frameworks vary end @@ -287,9 +285,9 @@ def test_homepage_analytics_integration content = script.text src = script["src"] content.include?("google-analytics") || - content.include?("gtag") || - content.include?("analytics") || - (src && (src.include?("google-analytics") || src.include?("gtag"))) + content.include?("gtag") || + content.include?("analytics") || + (src && (src.include?("google-analytics") || src.include?("gtag"))) end # Analytics is optional but if present should be properly configured @@ -328,7 +326,7 @@ def test_homepage_accessibility_landmarks end # Skip to content link - skip_links = doc.css("a[href*='#main'], a[href*='#content'], .skip-link") + doc.css("a[href*='#main'], a[href*='#content'], .skip-link") # Skip links are good practice but not required for testing end -end \ No newline at end of file +end diff --git a/test/unit/hugo_asset_validation_test.rb b/test/unit/hugo_asset_validation_test.rb index 9513d946f..38e1d6094 100644 --- a/test/unit/hugo_asset_validation_test.rb +++ b/test/unit/hugo_asset_validation_test.rb @@ -8,7 +8,6 @@ class HugoAssetValidationTest < BasePageTestCase CSS_ASSET_PATTERN = /href="([^"]*\.css[^"]*)"/ JS_ASSET_PATTERN = /src="([^"]*\.js[^"]*)"/ - def test_svg_assets_use_relative_urls doc = parse_html_file("index.html") diff --git a/test/unit/list_template_test.rb b/test/unit/list_template_test.rb index d2ef80618..978b99d69 100644 --- a/test/unit/list_template_test.rb +++ b/test/unit/list_template_test.rb @@ -28,7 +28,7 @@ def test_list_page_has_descriptive_title # Title should indicate it's a list/archive page list_indicators = ["blog", "posts", "articles", "archive", "category", "tag"] - has_list_indicator = list_indicators.any? { |indicator| + list_indicators.any? { |indicator| title_text.downcase.include?(indicator) } @@ -120,7 +120,7 @@ def test_list_page_meta_description # Should describe the list content list_keywords = ["blog", "posts", "articles", "archive", "latest", "recent"] - has_list_keyword = list_keywords.any? { |keyword| + list_keywords.any? { |keyword| description_content.downcase.include?(keyword) } @@ -216,12 +216,10 @@ def test_list_page_structured_data_blog json_scripts = extract_json_ld_schemas(doc) blog_schemas = json_scripts.select do |script| - begin - data = JSON.parse(script.text) - data.is_a?(Hash) && (data["@type"] == "Blog" || data["@type"] == "CollectionPage") - rescue JSON::ParserError - false - end + data = JSON.parse(script.text) + data.is_a?(Hash) && (data["@type"] == "Blog" || data["@type"] == "CollectionPage") + rescue JSON::ParserError + false end # Blog schema is optional but if present should be valid @@ -272,7 +270,7 @@ def test_list_page_breadcrumb_navigation # Should show hierarchy (Home > Blog, etc.) breadcrumb_text = breadcrumbs.text hierarchy_indicators = [">", "/", "»", "→"] - has_hierarchy = hierarchy_indicators.any? { |indicator| + hierarchy_indicators.any? { |indicator| breadcrumb_text.include?(indicator) } @@ -310,7 +308,7 @@ def test_list_page_search_functionality_if_present name = search_input["name"] assert name, "Search input should have name attribute" - placeholder = search_input["placeholder"] + search_input["placeholder"] # Placeholder is helpful for UX but not required end end @@ -445,7 +443,7 @@ def test_list_page_accessibility_features else # Avoid generic link text generic_text = ["click here", "read more", "more", "link"] - is_generic = generic_text.any? { |generic| text.downcase == generic } + generic_text.any? { |generic| text.downcase == generic } # Generic text is not ideal but not a hard requirement end end @@ -460,10 +458,10 @@ def test_list_page_loading_performance images = doc.css("img") images.each do |img| alt = img["alt"] - assert alt != nil, "Images should have alt attributes" + assert !alt.nil?, "Images should have alt attributes" # Check for lazy loading on non-critical images - loading = img["loading"] + img["loading"] # Lazy loading is beneficial but not required end @@ -472,7 +470,7 @@ def test_list_page_loading_performance external_stylesheets = doc.css("link[rel='stylesheet'][href^='http']") # Too many external resources can impact performance - total_external = external_scripts.length + external_stylesheets.length + external_scripts.length + external_stylesheets.length # This is informational - some external resources may be necessary end -end \ No newline at end of file +end diff --git a/test/unit/meta_tags/seo_schema_test.rb b/test/unit/meta_tags/seo_schema_test.rb index a2eb4c772..df6c46b10 100644 --- a/test/unit/meta_tags/seo_schema_test.rb +++ b/test/unit/meta_tags/seo_schema_test.rb @@ -62,7 +62,7 @@ def test_blog_article_schema_structure doc = parse_html_file(blog_file) - article_schemas = find_schema_elements_by_type(doc, 'Article') + article_schemas = find_schema_elements_by_type(doc, "Article") assert article_schemas.count > 0, "Article schema should be present on blog posts" @@ -83,7 +83,7 @@ def test_blog_article_schema_structure def test_organization_schema_structure doc = parse_html_file("about-us/index.html") - org_schemas = find_schema_elements_by_type(doc, 'Organization', 'LocalBusiness') + org_schemas = find_schema_elements_by_type(doc, "Organization", "LocalBusiness") assert org_schemas.count > 0, "Organization schema should be present" diff --git a/test/unit/single_template_test.rb b/test/unit/single_template_test.rb index 3d91e89e6..099d7f271 100644 --- a/test/unit/single_template_test.rb +++ b/test/unit/single_template_test.rb @@ -154,12 +154,10 @@ def test_single_page_structured_data_article json_scripts = extract_json_ld_schemas(doc) article_schemas = json_scripts.select do |script| - begin - data = JSON.parse(script.text) - data.is_a?(Hash) && data["@type"] == "Article" - rescue JSON::ParserError - false - end + data = JSON.parse(script.text) + data.is_a?(Hash) && data["@type"] == "Article" + rescue JSON::ParserError + false end # Article schema is optional but if present should be valid @@ -230,7 +228,7 @@ def test_single_page_reading_experience images = content_area.css("img") images.each do |img| alt = img["alt"] - assert alt != nil, "Content images should have alt attributes" + assert !alt.nil?, "Content images should have alt attributes" end end end @@ -239,7 +237,7 @@ def test_single_page_related_content_navigation doc = parse_html_file(@test_page) # Check for related content or navigation aids - related_indicators = [ + [ doc.css(".related, .related-posts, .related-content").any?, doc.css(".next-post, .prev-post, .post-navigation").any?, doc.css(".tags, .categories").any?, @@ -275,7 +273,7 @@ def test_single_page_accessibility_features doc = parse_html_file(@test_page) # Skip to content link - skip_links = doc.css("a[href*='#main'], a[href*='#content'], .skip-link") + doc.css("a[href*='#main'], a[href*='#content'], .skip-link") # Proper heading hierarchy headings = doc.css("h1, h2, h3, h4, h5, h6") @@ -322,15 +320,15 @@ def test_single_page_performance_considerations src = img["src"] if src # Check for responsive images - srcset = img["srcset"] - sizes = img["sizes"] + img["srcset"] + img["sizes"] # Modern images benefit from responsive attributes # This is a recommendation, not a strict requirement end # Lazy loading for below-the-fold images - loading = img["loading"] + img["loading"] # Lazy loading is an optimization, not a requirement end @@ -339,7 +337,7 @@ def test_single_page_performance_considerations external_stylesheets = doc.css("link[rel='stylesheet'][href^='http']") # Count is informational - some external resources may be necessary - total_external = external_scripts.length + external_stylesheets.length + external_scripts.length + external_stylesheets.length # This is informational rather than a hard requirement # Too many external resources can impact performance @@ -361,7 +359,7 @@ def test_single_page_security_considerations # This is a recommendation for security best practices if rel security_keywords = ["noopener", "noreferrer", "nofollow"] - has_security_attr = security_keywords.any? { |keyword| rel.include?(keyword) } + security_keywords.any? { |keyword| rel.include?(keyword) } # Security attributes are recommended but not strictly required end diff --git a/test/unit/template_cleanup_validation_test.rb b/test/unit/template_cleanup_validation_test.rb index a28dac05c..a4cdd5b32 100644 --- a/test/unit/template_cleanup_validation_test.rb +++ b/test/unit/template_cleanup_validation_test.rb @@ -166,18 +166,17 @@ def test_structured_data_schemas if json_ld_scripts.any? json_ld_scripts.each do |script| # Validate JSON is properly formatted - begin - parsed = JSON.parse(script.text) - assert parsed.is_a?(Hash) || parsed.is_a?(Array), "JSON-LD should be valid JSON on #{page_name}" - - # If it's schema.org data, check basic structure - if parsed.is_a?(Hash) && parsed["@context"] - assert parsed["@context"].include?("schema.org"), "Schema should use schema.org context on #{page_name}" - assert parsed["@type"], "Schema should have @type on #{page_name}" - end - rescue JSON::ParserError => e - flunk "Invalid JSON-LD on #{page_name}: #{e.message}" + + parsed = JSON.parse(script.text) + assert parsed.is_a?(Hash) || parsed.is_a?(Array), "JSON-LD should be valid JSON on #{page_name}" + + # If it's schema.org data, check basic structure + if parsed.is_a?(Hash) && parsed["@context"] + assert parsed["@context"].include?("schema.org"), "Schema should use schema.org context on #{page_name}" + assert parsed["@type"], "Schema should have @type on #{page_name}" end + rescue JSON::ParserError => e + flunk "Invalid JSON-LD on #{page_name}: #{e.message}" end end end @@ -295,7 +294,6 @@ def test_fl_node_removal_layout_preservation doc = parse_html_file(file_path) # Essential FL structural classes should remain (layout-critical) - structural_classes = %w[fl-row-content fl-col-content fl-module-content fl-row fl-col fl-module] # Verify structural FL classes are preserved for layout structural_elements = doc.css(".fl-row-content, .fl-col-content, .fl-module-content") @@ -337,7 +335,7 @@ def test_accessibility_basics images = doc.css("img") images.each do |img| alt = img["alt"] - assert alt != nil, "Images should have alt attribute on #{page_name}" + assert !alt.nil?, "Images should have alt attribute on #{page_name}" # Alt can be empty string for decorative images, so we just check it exists end diff --git a/themes/beaver/assets/css/homepage-sections.css b/themes/beaver/assets/css/homepage-sections.css new file mode 100644 index 000000000..2b2311f80 --- /dev/null +++ b/themes/beaver/assets/css/homepage-sections.css @@ -0,0 +1,4 @@ +/* Homepage semantic hooks migration layer. + * Intentionally starts empty to keep behavior identical. + * Future section-level styles should target .jt-home-* classes here. + */ diff --git a/themes/beaver/layouts/baseof.html b/themes/beaver/layouts/baseof.html index 2d21db66d..2945ce7a0 100644 --- a/themes/beaver/layouts/baseof.html +++ b/themes/beaver/layouts/baseof.html @@ -37,9 +37,45 @@ {{ partial "seo/faq-schema.html" . }} - + {{- $bodyClasses := slice -}} + {{- if .IsHome -}} + {{- $bodyClasses = $bodyClasses | append "home" -}} + {{- end -}} + {{- if .IsPage -}} + {{- $bodyClasses = $bodyClasses | append "page" "page-template-default" -}} + {{- end -}} + {{- $bodyClasses = $bodyClasses | append (printf "kind-%s" .Kind) -}} + {{- with .Type -}} + {{- $bodyClasses = $bodyClasses | append (printf "type-%s" (urlize .)) -}} + {{- end -}} + {{- with .Section -}} + {{- $bodyClasses = $bodyClasses | append (printf "section-%s" (urlize .)) -}} + {{- end -}} + {{- with .Layout -}} + {{- $bodyClasses = $bodyClasses | append (printf "layout-%s" (urlize .)) -}} + {{- end -}} + {{- with .File -}} + {{- with .ContentBaseName -}} + {{- $bodyClasses = $bodyClasses | append (printf "slug-%s" (urlize .)) -}} + {{- end -}} + {{- end -}} + {{- $frameworkClasses := slice + "fl-builder" + "fl-theme-builder-header" + "fl-theme-builder-header-header" + "fl-theme-builder-footer" + "fl-theme-builder-footer-footer" + "fl-framework-base-4" + "fl-preset-default" + "fl-full-width" + "fl-search-active" + -}} + {{- range $frameworkClasses -}} + {{- $bodyClasses = $bodyClasses | append . -}} + {{- end -}} + {{- $bodyClassAttr := delimit (uniq $bodyClasses) " " -}} + +
{{ partialCached "page/header.html" . "header" }} diff --git a/themes/beaver/layouts/careers/single.html b/themes/beaver/layouts/careers/single.html index 9c6442ee1..4f8a86bc0 100644 --- a/themes/beaver/layouts/careers/single.html +++ b/themes/beaver/layouts/careers/single.html @@ -1,16 +1,5 @@ {{ define "header" }} - {{- $CSS := slice - (resources.Get "css/critical/single-careers.css") - (resources.Get "css/3114-layout.css") - (resources.Get "css/e966db44b09892b8d7d492247c67e86c-layout-bundle.css") - (resources.Get "css/dynamic-icons.css" | resources.ExecuteAsTemplate "css/dynamic586.css" .) - (resources.Get "css/586.css") - (resources.Get "css/homepage.css") - (resources.Get "css/vendors/base-4.min.css") - (resources.Get "css/style.css") - (resources.Get "css/skin-65eda28877e04.css") - (resources.Get "css/footer.css") - -}} + {{- $CSS := partialCached "assets/single-career-css-resources.html" . "single-career-css-resources-v1" -}} {{ partialCached "assets/css-processor.html" (dict "resources" $CSS "bundleName" "single-career") "single-career" }} {{ end }} diff --git a/themes/beaver/layouts/home.html b/themes/beaver/layouts/home.html index 6b8dfda16..51f34b4d1 100644 --- a/themes/beaver/layouts/home.html +++ b/themes/beaver/layouts/home.html @@ -1,19 +1,5 @@ {{ define "header" }} - {{- $nonCriticalResources := slice - (resources.Get "css/critical/base.css") - (resources.Get "css/critical/homepage-critical.css") - (resources.Get "css/companies.css") - (resources.Get "css/footer.css") - (resources.Get "css/homepage.css") - (resources.Get "css/dynamic-404-590.css" | resources.ExecuteAsTemplate "css/dynamic.css" .) - (resources.Get "css/590-layout.css") - (resources.Get "css/skin-65eda28877e04.css") - (resources.Get "css/style.css") - (resources.Get "css/dynamic-icons.css" | resources.ExecuteAsTemplate "css/dynamic586.css" .) - (resources.Get "css/586.css") - (resources.Get "css/technologies.css") - (resources.Get "css/use-cases-dynamic.css" | resources.ExecuteAsTemplate "css/use-cases-dynamic.css" .) - }} + {{- $nonCriticalResources := partialCached "assets/homepage-css-resources.html" . "homepage-css-resources-v1" -}} {{ partialCached "assets/css-processor.html" (dict "resources" $nonCriticalResources "bundleName" "homepage") "homepage" }} {{ end }} @@ -44,7 +30,7 @@ class="fl-builder-content fl-builder-content-590 fl-builder-content-primary fl-builder-global-templates-locked" data-post-id="590">
- {{ partial "technologies.html" (dict "colorVariant" "white" "site" .Site) }} + {{ partial "technologies.html" (dict "colorVariant" "white") }}
{{ partial "page/testimonials.html" . }}
- {{ partial "technologies.html" (dict "colorVariant" "dark" "site" .Site) }} + {{ partial "technologies.html" (dict "colorVariant" "dark") }}
diff --git a/themes/beaver/layouts/page/use-cases.html b/themes/beaver/layouts/page/use-cases.html index 16022f16a..769dcfbcb 100644 --- a/themes/beaver/layouts/page/use-cases.html +++ b/themes/beaver/layouts/page/use-cases.html @@ -59,7 +59,7 @@
- {{ partial "technologies.html" (dict "colorVariant" "dark" "site" .Site) }} + {{ partial "technologies.html" (dict "colorVariant" "dark") }}
diff --git a/themes/beaver/layouts/partials/assets/careers-css-resources.html b/themes/beaver/layouts/partials/assets/careers-css-resources.html new file mode 100644 index 000000000..ce883ce6a --- /dev/null +++ b/themes/beaver/layouts/partials/assets/careers-css-resources.html @@ -0,0 +1,14 @@ +{{/* Careers page CSS bundle composition. + Order is intentional and must stay stable for legacy cascade behavior. */}} +{{- return (slice + (resources.Get "css/critical/careers-critical.css") + (resources.Get "css/careers.css") + (resources.Get "css/3086-layout2.css") + (resources.Get "css/dynamic-icons.css" | resources.ExecuteAsTemplate "css/dynamic586.css" .) + (resources.Get "css/586.css") + (resources.Get "css/homepage.css") + (resources.Get "css/vendors/base-4.min.css") + (resources.Get "css/style.css") + (resources.Get "css/skin-65eda28877e04.css") + (resources.Get "css/footer.css") +) -}} diff --git a/themes/beaver/layouts/partials/assets/contact-us-css-resources.html b/themes/beaver/layouts/partials/assets/contact-us-css-resources.html new file mode 100644 index 000000000..729bc91c3 --- /dev/null +++ b/themes/beaver/layouts/partials/assets/contact-us-css-resources.html @@ -0,0 +1,13 @@ +{{/* Contact Us page CSS bundle composition. + Keep ordering unchanged to avoid visual regressions. */}} +{{- return (slice + (resources.Get "css/critical/base.css") + (resources.Get "css/706-layout.css") + (resources.Get "css/dynamic-icons.css" | resources.ExecuteAsTemplate "css/dynamic586.css" .) + (resources.Get "css/586.css") + (resources.Get "css/homepage.css") + (resources.Get "css/vendors/base-4.min.css") + (resources.Get "css/style.css") + (resources.Get "css/skin-65eda28877e04.css") + (resources.Get "css/footer.css") +) -}} diff --git a/themes/beaver/layouts/partials/assets/free-consultation-css-resources.html b/themes/beaver/layouts/partials/assets/free-consultation-css-resources.html new file mode 100644 index 000000000..c3cf0c3fb --- /dev/null +++ b/themes/beaver/layouts/partials/assets/free-consultation-css-resources.html @@ -0,0 +1,14 @@ +{{/* Free consultation page CSS bundle composition. + Keep ordering unchanged to avoid layout shifts. */}} +{{- return (slice + (resources.Get "css/critical/base.css") + (resources.Get "css/critical/free-consultation-critical.css") + (resources.Get "css/homepage-layout.css") + (resources.Get "css/component-bundle.css") + (resources.Get "css/dynamic-icons.css" | resources.ExecuteAsTemplate "css/dynamic586.css" .) + (resources.Get "css/services-layout.css") + (resources.Get "css/vendors/base-4.min.css") + (resources.Get "css/style.css") + (resources.Get "css/theme-main.css") + (resources.Get "css/footer.css") +) -}} diff --git a/themes/beaver/layouts/partials/assets/homepage-css-resources.html b/themes/beaver/layouts/partials/assets/homepage-css-resources.html new file mode 100644 index 000000000..1102dc004 --- /dev/null +++ b/themes/beaver/layouts/partials/assets/homepage-css-resources.html @@ -0,0 +1,18 @@ +{{/* Homepage CSS bundle composition. + Keep order stable because legacy styles depend on cascade sequence. */}} +{{- return (slice + (resources.Get "css/critical/base.css") + (resources.Get "css/critical/homepage-critical.css") + (resources.Get "css/companies.css") + (resources.Get "css/footer.css") + (resources.Get "css/homepage.css") + (resources.Get "css/dynamic-404-590.css" | resources.ExecuteAsTemplate "css/dynamic.css" .) + (resources.Get "css/590-layout.css") + (resources.Get "css/skin-65eda28877e04.css") + (resources.Get "css/style.css") + (resources.Get "css/dynamic-icons.css" | resources.ExecuteAsTemplate "css/dynamic586.css" .) + (resources.Get "css/586.css") + (resources.Get "css/technologies.css") + (resources.Get "css/use-cases-dynamic.css" | resources.ExecuteAsTemplate "css/use-cases-dynamic.css" .) + (resources.Get "css/homepage-sections.css") +) -}} diff --git a/themes/beaver/layouts/partials/assets/single-career-css-resources.html b/themes/beaver/layouts/partials/assets/single-career-css-resources.html new file mode 100644 index 000000000..b3e54d7d2 --- /dev/null +++ b/themes/beaver/layouts/partials/assets/single-career-css-resources.html @@ -0,0 +1,14 @@ +{{/* Single career page CSS bundle composition. + Keep ordering unchanged to preserve rendering. */}} +{{- return (slice + (resources.Get "css/critical/single-careers.css") + (resources.Get "css/3114-layout.css") + (resources.Get "css/e966db44b09892b8d7d492247c67e86c-layout-bundle.css") + (resources.Get "css/dynamic-icons.css" | resources.ExecuteAsTemplate "css/dynamic586.css" .) + (resources.Get "css/586.css") + (resources.Get "css/homepage.css") + (resources.Get "css/vendors/base-4.min.css") + (resources.Get "css/style.css") + (resources.Get "css/skin-65eda28877e04.css") + (resources.Get "css/footer.css") +) -}} diff --git a/themes/beaver/layouts/partials/blog/json-ld.html b/themes/beaver/layouts/partials/blog/json-ld.html index 61c308400..0bdb911d1 100644 --- a/themes/beaver/layouts/partials/blog/json-ld.html +++ b/themes/beaver/layouts/partials/blog/json-ld.html @@ -14,7 +14,7 @@ "datePublished": "{{ with .Params.created_at }}{{ . }}{{ else }}{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}{{ end }}", "dateModified": "{{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" }}", "mainEntityOfPage": "{{ .Permalink }}", - "publisher": {{ .Site.Data.company | jsonify }}, + "publisher": {{ hugo.Data.company | jsonify }}, {{ with (.Resources.Get .Params.metatags.image) }} "image": { "@type": "ImageObject", diff --git a/themes/beaver/layouts/partials/components/testimonial.html b/themes/beaver/layouts/partials/components/testimonial.html index 8a9aa57ff..dc9b89752 100644 --- a/themes/beaver/layouts/partials/components/testimonial.html +++ b/themes/beaver/layouts/partials/components/testimonial.html @@ -32,6 +32,7 @@ {{- $rating := .rating | default "4.8" -}} {{- $rating_text := .rating_text | default "Based on client reviews" -}} {{- $node_id := .node_id | default "testimonials" -}} +{{- $testimonialsData := hugo.Data.testimonials -}} {{- $nonCriticalCSS := resources.Get "css/vendors/swiper.min.css" | fingerprint "md5" -}} @@ -149,8 +150,8 @@

- {{- if .Site.Data.testimonials.testimonials -}} - {{- range .Site.Data.testimonials.testimonials -}} + {{- if $testimonialsData.testimonials -}} + {{- range $testimonialsData.testimonials -}}
diff --git a/themes/beaver/layouts/partials/data/authors-cached.html b/themes/beaver/layouts/partials/data/authors-cached.html index a04fa4183..3c49cf0c2 100644 --- a/themes/beaver/layouts/partials/data/authors-cached.html +++ b/themes/beaver/layouts/partials/data/authors-cached.html @@ -1,3 +1,3 @@ {{/* Cached authors data access - invalidates when authors.yaml changes */}} -{{ $authorsHash := .Site.Data.authors | jsonify | md5 }} -{{ return partialCached "data/authors-content.html" . $authorsHash }} \ No newline at end of file +{{ $authorsHash := hugo.Data.authors | jsonify | md5 }} +{{ return partialCached "data/authors-content.html" . $authorsHash }} diff --git a/themes/beaver/layouts/partials/data/company-cached.html b/themes/beaver/layouts/partials/data/company-cached.html index 955d63fd4..de23944f4 100644 --- a/themes/beaver/layouts/partials/data/company-cached.html +++ b/themes/beaver/layouts/partials/data/company-cached.html @@ -1,3 +1,3 @@ {{/* Cached company data access - invalidates when company.yaml changes */}} -{{ $companyHash := .Site.Data.company | jsonify | md5 }} -{{ return partialCached "data/company-content.html" . $companyHash }} \ No newline at end of file +{{ $companyHash := hugo.Data.company | jsonify | md5 }} +{{ return partialCached "data/company-content.html" . $companyHash }} diff --git a/themes/beaver/layouts/partials/data/testimonials-cached.html b/themes/beaver/layouts/partials/data/testimonials-cached.html index f3dce5521..1c39d33f2 100644 --- a/themes/beaver/layouts/partials/data/testimonials-cached.html +++ b/themes/beaver/layouts/partials/data/testimonials-cached.html @@ -1,3 +1,3 @@ {{/* Cached testimonials data access - invalidates when testimonials.yaml changes */}} -{{ $testimonialsHash := .Site.Data.testimonials | jsonify | md5 }} -{{ return partialCached "data/testimonials-content.html" . $testimonialsHash }} \ No newline at end of file +{{ $testimonialsHash := hugo.Data.testimonials | jsonify | md5 }} +{{ return partialCached "data/testimonials-content.html" . $testimonialsHash }} diff --git a/themes/beaver/layouts/partials/homepage/companies.html b/themes/beaver/layouts/partials/homepage/companies.html index abcac5a82..56c959bb8 100644 --- a/themes/beaver/layouts/partials/homepage/companies.html +++ b/themes/beaver/layouts/partials/homepage/companies.html @@ -1,5 +1,5 @@