Web Development
FAQ Schema, Service CTAs & a Slick Mobile TOC
Adding FAQ schema, service CTAs, and a camera-wheel inspired mobile TOC to blog posts.
The article
Today I spent a few hours enhancing the blog system on this very website. What started as "add an FAQ section" turned into a deep dive on SEO optimization, user experience, and some genuinely fun UI experimentation. Here's everything I built.
The Starting Point
I had a solid blog post about Google Ads being rigged against advertisers. Good content, decent visuals. But it was missing several things that could make it work harder:
- No FAQ section - Missing out on rich snippets in Google
- No service CTA - Readers had no clear path to hire me
- Category badges weren't clickable - Dead-end navigation
- Mobile TOC was basic - Just a floating button
Let's fix all of that.
Adding FAQ Schema for SEO Rich Snippets
FAQ sections aren't just good for users—they can appear as expandable answers directly in Google search results. That's free SERP real estate.
The Visual Component
I created an accordion-style FAQ using HTML <details> and <summary> elements:
<details className="group bg-white border border-slate-200 rounded-lg">
<summary className="flex justify-between items-center cursor-pointer p-4 font-semibold text-slate-900">
Is Google Ads actually rigged or just poorly designed?
<span className="text-purple-600 group-open:rotate-180 transition-transform">▼</span>
</summary>
<div className="px-4 pb-4 text-slate-700">
The evidence suggests intentional design, not incompetence...
</div>
</details>
The group-open:rotate-180 is a nice Tailwind trick—the arrow rotates when the details element is open.
The Schema Markup
For Google to show FAQ rich snippets, you need structured data. I already had a generateFAQSchema helper, so I just needed to wire it up:
<StructuredData data={[
generateOrganizationSchema(),
generateArticleSchema({...}),
// Add FAQ schema for posts with FAQs
...(blogPost.slug === "google-ads-is-rigged" ? [generateFAQSchema([
{ question: "Is Google Ads actually rigged?", answer: "..." },
{ question: "Should I stop using Google Ads?", answer: "..." },
// ... more FAQs
])] : [])
]} />
The spread operator with conditional array is a clean pattern for optionally including schema.
Service CTA That Actually Converts
The old CTA was generic: "Ready to Grow Your Business?" with a purple gradient. Boring.
The New Design
I rebuilt it with:
- Dark slate background with animated emerald/teal glow orbs
- Pulsing badge drawing attention to "Free Strategy Session"
- Gradient headline: "Stop Guessing. Start Growing."
- Two clear CTAs: Primary gradient button + secondary outline
- Trust indicators: No obligation, 30-min call, Custom strategy
{/* Animated glow orbs */}
<motion.div
className="absolute top-0 left-1/4 w-96 h-96 bg-emerald-500/20 rounded-full blur-3xl"
animate={{
scale: [1, 1.2, 1],
opacity: [0.3, 0.5, 0.3],
}}
transition={{ duration: 8, repeat: Infinity, ease: "easeInOut" }}
/>
The breathing glow effect adds life without being distracting.
Making Categories & Tags Clickable
This seems obvious but was missing: clicking a category badge should filter the blog.
Before
<Badge className="bg-orange-500 text-white">PPC</Badge>
After
<Link href={`/blog?category=${encodeURIComponent(category)}`}>
<Badge className="bg-orange-500 text-white hover:opacity-80 cursor-pointer">
{category}
</Badge>
</Link>
Now categories link to /blog?category=PPC and tags link to /blog?tag=tagname. Better internal linking, better UX, better SEO.
The Mobile TOC: Camera Scroll Wheel Effect
This was the fun part. The mobile table of contents needed to:
- Not overlap article content
- Show reading progress
- Be discoverable but not intrusive
- Look cool
The Edge Tab
I created a sticky tab on the right edge with:
- Purple button with animated arrow
- Progress dots below showing position in article
<motion.div className="lg:hidden fixed right-0 top-1/3 z-40 flex flex-col items-end">
<motion.button
className="bg-purple-600 text-white rounded-l-lg shadow-lg p-2"
whileHover={{ x: -4 }}
whileTap={{ scale: 0.95 }}
>
{/* Animated arrow */}
<motion.div animate={{ x: [0, -2, 0] }} transition={{ repeat: Infinity, duration: 1.5 }}>
<ChevronLeft />
</motion.div>
</motion.button>
{/* Progress dots */}
...
</motion.div>
The Fisheye Dot Effect
I wanted the dots to feel like a camera scroll wheel—the active section bulges out, nearby sections are medium, distant sections are small.
{items.map((item, idx) => {
const distance = Math.abs(idx - activeIndex);
const isActive = idx === activeIndex;
return (
<motion.div
className={`rounded-full border-2 ${
isActive ? 'bg-purple-600 border-purple-600 shadow-md'
: isPast ? 'bg-purple-400 border-purple-400'
: 'bg-white border-slate-300'
}`}
animate={{
width: isActive ? 12 : distance === 1 ? 8 : 6,
height: isActive ? 12 : distance === 1 ? 8 : 6,
}}
transition={{ type: "spring", stiffness: 400, damping: 25 }}
/>
);
})}
The spring animation makes it feel organic as you scroll through sections.
The Slide-Out Panel
Tapping the tab reveals a full timeline navigation:
- Purple gradient header with progress bar
- "X of Y sections" counter
- Vertical timeline with numbered dots
- Active section highlighted
- Smooth spring animation on open/close
One gotcha: the panel needs to start below the navbar. I used top-[72px] to match the navbar height exactly.
Fixing Bugs Along the Way
SVG Path Error
The social media card backgrounds had an SVG with invalid path data:
<!-- ❌ Wrong - CSS values don't work in SVG paths -->
<path d="M 100% 0 L calc(100% - 100px) 0..." />
<!-- ✅ Fixed - Use actual numbers (viewBox is 1080x1080) -->
<path d="M 1080 0 L 980 0..." />
Sticky Sidebar Breaking
Adding overflow-x-hidden to fix mobile overflow broke the desktop sticky sidebar. position: sticky doesn't work when any ancestor has overflow: hidden. Had to be selective about where to apply it.
Dark Text on Dark Backgrounds
Tailwind's text-white on a parent doesn't cascade to headings—they inherit browser defaults. Always add explicit text-white to headings inside dark containers.
Results
The blog post now has:
- ✅ FAQ section with schema markup for rich snippets
- ✅ Contextual service CTA with animated design
- ✅ Clickable categories and tags for better navigation
- ✅ Slick mobile TOC with camera-scroll-wheel progress indicator
- ✅ Fixed visual bugs and improved mobile experience
What's Next
This is part 1 of documenting the build of JordanJamesMedia.com. Future posts will cover:
- The social media card generation system
- Featured image auto-generation
- The blog consolidation architecture
- SEO optimizations and structured data
Building in public means sharing the messy middle, not just the polished result. Hope this was useful!
Tell me what you need.
You get a straight answer, from me.