Repository navigation
Expand file tree
/
Copy pathprogress.txt
More file actions
1178 lines (917 loc) · 54.6 KB
/
Copy pathprogress.txt
File metadata and controls
1178 lines (917 loc) · 54.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
## Codebase Patterns
<!--
Reusable patterns discovered during implementation.
Add new patterns here as you learn them.
Read this section FIRST on each iteration.
-->
- Use pnpm, not npm
- Run `pnpm check && pnpm typecheck` after every change
- Content source: CONTENT_SOURCE=locale for local dev (reads from ~/projects/ocobo-posts/)
- i18n: remix-i18next with useTranslation('common') + t('page.section.key')
- Routes follow ($lang) prefix pattern: _main.($lang).<page>.tsx
- Panda CSS recipes in preset/recipes/, tokens in preset/tokens/
- Legacy recipes live in preset/recipes/_legacy/ — delete dir when migration complete
- New/prototype recipes go in preset/recipes/ (standard path)
- When a legacy recipe reaches 0 usage, delete from _legacy/
- Named exports only, kebab-case filenames, no barrel files
- Navbar uses ocobo.* token namespace (ocobo.yellow, ocobo.dark, etc.) to match prototype
- Desktop vs mobile CTA: contact.cta (short) vs contact.cta.mobile (long)
- WebKit browsers need explicit hstack() pattern, not just display: flex
- Font token for headings is 'display' (Bermia), not 'heading'
---
# Progress Log
Project: Website Migration — Plan A (Prep + P1)
Source: specs/prd-migration-plan-a.md
Branch: migration-plan-a
Created: 2026-02-01T12:00:00Z
## Initial Setup
- Created stories.json with 16 features
- High priority: 11 features (deps, design system, flags, i18n, layout, P1 pages)
- Medium priority: 5 features (existing page adaptations, final doc sync)
- Low priority: 0 features
- Ready for implementation
---
## Feature 1: Upgrade Panda CSS v0.37 → v1.8 and Ark UI to v5.30
- Upgraded @pandacss/dev, @pandacss/preset-base, @pandacss/preset-panda, @pandacss/types from ^0.37.2 → ^1.8.1
- Ark UI already at ^5.30.0, no change needed
- Removed unused @shadow-panda/preset devDependency
- Fixed: definePreset now requires `name` property (added 'ocobo-preset')
- Fixed: carousel pattern `display: 'auto'` → `display: 'block'` (stricter types in v1)
- Kept @shadow-panda/style-context (still used by NavigationMenu.tsx)
- pnpm check ✅ pnpm typecheck ✅ codegen ✅ tests ✅ (2 pre-existing failures unrelated)
---
## Feature 2: Port design system tokens from prototype
- Colours: updated light variants to match prototype (mint.light #EBFDF5, sky.light #F0F9FB, gray.light #F5F5F5)
- Shadows: added soft, soft-md, soft-lg, dark, card, ring, offset from prototype
- Animations: added 17 animation tokens + keyframes (float, fade, bounce, spin, marquee, entry)
- Typography: added sans/display font aliases, custom lineHeights per size, fixed 2xl fontSize (1.375rem)
- Radii: added 2xl (1rem), 3xl (1.5rem) from prototype
- Fonts now reference names directly (Bornia/Bermia) instead of CSS vars
- Removed unused --font-header/--font-body CSS vars from index.css
- pnpm check ✅ pnpm typecheck ✅ codegen ✅ tests 133/133 ✅
---
## Feature 3: Port design system recipes from prototype
- Added text recipe (display-xl/lg/md, subtitle, body, label + colour variants)
- Added badge recipe (yellow/mint/sky/coral/dark/gray, sm/md sizes, sm/full rounded)
- Added iconBox recipe (sm/md/lg sizes, solid/outline/ghost variants, colour compoundVariants)
- Updated section recipe (added bg variants: white/gray/dark/yellow/sky/mint/coral + padding sm/md/lg)
- Existing section() calls unaffected — defaults to bg:'white', padding:'lg' (same py:24 as before)
- Adapted prototype ocobo.* colour refs → flat token names (dark, yellow, mint, etc.)
- pnpm check ✅ pnpm typecheck ✅ codegen ✅ tests 133/133 ✅
---
## Feature 4: Create feature flag system for unreleased pages
- Created app/modules/feature-flags.ts with DISABLED_PAGES env var
- Added shouldHide to useMenuItems for nav filtering
- Feature-flagged pages: technology, studio, jobs, podcasts
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 5: Set up localisation with structured keys in common.json
- Added nav.*, footer.* keys to locales/{fr,en}/common.json
- Added i18n namespace skeletons for offer, method, about pages
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 6: Migrate Navbar from prototype (Ark UI Menu, responsive drawer, scroll behaviour)
- Replaced Radix NavigationMenu with Ark UI Menu (desktop) + Dialog (mobile)
- Created app/components/navbar/: navbar.tsx, mobile-menu.tsx, nav-dropdown-item.tsx, nav-item-with-dropdown.tsx, types.ts, use-navbar-scroll.ts
- Ported morphing scroll behaviour (transparent → rounded pill with blur, logo scales)
- Rich dropdown items with icon + label + description + colour variants
- Full-screen mobile menu with accordion-style sections
- Added ocobo.* namespace tokens to match prototype (ocobo.yellow, ocobo.dark, etc.)
- Renamed types to match prototype: label/path/color/dropdown (not title/url/variant/subItems)
- Separate CTA labels: contact.cta (desktop), contact.cta.mobile (mobile)
- Fixed WebKit/Orion browser compat with explicit hstack() pattern
- Fixed font token: fontFamily: 'display' not 'heading'
- Updated useMenuItems with correct dropdown colours (yellow/coral/coral/sky for resources)
- Deleted old files: Header.tsx, MainMenu.tsx, MainMobileMenu.tsx, MobileMenu.tsx, SubMenu.tsx, NavigationMenu.tsx, navigation-menu.ts slot recipe
- Learnings documented in docs/migration-notes.md
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 7: Migrate Footer from prototype
- Replaced old Footer with prototype design (4-column grid, darker bg, border separator)
- Uses Lucide icons (Linkedin, Youtube) in circular buttons with hover effects
- Outline CTA button matching prototype style (border, uppercase, arrow icon)
- Yellow link hover colour matching prototype (_hover: { color: 'ocobo.yellow' })
- Reuses Logocobo SVG component with white colour
- Keeps production components: NewsletterForm (HubSpot), LanguageSwitcher
- Dynamic menu items from useMenuItems (company, method dropdowns)
- All strings localised via t() using footer.* keys
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 8: Migrate Home page (redesign)
- Rebuilt Home route to match prototype section order and layout
- Added new home section components + shared layout pieces in app/components/home/
- Added Home i18n keys in locales/{fr,en}/home.json based on prototype content
- Synced dot-pattern backgrounds and other minor visual parity adjustments
- Aligned container sizing with prototype and set global font base styles
- Ensured CTA visibility respects feature flags (studio/technology)
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 9: Migrate Offer page + route consolidation with redirects
- Created offer route `_main.($lang).offer.tsx` with loader/meta
- Added offer sections in `app/components/offer/` including hero, symptoms, levers, method, implications, and CTA
- Localized offer content in `locales/{fr,en}/offer.json` including nested arrays (pyramid, services, levers)
- Updated legacy routes to 301 redirect to `/${lang}/offer`
- Updated sitemap, URL helpers, and legacy links to point to `/offer`
- Updated component inventory and route map documentation
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 10: Migrate Method page
- Added method route `_main.($lang).method.tsx` with loader/meta
- Ported method components and illustrations in `app/components/method/`
- Localized method content in `locales/{fr,en}/method.json` including diagram text
- Updated sitemap and docs route map + component inventory
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 11: Migrate About Us page
- Added about route `_main.($lang).about-us.tsx` with loader/meta
- Ported about components and illustration in `app/components/about/`
- Localized about content in `locales/{fr,en}/about.json` including manifesto structure
- Updated `/about-us` redirect to localized route and added sitemap entry
- Updated route map and component inventory
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 12: Adapt Blog pages to new design system
- Refreshed blog list cards to match new typography, spacing, and card treatments
- Updated blog article header styles and metadata presentation
- Localized blog empty state and back label via `blog` namespace
- Added blog i18n resources to localization registry
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 17: Migrate Technology/Partners page
- Created route `_main.($lang).technology.tsx` with `createHybridLoader` + `throwIfDisabled`
- Created 5 section components in `app/components/technology/`: hero, philosophy, ecosystem, partner-card, partner-form
- Ported 30 partners data into `locales/{fr,en}/technology.json` under `partners` key (translatable)
- Category filter with animation reset on filter change
- Partner card compound component pattern (Header/Body/Separator/Footer)
- Partner form with success state toggle
- Reused `CtaSection`, `ModularStackGrid`, `FlexPair` from existing components
- Converted `styled()` → `css()`, hardcoded text → `t()` calls
- Registered `technology` namespace in `resources.ts`
- Removed `'technology'` from `DISABLED_PAGES_DEFAULT`
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 18: Migrate Studio page
- Created route `_main.($lang).studio.tsx` with `createHybridLoader` + `throwIfDisabled`
- Created 5 section components in `app/components/studio/`: hero, team-photo-illustration, model, team, team-member-card
- Ported 12 team members data into `locales/{fr,en}/studio.json` under `team.members` key (translatable)
- Category filter with animation reset on filter change (same pattern as ecosystem-section)
- Team member card with grayscale→colour hover, fade-in animation, conditional LinkedIn link
- Hero section with `Trans` component for rich subtitle, floating illustration with blobs
- Model section with dark bg, dot pattern, 3 value cards using `iconBox()` + `text()` recipes
- Added `mint` variant to `CtaSection` component (section recipe already supported mint)
- Registered `studio` namespace in `resources.ts`
- Removed `'studio'` from `DISABLED_PAGES_DEFAULT`
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 19: Enable external links in nav: 'Nous rejoindre' and 'Podcasts'
- Removed `'jobs'` and `'podcasts'` from `DISABLED_PAGES_DEFAULT`
- 'Nous rejoindre' link in Company dropdown → Notion page (isExternal, already wired)
- 'Podcasts' link in Resources dropdown → Ausha page (isExternal, already wired)
- Both links already had correct URLs, icons, colours, and isExternal flag in useMenuItems
- Only change needed was removing feature flag entries
- pnpm check ✅ pnpm typecheck ✅
---
# QA P1 — QFX Progress Log
Project: Website Migration — QA P1 QFX
Source: specs/qa-plan-p1.md
Branch: qfx-qa-p1
Created: 2026-02-06T15:00:00Z
## Initial Setup
- Triaged ~28 QA items from client Notion export
- Created specs/qa-plan-p1.md with 5-phase plan (bugs → polish → content → features)
- Generated stories.json with 16 features (IDs 20-35)
- 3 bugs (high), 5 polish (high), 2 content (high/medium), 6 features (high/medium)
- 3 features blocked on assets (33: founder photos, 34: testimonial assets, 35: tool/team logos)
- Ready for implementation
---
## Feature 20: Fix navbar bugs — YouTube URL, z-index scroll, radius/gap
- Fixed YouTube URL in app/utils/url.ts: `@ocobo` → `@Ocobo-Revenue`
- Footer.tsx: replaced hardcoded YouTube URL with `url.youtube` reference
- Fixed dropdown z-index in nav-item-with-dropdown.tsx: 200 → 1001 (above navbar container at 1000)
- Increased nav items gap from 1 → 2 and CTA/toggle gap from 3 → 4 in navbar.tsx
- Navbar rounded: '3xl' in scrolled state already correct
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 21: Fix offer pyramid bullets missing on mobile
- Removed `slice(0, 1)` from mobile card rendering in symptoms-section.tsx
- All bullet points now visible on mobile viewport
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 22: Fix method page circle animation spinning off-center
- Added `transformOrigin: '250px 250px'` to dashed circle in attio-pillars-illustration.tsx
- Circle now spins in place around SVG center instead of orbiting around origin
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 23: Global polish — container margins, text selection, blog dropdown colour
- Container.tsx: maxW default 7xl (1280px) → 70rem (1120px), narrow 5xl → 56rem, px → 8 (2rem fixed)
- global-css.ts: added ::selection { backgroundColor: yellow, color: dark }
- useMenuItems.ts: blog dropdown color coral → sky
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 24: Home page polish — font sizes, badges, CTA
- transformation-section.tsx: description fontSize sm→base, method items xs→sm
- interventions-section.tsx: card description sm→base, card title 2xl→3xl
- feature-card.tsx: label xs→sm, description xs→sm
- stack-section.tsx: tagline xs→sm
- pain-point-section.tsx: "Notre mission" badge now solid mint bg + dark text
- Studio CTA already present in comparatif-section (label correct)
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 25: Offer page polish — hero button, font sizes, badge
- hero-section.tsx: added ArrowRight icon to CTA button
- Grey description fontSize sm→base: implications, leviers, offers-detail, transition sections
- Badge already uses badge({ variant: 'yellow' }) recipe — uniform with home
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 26: Method page polish — font sizes, card sizes, bowtie, border colours
- pillar-card.tsx: reduced padding 10→8, deliverable title sm→base, description xs→sm
- unified-bowtie.tsx: hidden on mobile (display none/md block), all SVG text fonts bumped +2
- scope-section.tsx: added "Quelques exemples d'activités" title between bowtie and scope cards
- hero-section.tsx: quote fontSize sm→base
- Locale keys added: scope.activitiesTitle (fr/en)
- Border top colours already correct (using borderColorMap[color])
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 27: About page polish — h3 font sizes, CTA yellow background
- manifesto-section.tsx: all h3-level text fontSize xl/3xl → 2xl/4xl (8 occurrences)
- about-us route: CtaSection variant sky → yellow
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 28: Offer page content updates — method texts, implications livrables, "pourquoi choisir"
- GROW title rename already done in commit 6bd07fc
- Method section: added `method.intro` + `method.closing` keys, `description` field per step, updated deliverable wording
- methode-section.tsx: added `description` to Step type, rendered intro paragraph, step descriptions, closing quote
- Implications section: added `deliverable` field to each block, renamed "Outiller" → "Outiller & structurer", merged AI items, added "Finance" to Diagnostiquer
- implications-section.tsx: added `deliverable` to Block type, rendered deliverable line with → arrow after items
- "Pourquoi choisir" rewrite: new title "Nous faisons ce que les autres promettent." + 3 new statement-style reasons
- All changes mirrored in EN locale
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 32: About page — add "Pourquoi nous choisir" reassurance block
- Created reassurance-section.tsx in app/components/about/ with 2-col grid of checkmark items
- Content from QA doc: title, subtitle, 6 checkmark items (senior team, proprietary method, agnostic tech, concrete KPIs, readable growth, durable system)
- Added reassurance.* locale keys in locales/{fr,en}/about.json
- Integrated in about-us route between ValuesSection and CtaSection
- Yellow CheckCircle2 icons, white cards with hover shadow, gray.50 section bg
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 34: Home page — complete testimonials section
- Loader fetches stories via `fetchStories(lang)`, shuffles with Fisher-Yates, picks random quote per story
- Returns `testimonials` array with slug, name, speaker, role, quote, logo, avatar URLs from ASSETS_BASE_URL
- All randomisation server-side (no SSR hydration mismatch)
- Cache strategy changed from `'static'` to `'story'`
- Client marquee converted from text spans to `<img>` logos (h-8, opacity 0.4 → 1 hover, onError hide)
- Testimonial card: SVG quote illustration replacing text `"`, fontWeight black on quote, avatar image on yellow square with initials fallback
- CTA NavLink fontSize xs→sm, letterSpacing 0.3em→0.2em
- Author role includes company name (`${role} @ ${name}`), no p+p margin override
- Removed hardcoded socialProof.clients and socialProof.testimonial.* from locale files, kept socialProof.ctaText
- pnpm check ✅ pnpm typecheck ✅
---
## Feature 33: About page — add founders photos
- Copied 3 founder photos from qa/assets to public/images/team/ (benjamin-boileux.jpeg, aude-cadiot.jpg, corentin-guerin.jpeg)
- Added `imageSrc` field to team.members in locales/{fr,en}/about.json
- Added `imageSrc` to TeamMember type in team-section.tsx, passed through to TeamMemberCard
- TeamMemberCard already supported imageSrc prop with circular display + coloured border
- pnpm check ✅ pnpm typecheck ✅
---
## Feature: Jobs page — Pocock branch (Plan B Step 2)
Issues: #64 (PRD), #65–72 (sub-issues) — github.com/ocobo-revops/website
Branch: feature/jobs-pocock (website) + feature/jobs (ocobo-posts)
### Key decisions
- Content source: ocobo-posts repo, jobs/ dir — Markdoc files with YAML frontmatter
- Schema: JobFrontmatter (title, contractType, seniority, location, startDate, hiringContact, applyEmail, status, publishedAt, intro)
- HiringContact registry: jobs/_contacts.yml (ref by slug, photo → ASSETS_BASE_URL/team/)
- Section IDs: mission / responsabilites / profil (matched to content headings via {% #id %})
- Photos: static local public/images/jobs/ (not Vercel blob — same pattern as public/images/team/)
- Heading shift: Markdoc h3→renders as h2, h4→h3 (ShiftedHeading in section.tsx)
- Callout: job-specific JobCallout (gray.50 bg, neutral border) with title attr → badge(mint)
- Buttons: button recipe (primary/white) via cx() on <a> tags for mailto links
### Commits (website repo)
- e693f1d feat: job schema, fetchJobs/fetchJob, cache strategy
- 6276a97 feat: minimal jobs list route + JobList + i18n namespace
- 4170ced feat: extractJobSections parser
- b67e2bb feat: detail route, section renderer, header
- 78c3458 feat: hiring contact registry I/O + block component
- b5cb8be feat: scroll-spy TOC on detail page
- d34502c feat: list page chrome (hero, values, process, about, job cards)
- e128c50 feat: detail page chrome (intro, apply CTA, sidebar)
- e354ae5 fix: align list/detail with prototype feedback (8 issues from QA)
- d832f5b feat: replace placeholder photos with real Ocobo Notion photos
- 7e16d25 feat: photos to public/images/jobs/ (static, not blob)
- c87a861 fix: spacing, DS buttons, Aude avatar (aude.jpeg)
- f2f8ca5 fix: heading hierarchy h3→h2, h4→h3
- d1d4e39 feat: bigger HiringContact card + branded callouts
- 20cff6d feat: callout title as badge (mint DS recipe), neutral bg
### Issues status
- #65 foundation ✅ — #66 detail route ✅ — #67 hiring contact ✅ — #68 scroll-spy ✅
- #69 list page HITL ✅ — #70 detail page HITL ✅
- #71 SEO (JSON-LD, OG, sitemap) ✅ — 0855377
- #72 nav switch (internal route + Notion cleanup) ✅ — 18f9fa9
- #58 Plan B: remaining steps after jobs PR merge ⏭️ NEXT
### #71 SEO — 0855377
- Files: app/modules/content/job-posting-ld.ts (+test), app/routes/_main.($lang).jobs.$slug.tsx, sitemap[.xml].tsx, content/index.ts
- buildJobPostingLd: CDI→FULL_TIME, CDD→CONTRACTOR, Stage→INTERN, Alternance→OTHER; validThrough=+90d; hiringOrg+jobLocation+applicantReqs
- JSON-LD injected as inline <script dangerouslySetInnerHTML> (reliable, Google supports in body)
- meta locale was hardcoded 'fr' — now uses loader lang
- Sitemap: /fr/jobs listing + per-offer URLs, published only, lastmod=publishedAt
- 177 tests ✅ (8 new, 1 snapshot)
### #72 Nav switch — 18f9fa9
- Files: app/utils/url.ts, app/hooks/useMenuItems.ts
- url.careers (Notion) removed → url.jobs = '/jobs'
- useMenuItems jobs entry: getLocalizedPath(url.jobs), no isExternal
- grep confirms 0 remaining Notion URL refs in app/
- 177 tests ✅
---
## Feature: Stories + Blog realignment on prototype rendering
Specs: GitHub #97 (parent) + #98–#105 (slices)
Branch: feat/stories-blog-realign (off main, post-#96 merge)
Companion content branch: ocobo-posts feat/tools-registry
Started: 2026-05-05
Slice plan (TDD, vertical, AFK):
- #98 tools registry foundation + tool chip on story card
- #99 story schema migration (drop tags, add team + featuredTool)
- #100 TOC extraction + active-section hook
- #101 blog index visual realign
- #102 team resolution on story detail (blocked by #99)
- #103 stories index visual realign (blocked by #98)
- #104 stories detail visual realign (blocked by #98 #99 #102)
- #105 blog detail visual realign (blocked by #100)
Resumption checklist:
1. `git switch feat/stories-blog-realign`
2. Confirm ocobo-posts feat/tools-registry exists (`tools/` + slug-normalised stories)
3. Pick next ⏭️ slice based on dep graph
4. TDD per slice: red → green → commit + log entry → pause
### Slice #98 — 6a46b55 (website) + ocobo-posts a4f7395
Files (website):
- app/modules/schemas.ts — ToolFrontmatterSchema (name + optional category/iconUrl/url)
- app/modules/content/tools.ts — loadToolRegistry, resolveTool (case-insensitive),
fetchTools; mirrors members.ts incl. cache keyed by readContentFrom+githubBranch
- app/modules/content/tools.test.ts — 9 tests (resolve, load, cache invalidation, fetchTools)
- app/modules/content/index.ts — re-export tools API
- app/routes/_main.($lang).clients._index.tsx — Promise.all(stories, registry); resolves
featuredTool from tools[0] (no featuredTool field yet — ships in #99)
- app/routes/_main.($lang).clients.$slug.tsx — resolves all story tools; forwards Tool[]
- app/components/stories/story-list.tsx — StoryListItem type with featuredTool field
- app/components/stories/story-item.tsx — small tool chip (icon optional, name fallback)
- app/components/stories/story-article.tsx — forwards resolvedTools to metas
- app/components/stories/story-metas.tsx — renders display names from resolved tools
(falls back to raw slug if registry empty)
Files (ocobo-posts):
- tools/ — 16 entries (Aircall, BigQuery, Gsheet, HubSpot, Hyperline, Lemlist, Modjo,
Notion, Odoo, Outreach, Planhat, Qobra, Salesforce, Tableau, Vitally, Zendesk)
with name + category; iconUrl deferred (content-author work)
- stories/fr/*.md (16 files) — tool references rewritten to slugs;
Hubspot/HubSpot collapsed onto single `hubspot` slug
Decisions:
- Lookup is case-insensitive (resolveTool falls back to lowered slug) so legacy
display-name references degrade rather than break during the migration window
- Icon URLs absent for now — chip renders name-only when no iconUrl; full icon
upload is content-author follow-up, not blocking the slice
- Detail aside fix bundled here (was a regression of the slug normalisation, not a
separate concern) — slice #104 will redesign the aside entirely
Surprises:
- Story detail aside renders item.tools verbatim — slug normalisation in content
required loader-side resolution to keep display names. Fixed inline.
Checks: pnpm typecheck ✅ pnpm check ✅ pnpm test 214 ✅ (+9 new tests)
Next ⏭️ slice: #99 (no blocker, parallel candidate with #100, #101)
### Slice #99 — b850ffe (website) + ocobo-posts 93b8ffd
Files (website):
- app/modules/schemas.ts — removed tags, added team?/featuredTool? to StoryFrontmatterSchema
- app/modules/schemas.test.ts — updated fixture, 5 new tests (strip tags, team, featuredTool, defaults)
- app/modules/validation.server.test.ts — fixture cleanup
- app/components/stories/story-metas.tsx — removed tags rendering block
- app/routes/_main.($lang).clients._index.tsx — removed ?tag= filter code path
Files (ocobo-posts):
- stories/fr/*.md (16 files) — tags field removed entirely
Decisions:
- Zod strips unknown fields by default → existing story YAMLs with tags pass validation
silently (no error, no output field). No need for strict mode.
Checks: pnpm typecheck ✅ pnpm check ✅ pnpm test 218 ✅ (+5 new tests, +2 fixture fixes)
Next ⏭️ slice: #100 (TOC extraction, no blocker) or #101 (blog index, no blocker)
### Slice #100 — 83396d3
Files:
- app/modules/content/toc.ts — extractToc() + slugify() (French diacritics via \p{M})
- app/modules/content/toc.test.ts — 13 tests (h2-only, h2+h3, maxLevel, accents, idempotent, explicit id)
- app/modules/config.ts — heading transform auto-generates id from text when no {% #id %}
- app/hooks/use-active-section.ts — IntersectionObserver hook, rootMargin -70% bottom
- app/routes/_main.blog.$slug.tsx — loader: extractToc(Markdoc.parse(article.markdown)), forwards toc
- app/components/blog/blog-article.tsx — BlogToc in aside above PostMetas
- app/components/blog/blog-toc.tsx — sticky TOC, active-section highlight
Decisions:
- TOC extracted from raw AST (article.markdown re-parsed) — no type changes to MarkdocFile
- slugify uses \p{M} Unicode property to strip diacritics (no hand-rolled range, Biome-safe)
- heading config transform generates matching ids server-side so anchor links resolve
Checks: pnpm check ✅ pnpm typecheck ✅ pnpm test 231 ✅ (+13 new)
### Slice #101 — 52147eb
Files:
- app/components/blog/blog-hero.tsx — sky.light header, badge + display-xl title + description
- app/routes/_main.blog._index.tsx — added BlogHero, removed tag URL-filter code path
- locales/{fr,en}/blog.json — hero.badge / hero.title / hero.description
Decisions:
- Blog still has tags in schema but no tab UI; loader simplified to sort-only
- Direct component imports (no barrel) per coding standards
Checks: pnpm check ✅ pnpm typecheck ✅ pnpm test 231 ✅
Next ⏭️ slice: #102 (team resolution on story detail, unblocked by #99) or #103 (stories index visual realign, unblocked by #98) or #105 (blog detail visual realign, unblocked by #100)
### Slice #102 — 0241a4b
Files:
- app/modules/content/members.ts — resolveTeam(slugs[], registry) → Member[]
- app/modules/content/members.test.ts — 4 new tests (valid list, unknown skip, empty, ordering)
- app/modules/content/index.ts — export resolveTeam
- app/routes/_main.($lang).clients.$slug.tsx — fetch memberRegistry in parallel, resolve team, forward
- app/components/stories/story-article.tsx — forward resolvedTeam to StoryMetas
- app/components/stories/story-metas.tsx — team section with UsersIcon + Avatar circles (32px)
Checks: pnpm check ✅ pnpm typecheck ✅ pnpm test 235 ✅ (+4 new)
### Slice #103 — c2ce730
Files:
- app/routes/_main.($lang).clients._index.tsx — use frontmatter.featuredTool over tools[0]
- app/components/stories/hero.tsx — add mint badge (clients.badge key)
- locales/{fr,en}/common.json — clients.badge added; EN description.items fixed (was in FR)
Checks: pnpm check ✅ pnpm typecheck ✅ pnpm test 235 ✅
### Slice #104 — e50ebcf
Files:
- app/components/stories/story-header.tsx — dark rotated card (rotate -3deg) + display-xl title
- app/components/stories/story-metas.tsx — 'Process & Outils' label strip (mint, uppercase)
- app/components/stories/story-quote-block.tsx — rotated avatar (3deg) + italic first quote + speaker cite
- app/components/stories/story-article.tsx — StoryQuoteBlock wired at end of main
- locales/{fr,en}/common.json — clients.processAndTools added
Decisions:
- StorySpeaker removed from header; speaker identity shown only in bottom quote block
- First quote item.quotes[0] used; renders nothing if array empty
Checks: pnpm check ✅ pnpm typecheck ✅ pnpm test 235 ✅
### Slice #105 — cf126ab
Files:
- app/modules/content/toc.ts — extractFirstParagraph() added
- app/modules/content/toc.test.ts — 4 new tests
- app/routes/_main.blog.$slug.tsx — loader simplified (no member registry), intro extracted, direct import
- app/components/blog/blog-article.tsx — PostMetas removed from aside; italic intro block in main
- app/components/blog/post-header.tsx — centred hero: tag badges + date + read + giant title + image
Decisions:
- PostMetas (author sidebar) removed entirely — author context moved to intro/header
- BlogToc + LayoutPost CTA = full aside content now
- intro = exerpt ?? firstParagraph ?? null
Checks: pnpm check ✅ pnpm typecheck ✅ pnpm test 239 ✅ (+4 new)
All 8 slices of feat/stories-blog-realign complete ✅
PR opened, then visual review against prototype screenshots showed structural
slices ≠ prototype's actual visual treatment. Issues filed:
- #107 tools registry iconUrl backfill (needs-info)
- #108 visual realignment (ready-for-agent, 8 sub-slices)
---
## Feature: Visual realignment with prototype (#108)
Specs: github.com/ocobo-revops/website#108
Branch: feat/stories-blog-realign (continued — same PR)
Created: 2026-05-05
Reference:
- Deployed proto: prototype-seven-delta.vercel.app
- Source: github.com/ocobo-revops/prototype (Panda CSS, same palette as website)
Resumption checklist:
1. Read top-of-file Codebase Patterns
2. Read #108 issue body for slice list
3. Pick next ⏭️ slice
4. One commit + log entry per slice + pause for feedback
### Slice #108-1 — 38c1a96 / reverted in 95266bb
Initial attempt: added per-story `accent: yellow|mint` field + static class maps.
Reverted after visual review — prototype's per-story accent variation is too
subtle to read on actual screenshots. Decision: mint everywhere, no schema
field, no content backfill.
Files: schemas.ts (revert), schemas.test.ts (revert), story-accent.ts (deleted)
Checks: pnpm check ✅ pnpm typecheck ✅ pnpm test 239 ✅
### Open rules for upcoming slices
- **Aside layout** — when rebuilding (slice #108-3), keep label+value
groups left-flushed inside each row. Prototype has them centered, which
looks awkward; left-flush is the correct ferrage for our website.
### Slice #108-2 — f736206
Files:
- app/components/stories/story-header.tsx — two-col layout, logo card rotate(2deg) + hover unrotation
- app/components/stories/story-article.tsx — Breadcrumb → ArrowLeft back link
- locales/fr|en/common.json — added clients.back key
Decisions:
- Description paragraph kept below title (no segment badge — out of scope per #97)
- Logo height h:12 with maxW:32 to keep size reasonable across logos
- Prototype uses rotate(2deg) not -3deg (signed diff from previous shipped)
Checks: pnpm check ✅ pnpm typecheck ✅ pnpm test 239 ✅
Next ⏭️ slice #108-3: story aside rebuild (label structure left-flush,
dark Process & Outils header, mint tools chips).
### Fixes post-108-2 — 69c86b3 / 5ee6dae / f0c9322 / 0a378fa / e50acc9
Visual review against actual screenshots:
- Description removed from hero (proto has none) — 69c86b3
- Hero spacing exact from local proto: gap:16, mb:24, logo col w:1/3
pt:20 justifyContent:end, img h:10 — 5ee6dae
- Back link exact: mb:12, color:gray.400, hover→ocobo.dark,
transition:colors, ArrowLeft size=14 mr:2 — f0c9322
- Layout: back link + StoryHeader above LayoutPost.Root; ScrollProgressBar
removed from story detail route — 0a378fa
- Container: maxW 70rem → 7xl (80rem), px base:4/sm:6/lg:8 — e50acc9
NOTE: global change, verify other pages visually after merge
Key pattern: use LOCAL prototype at ~/projects/ocobo/prototype/
for all future spacing/token lookups — faster than GitHub API, exact values.
Resumption checklist for next session:
1. Branch: feat/stories-blog-realign (PR open)
2. Local proto ref: ~/projects/ocobo/prototype/pages/StoryDetail.tsx
3. Next: slice #108-3 aside rebuild
4. Aside proto source: StoryDetail.tsx ~line 558 (grid section with aside)
### Slice #108-3 — 818d331
Files:
- app/components/stories/story-metas.tsx — rebuilt: white card (rounded-3xl, shadow-sm),
display-font header, icon-in-gray-square rows (duration/team/scopes/techStack), tool chips
- app/components/stories/story-metas.test.ts — 3 unit tests for prepareToolEntries (TDD)
- app/components/stories/story-deliverables.tsx — rebuilt: dark card (ocobo.dark, blur deco,
check-list with hover mint transition)
- app/components/stories/story-article.tsx — drop slug prop from StoryMetas
- locales/{fr,en}/common.json — clients.duration/team/techStack/scopes added
Decisions:
- Scopes row kept (not in proto but meaningful content) with MessageSquare icon
- Logo image removed from aside (no longer used)
- prepareToolEntries extracted as named export for testability
Checks: pnpm check ✅ pnpm typecheck ✅ pnpm test 242 ✅ (+3 new)
Next ⏭️ remaining #108 slices: story index vertical cards, blog index eyebrow, blog detail hero, TOC active styling
### Slice #108-4 — e30399b
Files:
- app/components/stories/story-markdown-container.tsx — h2 display/3xl/black/uppercase; h3 display/2xl/bold; p gray.700 + relaxed
- app/components/stories/story-quote-block.tsx — gray.50 card, grayscale-rotate avatar, display-font speaker name/role
- app/components/stories/story-cta-block.tsx — NEW: yellow ocobo.yellow block, dots radial pattern, eyebrow/h2/ButtonLink
- app/components/stories/story-article.tsx — StoryCtaBlock wired after LayoutPost.Root
- locales/{fr,en}/common.json — clients.cta.eyebrow/title added
Checks: pnpm check ✅ pnpm typecheck ✅ pnpm test 242 ✅
Next ⏭️ remaining #108 slices: story index vertical cards, blog index eyebrow, blog detail hero, TOC active styling
### Slice #108-5 — 9becd83
Files:
- app/components/stories/story-item.tsx — rebuilt: horizontal card (dark logo panel left md+, company eyebrow, display-font title, speaker·role, featured tool chip, read-more arrow)
- app/components/stories/story-list.tsx — grid 1-col (base/md/lg) → 2-col (xl), gap 8
Decisions:
- No per-story accent (dark everywhere)
- Logo panel: dark bg, white logo, fixed w:64 on md+
- No image overlay (old pattern removed entirely)
- 4px ocobo.dark top border
- TDD note: pure visual, no extractable logic → implement directly, no unit test
Checks: pnpm check ✅ pnpm typecheck ✅ pnpm test 242 ✅
Next ⏭️ remaining #108 slices: blog index eyebrow, blog detail hero, TOC active styling
### Slice #108-6 — 7d7eebe
Files:
- app/components/blog/blog-hero.tsx — coral eyebrow span (not badge), white bg, bottom-border separator, 5xl title explicit
- locales/{fr,en}/blog.json — hero.badge → hero.eyebrow, value → "RevOps Library"
Decisions:
- bg white (was sky.light) to match prototype
- Proper noun "RevOps Library" kept same in both locales
- No filter tabs (out of scope)
Checks: pnpm check ✅ pnpm typecheck ✅ pnpm test 242 ✅
### Slice #108-7 — d2d21be
Files:
- app/components/blog/post-header.tsx — coral badge (tags[0]), black 6xl title, Calendar ocobo.yellow + Clock ocobo.sky metadata, maxW:4xl centred
- app/components/blog/blog-article.tsx — ArrowLeft back link above LayoutPost (was inside content col), coral em-dash prefix on intro
Decisions:
- Share rail removed from scope (proto-faithful realign does not include share buttons; revisit in a future content-distribution slice if needed)
- Hero image kept (not in prototype but not explicitly removed in issue)
- Back link pattern matches story-article (gray.400, uppercase, widest, xs, hover ocobo.dark)
Checks: pnpm check ✅ pnpm typecheck ✅ pnpm test 242 ✅
### Slice #108-8 — 8fc0a8d
Files:
- app/components/blog/blog-toc.tsx — per-link borderLeft 2px, active: ocobo.yellow + translateX(4px) + bold ocobo.dark, inactive: gray.100 + gray.400
Decisions:
- Removed wrapper left border (moved to per-link)
- Static class maps (linkBase/linkInactive/linkActive) — Panda static analysis safe
- TDD note: pure visual, no extractable logic → implement directly
Checks: pnpm check ✅ pnpm typecheck ✅ pnpm test 242 ✅
All #108 slices complete ✅
### Visual feedback fixes — eeaf816 / cc1b33d / 7ef9a62
User review after #108 slices identified 4 gaps vs prototype:
1. **ScrollProgressBar** (eeaf816) — removed from blog detail route
2. **Blog sidebar CTA** (eeaf816) — LayoutPost Aside CTA rebuilt as dark ocobo.dark card
(yellow blur deco, display-font title, gray.400 description, yellow CTA button).
i18n: contact.sidebar.{title,description,cta} added fr+en.
3. **Blog index featured hero** (cc1b33d) — blog-list.tsx: items[0] shown as full-width
featured dark card (image left, yellow "À la une" badge, title, description, white pill CTA),
rest in 3-col grid. i18n: featured.badge added fr+en.
4. **Stories index cards** (7ef9a62) — story-item.tsx rebuilt: vertical card matching
prototype (grayscale avatar photo 16:10, hover logo overlay in dark box, company eyebrow,
display-font title, speaker+role, tool chip). Grid reverted to 1/2/3-col.
No KPI strip (out of scope decision maintained).
Checks: pnpm check ✅ pnpm typecheck ✅ pnpm test 242 ✅
### Heroes realign — bf8dbc0
User review showed stories + blog index heroes didn't match prototype (wrong bg, wrong badge, wrong title text/size).
Files:
- app/components/stories/hero.tsx — white bg (was mint.light), yellow badge (was mint), 4xl-5xl-6xl title with lineHeight:0.95/letterSpacing:tight, flex layout (removed 12-col grid + Illustration PNGs)
- app/components/blog/blog-hero.tsx — title updated to match proto ("Explorez la science du revenu."), description updated
- locales/fr/common.json — clients.badge → "Success Stories", clients.title → "Ils ont choisi l'architecture.", clients.description flat string (removed .items array with Trans/HTML)
- locales/en/common.json — same structure, clients.title → "They chose architecture."
- locales/fr/blog.json — hero.title/description updated
- locales/en/blog.json — hero.title/description updated
Decisions:
- Stories hero: illustration PNGs (radar SVG + satellite cards) deferred — not in current scope
- clients.description.items (Trans array) → flat clients.description string (no HTML markup needed)
- lineHeight: 0.95 + letterSpacing: tight for display heading — matches prototype tight stacking
Checks: pnpm check ✅ pnpm typecheck ✅ pnpm test 242 ✅
Feature #108 complete ✅ — branch feat/stories-blog-realign ready for visual review
### Tools-on-cards (f8331df + posts 0254c4e)
User feedback: featured tool icon above image, all tools as chips into the card.
Posts (`ocobo/posts`, branch `feat/tools-registry`, commit 0254c4e):
- Added `featuredTool: <slug>` to 16 FR stories per user mapping
- Mapping: citron→hubspot, combo→planhat, cybelangel→salesforce, ekodev→hubspot,
epack→qobra, jus-mundi→hyperline, qare→zendesk, qobra→salesforce, qonto→salesforce,
resilience→planhat, steeple→hubspot, tomorro→hubspot, vibe-co→salesforce,
vizzia→hubspot, wecandoo→hubspot, yousign→salesforce
- Resilience: featuredTool=planhat even though tools list lacks planhat (chip list only
shows tools[]; featured can highlight a different tool)
Website (commit f8331df):
- app/routes/_main.($lang).clients._index.tsx — loader resolves all tools per story
(resolvedTools: Tool[]) in addition to featuredTool
- app/components/stories/story-list.tsx — StoryListItem type extended; passes resolvedTools
- app/components/stories/story-item.tsx — featured tool icon+name above image (small row);
"Tech Stack" chip list below content (one chip per tool, with iconUrl-when-present)
Decisions:
- iconUrl currently unused on tool frontmatters (issue #107 tracks populating it). Layout
is structurally ready — icons drop in automatically once #107 lands.
- Reused existing i18n key `clients.techStack` for section label (no new keys).
- Featured tool can differ from tools[] list — frontmatter contract documented above.
Checks: pnpm check ✅ pnpm typecheck ✅ pnpm test 242 ✅
### Tool icons — interim PNG via Vercel Blob (56cabb6 + posts b37d2ca)
#107 stays open (proper SVG pass) but interim icons unblock today's cards.
Approach: option 2 from Vercel-Blob-vs-public-CDN tradeoff — self-host on the
existing blob store, no third-party flakiness, 100% control of URLs.
Website (commit 56cabb6):
- scripts/upload-tool-icons.js — uploads /public/logos/<slug>.<ext> →
content/tools/<slug>.<ext> on Vercel Blob, filtered by tool registry slugs
- Pattern mirrors scripts/migrate-to-blob.js (put with addRandomSuffix:false,
allowOverwrite:true)
- Run: `node --env-file=.env scripts/upload-tool-icons.js`
Posts (`ocobo/posts`, branch `feat/tools-registry`, commit b37d2ca):
- iconUrl populated on 7 tools: hubspot, lemlist, planhat, qobra, salesforce,
tableau, zendesk
- Remaining 9 (aircall, bigquery, gsheet, hyperline, modjo, notion, odoo,
outreach, vitally) keep iconUrl null → text fallback
Coverage: 5 of 6 distinct featuredTool slugs across 16 stories now have icons
(hubspot, planhat, salesforce, qobra, zendesk). Hyperline → text.
Decisions:
- PNG accepted as interim (sourced from existing /public/logos/). #107 remains
the right place to track the SVG sourcing pass + dark-bg variant.
- Upload script was kept generic — drop SVGs into /public/logos/ later, re-run,
done. No script change needed.
Checks: pnpm check ✅ pnpm typecheck ✅
---
## Feature: Contact page redesign (#113)
Specs: github.com/ocobo-revops/website#113
Slices: #114 → #115 → #116 → #117
Branch: feat/contact-redesign
Created: 2026-05-06
Reference:
- Prototype: prototype-seven-delta.vercel.app/#/contact
- Source: ../prototype/pages/Contact.tsx
Key decisions before code:
- Form integration = option A: keep HubSpot embed + Distro untouched, restyle
via scoped CSS only (slices #115/#116/#117). Distro depends on the
hsForm_<formId> element, so .hbspt-form DOM tree must not be modified.
- Tokens: ocobo.* already exist (preset/tokens/colors.ts) — no preset changes.
- ADR-0001 occurrence in _main.($lang).contact.tsx resolved by slice #117.
Resumption checklist:
1. Read top-of-file Codebase Patterns
2. Read #113 PRD + the next slice issue (#114/#115/#116/#117)
3. One commit + log entry per slice + pause for feedback
4. After #115/#116/#117: manual non-regression — submit form, verify Distro
inline calendar still appears
### Slice #114 — 12acc62
ContactHero left column + new i18n keys.
Files:
- app/components/contact/contact-hero.tsx (new)
- app/routes/_main.($lang).contact.tsx (replaced left col block, dropped
inline Description component + obsolete title/subtitle/description handling)
- locales/{fr,en}/contact.json (added eyebrow, lead, bullets.*, rewrote title
with <0> muted span; dropped obsolete subtitle + description array)
Decisions / deviations:
- iconBox recipe sizes (sm/md/xl/lg) don't include w-12/h-12 — used inline
css for the box layout, paired with static color maps (mint/sky/yellow) per
Panda CSS static-analysis memory note.
- text() variants picked: display-label (eyebrow), display-xl (h1),
subtitle (lead), display-card (bullet titles), body (bullet descriptions).
- FrenchText wraps eyebrow/lead/bullet strings; title goes through Trans for
the muted-span slot (no double punctuation in current copy → NBSP no-op,
but FrenchText future-proofs the strings).
- ContactForm now rendered without children (mobile duplicate Description
was redundant with ContactHero stacking on mobile).
Checks: pnpm typecheck ✅ pnpm check ✅ pnpm test 262 ✅
Next ⏭️ slice: #115 (ContactCard shell)
### Slice #115 — 3c77eb6
ContactCard shell wrapping HubSpot embed.
Files:
- app/components/contact/contact-card.tsx (new)
- app/routes/_main.($lang).contact.tsx (wraps ContactForm in ContactCard)
- locales/{fr,en}/contact.json (added card.heading, consent)
Decisions / deviations:
- card() recipe gives padding md and shadow sm by default; overrode with
responsive p:{base:8, md:12} + shadow:card to match prototype. The "no
inline rounded+border+shadow combo" rule is respected — recipe owns
rounded+border, only shadow is overridden.
- Heading uses display-md-bold (display, 2xl, bold) to match the prototype
card heading (fontSize:2xl + display + bold).
- overflow:hidden on the card so the gradient top bar respects the rounded
corners (1px h bar would otherwise sit flush on the card edges).
- ContactForm component itself untouched — Distro contract on .hbspt-form
preserved.
Checks: pnpm typecheck ✅ pnpm check ✅ pnpm test 262 ✅
Manual non-regression pending: form submit → Distro inline calendar.
Next ⏭️ slice: #116 (form chrome restyle)
### Slice #116 — dfd3f08
Scoped CSS restyle of HubSpot embed inputs.
Files:
- app/components/contact/contact-card.tsx (added formScopedStyles block)
Decisions / deviations:
- Used Panda nested selector keys (`'& .hbspt-form input[type="text"]'`,
etc.) on the card root via cx(). Each selector is a separate key —
comma-grouped selectors avoided to keep Panda parsing predictable.
- Shared `inputBaseStyles` const reused across input/email/tel/number/
select/textarea (textarea adds minHeight:32). Pure object literal
(with `as const`), composed into the css() call — Panda extracts the
literal values at build time.
- form-columns-2 flex layout applies at `md` breakpoint via Panda
nested responsive keys.
- No `!important` was needed — HubSpot's injected styles lose to our
scoped declarations because our class is more specific (descendant
combinator + class on card root).
- Submit button uses `font: display` to match the prototype CTA; non-DS
but matches the rest of the dark CTA family in the codebase.
Checks: pnpm typecheck ✅ pnpm check ✅ pnpm test 262 ✅
Manual non-regression pending: form submit → Distro inline calendar +
field validation messages.
Next ⏭️ slice: #117 (route ADR-0001 cleanup)
### Slice #117 — (commit pending)
Final route cleanup + ADR-0001 deferred occurrence closed.
Files:
- app/routes/_main.($lang).contact.tsx (rewritten — minimal, idiomatic)
- app/components/contact/contact-card.tsx (drop fontFamily:'display' on
the .hs-button override; uses default sans + bold)
- docs/adr/0001-design-system-adoption-policy.md (remove route row from
Deferred per-feature occurrences table)
Decisions / deviations:
- Dropped both Illustration placements (desktop + mobile) — prototype
doesn't include them; the right-column ContactCard already carries the
brand presence via the gradient bar.
- Dropped the coral-tinted absolute bg div + blur child — same reason.
- Dropped the inner xl:2/3 + mx:auto centering div around ContactCard;
prototype keeps the form col simple. GridItem owns the column width.
- Outer wrapper now <section className={section()}> + <Container>; no
inline cx(section(), css({position:relative})) anymore.
- Submit button override no longer carries inline fontFamily:'display'.
button() recipe convention is sans + semibold; my .hs-button uses
sans + bold (slightly heavier weight matching the prototype CTA).
ADR-0001 invariant verified: contact route + contact-card.tsx no longer
appear in `grep -rn 'fontFamily.*display' app/ --include='*.tsx'`.
Checks: pnpm typecheck ✅ pnpm check ✅ pnpm test 262 ✅
Manual non-regression pending: page renders fr/en, form submit →
Distro inline calendar.
Feature done — ready to push + open PR for #113.
---
## Feature: Speed Insights route patterns (#165, PRD #120)
Specs: GitHub issue #165 (parent: PRD #120 — Performance).
Branch: feat/speed-insights-routes (merged 2026-05-13 as 96ccda9).
Key decisions before code:
- Wrap `<SpeedInsights>` in a small component that reads `useMatches()`
rather than wiring the pattern at each route — single source of truth.
- Emit Next-style bracket syntax (`[slug]`, `[lang]`) for the `route` prop;
Vercel's Routes tab UI is tuned for that convention.
- Collapse `($lang)` to `[lang]` so FR and EN aggregate as one bucket per
page (e.g. `/[lang]/blog/[slug]`), not two separate routes.
Resumption checklist (closing the loop on #165):
- 2026-05-15: open Vercel Speed Insights, Last 24h window, Routes tab.
- Confirm ≥6 patterns split out (home, blog index/detail, jobs index/
detail, client story).
- Fill per-route table in docs/project/performance-baseline.md.
- Verify RES did not regress vs 95 mobile pre-deploy.
- Close #165.
### Slice #165-1 — cf3f7a7
Wrapper + util + tests + root wiring.
Files:
- app/utils/route-pattern.ts (new — `routeIdToPattern` + `matchesToRoutePattern`)
- app/utils/route-pattern.test.ts (new — 13 cases)
- app/components/speed-insights-routed.tsx (new — wraps SpeedInsights with
useMatches-derived route prop)