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: 9 additions & 0 deletions frontend/src/app/(auth)/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { Suspense } from 'react';
import type { Metadata } from 'next';
import Loader from '~/components/loader';
import { ForgotPasswordForm } from './form';

export const metadata: Metadata = {
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 },
};

export default async function ForgotPasswordPage() {
return (
<div className="bg-background flex h-full w-full min-h-screen items-center justify-center p-4">
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { auth } from '../auth';
import { LoginForm } from './form';
import type { Metadata } from 'next';
import { cache, Suspense } from 'react';
import Loader from '~/components/loader';
import { redirect } from 'next/navigation';

export const metadata: Metadata = {
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 },
};

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

export default async function LoginPage() {
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/app/(auth)/register/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { auth } from '../auth';
import { decodeJwt } from 'jose';
import type { Metadata } from 'next';
import { RegisterForm } from './form';
import { cache, Suspense } from 'react';
import Loader from '~/components/loader';
import { redirect } from 'next/navigation';

export const metadata: Metadata = {
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 },
};

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

function validateInviteToken(inviteToken: string): {
Expand Down
17 changes: 15 additions & 2 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Metadata } from 'next';
import { Suspense } from 'react';
import Loader from '~/components/loader';
import { Outfit } from 'next/font/google';
import { siteConfig } from '~/lib/site';
import { Toaster } from '~/components/ui/sonner';
import { Analytics } from '@vercel/analytics/next';

Expand All @@ -13,8 +14,20 @@ const outfit = Outfit({
});

export const metadata: Metadata = {
title: 'Retailytics',
description: 'Retailytics aka Retail Intelligence',
metadataBase: new URL(siteConfig.url),
title: {
default: `${siteConfig.name} — ${siteConfig.tagline}`,
template: `%s · ${siteConfig.name}`,
},
description: siteConfig.description,
applicationName: siteConfig.name,
keywords: [...siteConfig.keywords],
authors: [{ name: siteConfig.legalName, url: siteConfig.social.parent }],
creator: siteConfig.legalName,
publisher: siteConfig.legalName,
alternates: {
canonical: '/',
},
};

export default function RootLayout({
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
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 HowItWorks from '~/components/how-it-works';

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

export default function Home() {
return (
<main className="w-full h-full flex flex-col">
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ export const env = createEnv({
},
client: {
NEXT_PUBLIC_MIXPANEL_TOKEN: z.string(),
NEXT_PUBLIC_SITE_URL: z.url().default('https://retailytics.ajared.ng'),
},
emptyStringAsUndefined: true,
experimental__runtimeEnv: {
NEXT_PUBLIC_MIXPANEL_TOKEN: process.env.NEXT_PUBLIC_MIXPANEL_TOKEN,
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
},
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
});
52 changes: 52 additions & 0 deletions frontend/src/lib/site.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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',
'field data collection',
'market intelligence',
'retail analytics',
'location intelligence',
'market research',
'store data platform',
],
} as const;

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