diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx new file mode 100644 index 0000000..e96ba32 --- /dev/null +++ b/src/app/blog/[slug]/page.tsx @@ -0,0 +1,42 @@ +import { notFound } from 'next/navigation'; +import type { Metadata } from 'next'; +import ArticleView from '@/components/blog/ArticleView'; +import { ARTICLES, getArticle, relatedTo } from '@/content/articles'; + +/** Every article is known at build time, so all of them prerender as static. */ +export function generateStaticParams() { + return ARTICLES.map(a => ({ slug: a.slug })); +} + +export const dynamicParams = false; + +export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise { + const { slug } = await params; + const article = getArticle(slug); + if (!article) return { title: 'Article not found | Loyaly.ai' }; + + return { + title: `${article.title} | Loyaly.ai`, + description: article.excerpt, + openGraph: { + title: article.title, + description: article.excerpt, + type: 'article', + images: [{ url: article.heroImage }], + }, + twitter: { + card: 'summary_large_image', + title: article.title, + description: article.excerpt, + images: [article.heroImage], + }, + }; +} + +export default async function ArticlePage({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + const article = getArticle(slug); + if (!article) notFound(); + + return ; +} diff --git a/src/app/blog_main/page.tsx b/src/app/blog_main/page.tsx index ec401aa..eb15012 100644 --- a/src/app/blog_main/page.tsx +++ b/src/app/blog_main/page.tsx @@ -1,10 +1,19 @@ 'use client'; -import React, { useState } from 'react'; -import Link from 'next/link'; +import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; import Image from 'next/image'; +import { gsap } from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; import { useReveal } from '@/hooks/useReveal'; import GSAPAnimator from '@/components/GSAPAnimator'; +import FilterBar from '@/components/blog/FilterBar'; +import FeaturedArticle from '@/components/blog/FeaturedArticle'; +import ArticleCard from '@/components/blog/ArticleCard'; +import SplitHeading from '@/components/blog/SplitHeading'; +import { metaFor, BLOG_CANVAS } from '@/components/blog/topics'; +import { ARTICLES, articleHref } from '@/content/articles'; + +if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); function Reveal({ children, delay = 0 }: { children: React.ReactNode; delay?: number }) { const { ref, visible } = useReveal(); @@ -17,67 +26,6 @@ function Reveal({ children, delay = 0 }: { children: React.ReactNode; delay?: nu ); } -const TAG_META: Record = { - Analytics: { - color: '#f4be28', - icon: , - }, - Strategy: { - color: '#a78bfa', - icon: , - }, - Technology: { - color: '#34d399', - icon: ( - <> - - - - ), - }, - Growth: { - color: '#3b82f6', - icon: , - }, - Future: { - color: '#ec4899', - icon: , - }, -}; - -const ARTICLES = [ - { - n: '01', tag: 'Analytics', readTime: '6 min', - title: 'Why Footfall Data is the New Gold', - excerpt: "In an era where every click is tracked online, offline retailers are sitting on untapped behavioural gold, and most don't even know it.", - featured: true, - }, - { - n: '02', tag: 'Strategy', readTime: '5 min', - title: 'The End of Blind Discounts', - excerpt: "Discounting without data is just burning margin. Here's how AI is changing the way smart retailers think about price reductions.", - featured: false, - }, - { - n: '03', tag: 'Technology', readTime: '8 min', - title: 'AI in Offline Retail', - excerpt: 'From footfall heatmaps to purchase prediction, a deep look at how artificial intelligence is rewriting the rules for physical stores.', - featured: false, - }, - { - n: '04', tag: 'Growth', readTime: '4 min', - title: 'How to Increase Repeat Customers', - excerpt: 'Acquiring a new customer costs 5× more than retaining one. The playbook for building genuine loyalty in offline retail.', - featured: false, - }, - { - n: '05', tag: 'Future', readTime: '7 min', - title: 'The Future of Connected Commerce', - excerpt: 'Online and offline are no longer separate worlds. The retailers who win will be the ones who blur the line completely.', - featured: false, - }, -]; - const FILTERS = ['All', 'Analytics', 'Strategy', 'Technology', 'Growth', 'Future']; const NEWSLETTER_PERKS = [ @@ -88,19 +36,46 @@ const NEWSLETTER_PERKS = [ export default function BlogMainPage() { const [activeFilter, setActiveFilter] = useState('All'); - const [hovered, setHovered] = useState(null); + const listRef = useRef(null); const filtered = activeFilter === 'All' ? ARTICLES : ARTICLES.filter(a => a.tag === activeFilter); const featured = filtered.find(a => a.featured) || filtered[0]; const rest = filtered.filter(a => a !== featured); + const counts = useMemo(() => { + const c: Record = { All: ARTICLES.length }; + for (const f of FILTERS) { + if (f === 'All') continue; + c[f] = ARTICLES.filter(a => a.tag === f).length; + } + return c; + }, []); + + // Re-run the list entrance whenever the filter changes, since the cards are + // remounted and the page-level GSAPAnimator only sweeps the DOM once. + useLayoutEffect(() => { + const list = listRef.current; + if (!list) return; + const ctx = gsap.context(() => { + gsap.fromTo(Array.from(list.children), + { y: 40, opacity: 0 }, + { y: 0, opacity: 1, duration: 0.7, ease: 'power3.out', stagger: 0.07, overwrite: true } + ); + }, list); + return () => ctx.revert(); + }, [activeFilter]); + return ( -
+
{/* ── HERO ── */}
@@ -113,7 +88,7 @@ export default function BlogMainPage() {
-
+
The Loyaly Journal
-
-

- Insights for the
- Future of Retail. + {/* Must fit the headline at its largest clamped size (~876px), or the + deliberate two-line break collapses into orphaned words on wide screens. */} +
+

+ +
+

Explore ideas, trends, and strategies shaping modern retail, from our team to yours. @@ -153,146 +137,82 @@ export default function BlogMainPage() {

- {/* ── FILTERS ── */} -
-
- {FILTERS.map(f => { - const meta = TAG_META[f]; - const active = activeFilter === f; - return ( - - ); - })} + {/* ── EDITORIAL BAR + FILTERS ── */} +
+
+
+
+ Latest writing +
+

+ The Journal +

+
+
+ {String(filtered.length).padStart(2, '0')} + {' '}/ {String(ARTICLES.length).padStart(2, '0')} articles +
+ + (f === 'All' ? '#f4be28' : metaFor(f).color)} + iconFor={f => metaFor(f).icon} + onChange={setActiveFilter} + />
{/* ── FEATURED ARTICLE ── */} {featured && ( -
+
- -
-
- {/* Left — article info */} -
-
- - {TAG_META[featured.tag]?.icon} - {featured.tag} - - {featured.readTime} read - Featured -
-

{featured.title}

-

{featured.excerpt}

- - Read Article → - -
- {/* Right — visual */} -
- {TAG_META[featured.tag]?.icon} -
{featured.n}
-
-
- +
)} {/* ── ARTICLE GRID ── */} -
-
- {rest.map((article, i) => { - const meta = TAG_META[article.tag]; - return ( - - -
setHovered(i)} - onMouseLeave={() => setHovered(null)} - style={{ - padding: '36px', borderRadius: 22, - background: '#fff', - border: `1.5px solid ${hovered === i ? `${meta?.color}45` : 'rgba(0,0,0,0.08)'}`, - transition: 'all 0.35s cubic-bezier(0.22,1,0.36,1)', - transform: hovered === i ? 'translateY(-6px)' : 'translateY(0)', - boxShadow: hovered === i ? `0 24px 60px -16px ${meta?.color}30` : '0 2px 12px rgba(0,0,0,0.04)', - cursor: 'pointer', position: 'relative', overflow: 'hidden', - display: 'flex', flexDirection: 'column', justifyContent: 'space-between', minHeight: 300, - }} - > -
-
- - {meta?.icon} - - {article.n} -
-
- {article.tag} - - {article.readTime} read -
-

{article.title}

-

{article.excerpt}

-
- - Read Article → - -
- -
- ); - })} +
+ {rest.length > 0 && ( +
+ + More reading + + +
+ )} + +
+ {rest.map(article => ( + + ))}
+ + {filtered.length === 0 && ( +

+ No articles in this topic yet. +

+ )}
{/* ── NEWSLETTER CTA ── */} @@ -355,10 +275,20 @@ export default function BlogMainPage() {
); diff --git a/src/app/blog_single_page/page.tsx b/src/app/blog_single_page/page.tsx index bbf756f..9b57e9d 100644 --- a/src/app/blog_single_page/page.tsx +++ b/src/app/blog_single_page/page.tsx @@ -1,144 +1,12 @@ -'use client'; +import { permanentRedirect } from 'next/navigation'; +import { ARTICLES, articleHref } from '@/content/articles'; -import React from 'react'; -import Link from 'next/link'; -import Image from 'next/image'; - -const ARTICLE = { - tag: 'Analytics', - readTime: '6 min', - title: 'Why Footfall Data is the New Gold', - date: 'June 2026', - author: 'Loyaly.ai Team', - excerpt: 'In an era where every online click is tracked, offline retailers are sitting on untapped behavioural gold, and most don\'t even know it exists.', - sections: [ - { - heading: 'The data gap in offline retail', - body: 'E-commerce has always had an advantage: every click, scroll, and abandoned cart is tracked and analysed. Offline retail never had this luxury, until now. Footfall data is rapidly becoming the most valuable asset a physical store can own. It tells you not just how many people walk in, but when, where they go, how long they stay, and whether they convert.', - }, - { - heading: 'What footfall data actually tells you', - body: 'A modern footfall intelligence system captures far more than a head count. It maps movement patterns through your store, identifies dwell zones, flags bottlenecks, and connects visit behaviour to purchase outcomes. When you overlay this with loyalty data: who the customer is, what they\'ve bought before, how long since their last visit, you get a complete picture that was previously impossible for offline businesses.', - }, - { - heading: 'Turning insights into revenue', - body: 'The real power of footfall data emerges when it drives action. If you know that 60% of visitors who enter your electronics section but leave without buying spend more than 12 minutes near the display stands, that\'s a cue for a targeted offer. Loyaly\'s Behavision AI does exactly this: it detects intent signals and triggers personalised engagements in real time, closing the gap between visit and purchase.', - }, - { - heading: 'The competitive advantage is closing', - body: 'Retailers who adopt footfall intelligence today are building a data moat that will be very hard for competitors to close later. Every day without this data is a day of lost insight. The good news: AI has made footfall analytics accessible to stores of all sizes — not just enterprise chains with seven-figure tech budgets. Loyaly brings this capability to any merchant who wants it.', - }, - ], - relatedArticles: [ - { n: '02', tag: 'Strategy', title: 'The End of Blind Discounts', readTime: '5 min' }, - { n: '03', tag: 'Technology', title: 'AI in Offline Retail', readTime: '8 min' }, - { n: '04', tag: 'Growth', title: 'How to Increase Repeat Customers', readTime: '4 min' }, - ], -}; - -export default function BlogSinglePage() { - return ( -
- - {/* Hero */} -
- {/* Background photo */} - -
-
-
-
-
-
-
- - ← Market Insights - - / - {ARTICLE.tag} -
-

- {ARTICLE.title} -

-

- {ARTICLE.excerpt} -

-
-
-
-
{ARTICLE.author}
-
{ARTICLE.date} · {ARTICLE.readTime} read
-
-
-
-
- - {/* Article body */} -
-
- {ARTICLE.sections.map((s, i) => ( -
-

- {s.heading} -

-

- {s.body} -

- {i < ARTICLE.sections.length - 1 && ( -
- )} -
- ))} - - {/* Pull quote */} -
-

- "Every day without footfall data is a day of lost insight." -

-
Loyaly.ai, 2026
-
-
-
- - {/* CTA */} -
-
-

- Ready to see your store's data? -

-

Book a 30-minute demo and see Behavision AI in action.

-
- - Book a Demo → - -
- - {/* Related articles */} -
-
More from Market Insights
-
- {ARTICLE.relatedArticles.map((a) => ( - { (e.currentTarget as HTMLAnchorElement).style.background = '#fff8e0'; (e.currentTarget as HTMLAnchorElement).style.borderColor = 'rgba(244,190,40,0.3)'; }} - onMouseLeave={e => { (e.currentTarget as HTMLAnchorElement).style.background = '#fafafa'; (e.currentTarget as HTMLAnchorElement).style.borderColor = 'rgba(0,0,0,0.07)'; }} - > -
- {a.tag} - {a.readTime} read -
-

{a.title}

- - ))} -
-
-
- ); +/** + * Legacy route. Articles now live at /blog/[slug]; this URL predates them and + * may still be linked or indexed, so it forwards to the lead article rather + * than 404ing. + */ +export default function BlogSinglePageRedirect() { + const lead = ARTICLES.find(a => a.featured) ?? ARTICLES[0]; + permanentRedirect(articleHref(lead.slug)); } diff --git a/src/components/GetInTouch.tsx b/src/components/GetInTouch.tsx index b6eab60..1c98ada 100644 --- a/src/components/GetInTouch.tsx +++ b/src/components/GetInTouch.tsx @@ -179,7 +179,7 @@ export default function GetInTouch() {