content.tinajs.io and *.tinajs.io were blocked by CSP, preventing
the visual editor from connecting to TinaCloud.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When \$uri ends with / and the directory exists (dist/blog/), nginx
considers it "found" and returns 403 (no autoindex, no index.html).
Removing \$uri/ ensures SPA fallback always works for route paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add puppeteer + mime-types to devDependencies
- Integrate prerender.mjs into build script (runs after vite build)
- Add Linux/Docker-safe Chrome flags (--disable-setuid-sandbox, --disable-dev-shm-usage, --disable-gpu)
- Fix static server to strip query strings from URLs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Manual script (node scripts/prerender.mjs) that starts a local static
server, visits each route with Puppeteer, waits 3s for React to render,
and saves full HTML to dist/. Covers all static routes + blog posts.
Run after vite build when prerendering is needed. Requires puppeteer
installed (npm i -D puppeteer mime-types).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New script reads public/blog/posts.json at build time and outputs
public/sitemap.xml with all static routes + /blog/:slug entries,
each with lastmod from post dates or current date.
Build pipeline: sync-blog → generate-sitemap → tsc → vite build
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Single URL for both languages is correct for client-side i18n — signals
to search engines that both language versions exist at the same URL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Nginx was intercepting /blog/ before the SPA fallback, causing 403 when
it found the blog/ directory without autoindex. Adding try_files ensures
SPA routing handles all /blog/* paths correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Refactored LanguageProvider to avoid remounting children when switching
from static to live translations. Previously, the switch from
LanguageContext.Provider to TinaConnectedProvider caused all children
to unmount/remount, resetting CookieConsent state and hiding the banner
if cookie_consent was already set in localStorage.
New approach: TinaLiveSync is a null-rendering component that calls useTina
and syncs live data back to LanguageProvider via a stable callback, while
LanguageContext.Provider remains the stable root wrapper — children never remount.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add router: () => '/' back to translationsEn and translationsUk collections
so TinaCloud shows the homepage preview alongside the editing form.
The EOF error that previously blocked this is fixed (TinaConnectedProvider
defers useTina calls until both queries are loaded).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Converts existing server-side JSON blog posts to Markdown format with
YAML frontmatter so they appear in TinaCloud admin and are managed via git.
Also fixes sync-blog.mjs parseFrontmatter to support multi-line YAML lists
(TinaCMS writes hashtags as multi-line list items).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Configured TinaCloud for visual editing of translation files with useTina hook integration. Updated .gitignore to exclude generated runtime files while preserving schema files needed by TinaCloud for branch indexing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Integrate useTina hook for live translation editing in LanguageContext
- Configure translationsEn/translationsUk with global=true and router
- Add blog post router and auto-slugify filename generator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TinaCloud requires _schema.json and _graphql.json to index branches.
Generated by running tinacms dev locally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add tina/config.ts with full schema for all site sections
- Convert i18n from TypeScript to nested JSON (content/translations/)
- Update LanguageContext to import JSON with flattenObject utility
- Update dev/build scripts to run tinacms build
- Add sync-blog.mjs support for content/blog/*.md (TinaCMS posts)
- Update CI/CD with Tina env vars, remove blog rsync exclusion
- Add tina/__generated__/ to .gitignore
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reduced .banner2-text-overlay width at all breakpoints to keep the
circular "Get Your Free Consultation" text within the banner bounds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Custom i18n system with typed translation dictionaries (~570 keys),
LanguageProvider context, and useTranslation hook. All 31 components
and pages wired with t() calls. Chatbot backend passes language hint
to Claude for Ukrainian responses. Language preference persists via
localStorage. SEO meta tags and html lang attribute update dynamically.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix Twenty CRM filter syntax (dot notation for composite fields)
- Fix noteTargets/taskTargets to use targetPerson instead of personId
- Handle duplicate person creation gracefully (find existing on 400)
- Add task creation for new leads (follow-up TODO)
- Save conversation transcript to CRM on escalation and rate limit
- Strengthen system prompt to make Claude call update_lead proactively
- Tell Claude that form-submitted leads are already in CRM
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Chat messages now render bold, italic, links, and line breaks
- Bare URLs auto-linked, XSS-safe via HTML escaping before markdown
- New leads create Opportunity with stage NEW in Twenty CRM
- Applied to both chatbot-api and email-api (contact + quote forms)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Bot now acts as sales consultant: identifies needs, proposes services, pushes for booking
- escalate_to_human tool: triggers on user request, bot stuck, or hot lead
- Escalation notifies RC with reason + conversation summary
- Contact form and quote form now create leads in Twenty CRM
- Fix RC webhook to use correct payload format (visitor.token as session_id)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Store lead info in Redis session meta so bot remembers name across messages
- Create Twenty CRM lead immediately from form data (bypass tool-call flow)
- Store room→session reverse mapping in Redis for RC webhook delivery
- Update system prompt: don't re-ask for form-provided info
- Fix "Most Popular" badge clipped on mobile (overflow: visible)
- Fix contact form inputs overflowing on small screens (box-sizing)
- Reduce chat tooltip size on mobile to avoid overlapping content
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New twenty_crm.py: full CRUD for people, companies, notes via Twenty REST API
- Lead capture now creates person + company in Twenty CRM automatically
- New update_lead tool: enriches CRM profile as conversation progresses
(job title, phone, city, budget, requirements)
- Session meta stored in Redis to track Twenty person ID across messages
- Docker-compose updated with TWENTY_CRM env vars
- Chat bubble: pulsating ring animation with gradient background
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New ChatLeadForm component: collects name, email, company before chat starts
- GDPR consent checkbox with Privacy Policy link
- Lead info passed to backend and injected as LLM context
- Visitor name from form used in Rocket.Chat room
- RC bot messages: added logging + fallback to livechat/message endpoint
- RC room caching to avoid repeated API calls
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When Claude returns text + tool_use together, the bot was not sending the tool_result
back, so no follow-up message reached the user. Now always sends tool_result to get
a proper response. Also added Cal.com booking link to system prompt so bot offers
consultation scheduling after capturing lead data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add margin-bottom: 0 on circle-3 at 768px and 480px breakpoints
- Reduce mobile nav font from 1.4rem to 1.1rem
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move Popular badges inside cards (top-right) instead of overlapping top edge
- Remove align-items: start from bundles grid so all cards stretch equally
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Metrics section: 4 gradient stat cards with spring entrance animation
- Interactive selector: 3-step wizard (goal → budget → recommendations)
- Popular Bundles: 3 package tiers (Starter, Growth, Full Stack) with CTA
- Full responsive support for all new sections
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Apply glassmorphism, radial glows, gradient banners to About/Services/Pricing
- Remove How We Work and Technology Stack sections from Services
- Add gradient banner styling to discount section on Pricing
- Update About values section with full-width gradient banner
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Burger button z-index 1100 (above overlay 1002)
- Hamburger lines animate to X when menu is open
- Menu auto-closes on route change
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New services: AI Chatbots & Virtual Assistants (£3K-£10K), Custom Website Development (£2.5K-£15K)
- Both marked as Popular with orange badge on Services and Pricing pages
- Widen all content blocks to max-width 1200px to align with header container
- Widen CTA blocks to 900px
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>