Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions frontend/src/app/(auth)/forgot-password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { Suspense } from 'react';
import type { Metadata } from 'next';
import Loader from '~/components/loader';
import { ForgotPasswordForm } from './form';
import { buildMetadata } from '~/lib/metadata';

export const metadata: Metadata = {
export const metadata: Metadata = buildMetadata({
title: 'Reset Password',
description:
'Reset your Retailytics password to regain access to your retail intelligence dashboards and store data.',
alternates: { canonical: '/forgot-password' },
robots: { index: false, follow: true },
};
path: '/forgot-password',
index: false,
});

export default async function ForgotPasswordPage() {
return (
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import type { Metadata } from 'next';
import { cache, Suspense } from 'react';
import Loader from '~/components/loader';
import { redirect } from 'next/navigation';
import { buildMetadata } from '~/lib/metadata';

export const metadata: Metadata = {
export const metadata: Metadata = buildMetadata({
title: 'Sign In',
description:
'Sign in to your Retailytics account to access store enumeration dashboards, field data, and market intelligence reports.',
alternates: { canonical: '/login' },
robots: { index: false, follow: true },
};
path: '/login',
index: false,
});

const getSession = cache(() => auth());

Expand Down
9 changes: 5 additions & 4 deletions frontend/src/app/(auth)/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import { RegisterForm } from './form';
import { cache, Suspense } from 'react';
import Loader from '~/components/loader';
import { redirect } from 'next/navigation';
import { buildMetadata } from '~/lib/metadata';

export const metadata: Metadata = {
export const metadata: Metadata = buildMetadata({
title: 'Create Account',
description:
'Create your Retailytics account to start collecting store data, validating field submissions, and turning local data into market intelligence.',
alternates: { canonical: '/register' },
robots: { index: false, follow: true },
};
path: '/register',
index: false,
});

const getSession = cache(() => auth());

Expand Down
15 changes: 15 additions & 0 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ export const metadata: Metadata = {
alternates: {
canonical: '/',
},
openGraph: {
type: 'website',
siteName: siteConfig.name,
locale: siteConfig.locale,
url: siteConfig.url,
title: `${siteConfig.name} — ${siteConfig.tagline}`,
description: siteConfig.description,
},
twitter: {
card: 'summary_large_image',
site: siteConfig.twitter.handle,
creator: siteConfig.twitter.handle,
title: `${siteConfig.name} — ${siteConfig.tagline}`,
description: siteConfig.description,
},
};

export default function RootLayout({
Expand Down
72 changes: 72 additions & 0 deletions frontend/src/app/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { ImageResponse } from 'next/og';
import { siteConfig } from '~/lib/site';

export const alt = `${siteConfig.name} — ${siteConfig.tagline}`;
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

export default function OpengraphImage() {
return new ImageResponse(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
background:
'radial-gradient(circle at 20% 20%, #1e293b 0%, #0a0a0a 60%)',
padding: '80px',
color: '#f8fafc',
fontFamily: 'sans-serif',
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
fontSize: 34,
fontWeight: 700,
letterSpacing: '-0.02em',
}}
>
{siteConfig.name}
</div>

<div style={{ display: 'flex', flexDirection: 'column', gap: 28 }}>
<div
style={{
fontSize: 76,
fontWeight: 800,
lineHeight: 1.05,
letterSpacing: '-0.03em',
maxWidth: 900,
background: 'linear-gradient(90deg, #f8fafc 0%, #94a3b8 100%)',
backgroundClip: 'text',
color: 'transparent',
}}
>
{siteConfig.tagline}
</div>
<div style={{ fontSize: 30, color: '#94a3b8', maxWidth: 880 }}>
Store enumeration, field data collection, and market analysis in one
platform.
</div>
</div>

<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
fontSize: 26,
color: '#64748b',
}}
>
<span>{siteConfig.url.replace(/^https?:\/\//, '')}</span>
<span>by {siteConfig.legalName}</span>
</div>
</div>,
{ ...size },
);
}
10 changes: 3 additions & 7 deletions frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import CTA from '~/components/cta';
import FAQ from '~/components/faq';
import Hero from '~/components/hero';
import type { Metadata } from 'next';
import { siteConfig } from '~/lib/site';
import Footer from '~/components/footer';
import type { Metadata } from 'next';
import { buildMetadata } from '~/lib/metadata';
import HowItWorks from '~/components/how-it-works';

export const metadata: Metadata = {
title: { absolute: `${siteConfig.name} — ${siteConfig.tagline}` },
description: siteConfig.description,
alternates: { canonical: '/' },
};
export const metadata: Metadata = buildMetadata({ path: '/' });

export default function Home() {
return (
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/twitter-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default, alt, size, contentType } from '~/app/opengraph-image';
42 changes: 42 additions & 0 deletions frontend/src/lib/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Metadata } from 'next';
import { siteConfig } from '~/lib/site';

type BuildMetadataOptions = {
title?: string;
description?: string;
path?: string;
index?: boolean;
};

export function buildMetadata({
title,
description = siteConfig.description,
path = '/',
index = true,
}: BuildMetadataOptions = {}): Metadata {
const socialTitle = title
? `${title} · ${siteConfig.name}`
: `${siteConfig.name} — ${siteConfig.tagline}`;

return {
title: title ?? { absolute: socialTitle },
description,
alternates: { canonical: path },
...(index ? {} : { robots: { index: false, follow: true } }),
openGraph: {
type: 'website',
siteName: siteConfig.name,
locale: siteConfig.locale,
url: path,
title: socialTitle,
description,
},
twitter: {
card: 'summary_large_image',
site: siteConfig.twitter.handle,
creator: siteConfig.twitter.handle,
title: socialTitle,
description,
},
};
}
15 changes: 0 additions & 15 deletions frontend/src/lib/site.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,23 @@
import { env } from '~/env';

/**
* Central, single-source-of-truth metadata for the Retailytics marketing
* surface. Reused by page metadata, Open Graph/Twitter cards, robots.txt,
* sitemap.xml, JSON-LD structured data and the llms.txt manifest so the
* product is described consistently everywhere search engines and AI agents
* look.
*/
export const siteConfig = {
name: 'Retailytics',
/** Parent organisation that builds and operates Retailytics. */
legalName: 'Ajared Research Inc.',
/** Canonical, absolute production URL (no trailing slash). */
url: env.NEXT_PUBLIC_SITE_URL.replace(/\/$/, ''),
tagline: 'Turn Local Data into Market Intelligence',
description:
'Retailytics turns local store data into market intelligence — enumeration, field data collection, and analysis in one platform. Start free today.',
/** Monitored contact address used on public pages and structured data. */
contactEmail: 'innovation@ajared.ca',
/** Default social/OG share image (1200×630), served from /public. */
ogImage: '/og.png',
locale: 'en_US',
twitter: {
handle: '@ajaREDiA',
},
/** Profiles used for the Organization `sameAs` graph. */
social: {
twitter: 'https://twitter.com/ajaREDiA',
github: 'https://github.com/ajared',
parent: 'https://www.ajared.ng',
parentCa: 'https://www.ajared.ca',
},
/** Keyword targets shared across pages. */
keywords: [
'retail intelligence',
'store enumeration',
Expand All @@ -46,7 +32,6 @@ export const siteConfig = {

export type SiteConfig = typeof siteConfig;

/** Build an absolute URL from a site-relative path. */
export function absoluteUrl(path = '/'): string {
return `${siteConfig.url}${path.startsWith('/') ? path : `/${path}`}`;
}
Loading