Compare commits

..

No commits in common. "main" and "develop" have entirely different histories.

53 changed files with 994 additions and 11043 deletions

View file

@ -32,28 +32,7 @@
"Bash(node --input-type=module:*)",
"Bash(python3:*)",
"Bash(ls:*)",
"WebFetch(domain:axilaccountants.co.uk)",
"Bash(git -C \"/Volumes/SSD/Projects/Clients/Axil Accountants\" remote get-url origin)",
"Bash(git -C \"/Volumes/SSD/Projects/Clients/Axil Accountants\" status)",
"Bash(git -C \"/Volumes/SSD/Projects/Clients/Axil Accountants\" add deploy.sh docker-compose.prod.yml .gitignore)",
"Bash(git -C:*)",
"Bash(pnpm install:*)",
"Bash(pnpm exec tsc:*)",
"Bash(pnpm add:*)",
"Bash(pnpm payload migrate:create:*)",
"Bash(NODE_OPTIONS=\"--require tsx/cjs\" pnpm payload migrate:create:*)",
"Bash(NODE_OPTIONS=\"--require /Volumes/SSD/Projects/Clients/Axil\\\\ Accountants/node_modules/.pnpm/tsx@4.21.0/node_modules/tsx/dist/cjs/index.cjs\" node:*)",
"Bash(git merge:*)",
"Bash(gh auth status:*)",
"Bash(ssh-keygen:*)",
"Bash(ssh-copy-id:*)",
"Bash(ssh:*)",
"Bash(gh auth login:*)",
"Bash(curl:*)",
"Bash(DATABASE_URI=\"postgresql://axil:axil_dev@localhost:5432/axil\" NODE_OPTIONS=\"--experimental-strip-types --no-require-module\" pnpm payload generate:types)",
"Bash(DATABASE_URI=\"postgresql://axil:axil_dev@localhost:5432/axil\" NODE_OPTIONS=\"--experimental-strip-types\" pnpm payload generate:types:*)",
"Bash(DATABASE_URI=\"postgresql://axil:axil_dev@localhost:5432/axil\" NODE_OPTIONS=\"--experimental-strip-types --no-require-module\" pnpm payload generate:types:*)",
"Bash(pnpm lint:*)"
"WebFetch(domain:axilaccountants.co.uk)"
]
}
}

View file

@ -32,17 +32,10 @@ jobs:
git -C "$PROJECT_DIR" reset --hard origin/main
echo "▶ Building Docker image..."
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" build
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" build --pull
echo "▶ Restarting containers..."
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d --remove-orphans || {
echo "✗ Startup failed — migrator logs:"
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" logs migrator
exit 1
}
echo "▶ Migrator logs:"
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" logs migrator
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d --remove-orphans
echo "▶ Pruning unused images..."
docker image prune -f

View file

@ -18,16 +18,11 @@ ENV WATCHPACK_POLLING=true
EXPOSE 3000
CMD ["pnpm", "dev"]
# --- Migrator (deps + src only — runs migrations before app starts) ---
# --- Migrator (deps + src only, no Next.js build — used for pnpm payload migrate) ---
FROM base AS migrator
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# --experimental-strip-types: run migrate.ts + migration files as TypeScript natively.
# --no-require-module is intentionally omitted: it was added in Node 22.12.0 and the
# cached base image may be older; our minimal migrate script doesn't load lexical so
# there are no TLA/ESM issues that required that flag.
ENV NODE_OPTIONS="--experimental-strip-types"
CMD ["node", "src/scripts/migrate.ts"]
CMD ["pnpm", "payload", "migrate"]
# --- Build ---
FROM base AS build

View file

@ -1,19 +1,4 @@
services:
# Run pending Payload migrations before the app starts.
# Uses the `migrator` image stage (has full src/) so TS files are discoverable.
migrator:
build:
context: .
target: migrator
restart: "no"
networks:
- internal
env_file:
- .env.production
depends_on:
db:
condition: service_healthy
app:
build:
context: .
@ -35,8 +20,6 @@ services:
depends_on:
db:
condition: service_healthy
migrator:
condition: service_completed_successfully
db:
image: postgres:17-alpine

View file

@ -32,7 +32,6 @@
"@swc-node/register": "^1.11.1",
"@swc/core": "^1.15.11",
"@tailwindcss/postcss": "^4",
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",

34
pnpm-lock.yaml generated
View file

@ -66,9 +66,6 @@ importers:
'@tailwindcss/postcss':
specifier: ^4
version: 4.2.0
'@tailwindcss/typography':
specifier: ^0.5.19
version: 0.5.19(tailwindcss@4.2.0)
'@types/node':
specifier: ^20
version: 20.19.33
@ -1500,11 +1497,6 @@ packages:
'@tailwindcss/postcss@4.2.0':
resolution: {integrity: sha512-u6YBacGpOm/ixPfKqfgrJEjMfrYmPD7gEFRoygS/hnQaRtV0VCBdpkx5Ouw9pnaLRwwlgGCuJw8xLpaR0hOrQg==}
'@tailwindcss/typography@0.5.19':
resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==}
peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
'@tokenizer/token@0.3.0':
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
@ -1982,11 +1974,6 @@ packages:
crypt@0.0.2:
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
cssfilter@0.0.10:
resolution: {integrity: sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==}
@ -3363,10 +3350,6 @@ packages:
postal-mime@2.7.3:
resolution: {integrity: sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw==}
postcss-selector-parser@6.0.10:
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
engines: {node: '>=4'}
postcss@8.4.31:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
@ -3982,9 +3965,6 @@ packages:
utf8-byte-length@1.0.5:
resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@10.0.0:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
hasBin: true
@ -5368,11 +5348,6 @@ snapshots:
postcss: 8.5.6
tailwindcss: 4.2.0
'@tailwindcss/typography@0.5.19(tailwindcss@4.2.0)':
dependencies:
postcss-selector-parser: 6.0.10
tailwindcss: 4.2.0
'@tokenizer/token@0.3.0': {}
'@tybys/wasm-util@0.10.1':
@ -5857,8 +5832,6 @@ snapshots:
crypt@0.0.2: {}
cssesc@3.0.0: {}
cssfilter@0.0.10: {}
csstype@3.1.3: {}
@ -7511,11 +7484,6 @@ snapshots:
postal-mime@2.7.3: {}
postcss-selector-parser@6.0.10:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss@8.4.31:
dependencies:
nanoid: 3.3.11
@ -8167,8 +8135,6 @@ snapshots:
utf8-byte-length@1.0.5: {}
util-deprecate@1.0.2: {}
uuid@10.0.0: {}
uuid@9.0.0: {}

View file

@ -1,236 +0,0 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { ServerHeader } from '@/components/layout/ServerHeader';
import { ServerFooter } from '@/components/layout/ServerFooter';
import { Button } from '@/components/ui/Button';
import { FadeIn } from '@/components/ui/FadeIn';
import { SpotlightCard } from '@/components/ui/SpotlightCard';
import { Tag } from '@/components/ui/Tag';
import { getPublishedPosts, getPostBySlugCached } from '@/lib/payload';
import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html';
import type { Category, Media, User } from '@/payload-types';
const CATEGORY_COLOR: Record<string, 'green' | 'blue'> = {
Tax: 'green',
Payroll: 'green',
HMRC: 'green',
Finance: 'blue',
Business: 'blue',
};
function formatDate(iso: string | null | undefined): string {
if (!iso) return '';
return new Date(iso).toLocaleDateString('en-GB', {
day: 'numeric',
month: 'short',
year: 'numeric',
});
}
export async function generateStaticParams() {
try {
const posts = await getPublishedPosts(100);
return posts.map((p) => ({ slug: p.slug }));
} catch {
// DB unavailable at build time (no `db` host in Docker build network) — pages render on demand
return [];
}
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPostBySlugCached(slug);
if (!post) return { title: 'Post Not Found' };
const ogImage =
post.seo?.ogImage && typeof post.seo.ogImage === 'object'
? (post.seo.ogImage as Media).url
: undefined;
return {
title: post.seo?.metaTitle ?? `${post.title} — Axil Accountants Blog`,
description: post.seo?.metaDescription ?? post.excerpt ?? undefined,
openGraph: ogImage ? { images: [{ url: ogImage }] } : undefined,
};
}
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const [post, allPosts] = await Promise.all([getPostBySlugCached(slug), getPublishedPosts(10)]);
if (!post) notFound();
const category =
post.category && typeof post.category === 'object' ? (post.category as Category) : null;
const author = post.author && typeof post.author === 'object' ? (post.author as User) : null;
const categoryColor = CATEGORY_COLOR[category?.name ?? ''] ?? 'green';
const related = allPosts
.filter((p) => p.slug !== slug)
.slice(0, 2)
.map((p) => {
const pCategory =
p.category && typeof p.category === 'object' ? (p.category as Category) : null;
return {
slug: p.slug,
title: p.title,
date: formatDate(p.publishedAt),
category: pCategory?.name ?? 'Article',
color: (CATEGORY_COLOR[pCategory?.name ?? ''] ?? 'green') as 'green' | 'blue',
};
});
const contentHtml = post.content
? convertLexicalToHTML({
data: post.content as Parameters<typeof convertLexicalToHTML>[0]['data'],
})
: null;
const avatarUrl = author?.name
? `https://ui-avatars.com/api/?name=${encodeURIComponent(author.name)}&background=3CC68A&color=fff&bold=true&size=80`
: null;
return (
<>
<ServerHeader />
<main>
{/* Hero */}
<section className="bg-bg relative overflow-hidden pt-32 pb-12">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_60%_50%_at_50%_0%,var(--color-emerald-mist)_0%,transparent_70%)]" />
<div className="relative mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
<FadeIn>
<div className="mx-auto max-w-3xl">
<Link
href="/blog"
className="text-muted hover:text-emerald mb-6 inline-flex items-center gap-1.5 text-sm font-medium transition-colors"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m15 18-6-6 6-6" />
</svg>
Back to blog
</Link>
<div className="mb-4 flex flex-wrap items-center gap-3">
{category && <Tag variant={categoryColor}>{category.name}</Tag>}
<span className="text-muted text-sm">{formatDate(post.publishedAt)}</span>
{post.readingTime && (
<>
<span className="text-muted text-sm">·</span>
<span className="text-muted text-sm">{post.readingTime} min read</span>
</>
)}
</div>
<h1 className="font-display text-charcoal mb-6 text-3xl leading-tight font-bold tracking-tight sm:text-4xl lg:text-5xl">
{post.title}
</h1>
{post.excerpt && (
<p className="text-muted mb-8 text-xl leading-relaxed">{post.excerpt}</p>
)}
{author && (
<div className="flex items-center gap-3 border-t border-black/8 pt-6">
{avatarUrl && (
<img
src={avatarUrl}
alt={author.name}
width={44}
height={44}
className="size-11 rounded-full"
/>
)}
<div>
<p className="text-charcoal text-sm font-semibold">{author.name}</p>
<p className="text-muted text-xs">{author.role}</p>
</div>
</div>
)}
</div>
</FadeIn>
</div>
</section>
{/* Article body */}
<section className="bg-white py-16">
<div className="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
<div className="mx-auto max-w-3xl">
<FadeIn>
{contentHtml ? (
<div
className="prose prose-lg prose-headings:font-display prose-headings:text-charcoal prose-headings:font-bold prose-p:text-muted prose-p:leading-relaxed prose-a:text-emerald prose-a:no-underline hover:prose-a:underline prose-strong:text-charcoal max-w-none"
dangerouslySetInnerHTML={{ __html: contentHtml }}
/>
) : (
<p className="text-muted">Content coming soon.</p>
)}
</FadeIn>
{/* CTA inline */}
<FadeIn delay={0.1}>
<div className="rounded-hero border-emerald/25 bg-emerald-mist mt-14 border p-8 text-center">
<p className="font-display text-charcoal mb-2 text-xl font-bold">
Want personalised advice?
</p>
<p className="text-muted mb-5">
Book a free 30-minute consultation with one of our qualified accountants.
</p>
<Button size="lg" trailingArrow href="/contact">
Book Free Consultation
</Button>
</div>
</FadeIn>
</div>
</div>
</section>
{/* Related posts */}
{related.length > 0 && (
<section className="bg-bg py-16">
<div className="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
<FadeIn>
<h2 className="font-display text-charcoal mb-8 text-2xl font-bold">
More from the blog
</h2>
</FadeIn>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
{related.map((p, i) => (
<FadeIn key={p.slug} delay={i * 0.08}>
<Link href={`/blog/${p.slug}`} className="group block h-full">
<SpotlightCard
className="h-full p-6 transition-transform duration-300 group-hover:-translate-y-1"
glowColor="green"
customSize
>
<div className="mb-3 flex items-center gap-3">
<Tag variant={p.color}>{p.category}</Tag>
<span className="text-muted text-xs">{p.date}</span>
</div>
<h3 className="font-display text-charcoal group-hover:text-emerald text-base leading-snug font-semibold transition-colors">
{p.title}
</h3>
<p className="text-emerald mt-3 text-xs font-semibold">Read more </p>
</SpotlightCard>
</Link>
</FadeIn>
))}
</div>
</div>
</section>
)}
</main>
<ServerFooter />
</>
);
}

View file

@ -1,130 +0,0 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { ServerHeader } from '@/components/layout/ServerHeader';
import { ServerFooter } from '@/components/layout/ServerFooter';
import { Button } from '@/components/ui/Button';
import { FadeIn } from '@/components/ui/FadeIn';
import { SpotlightCard } from '@/components/ui/SpotlightCard';
import { Tag } from '@/components/ui/Tag';
import { getPublishedPosts } from '@/lib/payload';
import type { Category } from '@/payload-types';
export const metadata: Metadata = {
title: 'Blog — Axil Accountants',
description: 'Accounting tips, tax guides and financial insights for UK business owners.',
};
const CATEGORY_COLOR: Record<string, 'green' | 'blue'> = {
Tax: 'green',
Payroll: 'green',
HMRC: 'green',
Finance: 'blue',
Business: 'blue',
};
function formatDate(iso: string | null | undefined): string {
if (!iso) return '';
return new Date(iso).toLocaleDateString('en-GB', {
day: 'numeric',
month: 'short',
year: 'numeric',
});
}
export default async function BlogPage() {
const posts = await getPublishedPosts(20);
return (
<>
<ServerHeader />
<main>
{/* Hero */}
<section className="bg-bg relative overflow-hidden pt-32 pb-20">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_60%_50%_at_50%_0%,var(--color-emerald-mist)_0%,transparent_70%)]" />
<div className="relative mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
<FadeIn>
<div className="mx-auto max-w-3xl text-center">
<p className="text-emerald mb-3 text-sm font-semibold tracking-widest uppercase">
Insights
</p>
<h1 className="font-display text-charcoal mb-6 text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
Practical insights for UK business owners
</h1>
<p className="text-muted text-xl leading-relaxed">
Tax guides, accounting tips and financial strategies written in plain English by
our team of qualified accountants.
</p>
</div>
</FadeIn>
</div>
</section>
{/* Posts grid */}
<section className="bg-white py-20">
<div className="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
{posts.length === 0 && (
<p className="text-muted py-20 text-center text-lg">
No posts published yet check back soon.
</p>
)}
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{posts.map((post, i) => {
const categoryName =
post.category && typeof post.category === 'object'
? (post.category as Category).name
: null;
const color = CATEGORY_COLOR[categoryName ?? ''] ?? 'green';
return (
<FadeIn key={post.slug} delay={i * 0.07}>
<Link href={`/blog/${post.slug}`} className="group block h-full">
<SpotlightCard
className="flex h-full flex-col p-6 transition-transform duration-300 group-hover:-translate-y-1"
glowColor="green"
customSize
>
<div className="mb-4 flex items-center justify-between">
<Tag variant={color}>{categoryName ?? 'Article'}</Tag>
{post.readingTime && (
<span className="text-muted text-xs">{post.readingTime} min read</span>
)}
</div>
<h2 className="font-display text-charcoal group-hover:text-emerald mb-3 text-lg leading-snug font-semibold transition-colors">
{post.title}
</h2>
<p className="text-muted mb-4 flex-1 text-sm leading-relaxed">
{post.excerpt}
</p>
<div className="flex items-center justify-between border-t border-black/6 pt-4">
<span className="text-muted text-xs">{formatDate(post.publishedAt)}</span>
<span className="text-emerald text-xs font-semibold">Read more </span>
</div>
</SpotlightCard>
</Link>
</FadeIn>
);
})}
</div>
</div>
</section>
{/* Newsletter CTA */}
<section className="bg-charcoal py-20">
<div className="mx-auto max-w-[1440px] px-4 text-center sm:px-6 lg:px-8 xl:px-16">
<FadeIn>
<h2 className="font-display mb-4 text-3xl font-bold text-white sm:text-4xl">
Get accounting insights in your inbox
</h2>
<p className="mx-auto mb-8 max-w-xl text-lg text-white/70">
Monthly tax tips, deadline reminders and practical guides free, no spam.
</p>
<Button size="lg" trailingArrow href="/contact">
Subscribe to newsletter
</Button>
</FadeIn>
</div>
</section>
</main>
<ServerFooter />
</>
);
}

View file

@ -1,45 +0,0 @@
import type { Metadata } from 'next';
import { Inter, DM_Mono } from 'next/font/google';
import { SmoothScrollProvider } from '@/components/providers/SmoothScrollProvider';
import '../globals.css';
const inter = Inter({
variable: '--font-inter',
subsets: ['latin'],
display: 'swap',
});
const dmMono = DM_Mono({
variable: '--font-dm-mono',
weight: ['400', '500'],
subsets: ['latin'],
display: 'swap',
});
export const metadata: Metadata = {
title: 'Axil Accountants — Smart Accounting for Growing British Businesses',
description:
'ICAEW-certified accountants offering bookkeeping, tax returns, payroll and VAT services for UK businesses. Fixed monthly fees, dedicated account manager.',
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<head>
{/* Satoshi variable font — Fontshare CDN */}
<link rel="preconnect" href="https://api.fontshare.com" />
<link
href="https://api.fontshare.com/v2/css?f[]=satoshi@1,900,700,500,400&display=swap"
rel="stylesheet"
/>
</head>
<body className={`${inter.variable} ${dmMono.variable} antialiased`}>
<SmoothScrollProvider>{children}</SmoothScrollProvider>
</body>
</html>
);
}

View file

@ -1,46 +0,0 @@
import { ServerHeader } from '@/components/layout/ServerHeader';
import { ServerFooter } from '@/components/layout/ServerFooter';
import { HeroSection } from '@/components/sections/home/HeroSection';
import { PainPointsSection } from '@/components/sections/home/PainPointsSection';
import { SolutionSection } from '@/components/sections/home/SolutionSection';
import { ServicesSection } from '@/components/sections/home/ServicesSection';
import { WhyAxilSection } from '@/components/sections/home/WhyAxilSection';
import { TestimonialsSection } from '@/components/sections/home/TestimonialsSection';
import { AudienceSection } from '@/components/sections/home/AudienceSection';
import { ProcessSection } from '@/components/sections/home/ProcessSection';
import { BlogPreviewSection } from '@/components/sections/home/BlogPreviewSection';
import { FinalCTASection } from '@/components/sections/home/FinalCTASection';
import {
getPublishedServices,
getFeaturedTestimonials,
getPublishedPosts,
getHomePage,
} from '@/lib/payload';
export default async function Home() {
const [services, testimonials, posts, homePage] = await Promise.all([
getPublishedServices(4),
getFeaturedTestimonials(8),
getPublishedPosts(3),
getHomePage(),
]);
return (
<>
<ServerHeader />
<main>
<HeroSection data={homePage?.hero} />
<PainPointsSection data={homePage?.painPoints} />
<SolutionSection data={homePage?.solution} />
<ServicesSection services={services} />
<WhyAxilSection data={homePage?.whyAxil} />
<TestimonialsSection testimonials={testimonials} />
<AudienceSection data={homePage?.audience} />
<ProcessSection data={homePage?.process} />
<BlogPreviewSection posts={posts} />
<FinalCTASection data={homePage?.finalCta} />
</main>
<ServerFooter />
</>
);
}

View file

@ -1,6 +1,6 @@
import type { Metadata } from 'next';
import { ServerHeader } from '@/components/layout/ServerHeader';
import { ServerFooter } from '@/components/layout/ServerFooter';
import { Header } from '@/components/layout/Header';
import { Footer } from '@/components/layout/Footer';
import { BeamButton } from '@/components/ui/BeamButton';
import { FadeIn } from '@/components/ui/FadeIn';
import { SpotlightCard } from '@/components/ui/SpotlightCard';
@ -69,7 +69,7 @@ const STATS = [
export default function AboutPage() {
return (
<>
<ServerHeader />
<Header />
<main>
{/* Hero */}
<section className="bg-bg relative overflow-hidden pt-32 pb-20">
@ -275,7 +275,7 @@ export default function AboutPage() {
</div>
</section>
</main>
<ServerFooter />
<Footer />
</>
);
}

View file

@ -0,0 +1,438 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { Header } from '@/components/layout/Header';
import { Footer } from '@/components/layout/Footer';
import { Button } from '@/components/ui/Button';
import { FadeIn } from '@/components/ui/FadeIn';
import { SpotlightCard } from '@/components/ui/SpotlightCard';
import { Tag } from '@/components/ui/Tag';
const POSTS: Record<
string,
{
slug: string;
category: string;
categoryColor: 'green' | 'blue';
date: string;
readTime: string;
title: string;
excerpt: string;
author: string;
authorRole: string;
authorBg: string;
content: { heading: string; body: string }[];
}
> = {
'hmrc-making-tax-digital-guide': {
slug: 'hmrc-making-tax-digital-guide',
category: 'Tax',
categoryColor: 'green',
date: '12 Feb 2025',
readTime: '6 min',
title: 'Making Tax Digital: Everything UK Business Owners Need to Know',
excerpt:
"MTD is now mandatory for most VAT-registered businesses. Here's what it means for you and how to make sure you're compliant.",
author: 'Priya Sharma',
authorRole: 'Head of Tax · ACCA · CTA',
authorBg: '3CC68A',
content: [
{
heading: 'What is Making Tax Digital?',
body: "Making Tax Digital (MTD) is HMRC's initiative to move the UK tax system online. The goal is to reduce errors, improve record-keeping, and make it easier for businesses to stay on top of their tax affairs. For most VAT-registered businesses with a taxable turnover above the VAT threshold (£85,000), MTD for VAT has been mandatory since April 2019.",
},
{
heading: 'Who Does MTD Affect?',
body: 'From April 2022, MTD for VAT was extended to all VAT-registered businesses — even those voluntarily registered below the threshold. If you submit a VAT return, you must use MTD-compatible software to keep digital records and submit returns. MTD for Income Tax Self Assessment (ITSA) is the next phase, expected to roll out from April 2026 for sole traders and landlords with income over £50,000.',
},
{
heading: 'What Do You Need to Do?',
body: 'The key requirements are simple: keep digital records of all VAT transactions, use MTD-compatible software (such as Xero, QuickBooks, Sage or FreeAgent), and submit your VAT returns directly through that software. Paper records and manual spreadsheets alone are no longer acceptable — though a bridging solution that converts spreadsheet data to digital submissions is permitted.',
},
{
heading: 'Penalties for Non-Compliance',
body: "HMRC introduced a new points-based penalty system in January 2023. Each late submission earns one penalty point. When you hit a threshold (4 points for quarterly filers), you'll receive a £200 fine — plus £200 for every subsequent late submission. Separate penalties apply for late payment of VAT: 2% of unpaid VAT after 15 days, rising to 4% after 30 days, then 4% per annum thereafter.",
},
{
heading: 'How Axil Can Help',
body: "At Axil, we set up and manage MTD-compliant bookkeeping for all our clients as standard. We use cloud accounting software to keep your records up to date and handle VAT submissions on your behalf — so you're always compliant and never miss a deadline. If you're not sure whether you're currently compliant, get in touch for a free review.",
},
],
},
'limited-company-vs-sole-trader': {
slug: 'limited-company-vs-sole-trader',
category: 'Business',
categoryColor: 'blue',
date: '28 Jan 2025',
readTime: '8 min',
title: 'Limited Company vs Sole Trader: Which Is Right for Your Business?',
excerpt:
'One of the most common questions from new business owners. We break down the tax implications, liability and admin for both structures.',
author: 'James Whitfield',
authorRole: 'Founder & Senior Accountant · ICAEW · ACA',
authorBg: '1B9AD6',
content: [
{
heading: 'The Core Difference',
body: 'A sole trader is the simplest business structure: you and your business are legally the same entity. A limited company, on the other hand, is a separate legal entity from its owners (shareholders and directors). This distinction has far-reaching implications for tax, liability, and administration.',
},
{
heading: 'Tax: Sole Trader',
body: "As a sole trader, your profits are taxed as personal income through Self Assessment. You'll pay Income Tax at 20%, 40% or 45% depending on your earnings, plus Class 2 and Class 4 National Insurance contributions. The simplicity is a genuine advantage at lower income levels — you'll spend less on accountancy fees and have fewer filing obligations.",
},
{
heading: 'Tax: Limited Company',
body: 'A limited company pays Corporation Tax on its profits — currently 19% for profits up to £50,000, rising to 25% for profits over £250,000 (with marginal relief in between). Directors typically pay themselves a combination of salary (up to the NI threshold) and dividends, which are taxed at lower rates than income. This can result in significant tax savings once profits exceed roughly £30,000£40,000.',
},
{
heading: 'Liability',
body: "This is where limited companies offer a major advantage. As a sole trader, you're personally liable for all business debts — creditors can pursue your personal assets (home, savings) if the business fails. A limited company's liability is limited to the share capital, protecting your personal assets. Note that banks often require personal guarantees on business loans, which can reduce this protection.",
},
{
heading: 'Administration',
body: 'Sole trader: one Self Assessment tax return per year, straightforward record-keeping. Limited company: annual accounts filed with Companies House, Confirmation Statement, Corporation Tax return, potentially VAT returns and payroll (PAYE). The admin burden is genuinely higher — which is why a good accountant is especially valuable for limited company owners.',
},
{
heading: 'Our Recommendation',
body: "For most self-employed people earning under £30,000, sole trader is the most practical choice. Above £50,000 in consistent annual profit, the tax savings of a limited company usually outweigh the additional admin costs. Between those figures, it depends on your circumstances, risk tolerance, and growth plans. Book a free consultation and we'll run the numbers for your specific situation.",
},
],
},
'self-assessment-tips-2025': {
slug: 'self-assessment-tips-2025',
category: 'Tax',
categoryColor: 'green',
date: '10 Jan 2025',
readTime: '7 min',
title: '10 Self-Assessment Tips That Could Save You Thousands',
excerpt:
"Filing your self-assessment tax return? These allowances and deductions are commonly missed — even by people who've been filing for years.",
author: 'Priya Sharma',
authorRole: 'Head of Tax · ACCA · CTA',
authorBg: '3CC68A',
content: [
{
heading: '1. Claim Your Full Trading Allowance',
body: "The £1,000 trading allowance lets you earn up to £1,000 from self-employment without paying tax on it. If your gross trading income is under £1,000, you don't even need to file a return for that income. If it's over £1,000, you can simply deduct £1,000 instead of calculating actual expenses — useful if your expenses are modest.",
},
{
heading: "2. Don't Forget the Marriage Allowance",
body: "If you're married or in a civil partnership and one of you earns below the Personal Allowance (£12,570), you can transfer up to £1,260 of unused allowance to the higher earner — potentially saving up to £252 per year. You can also backdate claims up to 4 years.",
},
{
heading: '3. Claim All Allowable Business Expenses',
body: 'Many self-employed people underclaim expenses. Allowable costs include: office costs (stationery, phone bills), travel expenses (not commuting), clothing (uniforms only), staff costs, advertising and marketing, professional fees (accountants, solicitors), and premises costs. If you work from home, you can claim a proportion of household bills.',
},
{
heading: '4. Use Your Capital Gains Tax Allowance',
body: "The Capital Gains Tax annual exempt amount is £3,000 for 2024/25. If you've sold assets (shares, property that isn't your main home, business assets) and made a profit, make sure you're factoring in this allowance. Couples can also effectively double their allowance by transferring assets between spouses before selling.",
},
{
heading: '5. Pension Contributions Reduce Your Tax Bill',
body: "Pension contributions get tax relief at your marginal rate. If you're a higher-rate taxpayer, a £1,000 pension contribution only costs you £600 after tax relief. You can contribute up to 100% of your earnings (up to £60,000 per year) and carry forward unused allowance from the previous 3 tax years.",
},
{
heading: '6. Check Your National Insurance Record',
body: 'Gaps in your NI record reduce your State Pension entitlement. You need 35 qualifying years for the full new State Pension (currently £221.20/week). Check your record at the Government Gateway and consider making voluntary Class 3 contributions to fill gaps — this is often an excellent return on investment.',
},
],
},
'r-and-d-tax-credits-explained': {
slug: 'r-and-d-tax-credits-explained',
category: 'Tax',
categoryColor: 'blue',
date: '15 Dec 2024',
readTime: '5 min',
title: 'R&D Tax Credits: Are You Leaving Money on the Table?',
excerpt:
'Many UK businesses qualify for R&D tax credits without realising it. Find out if your business is eligible and how much you could claim.',
author: 'James Whitfield',
authorRole: 'Founder & Senior Accountant · ICAEW · ACA',
authorBg: '1B9AD6',
content: [
{
heading: 'What Are R&D Tax Credits?',
body: 'Research and Development (R&D) tax credits are a government incentive designed to reward UK companies that invest in innovation. If your business has worked to develop new products, processes or services — or improve existing ones — you may be able to claim a significant tax saving or cash payment from HMRC.',
},
{
heading: "You Probably Qualify (Even If You Don't Think So)",
body: "The definition of R&D for tax purposes is broader than most people realise. You don't need a laboratory or a team of scientists. If your business has worked to overcome a technical challenge where the solution wasn't readily available — even in software development, manufacturing, food production or construction — that may well qualify.",
},
{
heading: 'How Much Can You Claim?',
body: "From April 2024, most companies fall under the merged R&D scheme. You can claim an enhanced deduction of 186% of qualifying R&D costs — meaning £100 of R&D spending becomes £186 of deductible costs. Loss-making companies can surrender losses for a cash credit at 10% (or up to 14.5% if you're an R&D-intensive SME with 30%+ qualifying expenditure).",
},
{
heading: 'What Costs Qualify?',
body: 'Qualifying costs include: staff costs (salaries, NI, pension contributions) for those directly involved in R&D; subcontractor costs (at 65%); software and consumables used directly in the R&D process; and externally provided workers. Your accountant can help identify all claimable costs — many businesses significantly underestimate their eligible expenditure.',
},
{
heading: 'How to Claim',
body: "R&D tax credits are claimed through your Corporation Tax return (CT600). You'll need to submit a technical narrative describing the R&D activities, plus a financial breakdown. HMRC scrutiny has increased since 2023, so it's important to have proper documentation. At Axil, we've successfully submitted R&D claims across multiple sectors — get in touch to see if you qualify.",
},
],
},
'payroll-auto-enrolment-guide': {
slug: 'payroll-auto-enrolment-guide',
category: 'Payroll',
categoryColor: 'green',
date: '2 Dec 2024',
readTime: '6 min',
title: 'Auto-Enrolment in 2025: A Plain English Guide for Employers',
excerpt:
"Auto-enrolment pension duties catch many small employers off guard. Here's everything you need to know to stay compliant.",
author: 'Tom Aldridge',
authorRole: 'Payroll & VAT Specialist · ATT',
authorBg: '27A870',
content: [
{
heading: 'What Is Auto-Enrolment?',
body: 'Auto-enrolment is the legal requirement for employers to automatically enrol eligible workers into a workplace pension scheme and make employer contributions. Introduced in 2012, it now applies to virtually all employers in the UK — even if you only have one member of staff.',
},
{
heading: 'Who Must Be Enrolled?',
body: 'You must automatically enrol employees who are: aged between 22 and State Pension age, earning above £10,000 per year, and working in the UK. Workers earning between £6,240 and £10,000 can opt in (and you must still contribute if they do). Workers earning under £6,240 can join voluntarily but employer contributions are not required.',
},
{
heading: 'Minimum Contribution Rates',
body: 'The current minimum total contribution is 8% of qualifying earnings (the band between £6,240 and £50,270 per year). At minimum, you as the employer must contribute at least 3%, with the employee making up the remainder (at least 5%). You can contribute more if you choose — and many employers offer enhanced contributions as a staff benefit.',
},
{
heading: 'Choosing a Pension Scheme',
body: "You'll need to choose a pension scheme that meets the qualifying criteria. NEST (National Employment Savings Trust) is the government-backed scheme and accepts all employers. Other popular options include The People's Pension, Aviva, and Royal London. The scheme must meet minimum standards set by The Pensions Regulator.",
},
{
heading: 'Re-Enrolment and Declaration of Compliance',
body: 'Every three years, you must re-enrol any eligible workers who have opted out — this is called re-enrolment. You must also submit a Declaration of Compliance to The Pensions Regulator within 5 months of your re-enrolment date. Missing this can result in fixed penalty notices starting at £400, rising to £10,000 per day for larger employers.',
},
],
},
'cash-flow-tips-small-business': {
slug: 'cash-flow-tips-small-business',
category: 'Finance',
categoryColor: 'blue',
date: '18 Nov 2024',
readTime: '5 min',
title: '7 Cash Flow Strategies Every Small Business Should Use',
excerpt:
'Cash flow problems are the number one reason small businesses fail. These practical strategies will help you stay ahead of the curve.',
author: 'James Whitfield',
authorRole: 'Founder & Senior Accountant · ICAEW · ACA',
authorBg: '1B9AD6',
content: [
{
heading: '1. Invoice Immediately and Chase Promptly',
body: "The moment you complete work, send the invoice. Don't batch invoices at month-end — every day of delay is cash you don't have. Set up automatic payment reminders at 7, 14 and 30 days overdue. Most late payments are the result of invoices falling through the cracks rather than deliberate non-payment.",
},
{
heading: '2. Shorten Your Payment Terms',
body: 'Many businesses default to 30-day payment terms out of habit. Consider switching to 14 days — or even 7 days for smaller clients. Better still, require payment upfront for new clients and smaller projects. Most clients will accept whatever terms you set from the outset; the key is to be clear and consistent.',
},
{
heading: '3. Build a 3-Month Cash Reserve',
body: 'A cash buffer equal to 3 months of operating expenses gives you breathing room to handle slow-paying clients, seasonal dips, or unexpected costs without crisis. Build it gradually by setting aside a percentage of every payment received. Treat it as an operating cost, not savings.',
},
{
heading: '4. Negotiate Extended Terms With Suppliers',
body: 'While you work to get paid faster, you should simultaneously try to pay suppliers more slowly (within your agreed terms). If you currently pay on 14 days, ask your main suppliers if 30 or 45 days is possible. This stretches your cash cycle without damaging relationships.',
},
{
heading: '5. Use a Cash Flow Forecast',
body: "A simple 13-week rolling cash flow forecast tells you exactly when you'll be tight — and gives you time to act before you're in trouble. Most cloud accounting packages have built-in forecasting tools. At Axil, we build and maintain cash flow forecasts for our clients as part of our standard service.",
},
{
heading: '6. Review Your Pricing',
body: "Underpricing is one of the most common causes of cash flow problems. If your margins are thin, there's no buffer when costs rise or revenue dips. Review your pricing annually. Factor in all costs including your own time, overheads, tax, and a reasonable profit margin — not just direct costs.",
},
{
heading: '7. Consider Invoice Finance',
body: "If you work with large clients on long payment cycles, invoice finance (factoring or discounting) lets you unlock up to 90% of invoice value immediately, with the finance company collecting payment on your behalf. It's not right for every business, but for B2B service businesses with slow-paying clients, it can transform cash flow.",
},
],
},
};
const RELATED_POSTS = [
{
slug: 'hmrc-making-tax-digital-guide',
category: 'Tax',
color: 'green' as const,
title: 'Making Tax Digital: Everything UK Business Owners Need to Know',
date: '12 Feb 2025',
},
{
slug: 'limited-company-vs-sole-trader',
category: 'Business',
color: 'blue' as const,
title: 'Limited Company vs Sole Trader: Which Is Right for Your Business?',
date: '28 Jan 2025',
},
{
slug: 'self-assessment-tips-2025',
category: 'Tax',
color: 'green' as const,
title: '10 Self-Assessment Tips That Could Save You Thousands',
date: '10 Jan 2025',
},
];
export async function generateStaticParams() {
return Object.keys(POSTS).map((slug) => ({ slug }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = POSTS[slug];
if (!post) return { title: 'Post Not Found' };
return {
title: `${post.title} — Axil Accountants Blog`,
description: post.excerpt,
};
}
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = POSTS[slug];
if (!post) notFound();
const related = RELATED_POSTS.filter((p) => p.slug !== slug).slice(0, 2);
return (
<>
<Header />
<main>
{/* Hero */}
<section className="bg-bg relative overflow-hidden pt-32 pb-12">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_60%_50%_at_50%_0%,var(--color-emerald-mist)_0%,transparent_70%)]" />
<div className="relative mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
<FadeIn>
<div className="mx-auto max-w-3xl">
<Link
href="/blog"
className="text-muted hover:text-emerald mb-6 inline-flex items-center gap-1.5 text-sm font-medium transition-colors"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m15 18-6-6 6-6" />
</svg>
Back to blog
</Link>
<div className="mb-4 flex flex-wrap items-center gap-3">
<Tag variant={post.categoryColor}>{post.category}</Tag>
<span className="text-muted text-sm">{post.date}</span>
<span className="text-muted text-sm">·</span>
<span className="text-muted text-sm">{post.readTime} read</span>
</div>
<h1 className="font-display text-charcoal mb-6 text-3xl leading-tight font-bold tracking-tight sm:text-4xl lg:text-5xl">
{post.title}
</h1>
<p className="text-muted mb-8 text-xl leading-relaxed">{post.excerpt}</p>
{/* Author */}
<div className="flex items-center gap-3 border-t border-black/8 pt-6">
<img
src={`https://ui-avatars.com/api/?name=${encodeURIComponent(post.author)}&background=${post.authorBg}&color=fff&bold=true&size=80`}
alt={post.author}
width={44}
height={44}
className="size-11 rounded-full"
/>
<div>
<p className="text-charcoal text-sm font-semibold">{post.author}</p>
<p className="text-muted text-xs">{post.authorRole}</p>
</div>
</div>
</div>
</FadeIn>
</div>
</section>
{/* Article body */}
<section className="bg-white py-16">
<div className="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
<div className="mx-auto max-w-3xl">
<FadeIn>
<div className="space-y-10">
{post.content.map((section, i) => (
<div key={i}>
<h2 className="font-display text-charcoal mb-3 text-xl font-bold sm:text-2xl">
{section.heading}
</h2>
<p className="text-muted text-base leading-relaxed">{section.body}</p>
</div>
))}
</div>
</FadeIn>
{/* CTA inline */}
<FadeIn delay={0.1}>
<div className="rounded-hero border-emerald/25 bg-emerald-mist mt-14 border p-8 text-center">
<p className="font-display text-charcoal mb-2 text-xl font-bold">
Want personalised advice?
</p>
<p className="text-muted mb-5">
Book a free 30-minute consultation with one of our qualified accountants.
</p>
<Button size="lg" trailingArrow href="/contact">
Book Free Consultation
</Button>
</div>
</FadeIn>
</div>
</div>
</section>
{/* Related posts */}
{related.length > 0 && (
<section className="bg-bg py-16">
<div className="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
<FadeIn>
<h2 className="font-display text-charcoal mb-8 text-2xl font-bold">
More from the blog
</h2>
</FadeIn>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
{related.map((p, i) => (
<FadeIn key={p.slug} delay={i * 0.08}>
<Link href={`/blog/${p.slug}`} className="group block h-full">
<SpotlightCard
className="h-full p-6 transition-transform duration-300 group-hover:-translate-y-1"
glowColor="green"
customSize
>
<div className="mb-3 flex items-center gap-3">
<Tag variant={p.color}>{p.category}</Tag>
<span className="text-muted text-xs">{p.date}</span>
</div>
<h3 className="font-display text-charcoal group-hover:text-emerald text-base leading-snug font-semibold transition-colors">
{p.title}
</h3>
<p className="text-emerald mt-3 text-xs font-semibold">Read more </p>
</SpotlightCard>
</Link>
</FadeIn>
))}
</div>
</div>
</section>
)}
</main>
<Footer />
</>
);
}

158
src/app/blog/page.tsx Normal file
View file

@ -0,0 +1,158 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { Header } from '@/components/layout/Header';
import { Footer } from '@/components/layout/Footer';
import { Button } from '@/components/ui/Button';
import { FadeIn } from '@/components/ui/FadeIn';
import { SpotlightCard } from '@/components/ui/SpotlightCard';
import { Tag } from '@/components/ui/Tag';
export const metadata: Metadata = {
title: 'Blog — Axil Accountants',
description: 'Accounting tips, tax guides and financial insights for UK business owners.',
};
const POSTS = [
{
slug: 'hmrc-making-tax-digital-guide',
category: 'Tax',
date: '12 Feb 2025',
title: 'Making Tax Digital: Everything UK Business Owners Need to Know',
excerpt:
"MTD is now mandatory for most VAT-registered businesses. Here's what it means for you and how to make sure you're compliant.",
readTime: '6 min',
color: 'green' as const,
},
{
slug: 'limited-company-vs-sole-trader',
category: 'Business',
date: '28 Jan 2025',
title: 'Limited Company vs Sole Trader: Which Is Right for Your Business?',
excerpt:
'One of the most common questions from new business owners. We break down the tax implications, liability and admin for both structures.',
readTime: '8 min',
color: 'blue' as const,
},
{
slug: 'self-assessment-tips-2025',
category: 'Tax',
date: '10 Jan 2025',
title: '10 Self-Assessment Tips That Could Save You Thousands',
excerpt:
"Filing your self-assessment tax return? These allowances and deductions are commonly missed — even by people who've been filing for years.",
readTime: '7 min',
color: 'green' as const,
},
{
slug: 'r-and-d-tax-credits-explained',
category: 'Tax',
date: '15 Dec 2024',
title: 'R&D Tax Credits: Are You Leaving Money on the Table?',
excerpt:
'Many UK businesses qualify for R&D tax credits without realising it. Find out if your business is eligible and how much you could claim.',
readTime: '5 min',
color: 'blue' as const,
},
{
slug: 'payroll-auto-enrolment-guide',
category: 'Payroll',
date: '2 Dec 2024',
title: 'Auto-Enrolment in 2025: A Plain English Guide for Employers',
excerpt:
"Auto-enrolment pension duties catch many small employers off guard. Here's everything you need to know to stay compliant.",
readTime: '6 min',
color: 'green' as const,
},
{
slug: 'cash-flow-tips-small-business',
category: 'Finance',
date: '18 Nov 2024',
title: '7 Cash Flow Strategies Every Small Business Should Use',
excerpt:
'Cash flow problems are the number one reason small businesses fail. These practical strategies will help you stay ahead of the curve.',
readTime: '5 min',
color: 'blue' as const,
},
];
export default function BlogPage() {
return (
<>
<Header />
<main>
{/* Hero */}
<section className="bg-bg relative overflow-hidden pt-32 pb-20">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_60%_50%_at_50%_0%,var(--color-emerald-mist)_0%,transparent_70%)]" />
<div className="relative mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
<FadeIn>
<div className="mx-auto max-w-3xl text-center">
<p className="text-emerald mb-3 text-sm font-semibold tracking-widest uppercase">
Insights
</p>
<h1 className="font-display text-charcoal mb-6 text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
Practical insights for UK business owners
</h1>
<p className="text-muted text-xl leading-relaxed">
Tax guides, accounting tips and financial strategies written in plain English by
our team of qualified accountants.
</p>
</div>
</FadeIn>
</div>
</section>
{/* Posts grid */}
<section className="bg-white py-20">
<div className="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{POSTS.map((post, i) => (
<FadeIn key={post.slug} delay={i * 0.07}>
<Link href={`/blog/${post.slug}`} className="group block h-full">
<SpotlightCard
className="flex h-full flex-col p-6 transition-transform duration-300 group-hover:-translate-y-1"
glowColor="green"
customSize
>
<div className="mb-4 flex items-center justify-between">
<Tag variant={post.color}>{post.category}</Tag>
<span className="text-muted text-xs">{post.readTime} read</span>
</div>
<h2 className="font-display text-charcoal group-hover:text-emerald mb-3 text-lg leading-snug font-semibold transition-colors">
{post.title}
</h2>
<p className="text-muted mb-4 flex-1 text-sm leading-relaxed">
{post.excerpt}
</p>
<div className="flex items-center justify-between border-t border-black/6 pt-4">
<span className="text-muted text-xs">{post.date}</span>
<span className="text-emerald text-xs font-semibold">Read more </span>
</div>
</SpotlightCard>
</Link>
</FadeIn>
))}
</div>
</div>
</section>
{/* Newsletter CTA */}
<section className="bg-charcoal py-20">
<div className="mx-auto max-w-[1440px] px-4 text-center sm:px-6 lg:px-8 xl:px-16">
<FadeIn>
<h2 className="font-display mb-4 text-3xl font-bold text-white sm:text-4xl">
Get accounting insights in your inbox
</h2>
<p className="mx-auto mb-8 max-w-xl text-lg text-white/70">
Monthly tax tips, deadline reminders and practical guides free, no spam.
</p>
<Button size="lg" trailingArrow href="/contact">
Subscribe to newsletter
</Button>
</FadeIn>
</div>
</section>
</main>
<Footer />
</>
);
}

View file

@ -1,6 +1,6 @@
import type { Metadata } from 'next';
import { ServerHeader } from '@/components/layout/ServerHeader';
import { ServerFooter } from '@/components/layout/ServerFooter';
import { Header } from '@/components/layout/Header';
import { Footer } from '@/components/layout/Footer';
import { Button } from '@/components/ui/Button';
import { FadeIn } from '@/components/ui/FadeIn';
import { SpotlightCard } from '@/components/ui/SpotlightCard';
@ -96,7 +96,7 @@ const CONTACT_DETAILS = [
export default function ContactPage() {
return (
<>
<ServerHeader />
<Header />
<main>
{/* Hero */}
<section className="bg-bg relative overflow-hidden pt-32 pb-16">
@ -196,7 +196,7 @@ export default function ContactPage() {
</div>
</section>
</main>
<ServerFooter />
<Footer />
</>
);
}

View file

@ -1,7 +1,7 @@
import type { Metadata } from 'next';
import Image from 'next/image';
import { ServerHeader } from '@/components/layout/ServerHeader';
import { ServerFooter } from '@/components/layout/ServerFooter';
import { Header } from '@/components/layout/Header';
import { Footer } from '@/components/layout/Footer';
import { Button } from '@/components/ui/Button';
import { BeamButton } from '@/components/ui/BeamButton';
import { FadeIn } from '@/components/ui/FadeIn';
@ -148,7 +148,7 @@ const WHY_ITEMS = [
export default function CoursesPage() {
return (
<>
<ServerHeader />
<Header />
<main>
{/* Hero */}
<section className="bg-bg relative overflow-hidden pt-32 pb-20">
@ -325,7 +325,7 @@ export default function CoursesPage() {
</div>
</section>
</main>
<ServerFooter />
<Footer />
</>
);
}

View file

@ -1,5 +1,4 @@
@import 'tailwindcss';
@plugin '@tailwindcss/typography';
/* ============================================================
DESIGN TOKENS Axil Accountants

View file

@ -1,8 +1,45 @@
import type { ReactNode } from 'react';
import type { Metadata } from 'next';
import { Inter, DM_Mono } from 'next/font/google';
import { SmoothScrollProvider } from '@/components/providers/SmoothScrollProvider';
import './globals.css';
// Root layout is intentionally minimal — html/body are provided by child layouts:
// - (site)/layout.tsx for website pages
// - (payload)/admin/[[...segments]]/layout.tsx for Payload admin
export default function RootLayout({ children }: { children: ReactNode }) {
return children as React.ReactElement;
const inter = Inter({
variable: '--font-inter',
subsets: ['latin'],
display: 'swap',
});
const dmMono = DM_Mono({
variable: '--font-dm-mono',
weight: ['400', '500'],
subsets: ['latin'],
display: 'swap',
});
export const metadata: Metadata = {
title: 'Axil Accountants — Smart Accounting for Growing British Businesses',
description:
'ICAEW-certified accountants offering bookkeeping, tax returns, payroll and VAT services for UK businesses. Fixed monthly fees, dedicated account manager.',
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<head>
{/* Satoshi variable font — Fontshare CDN */}
<link rel="preconnect" href="https://api.fontshare.com" />
<link
href="https://api.fontshare.com/v2/css?f[]=satoshi@1,900,700,500,400&display=swap"
rel="stylesheet"
/>
</head>
<body className={`${inter.variable} ${dmMono.variable} antialiased`}>
<SmoothScrollProvider>{children}</SmoothScrollProvider>
</body>
</html>
);
}

33
src/app/page.tsx Normal file
View file

@ -0,0 +1,33 @@
import { Header } from '@/components/layout/Header';
import { Footer } from '@/components/layout/Footer';
import { HeroSection } from '@/components/sections/home/HeroSection';
import { PainPointsSection } from '@/components/sections/home/PainPointsSection';
import { SolutionSection } from '@/components/sections/home/SolutionSection';
import { ServicesSection } from '@/components/sections/home/ServicesSection';
import { WhyAxilSection } from '@/components/sections/home/WhyAxilSection';
import { TestimonialsSection } from '@/components/sections/home/TestimonialsSection';
import { AudienceSection } from '@/components/sections/home/AudienceSection';
import { ProcessSection } from '@/components/sections/home/ProcessSection';
import { BlogPreviewSection } from '@/components/sections/home/BlogPreviewSection';
import { FinalCTASection } from '@/components/sections/home/FinalCTASection';
export default function Home() {
return (
<>
<Header />
<main>
<HeroSection />
<PainPointsSection />
<SolutionSection />
<ServicesSection />
<WhyAxilSection />
<TestimonialsSection />
<AudienceSection />
<ProcessSection />
<BlogPreviewSection />
<FinalCTASection />
</main>
<Footer />
</>
);
}

View file

@ -1,25 +1,24 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import Link from 'next/link';
import { ServerHeader } from '@/components/layout/ServerHeader';
import { ServerFooter } from '@/components/layout/ServerFooter';
import { Header } from '@/components/layout/Header';
import { Footer } from '@/components/layout/Footer';
import { Button } from '@/components/ui/Button';
import { FadeIn } from '@/components/ui/FadeIn';
import { SpotlightCard } from '@/components/ui/SpotlightCard';
import { CheckCircleIcon } from '@/components/ui/icons';
import { getPublishedServices, getServiceBySlugCached } from '@/lib/payload';
import { convertLexicalToPlaintext } from '@payloadcms/richtext-lexical/plaintext';
type DisplayService = {
title: string;
tagline: string;
intro: string;
includes: string[];
steps: { step: number; title: string; description: string }[];
faq: { q: string; a: string }[];
};
const SERVICES_FALLBACK: Record<string, DisplayService> = {
const SERVICES: Record<
string,
{
title: string;
tagline: string;
intro: string;
includes: string[];
benefits: { title: string; desc: string }[];
faq: { q: string; a: string }[];
}
> = {
bookkeeping: {
title: 'Bookkeeping',
tagline: 'Accurate records, zero stress',
@ -33,23 +32,18 @@ const SERVICES_FALLBACK: Record<string, DisplayService> = {
'Monthly management accounts',
'Real-time financial dashboard access',
],
steps: [
benefits: [
{
step: 1,
title: 'Save 10+ hours per month',
description:
'Stop wrestling with spreadsheets. We handle every transaction so you never have to.',
desc: 'Stop wrestling with spreadsheets. We handle every transaction so you never have to.',
},
{
step: 2,
title: 'Always audit-ready',
description: 'Clean, HMRC-compliant records mean no panic when a filing deadline arrives.',
desc: 'Clean, HMRC-compliant records mean no panic when a filing deadline arrives.',
},
{
step: 3,
title: 'Real-time visibility',
description:
'Log in any time to see exactly how your business is performing, with no surprises.',
desc: 'Log in any time to see exactly how your business is performing, with no surprises.',
},
],
faq: [
@ -80,24 +74,18 @@ const SERVICES_FALLBACK: Record<string, DisplayService> = {
'HMRC correspondence management',
'Tax planning & year-end strategy',
],
steps: [
benefits: [
{
step: 1,
title: 'Never miss a deadline',
description:
"We track every HMRC deadline and file well in advance — you'll never pay a late filing penalty.",
desc: "We track every HMRC deadline and file well in advance — you'll never pay a late filing penalty.",
},
{
step: 2,
title: 'Maximum allowances',
description:
'Our tax specialists review every available allowance and relief to legally minimise your tax bill.',
desc: 'Our tax specialists review every available allowance and relief to legally minimise your tax bill.',
},
{
step: 3,
title: 'HMRC as a proxy',
description:
'We handle all correspondence with HMRC. You never have to speak to them directly unless you want to.',
desc: 'We handle all correspondence with HMRC. You never have to speak to them directly unless you want to.',
},
],
faq: [
@ -128,24 +116,18 @@ const SERVICES_FALLBACK: Record<string, DisplayService> = {
'Payslips for every employee',
'P60s, P45s and P11Ds',
],
steps: [
benefits: [
{
step: 1,
title: 'Zero errors',
description:
'Manual payroll errors are costly. Our systems double-check every calculation before submission.',
desc: 'Manual payroll errors are costly. Our systems double-check every calculation before submission.',
},
{
step: 2,
title: 'Full compliance',
description:
'RTI submissions, auto-enrolment, NMW compliance — all handled on time, every time.',
desc: 'RTI submissions, auto-enrolment, NMW compliance — all handled on time, every time.',
},
{
step: 3,
title: 'Scales with you',
description:
'Whether you have 1 employee or 100, our payroll service scales effortlessly as your team grows.',
desc: 'Whether you have 1 employee or 100, our payroll service scales effortlessly as your team grows.',
},
],
faq: [
@ -176,24 +158,18 @@ const SERVICES_FALLBACK: Record<string, DisplayService> = {
'HMRC VAT correspondence',
'VAT registration & deregistration',
],
steps: [
benefits: [
{
step: 1,
title: 'Never miss a VAT deadline',
description:
'Late VAT returns trigger automatic penalties. We file every return well before the deadline.',
desc: 'Late VAT returns trigger automatic penalties. We file every return well before the deadline.',
},
{
step: 2,
title: 'Right scheme for your business',
description:
"Flat Rate, Cash Accounting or Standard — we make sure you're on the scheme that minimises your VAT bill.",
desc: "Flat Rate, Cash Accounting or Standard — we make sure you're on the scheme that minimises your VAT bill.",
},
{
step: 3,
title: 'MTD fully handled',
description:
"HMRC's Making Tax Digital requirements are completely managed by us. No software headaches.",
desc: "HMRC's Making Tax Digital requirements are completely managed by us. No software headaches.",
},
],
faq: [
@ -213,16 +189,8 @@ const SERVICES_FALLBACK: Record<string, DisplayService> = {
},
};
export async function generateStaticParams() {
try {
const services = await getPublishedServices(100);
const cmsSlugSet = new Set(services.map((s) => s.slug));
const fallbackSlugs = Object.keys(SERVICES_FALLBACK).filter((s) => !cmsSlugSet.has(s));
return [...services.map((s) => ({ slug: s.slug })), ...fallbackSlugs.map((s) => ({ slug: s }))];
} catch {
// DB unavailable at build time — pre-render fallback slugs only; CMS pages render on demand
return Object.keys(SERVICES_FALLBACK).map((s) => ({ slug: s }));
}
export function generateStaticParams() {
return Object.keys(SERVICES).map((slug) => ({ slug }));
}
export async function generateMetadata({
@ -231,64 +199,22 @@ export async function generateMetadata({
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const cmsSvc = await getServiceBySlugCached(slug);
if (cmsSvc) {
return {
title: cmsSvc.seo?.metaTitle ?? `${cmsSvc.title} — Axil Accountants`,
description: cmsSvc.seo?.metaDescription ?? cmsSvc.tagline ?? undefined,
};
}
const fallback = SERVICES_FALLBACK[slug];
if (!fallback) return {};
const svc = SERVICES[slug];
if (!svc) return {};
return {
title: `${fallback.title} — Axil Accountants`,
description: fallback.intro,
title: `${svc.title} — Axil Accountants`,
description: svc.intro,
};
}
function toDisplayService(slug: string): DisplayService | null {
return SERVICES_FALLBACK[slug] ?? null;
}
export default async function ServicePage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const cmsSvc = await getServiceBySlugCached(slug);
let svc: DisplayService;
if (cmsSvc) {
const intro = cmsSvc.description
? convertLexicalToPlaintext({
data: cmsSvc.description as Parameters<typeof convertLexicalToPlaintext>[0]['data'],
})
: (cmsSvc.tagline ?? '');
svc = {
title: cmsSvc.title,
tagline: cmsSvc.tagline ?? '',
intro,
includes: (cmsSvc.whatsIncluded ?? []).map((w) => w.item),
steps: (cmsSvc.howItWorks ?? []).map((s, i) => ({
step: s.step ?? i + 1,
title: s.title,
description: s.description,
})),
faq: (cmsSvc.faq ?? []).map((item) => ({
q: item.question,
a: item.answer
? convertLexicalToPlaintext({
data: item.answer as Parameters<typeof convertLexicalToPlaintext>[0]['data'],
})
: '',
})),
};
} else {
const fallback = toDisplayService(slug);
if (!fallback) notFound();
svc = fallback;
}
const svc = SERVICES[slug];
if (!svc) notFound();
return (
<>
<ServerHeader />
<Header />
<main>
{/* Hero */}
<section className="bg-bg relative overflow-hidden pt-32 pb-20">
@ -317,91 +243,83 @@ export default async function ServicePage({ params }: { params: Promise<{ slug:
</div>
</section>
{/* What's included */}
{(svc.includes.length > 0 || svc.steps.length > 0) && (
<section className="bg-white py-20">
<div className="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
<div className="grid grid-cols-1 gap-16 lg:grid-cols-2 lg:items-start">
{svc.includes.length > 0 && (
<FadeIn>
<p className="text-emerald mb-3 text-sm font-semibold tracking-widest uppercase">
What&apos;s included
</p>
<h2 className="font-display text-charcoal mb-6 text-2xl font-bold sm:text-3xl">
Everything covered, nothing extra
</h2>
<ul className="space-y-3">
{svc.includes.map((item) => (
<li key={item} className="flex items-start gap-3">
<CheckCircleIcon
size={18}
color="var(--emerald)"
className="mt-0.5 shrink-0"
/>
<span className="text-charcoal text-base">{item}</span>
</li>
))}
</ul>
</FadeIn>
)}
{svc.steps.length > 0 && (
<FadeIn delay={0.15}>
<p className="text-emerald mb-3 text-sm font-semibold tracking-widest uppercase">
Why it matters
</p>
<h2 className="font-display text-charcoal mb-6 text-2xl font-bold sm:text-3xl">
The benefits
</h2>
<div className="space-y-4">
{svc.steps.map((s) => (
<SpotlightCard key={s.title} className="p-5" glowColor="green" customSize>
<div className="mb-1 flex items-center gap-2">
<span className="bg-emerald flex size-6 items-center justify-center rounded-full text-xs font-bold text-white">
{s.step}
</span>
<h3 className="font-display text-charcoal text-base font-semibold">
{s.title}
</h3>
</div>
<p className="text-muted text-sm leading-relaxed">{s.description}</p>
</SpotlightCard>
))}
</div>
</FadeIn>
)}
</div>
</div>
</section>
)}
{/* FAQ */}
{svc.faq.length > 0 && (
<section className="bg-bg py-20">
<div className="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
{/* What&apos;s included */}
<section className="bg-white py-20">
<div className="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
<div className="grid grid-cols-1 gap-16 lg:grid-cols-2 lg:items-start">
<FadeIn>
<div className="mx-auto max-w-2xl">
<p className="text-emerald mb-3 text-sm font-semibold tracking-widest uppercase">
FAQ
</p>
<h2 className="font-display text-charcoal mb-10 text-2xl font-bold sm:text-3xl">
Common questions about {svc.title.toLowerCase()}
</h2>
<div className="space-y-4">
{svc.faq.map((item) => (
<div key={item.q} className="rounded-hero border border-black/8 bg-white p-6">
<h3 className="font-display text-charcoal mb-2 text-base font-semibold">
{item.q}
<p className="text-emerald mb-3 text-sm font-semibold tracking-widest uppercase">
What&apos;s included
</p>
<h2 className="font-display text-charcoal mb-6 text-2xl font-bold sm:text-3xl">
Everything covered, nothing extra
</h2>
<ul className="space-y-3">
{svc.includes.map((item) => (
<li key={item} className="flex items-start gap-3">
<CheckCircleIcon
size={18}
color="var(--emerald)"
className="mt-0.5 shrink-0"
/>
<span className="text-charcoal text-base">{item}</span>
</li>
))}
</ul>
</FadeIn>
<FadeIn delay={0.15}>
<p className="text-emerald mb-3 text-sm font-semibold tracking-widest uppercase">
Why it matters
</p>
<h2 className="font-display text-charcoal mb-6 text-2xl font-bold sm:text-3xl">
The benefits
</h2>
<div className="space-y-4">
{svc.benefits.map((b, i) => (
<SpotlightCard key={b.title} className="p-5" glowColor="green" customSize>
<div className="mb-1 flex items-center gap-2">
<span className="bg-emerald flex size-6 items-center justify-center rounded-full text-xs font-bold text-white">
{i + 1}
</span>
<h3 className="font-display text-charcoal text-base font-semibold">
{b.title}
</h3>
<p className="text-muted text-sm leading-relaxed">{item.a}</p>
</div>
))}
</div>
<p className="text-muted text-sm leading-relaxed">{b.desc}</p>
</SpotlightCard>
))}
</div>
</FadeIn>
</div>
</section>
)}
</div>
</section>
{/* FAQ */}
<section className="bg-bg py-20">
<div className="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
<FadeIn>
<div className="mx-auto max-w-2xl">
<p className="text-emerald mb-3 text-sm font-semibold tracking-widest uppercase">
FAQ
</p>
<h2 className="font-display text-charcoal mb-10 text-2xl font-bold sm:text-3xl">
Common questions about {svc.title.toLowerCase()}
</h2>
<div className="space-y-4">
{svc.faq.map((item) => (
<div key={item.q} className="rounded-hero border border-black/8 bg-white p-6">
<h3 className="font-display text-charcoal mb-2 text-base font-semibold">
{item.q}
</h3>
<p className="text-muted text-sm leading-relaxed">{item.a}</p>
</div>
))}
</div>
</div>
</FadeIn>
</div>
</section>
{/* CTA */}
<section className="bg-charcoal py-20">
@ -430,7 +348,7 @@ export default async function ServicePage({ params }: { params: Promise<{ slug:
</div>
</section>
</main>
<ServerFooter />
<Footer />
</>
);
}

View file

@ -1,7 +1,7 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { ServerHeader } from '@/components/layout/ServerHeader';
import { ServerFooter } from '@/components/layout/ServerFooter';
import { Header } from '@/components/layout/Header';
import { Footer } from '@/components/layout/Footer';
import { Button } from '@/components/ui/Button';
import { BeamButton } from '@/components/ui/BeamButton';
import { FadeIn } from '@/components/ui/FadeIn';
@ -13,9 +13,6 @@ import {
PayrollIcon,
VATIcon,
} from '@/components/ui/icons';
import { getPublishedServices } from '@/lib/payload';
import type { Service } from '@/payload-types';
import { convertLexicalToPlaintext } from '@payloadcms/richtext-lexical/plaintext';
export const metadata: Metadata = {
title: 'Services — Axil Accountants',
@ -23,16 +20,7 @@ export const metadata: Metadata = {
'Bookkeeping, tax returns, payroll and VAT returns for UK businesses. Fixed monthly fees, dedicated account manager.',
};
type IconKey = 'bookkeeping' | 'tax' | 'payroll' | 'vat';
const ICON_MAP = {
bookkeeping: { icon: BookkeepingIcon, color: 'bg-emerald-mist', accent: 'text-emerald' },
tax: { icon: TaxIcon, color: 'bg-blue-mist', accent: 'text-blue' },
payroll: { icon: PayrollIcon, color: 'bg-emerald-mist', accent: 'text-emerald' },
vat: { icon: VATIcon, color: 'bg-blue-mist', accent: 'text-blue' },
} as const;
const SERVICES_FALLBACK = [
const SERVICES = [
{
slug: 'bookkeeping',
icon: BookkeepingIcon,
@ -73,38 +61,17 @@ const SERVICES_FALLBACK = [
icon: VATIcon,
title: 'VAT Returns',
tagline: 'MTD-compliant filing',
desc: 'Making Tax Digital-compliant VAT returns filed on time, every quarter.',
desc: 'Making Tax Digital-compliant VAT returns filed on time, every quarter. We also advise on the right VAT scheme for your business.',
perks: ['Quarterly VAT returns', 'MTD compliance', 'VAT scheme advice', 'HMRC correspondence'],
color: 'bg-blue-mist',
accent: 'text-blue',
},
];
function toDisplayService(s: Service) {
const meta = ICON_MAP[s.icon as IconKey] ?? ICON_MAP.bookkeeping;
const desc = s.description
? convertLexicalToPlaintext({
data: s.description as Parameters<typeof convertLexicalToPlaintext>[0]['data'],
})
: (s.tagline ?? '');
return {
slug: s.slug,
icon: meta.icon,
title: s.title,
tagline: s.tagline ?? '',
desc,
perks: (s.whatsIncluded ?? []).map((w) => w.item),
color: meta.color,
accent: meta.accent,
};
}
export default async function ServicesPage() {
const cmsServices = await getPublishedServices(10);
const SERVICES = cmsServices.length ? cmsServices.map(toDisplayService) : SERVICES_FALLBACK;
export default function ServicesPage() {
return (
<>
<ServerHeader />
<Header />
<main>
{/* Hero */}
<section className="bg-bg relative overflow-hidden pt-32 pb-20">
@ -211,7 +178,7 @@ export default async function ServicesPage() {
</div>
</section>
</main>
<ServerFooter />
<Footer />
</>
);
}

View file

@ -1,32 +1,20 @@
import Link from 'next/link';
import Image from 'next/image';
import { TextHoverEffect, FooterBackgroundGradient } from '@/components/ui/TextHoverEffect';
import type { Footer as FooterType } from '@/payload-types';
type LinkItem = { label: string; href: string };
const COLUMNS_FALLBACK = [
{
heading: 'Services',
links: [
{ label: 'Bookkeeping', href: '/services/bookkeeping' },
{ label: 'Tax Returns', href: '/services/tax-returns' },
{ label: 'Payroll', href: '/services/payroll' },
{ label: 'VAT Returns', href: '/services/vat-returns' },
],
},
{
heading: 'Company',
links: [
{ label: 'About Us', href: '/about' },
{ label: 'Our Team', href: '/about#team' },
{ label: 'Blog', href: '/blog' },
{ label: 'Contact', href: '/contact' },
],
},
const SERVICES = [
{ label: 'Bookkeeping', href: '/services/bookkeeping' },
{ label: 'Tax Returns', href: '/services/tax-returns' },
{ label: 'Payroll', href: '/services/payroll' },
{ label: 'VAT Returns', href: '/services/vat-returns' },
];
const LEGAL_FALLBACK: LinkItem[] = [
const COMPANY = [
{ label: 'About Us', href: '/about' },
{ label: 'Our Team', href: '/about#team' },
{ label: 'Blog', href: '/blog' },
{ label: 'Contact', href: '/contact' },
];
const LEGAL = [
{ label: 'Privacy Policy', href: '/privacy-policy' },
{ label: 'Cookie Policy', href: '/cookie-policy' },
{ label: 'Terms of Use', href: '/terms' },
@ -60,21 +48,7 @@ function FooterCol({
);
}
export function Footer({ footerData }: { footerData?: FooterType | null }) {
const columns = footerData?.columns?.length
? footerData.columns.map((col) => ({
heading: col.heading,
links: (col.links ?? []).map((l) => ({ label: l.label, href: l.href })),
}))
: COLUMNS_FALLBACK;
const legalLinks = footerData?.legalLinks?.length
? footerData.legalLinks.map((l) => ({ label: l.label, href: l.href }))
: LEGAL_FALLBACK;
const linkedIn = footerData?.socialLinks?.linkedIn ?? 'https://linkedin.com';
const facebook = footerData?.socialLinks?.facebook ?? 'https://facebook.com';
export function Footer() {
return (
<footer className="bg-charcoal relative overflow-hidden text-white">
{/* Radial gradient overlay */}
@ -107,7 +81,7 @@ export function Footer({ footerData }: { footerData?: FooterType | null }) {
<div className="flex gap-3">
<a
href={linkedIn}
href="https://linkedin.com"
target="_blank"
rel="noopener noreferrer"
aria-label="LinkedIn"
@ -119,7 +93,7 @@ export function Footer({ footerData }: { footerData?: FooterType | null }) {
</svg>
</a>
<a
href={facebook}
href="https://facebook.com"
target="_blank"
rel="noopener noreferrer"
aria-label="Facebook"
@ -132,10 +106,9 @@ export function Footer({ footerData }: { footerData?: FooterType | null }) {
</div>
</div>
{columns.map((col) => (
<FooterCol key={col.heading} heading={col.heading} links={col.links} />
))}
<FooterCol heading="Legal" links={legalLinks} />
<FooterCol heading="Services" links={SERVICES} />
<FooterCol heading="Company" links={COMPANY} />
<FooterCol heading="Legal" links={LEGAL} />
</div>
{/* Bottom bar */}

View file

@ -10,12 +10,8 @@ import { MenuIcon, XIcon, ChevronDownIcon } from '@/components/ui/icons';
import { InteractiveMenu } from '@/components/ui/InteractiveMenu';
import { Home, Briefcase, BookOpen, Info, Mail, GraduationCap } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { Navigation } from '@/payload-types';
type NavChild = { label: string; href: string; desc: string };
type NavItem = { label: string; href: string; dropdown?: NavChild[] };
const NAV_ITEMS_FALLBACK: NavItem[] = [
const NAV_ITEMS = [
{ label: 'Home', href: '/' },
{
label: 'Services',
@ -37,18 +33,6 @@ const NAV_ITEMS_FALLBACK: NavItem[] = [
{ label: 'Contact', href: '/contact' },
];
function buildNavItems(navData: Navigation | null | undefined): NavItem[] {
if (!navData?.items?.length) return NAV_ITEMS_FALLBACK;
return navData.items.map((item) => ({
label: item.label,
href: item.href ?? '/',
dropdown:
item.isDropdown && item.children?.length
? item.children.map((c) => ({ label: c.label, href: c.href, desc: c.description ?? '' }))
: undefined,
}));
}
const DOCK_ITEMS = [
{ label: 'Home', icon: Home, href: '/' },
{ label: 'Services', icon: Briefcase, href: '/services' },
@ -73,13 +57,11 @@ function AxilLogo({ className }: { className?: string }) {
/** Nav links shared between static header and floating pill */
function NavLinks({
items,
pathname,
dropdownOpen,
setDropdownOpen,
size = 'md',
}: {
items: NavItem[];
pathname: string;
dropdownOpen: boolean;
setDropdownOpen: (v: boolean) => void;
@ -90,7 +72,7 @@ function NavLinks({
return (
<>
{items.map((item) =>
{NAV_ITEMS.map((item) =>
item.dropdown ? (
<div
key={item.label}
@ -150,8 +132,7 @@ function NavLinks({
);
}
export function Header({ navData }: { navData?: Navigation | null }) {
const navItems = buildNavItems(navData);
export function Header() {
const lastScrollY = useRef(0);
const [atTop, setAtTop] = useState(true);
const [floatVisible, setFloatVisible] = useState(false);
@ -198,7 +179,6 @@ export function Header({ navData }: { navData?: Navigation | null }) {
<nav className="hidden items-center gap-1 lg:flex">
<NavLinks
items={navItems}
pathname={pathname}
dropdownOpen={dropdownOpen}
setDropdownOpen={setDropdownOpen}
@ -246,7 +226,6 @@ export function Header({ navData }: { navData?: Navigation | null }) {
{/* Nav links */}
<nav className="hidden items-center lg:flex">
<NavLinks
items={navItems}
pathname={pathname}
dropdownOpen={dropdownOpenF}
setDropdownOpen={setDropdownOpenF}
@ -267,7 +246,7 @@ export function Header({ navData }: { navData?: Navigation | null }) {
{mobileOpen && (
<div className="fixed inset-0 z-40 flex flex-col bg-white pt-18 lg:hidden">
<nav className="flex flex-col gap-1 px-4 py-6">
{navItems.map((item) => (
{NAV_ITEMS.map((item) => (
<Link
key={item.label}
href={item.href}

View file

@ -1,7 +0,0 @@
import { getFooter } from '@/lib/payload';
import { Footer } from './Footer';
export async function ServerFooter() {
const footerData = await getFooter();
return <Footer footerData={footerData} />;
}

View file

@ -1,7 +0,0 @@
import { getNavigation } from '@/lib/payload';
import { Header } from './Header';
export async function ServerHeader() {
const navData = await getNavigation();
return <Header navData={navData} />;
}

View file

@ -1,10 +1,10 @@
import Link from 'next/link';
import { FadeIn } from '@/components/ui/FadeIn';
import { ArrowRightIcon, CheckCircleIcon } from '@/components/ui/icons';
import type { HomePage } from '@/payload-types';
const AUDIENCES_FALLBACK = [
const AUDIENCES = [
{
label: 'Sole Traders',
title: 'Sole Traders',
tagline: 'Self-assessment made simple',
body: 'From freelancers to consultants, we handle your self-assessment, expenses, and tax planning so you keep more of what you earn.',
@ -14,6 +14,7 @@ const AUDIENCES_FALLBACK = [
featured: false,
},
{
label: 'Limited Companies',
title: 'Limited Companies',
tagline: 'Full-service company accounting',
body: 'Corporation tax, payroll, dividends, R&D credits — we manage your entire financial picture and help you extract value tax-efficiently.',
@ -23,6 +24,7 @@ const AUDIENCES_FALLBACK = [
featured: true,
},
{
label: 'Startups',
title: 'Startups & Scaleups',
tagline: 'Built for fast-growing businesses',
body: 'SEIS/EIS compliance, R&D tax credits, investor-ready accounts — we understand startup finance and help you grow without financial friction.',
@ -33,34 +35,22 @@ const AUDIENCES_FALLBACK = [
},
];
export function AudienceSection({ data }: { data?: HomePage['audience'] | null }) {
const overline = data?.overline ?? 'Built for businesses like yours';
const headline = data?.headline ?? 'Who we work with';
const audiences = data?.cards?.length
? data.cards.map((c) => ({
title: c.title,
tagline: c.tagline ?? '',
body: c.body ?? '',
perks: (c.perks ?? []).map((p) => p.perk),
href: c.href ?? '/services',
cta: c.cta ?? 'Learn more',
featured: c.featured ?? false,
}))
: AUDIENCES_FALLBACK;
export function AudienceSection() {
return (
<section className="bg-emerald-mist py-24 lg:py-32">
<div className="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
<FadeIn className="mb-14 text-center">
<p className="text-emerald-deeper mb-3 text-sm font-semibold tracking-widest uppercase">
{overline}
Built for businesses like yours
</p>
<h2 className="font-display text-charcoal text-4xl font-bold sm:text-5xl">{headline}</h2>
<h2 className="font-display text-charcoal text-4xl font-bold sm:text-5xl">
Who we work with
</h2>
</FadeIn>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-3">
{audiences.map((a, i) => (
<FadeIn key={a.title} delay={i * 0.1}>
{AUDIENCES.map((a, i) => (
<FadeIn key={a.label} delay={i * 0.1}>
<div
className={`rounded-card relative flex h-full flex-col overflow-hidden border bg-white ${a.featured ? 'border-emerald/40 shadow-emerald/8 shadow-lg' : 'border-black/7'}`}
>

View file

@ -3,27 +3,8 @@ import { FadeIn } from '@/components/ui/FadeIn';
import { Tag } from '@/components/ui/Tag';
import { Button } from '@/components/ui/Button';
import { ArrowRightIcon } from '@/components/ui/icons';
import type { Post, Category } from '@/payload-types';
type TagVariant = 'green' | 'blue';
const CATEGORY_VARIANT: Record<string, TagVariant> = {
Tax: 'green',
Payroll: 'green',
HMRC: 'green',
Finance: 'blue',
Business: 'blue',
Updates: 'blue',
};
const CATEGORY_GRADIENT: Record<string, string> = {
Tax: 'from-emerald/12 to-emerald-mist',
Payroll: 'from-emerald/8 to-bg',
HMRC: 'from-emerald/12 to-emerald-mist',
Finance: 'from-blue/10 to-blue-mist',
Business: 'from-blue/10 to-blue-mist',
};
const POSTS_FALLBACK = [
const POSTS = [
{
category: 'Tax Tips',
title: 'How to Legally Reduce Your Tax Bill in 2026',
@ -33,7 +14,7 @@ const POSTS_FALLBACK = [
readTime: '5 min read',
slug: 'reduce-tax-bill-2026',
gradient: 'from-emerald/12 to-emerald-mist',
tagVariant: 'green' as TagVariant,
tagVariant: 'green' as const,
},
{
category: 'HMRC Updates',
@ -44,7 +25,7 @@ const POSTS_FALLBACK = [
readTime: '4 min read',
slug: 'making-tax-digital-2026',
gradient: 'from-blue/10 to-blue-mist',
tagVariant: 'blue' as TagVariant,
tagVariant: 'blue' as const,
},
{
category: 'Payroll Guide',
@ -55,39 +36,11 @@ const POSTS_FALLBACK = [
readTime: '6 min read',
slug: 'first-employee-payroll-guide',
gradient: 'from-emerald/8 to-bg',
tagVariant: 'green' as TagVariant,
tagVariant: 'green' as const,
},
];
function formatDate(iso: string | null | undefined): string {
if (!iso) return '';
return new Date(iso).toLocaleDateString('en-GB', {
day: 'numeric',
month: 'short',
year: 'numeric',
});
}
export function BlogPreviewSection({ posts }: { posts?: Post[] }) {
const items = posts?.length
? posts.map((p) => {
const categoryName =
p.category && typeof p.category === 'object' ? (p.category as Category).name : null;
const tagVariant: TagVariant = CATEGORY_VARIANT[categoryName ?? ''] ?? 'green';
const gradient = CATEGORY_GRADIENT[categoryName ?? ''] ?? 'from-emerald/8 to-bg';
return {
category: categoryName ?? 'Article',
title: p.title,
excerpt: p.excerpt ?? '',
date: formatDate(p.publishedAt),
readTime: p.readingTime ? `${p.readingTime} min read` : '',
slug: p.slug,
gradient,
tagVariant,
};
})
: POSTS_FALLBACK;
export function BlogPreviewSection() {
return (
<section className="bg-bg py-24 lg:py-32">
<div className="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
@ -108,7 +61,7 @@ export function BlogPreviewSection({ posts }: { posts?: Post[] }) {
</FadeIn>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-3">
{items.map((post, i) => (
{POSTS.map((post, i) => (
<FadeIn key={post.slug} delay={i * 0.1}>
<Link href={`/blog/${post.slug}`} className="group block h-full">
<article className="rounded-card flex h-full flex-col overflow-hidden border border-black/7 bg-white transition-all duration-300 hover:-translate-y-1 hover:border-black/12 hover:shadow-lg hover:shadow-black/5">
@ -128,12 +81,8 @@ export function BlogPreviewSection({ posts }: { posts?: Post[] }) {
<div className="flex items-center justify-between border-t border-black/5 pt-4">
<div className="text-muted flex items-center gap-2 text-xs">
<span>{post.date}</span>
{post.readTime && (
<>
<span>·</span>
<span>{post.readTime}</span>
</>
)}
<span>·</span>
<span>{post.readTime}</span>
</div>
<ArrowRightIcon
size={13}

View file

@ -2,27 +2,15 @@ import { FadeIn } from '@/components/ui/FadeIn';
import { Button } from '@/components/ui/Button';
import { BeamButton } from '@/components/ui/BeamButton';
import { CheckCircleIcon } from '@/components/ui/icons';
import type { HomePage } from '@/payload-types';
const REASSURANCES_FALLBACK = [
const REASSURANCES = [
'4.9/5 on Google',
'No lock-in contracts',
'ICAEW & ACCA Certified',
'Free 30-min consultation',
];
export function FinalCTASection({ data }: { data?: HomePage['finalCta'] | null }) {
const overline = data?.overline ?? 'Ready to start?';
const headline = data?.headline ?? 'Take the stress out of your finances — today.';
const body =
data?.body ??
'Book a free 30-minute consultation with one of our accountants. No commitment, no hard sell — just honest advice for your business.';
const ctaPrimary = data?.ctaPrimary ?? 'Book a Free Consultation';
const ctaSecondary = data?.ctaSecondary ?? 'See Our Services';
const reassurances = data?.reassurances?.length
? data.reassurances.map((r) => r.item)
: REASSURANCES_FALLBACK;
export function FinalCTASection() {
return (
<section className="bg-charcoal relative overflow-hidden py-28 lg:py-36">
{/* Background decoration */}
@ -33,18 +21,23 @@ export function FinalCTASection({ data }: { data?: HomePage['finalCta'] | null }
<div className="relative mx-auto max-w-[1440px] px-4 text-center sm:px-6 lg:px-8 xl:px-16">
<FadeIn>
<p className="text-emerald mb-4 text-sm font-semibold tracking-widest uppercase">
{overline}
Ready to start?
</p>
<h2 className="font-display mx-auto mb-6 max-w-3xl text-4xl leading-tight font-bold text-white sm:text-5xl lg:text-6xl">
{headline}
Take the stress out of
<br />
your finances today.
</h2>
<p className="mx-auto mb-10 max-w-xl text-lg leading-relaxed text-white/50">{body}</p>
<p className="mx-auto mb-10 max-w-xl text-lg leading-relaxed text-white/50">
Book a free 30-minute consultation with one of our accountants. No commitment, no hard
sell just honest advice for your business.
</p>
<div className="mb-10 flex flex-wrap items-center justify-center gap-4">
<BeamButton size="lg" variant="light" trailingArrow href="/contact">
{ctaPrimary}
Book a Free Consultation
</BeamButton>
<Button
size="lg"
@ -52,13 +45,13 @@ export function FinalCTASection({ data }: { data?: HomePage['finalCta'] | null }
href="/services"
className="border-white/20 text-white hover:border-white/40 hover:bg-white/8"
>
{ctaSecondary}
See Our Services
</Button>
</div>
{/* Reassurances */}
<div className="flex flex-wrap items-center justify-center gap-x-7 gap-y-2">
{reassurances.map((r) => (
{REASSURANCES.map((r) => (
<span key={r} className="flex items-center gap-1.5 text-sm text-white/40">
<CheckCircleIcon size={13} color="var(--emerald)" />
{r}

View file

@ -5,22 +5,14 @@ import { Button } from '@/components/ui/Button';
import { BeamButton } from '@/components/ui/BeamButton';
import { StarRating } from '@/components/ui/StarRating';
import { CheckCircleIcon } from '@/components/ui/icons';
import type { HomePage } from '@/payload-types';
const TRUST_ITEMS_FALLBACK = [
const TRUST_ITEMS = [
'ICAEW Member',
'ACCA Certified',
'500+ UK Businesses',
'No lock-in contracts',
];
const TRUST_STRIP_FALLBACK = [
'🇬🇧 UK-Based Team',
'ICAEW Member · ACCA Certified',
'Fixed monthly fees — no hidden charges',
'Serving UK businesses since 2012',
];
function DashboardPreview() {
const bars = [40, 58, 44, 76, 52, 88, 68, 82, 96, 72, 90, 100];
@ -133,22 +125,7 @@ function DashboardPreview() {
);
}
export function HeroSection({ data }: { data?: HomePage['hero'] | null }) {
const eyebrow = data?.eyebrow ?? 'Trusted by 500+ UK Businesses';
const body =
data?.body ??
'ICAEW-certified accountants with fixed monthly fees, a dedicated account manager, and zero HMRC surprises.';
const ctaPrimary = data?.ctaPrimary ?? 'Book a Free Consultation';
const ctaSecondary = data?.ctaSecondary ?? 'See Our Services';
const statsRating = data?.statsRating ?? '4.9/5';
const statsSource = data?.statsSource ?? 'Google Reviews';
const trustItems = data?.trustItems?.length
? data.trustItems.map((t) => t.item)
: TRUST_ITEMS_FALLBACK;
const trustStrip = data?.trustStrip?.length
? data.trustStrip.map((t) => t.item)
: TRUST_STRIP_FALLBACK;
export function HeroSection() {
return (
<section className="bg-bg relative mt-18 flex min-h-[calc(100vh-4.5rem)] flex-col overflow-x-hidden">
{/* Background */}
@ -175,24 +152,29 @@ export function HeroSection({ data }: { data?: HomePage['hero'] | null }) {
{/* Eyebrow */}
<div className="rounded-pill border-emerald/25 bg-emerald/8 mb-5 inline-flex items-center gap-1.5 border px-3.5 py-1.5">
<span className="bg-emerald size-1.5 rounded-full" />
<span className="text-emerald-dark text-xs font-semibold">{eyebrow}</span>
<span className="text-emerald-dark text-xs font-semibold">
Trusted by 500+ UK Businesses
</span>
</div>
<p className="text-muted mb-7 max-w-sm text-base leading-relaxed">{body}</p>
<p className="text-muted mb-7 max-w-sm text-base leading-relaxed">
ICAEW-certified accountants with fixed monthly fees, a dedicated account manager, and
zero HMRC surprises.
</p>
<div className="mb-7 flex flex-wrap gap-3">
<BeamButton size="lg" trailingArrow href="/contact">
{ctaPrimary}
Book a Free Consultation
</BeamButton>
<Button size="lg" variant="secondary" href="/services">
{ctaSecondary}
See Our Services
</Button>
</div>
<div className="flex items-center gap-2">
<StarRating rating={5} size="sm" />
<span className="text-charcoal text-sm font-semibold">{statsRating}</span>
<span className="text-muted text-sm">{statsSource}</span>
<span className="text-charcoal text-sm font-semibold">4.9/5</span>
<span className="text-muted text-sm">Google Reviews</span>
</div>
</motion.div>
@ -261,7 +243,7 @@ export function HeroSection({ data }: { data?: HomePage['hero'] | null }) {
for British Business
</p>
<div className="mt-6 flex flex-col gap-2">
{trustItems.map((item) => (
{TRUST_ITEMS.map((item) => (
<span key={item} className="text-muted flex items-center gap-2 text-sm">
<CheckCircleIcon size={13} color="var(--emerald)" />
{item}
@ -275,19 +257,13 @@ export function HeroSection({ data }: { data?: HomePage['hero'] | null }) {
{/* Trust strip */}
<div className="relative border-t border-black/6 bg-white/70 backdrop-blur-sm">
<div className="mx-auto flex max-w-[1440px] flex-wrap items-center justify-between gap-4 px-4 py-4 sm:px-6 lg:px-8 xl:px-16">
{trustStrip.flatMap((item, i) => {
const span = (
<span
key={item}
className={i === 0 ? 'text-charcoal text-sm font-semibold' : 'text-muted text-sm'}
>
{item}
</span>
);
return i === 0
? [span]
: [<div key={`sep-${i}`} className="hidden h-4 w-px bg-black/10 sm:block" />, span];
})}
<span className="text-charcoal text-sm font-semibold">🇬🇧 UK-Based Team</span>
<div className="hidden h-4 w-px bg-black/10 sm:block" />
<span className="text-muted text-sm">ICAEW Member · ACCA Certified</span>
<div className="hidden h-4 w-px bg-black/10 sm:block" />
<span className="text-muted text-sm">Fixed monthly fees no hidden charges</span>
<div className="hidden h-4 w-px bg-black/10 sm:block" />
<span className="text-muted text-sm">Serving UK businesses since 2012</span>
</div>
</div>
</section>

View file

@ -1,33 +1,24 @@
import { FadeIn } from '@/components/ui/FadeIn';
import type { HomePage } from '@/payload-types';
const PAINS_FALLBACK = [
const PAINS = [
{
number: '01',
title: 'HMRC penalties for missed deadlines',
body: 'Self-assessment, VAT, corporation tax — the dates stack up. One missed deadline can cost hundreds in fines.',
},
{
number: '02',
title: 'Tax rules that change every year',
body: 'Making Tax Digital, dividend tax hikes, NICs changes — staying compliant feels like a full-time job.',
},
{
number: '03',
title: 'Hours lost on bookkeeping',
body: 'Reconciling bank feeds, chasing receipts, producing reports — time you should be spending on your business.',
},
];
export function PainPointsSection({ data }: { data?: HomePage['painPoints'] | null }) {
const overline = data?.overline ?? 'The problem';
const headline = data?.headline ?? 'Most businesses are losing money to bad accounting.';
const statValue = data?.statValue ?? '73%';
const statDescription =
data?.statDescription ??
'of UK small businesses overpay tax due to unclaimed allowances and poor financial planning.';
const statSource = data?.statSource ?? '* HMRC data and independent SME research, 2024';
const pains = data?.pains?.length
? data.pains.map((p) => ({ title: p.title, body: p.body }))
: PAINS_FALLBACK;
export function PainPointsSection() {
return (
<section className="bg-charcoal relative overflow-hidden py-24 lg:py-32">
{/* Subtle dot grid */}
@ -37,46 +28,45 @@ export function PainPointsSection({ data }: { data?: HomePage['painPoints'] | nu
{/* Header */}
<FadeIn className="mb-16 max-w-3xl">
<p className="text-emerald mb-4 text-sm font-semibold tracking-widest uppercase">
{overline}
The problem
</p>
<h2 className="font-display text-4xl leading-tight font-bold text-white sm:text-5xl lg:text-[3.5rem]">
{headline.includes('bad accounting') ? (
<>
Most businesses are losing
<br />
money to <span className="text-emerald">bad accounting.</span>
</>
) : (
headline
)}
Most businesses are losing
<br />
money to <span className="text-emerald">bad accounting.</span>
</h2>
</FadeIn>
{/* Editorial stat */}
{/* Editorial 73% stat */}
<FadeIn
className="border-emerald/40 mb-16 flex flex-col gap-6 border-l-2 pl-8 sm:flex-row sm:items-center sm:gap-14 sm:pl-12"
delay={0.1}
>
<div className="shrink-0">
<p className="text-emerald font-mono text-[80px] leading-none font-bold sm:text-[108px]">
{statValue}
73<span className="text-5xl sm:text-6xl">%</span>
</p>
</div>
<div>
<p className="text-lg leading-relaxed text-white/60 sm:max-w-xs">{statDescription}</p>
<p className="mt-3 text-xs text-white/25">{statSource}</p>
<p className="text-lg leading-relaxed text-white/60 sm:max-w-xs">
of UK small businesses overpay tax due to unclaimed allowances and poor financial
planning.
</p>
<p className="mt-3 text-xs text-white/25">
* HMRC data and independent SME research, 2024
</p>
</div>
</FadeIn>
{/* Pain cards — editorial grid */}
<div className="grid grid-cols-1 border border-white/8 sm:grid-cols-3">
{pains.map((p, i) => (
<FadeIn key={p.title} delay={i * 0.08} className="h-full">
{PAINS.map((p, i) => (
<FadeIn key={p.number} delay={i * 0.08} className="h-full">
<div
className={`flex h-full flex-col p-8 lg:p-10 ${i < pains.length - 1 ? 'border-b border-white/8 sm:border-r sm:border-b-0' : ''}`}
className={`flex h-full flex-col p-8 lg:p-10 ${i < PAINS.length - 1 ? 'border-b border-white/8 sm:border-r sm:border-b-0' : ''}`}
>
<p className="text-emerald/50 mb-5 font-mono text-xs font-medium tracking-widest uppercase">
{String(i + 1).padStart(2, '0')}
{p.number}
</p>
<div className="bg-emerald/30 mb-5 h-px w-10" />
<h3 className="font-display mb-4 text-lg leading-snug font-semibold text-white">

View file

@ -3,14 +3,6 @@ import { Phone, Zap, FileCheck, TrendingUp, ArrowUpRight } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { SpotlightCard } from '@/components/ui/SpotlightCard';
import { FadeIn } from '@/components/ui/FadeIn';
import type { HomePage } from '@/payload-types';
const PROCESS_ICON_MAP: Record<string, React.ElementType> = {
phone: Phone,
zap: Zap,
'file-check': FileCheck,
'trending-up': TrendingUp,
};
interface ProcessItem {
icon: React.ElementType;
@ -18,7 +10,7 @@ interface ProcessItem {
description: string;
}
const PROCESS_ITEMS_FALLBACK: ProcessItem[] = [
const PROCESS_ITEMS: ProcessItem[] = [
{
icon: Phone,
title: 'Free Consultation',
@ -69,46 +61,35 @@ function ProcessCard({ icon: Icon, title, description }: ProcessItem) {
);
}
export function ProcessSection({ data }: { data?: HomePage['process'] | null }) {
const overline = data?.overline ?? 'How we do it';
const headline = data?.headline ?? 'From sign-up to sorted in days';
const body =
data?.body ??
"We've made switching accountants effortless. Most clients are fully onboarded within a week — then we handle everything so you never have to think about tax again.";
const ctaLabel = data?.ctaLabel ?? 'Get started';
const items: ProcessItem[] = data?.steps?.length
? data.steps.map((s) => ({
icon: PROCESS_ICON_MAP[s.icon ?? ''] ?? Phone,
title: s.title,
description: s.description,
}))
: PROCESS_ITEMS_FALLBACK;
export function ProcessSection() {
return (
<section className="w-full bg-white py-16 md:py-24">
<div className="mx-auto grid grid-cols-1 gap-12 px-4 sm:px-6 md:grid-cols-3 md:gap-8 lg:gap-16 lg:px-8 xl:max-w-[1440px] xl:px-16">
{/* Left: heading + description + CTA */}
<FadeIn className="flex flex-col items-start justify-center text-center md:col-span-1 md:text-left">
<span className="text-emerald mb-2 text-sm font-semibold tracking-widest uppercase">
{overline}
How we do it
</span>
<h2 className="font-display text-charcoal mb-4 text-3xl font-bold tracking-tight md:text-4xl">
{headline}
From sign-up to sorted in days
</h2>
<p className="text-muted mb-6 text-base leading-relaxed">{body}</p>
<p className="text-muted mb-6 text-base leading-relaxed">
We&apos;ve made switching accountants effortless. Most clients are fully onboarded
within a week then we handle everything so you never have to think about tax again.
</p>
<Button
size="lg"
href="/contact"
className="transition-transform duration-300 hover:scale-[1.04]"
>
{ctaLabel}
Get started
<ArrowUpRight className="ml-2 h-5 w-5" />
</Button>
</FadeIn>
{/* Right: 2×2 grid of SpotlightCards */}
<div className="grid grid-cols-1 gap-x-8 gap-y-10 sm:grid-cols-2 md:col-span-2">
{items.map((item, i) => (
{PROCESS_ITEMS.map((item, i) => (
<FadeIn key={item.title} delay={i * 0.1}>
<ProcessCard {...item} />
</FadeIn>

View file

@ -8,28 +8,8 @@ import {
PayrollIcon,
VATIcon,
} from '@/components/ui/icons';
import type { ComponentType } from 'react';
import type { Service } from '@/payload-types';
import { convertLexicalToPlaintext } from '@payloadcms/richtext-lexical/plaintext';
type IconKey = 'bookkeeping' | 'tax' | 'payroll' | 'vat';
type AccentKey = 'emerald' | 'blue';
const ICON_MAP: Record<IconKey, ComponentType<{ size: number; color: string }>> = {
bookkeeping: BookkeepingIcon,
tax: TaxIcon,
payroll: PayrollIcon,
vat: VATIcon,
};
const ACCENT_MAP: Record<IconKey, AccentKey> = {
bookkeeping: 'emerald',
tax: 'blue',
payroll: 'emerald',
vat: 'blue',
};
const SERVICES_FALLBACK = [
const SERVICES = [
{
icon: BookkeepingIcon,
title: 'Bookkeeping',
@ -37,7 +17,7 @@ const SERVICES_FALLBACK = [
description:
'Bank reconciliation, expense categorisation, monthly management accounts — all handled so you always know where you stand.',
href: '/services/bookkeeping',
accent: 'emerald' as AccentKey,
accent: 'emerald' as const,
},
{
icon: TaxIcon,
@ -46,7 +26,7 @@ const SERVICES_FALLBACK = [
description:
'Self-assessment, corporation tax, R&D credits — we file on time and make sure you never pay a penny more than you owe.',
href: '/services/tax-returns',
accent: 'blue' as AccentKey,
accent: 'blue' as const,
},
{
icon: PayrollIcon,
@ -55,7 +35,7 @@ const SERVICES_FALLBACK = [
description:
'RTI submissions, P60s, auto-enrolment — we manage your entire payroll cycle so your team gets paid correctly and on schedule.',
href: '/services/payroll',
accent: 'emerald' as AccentKey,
accent: 'emerald' as const,
},
{
icon: VATIcon,
@ -64,7 +44,7 @@ const SERVICES_FALLBACK = [
description:
'Making Tax Digital from day one. We prepare, review, and file your VAT returns accurately and on time, every quarter.',
href: '/services/vat-returns',
accent: 'blue' as AccentKey,
accent: 'blue' as const,
},
];
@ -83,26 +63,7 @@ const accentMap = {
},
};
export function ServicesSection({ services }: { services?: Service[] }) {
const items = services?.length
? services.map((s) => {
const accent = ACCENT_MAP[s.icon] ?? 'emerald';
const description = s.description
? convertLexicalToPlaintext({
data: s.description as Parameters<typeof convertLexicalToPlaintext>[0]['data'],
})
: (s.tagline ?? '');
return {
icon: ICON_MAP[s.icon] ?? BookkeepingIcon,
title: s.title,
tagline: s.tagline ?? '',
description,
href: `/services/${s.slug}`,
accent,
};
})
: SERVICES_FALLBACK;
export function ServicesSection() {
return (
<section className="bg-white py-24 lg:py-32">
<div className="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
@ -125,9 +86,9 @@ export function ServicesSection({ services }: { services?: Service[] }) {
{/* Card grid */}
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{items.map((s, i) => {
{SERVICES.map((s, i) => {
const Icon = s.icon;
const colors = accentMap[s.accent as AccentKey];
const colors = accentMap[s.accent];
return (
<FadeIn key={s.title} delay={i * 0.08}>
<Link href={s.href} className="group block h-full">

View file

@ -3,24 +3,26 @@
import { useState } from 'react';
import { FadeIn } from '@/components/ui/FadeIn';
import { CheckCircleIcon } from '@/components/ui/icons';
import type { HomePage } from '@/payload-types';
const FEATURES_FALLBACK = [
const FEATURES = [
{
number: '01',
title: 'Fixed monthly fee',
body: 'No surprise invoices. You know exactly what you pay on the 1st of every month — nothing more, ever.',
},
{
number: '02',
title: 'Your dedicated account manager',
body: 'A real person who knows your business, answers your calls, and proactively saves you money.',
},
{
number: '03',
title: '100% cloud-based',
body: 'All your accounts in one place, accessible anytime. We connect to your bank and automate the boring parts.',
},
];
const CHECKLIST_FALLBACK = [
const CHECKLIST = [
'Onboarded within 48 hours',
'We handle the HMRC transition',
'MTD-compliant from day one',
@ -46,25 +48,9 @@ const STACK = [
},
];
export function SolutionSection({ data }: { data?: HomePage['solution'] | null }) {
export function SolutionSection() {
const [active, setActive] = useState<number | null>(null);
const overline = data?.overline ?? 'The Axil way';
const headline = data?.headline ?? 'We found a better way to do accounting.';
const body =
data?.body ??
"Proactive, transparent, and technology-first. We don't wait for you to ask — we spot savings, flag issues, and keep you compliant before problems arise.";
const checklist = data?.checklist?.length
? data.checklist.map((c) => c.item)
: CHECKLIST_FALLBACK;
const features = data?.features?.length
? data.features.map((f, i) => ({
number: String(i + 1).padStart(2, '0'),
title: f.title,
body: f.body,
}))
: FEATURES_FALLBACK.map((f, i) => ({ ...f, number: String(i + 1).padStart(2, '0') }));
return (
<section className="bg-bg py-24 lg:py-32">
<div className="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
@ -72,14 +58,20 @@ export function SolutionSection({ data }: { data?: HomePage['solution'] | null }
{/* Left — text */}
<FadeIn x={-20} y={0}>
<p className="text-emerald mb-4 text-sm font-semibold tracking-widest uppercase">
{overline}
The Axil way
</p>
<h2 className="font-display text-charcoal mb-6 text-4xl leading-tight font-bold sm:text-5xl">
{headline}
We found a<br />
<span className="gradient-text">better way</span>
<br />
to do accounting.
</h2>
<p className="text-muted mb-8 text-lg leading-relaxed">{body}</p>
<p className="text-muted mb-8 text-lg leading-relaxed">
Proactive, transparent, and technology-first. We don&apos;t wait for you to ask we
spot savings, flag issues, and keep you compliant before problems arise.
</p>
<ul className="space-y-3.5">
{checklist.map((item) => (
{CHECKLIST.map((item) => (
<li key={item} className="text-charcoal flex items-center gap-3">
<CheckCircleIcon size={18} color="var(--emerald)" />
<span className="text-base font-medium">{item}</span>
@ -91,7 +83,7 @@ export function SolutionSection({ data }: { data?: HomePage['solution'] | null }
{/* Right — stacked feature cards */}
<FadeIn x={20} y={0} delay={0.15} className="flex items-center justify-center py-24">
<div className="grid place-items-center [grid-template-areas:'stack']">
{features.map((f, i) => {
{FEATURES.map((f, i) => {
const isActive = active === i;
const isDimmed = active !== null && !isActive;
const { pos, hover, idle } = STACK[i];

View file

@ -2,9 +2,8 @@ import { TestimonialsColumn, type TestimonialItem } from '@/components/ui/Testim
import { Tag } from '@/components/ui/Tag';
import { StarRating } from '@/components/ui/StarRating';
import { FadeIn } from '@/components/ui/FadeIn';
import type { Testimonial, Media } from '@/payload-types';
const TESTIMONIALS_FALLBACK: TestimonialItem[] = [
const TESTIMONIALS: TestimonialItem[] = [
{
name: 'Sarah T.',
role: 'Limited Company Director',
@ -57,18 +56,11 @@ const TESTIMONIALS_FALLBACK: TestimonialItem[] = [
},
];
function toItem(t: Testimonial): TestimonialItem {
const photoUrl = t.photo && typeof t.photo === 'object' ? ((t.photo as Media).url ?? null) : null;
const avatarUrl = `https://ui-avatars.com/api/?name=${encodeURIComponent(t.clientName)}&background=3CC68A&color=fff&bold=true&size=80`;
const role = [t.businessName, t.businessType].filter(Boolean).join(' · ') || 'Client';
return { name: t.clientName, role, image: photoUrl ?? avatarUrl, text: t.quote };
}
const col1 = TESTIMONIALS.slice(0, 3);
const col2 = TESTIMONIALS.slice(3, 6);
const col3 = [...TESTIMONIALS.slice(6, 8), TESTIMONIALS[0]];
export function TestimonialsSection({ testimonials }: { testimonials?: Testimonial[] }) {
const items = testimonials?.length ? testimonials.map(toItem) : TESTIMONIALS_FALLBACK;
const col1 = items.slice(0, 3);
const col2 = items.slice(3, 6);
const col3 = [...items.slice(6, 8), items[0]].filter(Boolean) as TestimonialItem[];
export function TestimonialsSection() {
return (
<section className="bg-bg overflow-hidden py-24 lg:py-32">
<div className="mx-auto mb-14 max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">

View file

@ -1,83 +1,65 @@
'use client';
import type { ComponentType } from 'react';
import { FadeIn } from '@/components/ui/FadeIn';
import { StatCounter } from '@/components/ui/StatCounter';
import { ShieldCheckIcon, ReceiptIcon, PersonCircleIcon, CloudIcon } from '@/components/ui/icons';
import type { HomePage } from '@/payload-types';
const USP_ICON_MAP: Record<string, ComponentType<{ size: number; color: string }>> = {
'shield-check': ShieldCheckIcon,
receipt: ReceiptIcon,
'person-circle': PersonCircleIcon,
cloud: CloudIcon,
};
const USP_COLOR_MAP: Record<string, { color: string; bg: string }> = {
'shield-check': { color: 'var(--emerald)', bg: 'bg-emerald/10' },
receipt: { color: 'var(--blue)', bg: 'bg-blue/10' },
'person-circle': { color: 'var(--emerald)', bg: 'bg-emerald/10' },
cloud: { color: 'var(--blue)', bg: 'bg-blue/10' },
};
const STATS_FALLBACK = [
const STATS = [
{ prefix: '', value: 500, suffix: '+', label: 'Businesses Served' },
{ prefix: '', value: 98, suffix: '%', label: 'Client Retention' },
{ prefix: '£', value: 2, suffix: 'M+', label: 'Tax Saved for Clients' },
{ prefix: '', value: 12, suffix: '+', label: 'Years of Experience' },
];
const USPS_FALLBACK = [
const USPS = [
{
icon: 'shield-check',
Icon: ShieldCheckIcon,
title: 'ICAEW & ACCA Qualified',
body: "Our accountants hold the highest professional qualifications in the UK. You're in expert hands.",
color: 'var(--emerald)',
bg: 'bg-emerald/10',
},
{
icon: 'receipt',
Icon: ReceiptIcon,
title: 'Fixed Monthly Engagement',
body: 'One predictable fee covers everything. No extra charges for emails, calls, or ad-hoc advice.',
color: 'var(--blue)',
bg: 'bg-blue/10',
},
{
icon: 'person-circle',
Icon: PersonCircleIcon,
title: 'Dedicated Account Manager',
body: 'One person who knows you and your business. No call centres, no being passed around.',
color: 'var(--emerald)',
bg: 'bg-emerald/10',
},
{
icon: 'cloud',
Icon: CloudIcon,
title: 'Cloud-Based & Paper-Free',
body: 'We connect to Xero, QuickBooks, and your bank. Everything is digital, accessible, and automated.',
color: 'var(--blue)',
bg: 'bg-blue/10',
},
];
export function WhyAxilSection({ data }: { data?: HomePage['whyAxil'] | null }) {
const overline = data?.overline ?? 'Why choose Axil';
const headline = data?.headline ?? 'Numbers that speak for themselves';
const stats = data?.stats?.length
? data.stats.map((s) => ({
prefix: s.prefix ?? '',
value: s.value,
suffix: s.suffix ?? '',
label: s.label,
}))
: STATS_FALLBACK;
const usps = data?.usps?.length
? data.usps.map((u) => ({ icon: u.icon ?? 'shield-check', title: u.title, body: u.body }))
: USPS_FALLBACK;
export function WhyAxilSection() {
return (
<section className="bg-bg py-24 lg:py-32">
<div className="mx-auto max-w-[1440px] px-4 sm:px-6 lg:px-8 xl:px-16">
<FadeIn className="mb-16 max-w-2xl">
<p className="text-emerald mb-3 text-sm font-semibold tracking-widest uppercase">
{overline}
Why choose Axil
</p>
<h2 className="font-display text-charcoal text-4xl font-bold sm:text-5xl">{headline}</h2>
<h2 className="font-display text-charcoal text-4xl font-bold sm:text-5xl">
Numbers that
<br />
speak for themselves
</h2>
</FadeIn>
{/* Stats row */}
<div className="mb-20 grid grid-cols-2 gap-x-8 gap-y-10 border-b border-black/8 pb-20 lg:grid-cols-4">
{stats.map((s, i) => (
{STATS.map((s, i) => (
<FadeIn key={s.label} delay={i * 0.08}>
<p className="text-charcoal mb-2 font-mono text-[clamp(2.8rem,5vw,4.5rem)] leading-none font-bold tracking-tight">
{s.prefix}
@ -91,24 +73,15 @@ export function WhyAxilSection({ data }: { data?: HomePage['whyAxil'] | null })
{/* USP grid */}
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
{usps.map((u, i) => {
const Icon = USP_ICON_MAP[u.icon] ?? ShieldCheckIcon;
const { color, bg } = USP_COLOR_MAP[u.icon] ?? {
color: 'var(--emerald)',
bg: 'bg-emerald/10',
};
return (
<FadeIn key={u.title} delay={i * 0.08}>
<div className={`mb-5 flex size-12 items-center justify-center rounded-xl ${bg}`}>
<Icon size={24} color={color} />
</div>
<h3 className="font-display text-charcoal mb-2 text-base font-semibold">
{u.title}
</h3>
<p className="text-muted text-sm leading-relaxed">{u.body}</p>
</FadeIn>
);
})}
{USPS.map((u, i) => (
<FadeIn key={u.title} delay={i * 0.08}>
<div className={`mb-5 flex size-12 items-center justify-center rounded-xl ${u.bg}`}>
<u.Icon size={24} color={u.color} />
</div>
<h3 className="font-display text-charcoal mb-2 text-base font-semibold">{u.title}</h3>
<p className="text-muted text-sm leading-relaxed">{u.body}</p>
</FadeIn>
))}
</div>
</div>
</section>

View file

@ -1,11 +1,14 @@
// Runs on Next.js startup (Node.js runtime only) before any requests are handled.
// Migrations are handled by the `migrator` Docker service (docker-compose.prod.yml)
// which runs before this container starts, so no migration call is needed here.
// Uses compiled/bundled code — no tsx/ESM resolution issues.
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { getPayload } = await import('payload');
const { default: config } = await import('@payload-config');
// Initialize Payload (connects to DB, registers collections/globals)
await getPayload({ config });
const payload = await getPayload({ config });
console.log('[startup] Running database migrations...');
await payload.db.migrate();
console.log('[startup] Migrations complete.');
}
}

View file

@ -1,171 +0,0 @@
import 'server-only';
import { getPayload } from 'payload';
import config from '@payload-config';
import { unstable_cache } from 'next/cache';
import type { Post, Service, Testimonial, Navigation, Footer, HomePage } from '@/payload-types';
// ─── Singleton client ──────────────────────────────────────────────────────
let _client: Awaited<ReturnType<typeof getPayload>> | null = null;
export async function getPayloadClient() {
if (!_client) _client = await getPayload({ config });
return _client;
}
// ─── Navigation ────────────────────────────────────────────────────────────
export const getNavigation = unstable_cache(
async (): Promise<Navigation | null> => {
try {
const payload = await getPayloadClient();
return await payload.findGlobal({ slug: 'navigation' });
} catch {
return null;
}
},
['navigation'],
{ tags: ['navigation'], revalidate: false },
);
// ─── Footer ────────────────────────────────────────────────────────────────
export const getFooter = unstable_cache(
async (): Promise<Footer | null> => {
try {
const payload = await getPayloadClient();
return await payload.findGlobal({ slug: 'footer' });
} catch {
return null;
}
},
['footer'],
{ tags: ['footer'], revalidate: false },
);
// ─── Posts ─────────────────────────────────────────────────────────────────
export const getPublishedPosts = unstable_cache(
async (limit = 10): Promise<Post[]> => {
try {
const payload = await getPayloadClient();
const result = await payload.find({
collection: 'posts',
where: { status: { equals: 'published' } },
sort: '-publishedAt',
limit,
depth: 1,
});
return result.docs as Post[];
} catch {
return [];
}
},
['posts'],
{ tags: ['posts'], revalidate: false },
);
export function getPostBySlugCached(slug: string): Promise<Post | undefined> {
return unstable_cache(
async () => {
try {
const payload = await getPayloadClient();
const result = await payload.find({
collection: 'posts',
where: {
and: [{ slug: { equals: slug } }, { status: { equals: 'published' } }],
},
limit: 1,
depth: 1,
});
return result.docs[0] as Post | undefined;
} catch {
return undefined;
}
},
[`post-${slug}`],
{ tags: ['posts', `post-${slug}`], revalidate: false },
)();
}
// ─── Services ──────────────────────────────────────────────────────────────
export const getPublishedServices = unstable_cache(
async (limit = 10): Promise<Service[]> => {
try {
const payload = await getPayloadClient();
const result = await payload.find({
collection: 'services',
where: { status: { equals: 'published' } },
sort: 'createdAt',
limit,
depth: 1,
});
return result.docs as Service[];
} catch {
return [];
}
},
['services'],
{ tags: ['services'], revalidate: false },
);
export function getServiceBySlugCached(slug: string): Promise<Service | undefined> {
return unstable_cache(
async () => {
try {
const payload = await getPayloadClient();
const result = await payload.find({
collection: 'services',
where: {
and: [{ slug: { equals: slug } }, { status: { equals: 'published' } }],
},
limit: 1,
depth: 2,
});
return result.docs[0] as Service | undefined;
} catch {
return undefined;
}
},
[`service-${slug}`],
{ tags: ['services', `service-${slug}`], revalidate: false },
)();
}
// ─── HomePage Global ───────────────────────────────────────────────────────
export const getHomePage = unstable_cache(
async (): Promise<HomePage | null> => {
try {
const payload = await getPayloadClient();
return await payload.findGlobal({ slug: 'home-page' });
} catch {
return null;
}
},
['homepage'],
{ tags: ['homepage'], revalidate: false },
);
// ─── Testimonials ──────────────────────────────────────────────────────────
export const getFeaturedTestimonials = unstable_cache(
async (limit = 8): Promise<Testimonial[]> => {
try {
const payload = await getPayloadClient();
const result = await payload.find({
collection: 'testimonials',
where: { featured: { equals: true } },
sort: '-publishedAt',
limit,
depth: 0,
});
return result.docs as Testimonial[];
} catch {
return [];
}
},
['testimonials-featured'],
{ tags: ['testimonials'], revalidate: false },
);

View file

@ -1,765 +0,0 @@
/**
* CMS Seed Script
* Populates all Payload CMS collections and globals from current hardcoded values.
*
* Run once after a fresh database:
* DATABASE_URI="postgresql://axil:axil_dev@localhost:5432/axil" \
* NODE_OPTIONS="--experimental-strip-types --no-require-module" \
* npx tsx src/lib/seed.ts
*/
import { getPayload } from 'payload';
import config from '../payload.config.ts';
// ─── Lexical helpers ────────────────────────────────────────────────────────
function lexicalParagraph(text: string) {
return {
root: {
type: 'root',
direction: 'ltr' as const,
format: '' as const,
indent: 0,
version: 1,
children: [
{
type: 'paragraph',
version: 1,
direction: 'ltr' as const,
format: '' as const,
indent: 0,
children: [
{
type: 'text',
version: 1,
text,
format: 0,
detail: 0,
mode: 'normal' as const,
style: '',
},
],
},
],
},
};
}
// ─── Main ────────────────────────────────────────────────────────────────────
async function seed() {
const payload = await getPayload({ config });
console.log('\n🌱 Seeding CMS data...\n');
// ── 1. Categories ──────────────────────────────────────────────────────────
console.log('─ Categories');
const CATEGORY_NAMES = ['Tax', 'HMRC', 'Payroll', 'Finance', 'Business'];
const categoryIds: Record<string, number> = {};
for (const name of CATEGORY_NAMES) {
const existing = await payload.find({
collection: 'categories',
where: { name: { equals: name } },
limit: 1,
});
if (existing.docs.length) {
categoryIds[name] = existing.docs[0].id as number;
console.log(` ✓ '${name}' exists`);
} else {
const cat = await payload.create({
collection: 'categories',
data: { name, slug: name.toLowerCase().replace(/\s+/g, '-') },
});
categoryIds[name] = cat.id as number;
console.log(` + Created '${name}'`);
}
}
// ── 2. Services ────────────────────────────────────────────────────────────
console.log('\n─ Services');
const SERVICES = [
{
title: 'Bookkeeping',
slug: 'bookkeeping',
icon: 'bookkeeping' as const,
tagline: 'Accurate records, zero stress',
descText:
'Up-to-date books every month, cloud-based and always accessible. We reconcile every transaction so your accounts are always audit-ready — and you always know where you stand.',
whatsIncluded: [
'Monthly bank reconciliation',
'Expense categorisation & receipts',
'Accounts payable & receivable tracking',
'Cloud accounting software setup (Xero/QuickBooks)',
'Monthly management accounts',
'Real-time financial dashboard access',
],
howItWorks: [
{
step: 1,
title: 'Save 10+ hours per month',
description:
'Stop wrestling with spreadsheets. We handle every transaction so you never have to.',
},
{
step: 2,
title: 'Always audit-ready',
description:
'Clean, HMRC-compliant records mean no panic when a filing deadline arrives.',
},
{
step: 3,
title: 'Real-time visibility',
description:
'Log in any time to see exactly how your business is performing, with no surprises.',
},
],
faq: [
{
question: 'Which accounting software do you use?',
answerText:
"We work with Xero, QuickBooks and Sage. We'll recommend the best option for your business and handle the setup.",
},
{
question: 'How do I send you my receipts?',
answerText:
'Via a simple mobile app — just photograph receipts on your phone. No more shoeboxes.',
},
{
question: 'How quickly are transactions recorded?',
answerText:
'All transactions are reconciled within 48 hours of your bank feed updating, usually faster.',
},
],
},
{
title: 'Tax Returns',
slug: 'tax-returns',
icon: 'tax' as const,
tagline: 'Every allowance claimed',
descText:
'From self-assessment to corporation tax, we handle every filing and actively look for every allowance, deduction and relief your business is entitled to.',
whatsIncluded: [
'Self-assessment tax return preparation & filing',
'Corporation tax (CT600) preparation & filing',
'Capital allowances review',
'R&D tax credits (if applicable)',
'HMRC correspondence management',
'Tax planning & year-end strategy',
],
howItWorks: [
{
step: 1,
title: 'Never miss a deadline',
description:
"We track every HMRC deadline and file well in advance — you'll never pay a late filing penalty.",
},
{
step: 2,
title: 'Maximum allowances',
description:
'Our tax specialists review every available allowance and relief to legally minimise your tax bill.',
},
{
step: 3,
title: 'HMRC as a proxy',
description:
'We handle all correspondence with HMRC. You never have to speak to them directly unless you want to.',
},
],
faq: [
{
question: 'When do you start my tax return?',
answerText:
"We begin collecting information 34 months before your filing deadline so there's never a rush.",
},
{
question: 'What if HMRC investigates me?',
answerText:
'We represent you fully and handle the entire process. Our fee protection cover is included as standard.',
},
{
question: 'Can you do R&D tax credits?',
answerText:
"Yes — if your business conducts qualifying R&D activities, we'll identify and claim the relief on your behalf.",
},
],
},
{
title: 'Payroll',
slug: 'payroll',
icon: 'payroll' as const,
tagline: 'On time, every time',
descText:
'Fully managed payroll for businesses of any size. We handle PAYE, National Insurance, pension auto-enrolment, RTI filings and payslips — so you never have to think about it.',
whatsIncluded: [
'Monthly payroll processing',
'PAYE & National Insurance calculations',
'Auto-enrolment pension management',
'Real Time Information (RTI) submissions',
'Payslips for every employee',
'P60s, P45s and P11Ds',
],
howItWorks: [
{
step: 1,
title: 'Zero errors',
description:
'Manual payroll errors are costly. Our systems double-check every calculation before submission.',
},
{
step: 2,
title: 'Full compliance',
description:
'RTI submissions, auto-enrolment, NMW compliance — all handled on time, every time.',
},
{
step: 3,
title: 'Scales with you',
description:
'Whether you have 1 employee or 100, our payroll service scales effortlessly as your team grows.',
},
],
faq: [
{
question: "What's the deadline for running payroll?",
answerText:
'We ask for any changes (new starters, leavers, salary changes) by the 20th of each month and process by the 25th.',
},
{
question: 'Do you handle auto-enrolment pensions?',
answerText:
'Yes — we set up and manage your workplace pension scheme, handle all enrolment communications and submissions.',
},
{
question: 'What if I need to add a new employee?',
answerText:
'Just email us the details. We add them to the next payroll run and handle all the HMRC notifications.',
},
],
},
{
title: 'VAT Returns',
slug: 'vat-returns',
icon: 'vat' as const,
tagline: 'MTD-compliant filing',
descText:
'Making Tax Digital-compliant VAT returns filed accurately and on time, every quarter. We also advise on the most tax-efficient VAT scheme for your business.',
whatsIncluded: [
'Quarterly VAT return preparation & filing',
'Making Tax Digital (MTD) compliance',
'VAT scheme assessment & advice',
'Input & output VAT reconciliation',
'HMRC VAT correspondence',
'VAT registration & deregistration',
],
howItWorks: [
{
step: 1,
title: 'Never miss a VAT deadline',
description:
'Late VAT returns trigger automatic penalties. We file every return well before the deadline.',
},
{
step: 2,
title: 'Right scheme for your business',
description:
"Flat Rate, Cash Accounting or Standard — we make sure you're on the scheme that minimises your VAT bill.",
},
{
step: 3,
title: 'MTD fully handled',
description:
"HMRC's Making Tax Digital requirements are completely managed by us. No software headaches.",
},
],
faq: [
{
question: 'Do I need to register for VAT?',
answerText:
"You must register once your taxable turnover exceeds £90,000. We'll advise on timing and handle the registration.",
},
{
question: 'What is Making Tax Digital?',
answerText:
'MTD requires businesses to keep digital records and submit VAT returns using approved software. We handle this entirely.',
},
{
question: 'Can you help with a VAT investigation?',
answerText:
'Yes — we represent you fully in any HMRC VAT enquiry and our fee protection covers the cost.',
},
],
},
];
for (const svc of SERVICES) {
const existing = await payload.find({
collection: 'services',
where: { slug: { equals: svc.slug } },
limit: 1,
});
if (existing.docs.length) {
console.log(` ✓ '${svc.title}' exists`);
continue;
}
await payload.create({
collection: 'services',
data: {
title: svc.title,
slug: svc.slug,
icon: svc.icon,
tagline: svc.tagline,
description: lexicalParagraph(svc.descText),
whatsIncluded: svc.whatsIncluded.map((item) => ({ item })),
howItWorks: svc.howItWorks,
faq: svc.faq.map((f) => ({
question: f.question,
answer: lexicalParagraph(f.answerText),
})),
status: 'published',
publishedAt: new Date().toISOString(),
},
});
console.log(` + Created '${svc.title}'`);
}
// ── 3. Testimonials ────────────────────────────────────────────────────────
console.log('\n─ Testimonials');
const TESTIMONIALS = [
{
clientName: 'Sarah T.',
businessName: 'Limited Company Director',
quote:
'Axil saved us over £8,000 in our first year alone. They spotted allowances our previous accountant had missed for three years running.',
rating: '5' as const,
featured: true,
},
{
clientName: 'James K.',
businessName: 'Sole Trader',
quote:
'Finally an accountant who speaks plain English. I actually understand my finances now, and my tax bill has never been lower.',
rating: '5' as const,
featured: true,
},
{
clientName: 'Emma R.',
businessName: 'Startup Founder',
quote:
"Payroll used to take me half a day every month. Now it takes zero minutes. Axil handles it completely and it's always perfect.",
rating: '5' as const,
featured: true,
},
{
clientName: 'Michael B.',
businessName: 'Limited Company Director',
quote:
'The dedicated account manager is worth every penny. She proactively flagged a VAT issue that would have cost us £4,000 in penalties.',
rating: '5' as const,
featured: true,
},
{
clientName: 'David O.',
businessName: 'Sole Trader',
quote:
'Switched from a Big 4 firm to Axil and never looked back. Same quality, half the price, ten times more personal service.',
rating: '5' as const,
featured: true,
},
{
clientName: 'Rachel M.',
businessName: 'E-commerce Brand Owner',
quote:
'MTD compliance, quarterly VAT, company accounts — Axil handles everything and I get a clean dashboard showing exactly how my business is doing.',
rating: '5' as const,
featured: true,
},
{
clientName: 'Tom H.',
businessName: 'Consulting Ltd',
quote:
'I was dreading my first year of corporation tax as a limited company. Axil made it completely painless and saved me significantly on my first filing.',
rating: '5' as const,
featured: true,
},
{
clientName: 'Priya S.',
businessName: 'Retail Business Owner',
quote:
"Three years with Axil and I've recommended them to five other business owners. Genuinely the best decision I made when starting my company.",
rating: '5' as const,
featured: true,
},
];
for (const t of TESTIMONIALS) {
const existing = await payload.find({
collection: 'testimonials',
where: { clientName: { equals: t.clientName } },
limit: 1,
});
if (existing.docs.length) {
console.log(` ✓ '${t.clientName}' exists`);
continue;
}
await payload.create({
collection: 'testimonials',
data: { ...t, publishedAt: new Date().toISOString() },
});
console.log(` + Created '${t.clientName}'`);
}
// ── 4. Blog Posts ──────────────────────────────────────────────────────────
console.log('\n─ Blog Posts');
const POSTS = [
{
title: 'How to Legally Reduce Your Tax Bill in 2026',
slug: 'reduce-tax-bill-2026',
excerpt:
'From pension contributions to trading allowances, these are the most overlooked ways UK business owners cut their tax liability — all HMRC-approved.',
categoryName: 'Tax',
contentText:
'Many UK business owners overpay tax simply because they are unaware of the allowances and reliefs available to them. This guide covers the most commonly missed deductions, from pension contributions and home office expenses to capital allowances and R&D tax credits. All of these strategies are fully HMRC-approved and legally reduce your tax liability.',
publishedAt: '2026-02-14T09:00:00.000Z',
},
{
title: 'Making Tax Digital: What Every Business Must Know',
slug: 'making-tax-digital-2026',
excerpt:
"MTD for income tax is expanding in April 2026. Here's exactly what changes, who's affected, and what you need to do before the deadline.",
categoryName: 'HMRC',
contentText:
'Making Tax Digital (MTD) for Income Tax Self Assessment is expanding from April 2026, requiring sole traders and landlords with income over £50,000 to submit quarterly digital returns. This article explains exactly who is affected, what software you need, and the steps to take before the deadline to ensure compliance.',
publishedAt: '2026-02-07T09:00:00.000Z',
},
{
title: 'Hiring Your First Employee? A Complete Payroll Guide',
slug: 'first-employee-payroll-guide',
excerpt:
'RTI submissions, auto-enrolment, P60s — setting up payroll correctly from day one avoids costly mistakes and HMRC penalties down the line.',
categoryName: 'Payroll',
contentText:
'Taking on your first employee is an exciting milestone, but it comes with significant payroll obligations. This guide walks you through everything you need to set up: PAYE registration with HMRC, Real Time Information (RTI) submissions, auto-enrolment pension duties, and the various year-end forms like P60s and P11Ds. Getting this right from day one avoids costly penalties.',
publishedAt: '2026-02-01T09:00:00.000Z',
},
];
for (const post of POSTS) {
const existing = await payload.find({
collection: 'posts',
where: { slug: { equals: post.slug } },
limit: 1,
});
if (existing.docs.length) {
console.log(` ✓ '${post.title}' exists`);
continue;
}
await payload.create({
collection: 'posts',
data: {
title: post.title,
slug: post.slug,
excerpt: post.excerpt,
category: categoryIds[post.categoryName] ?? undefined,
content: lexicalParagraph(post.contentText),
status: 'published',
publishedAt: post.publishedAt,
readingTime: Math.ceil(post.contentText.split(' ').length / 200) || 1,
},
});
console.log(` + Created '${post.title}'`);
}
// ── 5. Navigation Global ───────────────────────────────────────────────────
console.log('\n─ Navigation global');
await payload.updateGlobal({
slug: 'navigation',
data: {
items: [
{ label: 'Home', href: '/', isDropdown: false },
{
label: 'Services',
href: '/services',
isDropdown: true,
children: [
{
label: 'Bookkeeping',
href: '/services/bookkeeping',
icon: 'bookkeeping',
description: 'Accurate records, zero stress',
},
{
label: 'Tax Returns',
href: '/services/tax-returns',
icon: 'tax',
description: 'Every allowance claimed',
},
{
label: 'Payroll',
href: '/services/payroll',
icon: 'payroll',
description: 'On time, every time',
},
{
label: 'VAT Returns',
href: '/services/vat-returns',
icon: 'vat',
description: 'MTD-compliant filing',
},
],
},
{ label: 'Blog', href: '/blog', isDropdown: false },
{ label: 'About', href: '/about', isDropdown: false },
],
},
});
console.log(' + Navigation updated');
// ── 6. Footer Global ───────────────────────────────────────────────────────
console.log('\n─ Footer global');
await payload.updateGlobal({
slug: 'footer',
data: {
columns: [
{
heading: 'Services',
links: [
{ label: 'Bookkeeping', href: '/services/bookkeeping' },
{ label: 'Tax Returns', href: '/services/tax-returns' },
{ label: 'Payroll', href: '/services/payroll' },
{ label: 'VAT Returns', href: '/services/vat-returns' },
],
},
{
heading: 'Company',
links: [
{ label: 'About Us', href: '/about' },
{ label: 'Blog', href: '/blog' },
{ label: 'Contact', href: '/contact' },
],
},
],
contactInfo: {
address: 'London, United Kingdom',
phone: '+44 (0) 20 0000 0000',
email: 'hello@axilaccountants.co.uk',
},
socialLinks: {
linkedIn: 'https://linkedin.com/company/axil-accountants',
facebook: '',
instagram: '',
},
legalLinks: [
{ label: 'Privacy Policy', href: '/privacy' },
{ label: 'Terms of Service', href: '/terms' },
],
copyrightText: `© ${new Date().getFullYear()} Axil Accountants Ltd. All rights reserved.`,
},
});
console.log(' + Footer updated');
// ── 7. HomePage Global ─────────────────────────────────────────────────────
console.log('\n─ HomePage global');
await payload.updateGlobal({
slug: 'home-page',
data: {
hero: {
eyebrow: 'Trusted by 500+ UK Businesses',
body: 'ICAEW-certified accountants with fixed monthly fees, a dedicated account manager, and zero HMRC surprises.',
ctaPrimary: 'Book a Free Consultation',
ctaSecondary: 'See Our Services',
statsRating: '4.9/5',
statsSource: 'Google Reviews',
trustItems: [
{ item: 'ICAEW Member' },
{ item: 'ACCA Certified' },
{ item: '500+ UK Businesses' },
{ item: 'No lock-in contracts' },
],
trustStrip: [
{ item: '🇬🇧 UK-Based Team' },
{ item: 'ICAEW Member · ACCA Certified' },
{ item: 'Fixed monthly fees — no hidden charges' },
{ item: 'Serving UK businesses since 2012' },
],
},
painPoints: {
overline: 'The problem',
headline: 'Most businesses are losing money to bad accounting.',
statValue: '73%',
statDescription:
'of UK small businesses overpay tax due to unclaimed allowances and poor financial planning.',
statSource: '* HMRC data and independent SME research, 2024',
pains: [
{
title: 'HMRC penalties for missed deadlines',
body: 'Self-assessment, VAT, corporation tax — the dates stack up. One missed deadline can cost hundreds in fines.',
},
{
title: 'Tax rules that change every year',
body: 'Making Tax Digital, dividend tax hikes, NICs changes — staying compliant feels like a full-time job.',
},
{
title: 'Hours lost on bookkeeping',
body: 'Reconciling bank feeds, chasing receipts, producing reports — time you should be spending on your business.',
},
],
},
solution: {
overline: 'The Axil way',
headline: 'We found a better way to do accounting.',
body: "Proactive, transparent, and technology-first. We don't wait for you to ask — we spot savings, flag issues, and keep you compliant before problems arise.",
checklist: [
{ item: 'Onboarded within 48 hours' },
{ item: 'We handle the HMRC transition' },
{ item: 'MTD-compliant from day one' },
{ item: 'Real-time financial dashboard' },
],
features: [
{
title: 'Fixed monthly fee',
body: 'No surprise invoices. You know exactly what you pay on the 1st of every month — nothing more, ever.',
},
{
title: 'Your dedicated account manager',
body: 'A real person who knows your business, answers your calls, and proactively saves you money.',
},
{
title: '100% cloud-based',
body: 'All your accounts in one place, accessible anytime. We connect to your bank and automate the boring parts.',
},
],
},
whyAxil: {
overline: 'Why choose Axil',
headline: 'Numbers that speak for themselves',
stats: [
{ prefix: '', value: 500, suffix: '+', label: 'Businesses Served' },
{ prefix: '', value: 98, suffix: '%', label: 'Client Retention' },
{ prefix: '£', value: 2, suffix: 'M+', label: 'Tax Saved for Clients' },
{ prefix: '', value: 12, suffix: '+', label: 'Years of Experience' },
],
usps: [
{
icon: 'shield-check',
title: 'ICAEW & ACCA Qualified',
body: "Our accountants hold the highest professional qualifications in the UK. You're in expert hands.",
},
{
icon: 'receipt',
title: 'Fixed Monthly Engagement',
body: 'One predictable fee covers everything. No extra charges for emails, calls, or ad-hoc advice.',
},
{
icon: 'person-circle',
title: 'Dedicated Account Manager',
body: 'One person who knows you and your business. No call centres, no being passed around.',
},
{
icon: 'cloud',
title: 'Cloud-Based & Paper-Free',
body: 'We connect to Xero, QuickBooks, and your bank. Everything is digital, accessible, and automated.',
},
],
},
audience: {
overline: 'Built for businesses like yours',
headline: 'Who we work with',
cards: [
{
title: 'Sole Traders',
tagline: 'Self-assessment made simple',
body: 'From freelancers to consultants, we handle your self-assessment, expenses, and tax planning so you keep more of what you earn.',
perks: [
{ perk: 'Self-assessment filing' },
{ perk: 'Expense optimisation' },
{ perk: 'National Insurance planning' },
],
featured: false,
href: '/services',
cta: 'For sole traders',
},
{
title: 'Limited Companies',
tagline: 'Full-service company accounting',
body: 'Corporation tax, payroll, dividends, R&D credits — we manage your entire financial picture and help you extract value tax-efficiently.',
perks: [
{ perk: 'Corporation tax' },
{ perk: 'Dividend planning' },
{ perk: "Directors' payroll" },
],
featured: true,
href: '/services',
cta: 'For limited companies',
},
{
title: 'Startups & Scaleups',
tagline: 'Built for fast-growing businesses',
body: 'SEIS/EIS compliance, R&D tax credits, investor-ready accounts — we understand startup finance and help you grow without financial friction.',
perks: [
{ perk: 'R&D tax credits' },
{ perk: 'SEIS/EIS compliance' },
{ perk: 'Investor accounts' },
],
featured: false,
href: '/services',
cta: 'For startups',
},
],
},
process: {
overline: 'How we do it',
headline: 'From sign-up to sorted in days',
body: "We've made switching accountants effortless. Most clients are fully onboarded within a week — then we handle everything so you never have to think about tax again.",
ctaLabel: 'Get started',
steps: [
{
icon: 'phone',
title: 'Free Consultation',
description:
'Tell us about your business, current finances and goals. No pressure, no commitment — just an honest conversation.',
},
{
icon: 'zap',
title: 'Quick Onboarding',
description:
'We migrate your accounts and set up your Axil dashboard in under a week, with zero disruption to your business.',
},
{
icon: 'file-check',
title: 'We Handle Everything',
description:
'Bookkeeping, tax, payroll, VAT — handled every month with zero input from you, filed on time, every time.',
},
{
icon: 'trending-up',
title: 'You Focus on Growth',
description:
'Monthly reports, proactive tax advice, and a dedicated account manager who knows your business inside out.',
},
],
},
finalCta: {
overline: 'Ready to start?',
headline: 'Take the stress out of your finances — today.',
body: 'Book a free 30-minute consultation with one of our accountants. No commitment, no hard sell — just honest advice for your business.',
ctaPrimary: 'Book a Free Consultation',
ctaSecondary: 'See Our Services',
reassurances: [
{ item: '4.9/5 on Google' },
{ item: 'No lock-in contracts' },
{ item: 'ICAEW & ACCA Certified' },
{ item: 'Free 30-min consultation' },
],
},
},
});
console.log(' + HomePage updated');
console.log('\n✅ Seed complete!\n');
process.exit(0);
}
seed().catch((err) => {
console.error('\n❌ Seed failed:', err);
process.exit(1);
});

View file

@ -1,4 +1,4 @@
import { sql, type MigrateUpArgs, type MigrateDownArgs } from '@payloadcms/db-postgres';
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres';
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`

File diff suppressed because it is too large Load diff

View file

@ -1,182 +0,0 @@
import { sql, type MigrateUpArgs, type MigrateDownArgs } from '@payloadcms/db-postgres';
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
CREATE TYPE "public"."enum_home_page_why_axil_usps_icon" AS ENUM('shield-check', 'receipt', 'person-circle', 'cloud');
CREATE TYPE "public"."enum_home_page_process_steps_icon" AS ENUM('phone', 'zap', 'file-check', 'trending-up');
CREATE TABLE "home_page_hero_trust_items" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"item" varchar NOT NULL
);
CREATE TABLE "home_page_hero_trust_strip" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"item" varchar NOT NULL
);
CREATE TABLE "home_page_pain_points_pains" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"title" varchar NOT NULL,
"body" varchar NOT NULL
);
CREATE TABLE "home_page_solution_checklist" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"item" varchar NOT NULL
);
CREATE TABLE "home_page_solution_features" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"title" varchar NOT NULL,
"body" varchar NOT NULL
);
CREATE TABLE "home_page_why_axil_stats" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"prefix" varchar,
"value" numeric NOT NULL,
"suffix" varchar,
"label" varchar NOT NULL
);
CREATE TABLE "home_page_why_axil_usps" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"icon" "enum_home_page_why_axil_usps_icon",
"title" varchar NOT NULL,
"body" varchar NOT NULL
);
CREATE TABLE "home_page_audience_cards_perks" (
"_order" integer NOT NULL,
"_parent_id" varchar NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"perk" varchar NOT NULL
);
CREATE TABLE "home_page_audience_cards" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"title" varchar NOT NULL,
"tagline" varchar,
"body" varchar,
"featured" boolean DEFAULT false,
"href" varchar DEFAULT '/services',
"cta" varchar
);
CREATE TABLE "home_page_process_steps" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"icon" "enum_home_page_process_steps_icon",
"title" varchar NOT NULL,
"description" varchar NOT NULL
);
CREATE TABLE "home_page_final_cta_reassurances" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"item" varchar NOT NULL
);
CREATE TABLE "home_page" (
"id" serial PRIMARY KEY NOT NULL,
"hero_eyebrow" varchar DEFAULT 'Trusted by 500+ UK Businesses',
"hero_body" varchar DEFAULT 'ICAEW-certified accountants with fixed monthly fees, a dedicated account manager, and zero HMRC surprises.',
"hero_cta_primary" varchar DEFAULT 'Book a Free Consultation',
"hero_cta_secondary" varchar DEFAULT 'See Our Services',
"hero_stats_rating" varchar DEFAULT '4.9/5',
"hero_stats_source" varchar DEFAULT 'Google Reviews',
"pain_points_overline" varchar DEFAULT 'The problem',
"pain_points_headline" varchar DEFAULT 'Most businesses are losing money to bad accounting.',
"pain_points_stat_value" varchar DEFAULT '73%',
"pain_points_stat_description" varchar DEFAULT 'of UK small businesses overpay tax due to unclaimed allowances and poor financial planning.',
"pain_points_stat_source" varchar DEFAULT '* HMRC data and independent SME research, 2024',
"solution_overline" varchar DEFAULT 'The Axil way',
"solution_headline" varchar DEFAULT 'We found a better way to do accounting.',
"solution_body" varchar DEFAULT 'Proactive, transparent, and technology-first. We don''t wait for you to ask — we spot savings, flag issues, and keep you compliant before problems arise.',
"why_axil_overline" varchar DEFAULT 'Why choose Axil',
"why_axil_headline" varchar DEFAULT 'Numbers that speak for themselves',
"audience_overline" varchar DEFAULT 'Built for businesses like yours',
"audience_headline" varchar DEFAULT 'Who we work with',
"process_overline" varchar DEFAULT 'How we do it',
"process_headline" varchar DEFAULT 'From sign-up to sorted in days',
"process_body" varchar DEFAULT 'We''ve made switching accountants effortless. Most clients are fully onboarded within a week — then we handle everything so you never have to think about tax again.',
"process_cta_label" varchar DEFAULT 'Get started',
"final_cta_overline" varchar DEFAULT 'Ready to start?',
"final_cta_headline" varchar DEFAULT 'Take the stress out of your finances — today.',
"final_cta_body" varchar DEFAULT 'Book a free 30-minute consultation with one of our accountants. No commitment, no hard sell — just honest advice for your business.',
"final_cta_cta_primary" varchar DEFAULT 'Book a Free Consultation',
"final_cta_cta_secondary" varchar DEFAULT 'See Our Services',
"updated_at" timestamp(3) with time zone,
"created_at" timestamp(3) with time zone
);
ALTER TABLE "home_page_hero_trust_items" ADD CONSTRAINT "home_page_hero_trust_items_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."home_page"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "home_page_hero_trust_strip" ADD CONSTRAINT "home_page_hero_trust_strip_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."home_page"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "home_page_pain_points_pains" ADD CONSTRAINT "home_page_pain_points_pains_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."home_page"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "home_page_solution_checklist" ADD CONSTRAINT "home_page_solution_checklist_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."home_page"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "home_page_solution_features" ADD CONSTRAINT "home_page_solution_features_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."home_page"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "home_page_why_axil_stats" ADD CONSTRAINT "home_page_why_axil_stats_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."home_page"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "home_page_why_axil_usps" ADD CONSTRAINT "home_page_why_axil_usps_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."home_page"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "home_page_audience_cards_perks" ADD CONSTRAINT "home_page_audience_cards_perks_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."home_page_audience_cards"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "home_page_audience_cards" ADD CONSTRAINT "home_page_audience_cards_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."home_page"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "home_page_process_steps" ADD CONSTRAINT "home_page_process_steps_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."home_page"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "home_page_final_cta_reassurances" ADD CONSTRAINT "home_page_final_cta_reassurances_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."home_page"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "home_page_hero_trust_items_order_idx" ON "home_page_hero_trust_items" USING btree ("_order");
CREATE INDEX "home_page_hero_trust_items_parent_id_idx" ON "home_page_hero_trust_items" USING btree ("_parent_id");
CREATE INDEX "home_page_hero_trust_strip_order_idx" ON "home_page_hero_trust_strip" USING btree ("_order");
CREATE INDEX "home_page_hero_trust_strip_parent_id_idx" ON "home_page_hero_trust_strip" USING btree ("_parent_id");
CREATE INDEX "home_page_pain_points_pains_order_idx" ON "home_page_pain_points_pains" USING btree ("_order");
CREATE INDEX "home_page_pain_points_pains_parent_id_idx" ON "home_page_pain_points_pains" USING btree ("_parent_id");
CREATE INDEX "home_page_solution_checklist_order_idx" ON "home_page_solution_checklist" USING btree ("_order");
CREATE INDEX "home_page_solution_checklist_parent_id_idx" ON "home_page_solution_checklist" USING btree ("_parent_id");
CREATE INDEX "home_page_solution_features_order_idx" ON "home_page_solution_features" USING btree ("_order");
CREATE INDEX "home_page_solution_features_parent_id_idx" ON "home_page_solution_features" USING btree ("_parent_id");
CREATE INDEX "home_page_why_axil_stats_order_idx" ON "home_page_why_axil_stats" USING btree ("_order");
CREATE INDEX "home_page_why_axil_stats_parent_id_idx" ON "home_page_why_axil_stats" USING btree ("_parent_id");
CREATE INDEX "home_page_why_axil_usps_order_idx" ON "home_page_why_axil_usps" USING btree ("_order");
CREATE INDEX "home_page_why_axil_usps_parent_id_idx" ON "home_page_why_axil_usps" USING btree ("_parent_id");
CREATE INDEX "home_page_audience_cards_perks_order_idx" ON "home_page_audience_cards_perks" USING btree ("_order");
CREATE INDEX "home_page_audience_cards_perks_parent_id_idx" ON "home_page_audience_cards_perks" USING btree ("_parent_id");
CREATE INDEX "home_page_audience_cards_order_idx" ON "home_page_audience_cards" USING btree ("_order");
CREATE INDEX "home_page_audience_cards_parent_id_idx" ON "home_page_audience_cards" USING btree ("_parent_id");
CREATE INDEX "home_page_process_steps_order_idx" ON "home_page_process_steps" USING btree ("_order");
CREATE INDEX "home_page_process_steps_parent_id_idx" ON "home_page_process_steps" USING btree ("_parent_id");
CREATE INDEX "home_page_final_cta_reassurances_order_idx" ON "home_page_final_cta_reassurances" USING btree ("_order");
CREATE INDEX "home_page_final_cta_reassurances_parent_id_idx" ON "home_page_final_cta_reassurances" USING btree ("_parent_id");`);
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
DROP TABLE "home_page_hero_trust_items" CASCADE;
DROP TABLE "home_page_hero_trust_strip" CASCADE;
DROP TABLE "home_page_pain_points_pains" CASCADE;
DROP TABLE "home_page_solution_checklist" CASCADE;
DROP TABLE "home_page_solution_features" CASCADE;
DROP TABLE "home_page_why_axil_stats" CASCADE;
DROP TABLE "home_page_why_axil_usps" CASCADE;
DROP TABLE "home_page_audience_cards_perks" CASCADE;
DROP TABLE "home_page_audience_cards" CASCADE;
DROP TABLE "home_page_process_steps" CASCADE;
DROP TABLE "home_page_final_cta_reassurances" CASCADE;
DROP TABLE "home_page" CASCADE;
DROP TYPE "public"."enum_home_page_why_axil_usps_icon";
DROP TYPE "public"."enum_home_page_process_steps_icon";`);
}

View file

@ -1,5 +1,4 @@
import * as migration_20260223_152652 from './20260223_152652';
import * as migration_20260223_204039 from './20260223_204039';
export const migrations = [
{
@ -7,9 +6,4 @@ export const migrations = [
down: migration_20260223_152652.down,
name: '20260223_152652',
},
{
up: migration_20260223_204039.up,
down: migration_20260223_204039.down,
name: '20260223_204039',
},
];

File diff suppressed because it is too large Load diff

View file

@ -20,7 +20,6 @@ import { FAQs } from './payload/collections/FAQs.ts';
import { Navigation } from './payload/globals/Navigation.ts';
import { Footer } from './payload/globals/Footer.ts';
import { SiteSettings } from './payload/globals/SiteSettings.ts';
import { HomePage } from './payload/globals/HomePage.ts';
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
@ -47,7 +46,7 @@ export default buildConfig({
collections: [Users, Media, Services, Categories, Posts, TeamMembers, Testimonials, FAQs],
globals: [Navigation, Footer, SiteSettings, HomePage],
globals: [Navigation, Footer, SiteSettings],
upload: {
limits: {

View file

@ -1,43 +1,15 @@
import type {
CollectionConfig,
CollectionAfterChangeHook,
CollectionAfterDeleteHook,
} from 'payload';
import type { CollectionConfig } from 'payload';
import type { CollectionAfterChangeHook } from 'payload';
const calculateReadingTime: CollectionAfterChangeHook = async ({ doc, req: { payload } }) => {
if (!doc.content) return doc;
try {
// Rough estimate: 200 words per minute
const wordCount = JSON.stringify(doc.content).split(' ').length;
const minutes = Math.ceil(wordCount / 200);
await payload.update({ collection: 'posts', id: doc.id, data: { readingTime: minutes } });
} catch {
/* skip if update fails during seed */
}
// Rough estimate: 200 words per minute
const wordCount = JSON.stringify(doc.content).split(' ').length;
const minutes = Math.ceil(wordCount / 200);
await payload.update({ collection: 'posts', id: doc.id, data: { readingTime: minutes } });
return doc;
};
const revalidatePosts: CollectionAfterChangeHook = async ({ doc }) => {
try {
const { revalidateTag } = await import('next/cache');
revalidateTag('posts');
if (doc?.slug) revalidateTag(`post-${doc.slug}`);
} catch {
/* not in Next.js runtime */
}
return doc;
};
const revalidatePostsOnDelete: CollectionAfterDeleteHook = async ({ doc }) => {
try {
const { revalidateTag } = await import('next/cache');
revalidateTag('posts');
if (doc?.slug) revalidateTag(`post-${doc.slug}`);
} catch {
/* not in Next.js runtime */
}
};
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
@ -48,8 +20,7 @@ export const Posts: CollectionConfig = {
read: () => true,
},
hooks: {
afterChange: [calculateReadingTime, revalidatePosts],
afterDelete: [revalidatePostsOnDelete],
afterChange: [calculateReadingTime],
},
fields: [
{

View file

@ -1,29 +1,4 @@
import type {
CollectionConfig,
CollectionAfterChangeHook,
CollectionAfterDeleteHook,
} from 'payload';
const revalidateServices: CollectionAfterChangeHook = async ({ doc }) => {
try {
const { revalidateTag } = await import('next/cache');
revalidateTag('services');
if (doc?.slug) revalidateTag(`service-${doc.slug}`);
} catch {
/* not in Next.js runtime */
}
return doc;
};
const revalidateServicesOnDelete: CollectionAfterDeleteHook = async ({ doc }) => {
try {
const { revalidateTag } = await import('next/cache');
revalidateTag('services');
if (doc?.slug) revalidateTag(`service-${doc.slug}`);
} catch {
/* not in Next.js runtime */
}
};
import type { CollectionConfig } from 'payload';
export const Services: CollectionConfig = {
slug: 'services',
@ -34,10 +9,6 @@ export const Services: CollectionConfig = {
access: {
read: () => true,
},
hooks: {
afterChange: [revalidateServices],
afterDelete: [revalidateServicesOnDelete],
},
fields: [
{
name: 'title',

View file

@ -1,27 +1,4 @@
import type {
CollectionConfig,
CollectionAfterChangeHook,
CollectionAfterDeleteHook,
} from 'payload';
const revalidateTestimonials: CollectionAfterChangeHook = async ({ doc }) => {
try {
const { revalidateTag } = await import('next/cache');
revalidateTag('testimonials');
} catch {
/* not in Next.js runtime */
}
return doc;
};
const revalidateTestimonialsOnDelete: CollectionAfterDeleteHook = async () => {
try {
const { revalidateTag } = await import('next/cache');
revalidateTag('testimonials');
} catch {
/* not in Next.js runtime */
}
};
import type { CollectionConfig } from 'payload';
export const Testimonials: CollectionConfig = {
slug: 'testimonials',
@ -30,10 +7,6 @@ export const Testimonials: CollectionConfig = {
defaultColumns: ['clientName', 'rating', 'featured', 'publishedAt'],
},
access: { read: () => true },
hooks: {
afterChange: [revalidateTestimonials],
afterDelete: [revalidateTestimonialsOnDelete],
},
fields: [
{ name: 'clientName', type: 'text', required: true },
{ name: 'businessName', type: 'text' },

View file

@ -6,13 +6,6 @@ export const Users: CollectionConfig = {
admin: {
useAsTitle: 'name',
},
// Only authenticated admins can create, update or delete user accounts
access: {
read: ({ req: { user } }) => Boolean(user),
create: ({ req: { user } }) => user?.role === 'admin',
update: ({ req: { user } }) => user?.role === 'admin',
delete: ({ req: { user } }) => user?.role === 'admin',
},
fields: [
{
name: 'name',

View file

@ -1,19 +1,8 @@
import type { GlobalConfig, GlobalAfterChangeHook } from 'payload';
const revalidateFooter: GlobalAfterChangeHook = async ({ doc }) => {
try {
const { revalidateTag } = await import('next/cache');
revalidateTag('footer');
} catch {
/* not in Next.js runtime */
}
return doc;
};
import type { GlobalConfig } from 'payload';
export const Footer: GlobalConfig = {
slug: 'footer',
access: { read: () => true },
hooks: { afterChange: [revalidateFooter] },
fields: [
{
name: 'columns',

View file

@ -1,249 +0,0 @@
import type { GlobalConfig, GlobalAfterChangeHook } from 'payload';
const revalidateHomePage: GlobalAfterChangeHook = async ({ doc }) => {
try {
const { revalidateTag } = await import('next/cache');
revalidateTag('homepage');
} catch {
/* not in Next.js runtime */
}
return doc;
};
export const HomePage: GlobalConfig = {
slug: 'home-page',
access: { read: () => true },
hooks: { afterChange: [revalidateHomePage] },
fields: [
// ── Hero ─────────────────────────────────────────────────────────────────
{
name: 'hero',
type: 'group',
fields: [
{ name: 'eyebrow', type: 'text', defaultValue: 'Trusted by 500+ UK Businesses' },
{
name: 'body',
type: 'text',
defaultValue:
'ICAEW-certified accountants with fixed monthly fees, a dedicated account manager, and zero HMRC surprises.',
},
{ name: 'ctaPrimary', type: 'text', defaultValue: 'Book a Free Consultation' },
{ name: 'ctaSecondary', type: 'text', defaultValue: 'See Our Services' },
{ name: 'statsRating', type: 'text', defaultValue: '4.9/5' },
{ name: 'statsSource', type: 'text', defaultValue: 'Google Reviews' },
{
name: 'trustItems',
type: 'array',
fields: [{ name: 'item', type: 'text', required: true }],
},
{
name: 'trustStrip',
type: 'array',
fields: [{ name: 'item', type: 'text', required: true }],
},
],
},
// ── Pain Points ──────────────────────────────────────────────────────────
{
name: 'painPoints',
type: 'group',
fields: [
{ name: 'overline', type: 'text', defaultValue: 'The problem' },
{
name: 'headline',
type: 'text',
defaultValue: 'Most businesses are losing money to bad accounting.',
},
{ name: 'statValue', type: 'text', defaultValue: '73%' },
{
name: 'statDescription',
type: 'text',
defaultValue:
'of UK small businesses overpay tax due to unclaimed allowances and poor financial planning.',
},
{
name: 'statSource',
type: 'text',
defaultValue: '* HMRC data and independent SME research, 2024',
},
{
name: 'pains',
type: 'array',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'body', type: 'text', required: true },
],
},
],
},
// ── Solution ─────────────────────────────────────────────────────────────
{
name: 'solution',
type: 'group',
fields: [
{ name: 'overline', type: 'text', defaultValue: 'The Axil way' },
{
name: 'headline',
type: 'text',
defaultValue: 'We found a better way to do accounting.',
},
{
name: 'body',
type: 'text',
defaultValue:
"Proactive, transparent, and technology-first. We don't wait for you to ask — we spot savings, flag issues, and keep you compliant before problems arise.",
},
{
name: 'checklist',
type: 'array',
fields: [{ name: 'item', type: 'text', required: true }],
},
{
name: 'features',
type: 'array',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'body', type: 'text', required: true },
],
},
],
},
// ── Why Axil ─────────────────────────────────────────────────────────────
{
name: 'whyAxil',
type: 'group',
fields: [
{ name: 'overline', type: 'text', defaultValue: 'Why choose Axil' },
{
name: 'headline',
type: 'text',
defaultValue: 'Numbers that speak for themselves',
},
{
name: 'stats',
type: 'array',
fields: [
{ name: 'prefix', type: 'text' },
{ name: 'value', type: 'number', required: true },
{ name: 'suffix', type: 'text' },
{ name: 'label', type: 'text', required: true },
],
},
{
name: 'usps',
type: 'array',
fields: [
{
name: 'icon',
type: 'select',
options: [
{ label: 'Shield Check', value: 'shield-check' },
{ label: 'Receipt', value: 'receipt' },
{ label: 'Person', value: 'person-circle' },
{ label: 'Cloud', value: 'cloud' },
],
},
{ name: 'title', type: 'text', required: true },
{ name: 'body', type: 'text', required: true },
],
},
],
},
// ── Audience ─────────────────────────────────────────────────────────────
{
name: 'audience',
type: 'group',
fields: [
{ name: 'overline', type: 'text', defaultValue: 'Built for businesses like yours' },
{ name: 'headline', type: 'text', defaultValue: 'Who we work with' },
{
name: 'cards',
type: 'array',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'tagline', type: 'text' },
{ name: 'body', type: 'text' },
{
name: 'perks',
type: 'array',
fields: [{ name: 'perk', type: 'text', required: true }],
},
{ name: 'featured', type: 'checkbox', defaultValue: false },
{ name: 'href', type: 'text', defaultValue: '/services' },
{ name: 'cta', type: 'text' },
],
},
],
},
// ── Process ──────────────────────────────────────────────────────────────
{
name: 'process',
type: 'group',
fields: [
{ name: 'overline', type: 'text', defaultValue: 'How we do it' },
{
name: 'headline',
type: 'text',
defaultValue: 'From sign-up to sorted in days',
},
{
name: 'body',
type: 'text',
defaultValue:
"We've made switching accountants effortless. Most clients are fully onboarded within a week — then we handle everything so you never have to think about tax again.",
},
{ name: 'ctaLabel', type: 'text', defaultValue: 'Get started' },
{
name: 'steps',
type: 'array',
fields: [
{
name: 'icon',
type: 'select',
options: [
{ label: 'Phone', value: 'phone' },
{ label: 'Zap / Bolt', value: 'zap' },
{ label: 'File Check', value: 'file-check' },
{ label: 'Trending Up', value: 'trending-up' },
],
},
{ name: 'title', type: 'text', required: true },
{ name: 'description', type: 'text', required: true },
],
},
],
},
// ── Final CTA ────────────────────────────────────────────────────────────
{
name: 'finalCta',
type: 'group',
fields: [
{ name: 'overline', type: 'text', defaultValue: 'Ready to start?' },
{
name: 'headline',
type: 'text',
defaultValue: 'Take the stress out of your finances — today.',
},
{
name: 'body',
type: 'text',
defaultValue:
'Book a free 30-minute consultation with one of our accountants. No commitment, no hard sell — just honest advice for your business.',
},
{ name: 'ctaPrimary', type: 'text', defaultValue: 'Book a Free Consultation' },
{ name: 'ctaSecondary', type: 'text', defaultValue: 'See Our Services' },
{
name: 'reassurances',
type: 'array',
fields: [{ name: 'item', type: 'text', required: true }],
},
],
},
],
};

View file

@ -1,19 +1,8 @@
import type { GlobalConfig, GlobalAfterChangeHook } from 'payload';
const revalidateNavigation: GlobalAfterChangeHook = async ({ doc }) => {
try {
const { revalidateTag } = await import('next/cache');
revalidateTag('navigation');
} catch {
/* not in Next.js runtime */
}
return doc;
};
import type { GlobalConfig } from 'payload';
export const Navigation: GlobalConfig = {
slug: 'navigation',
access: { read: () => true },
hooks: { afterChange: [revalidateNavigation] },
fields: [
{
name: 'items',

View file

@ -1,36 +0,0 @@
/**
* Lightweight production migration runner.
*
* Uses a minimal inline Payload config no lexical editor, no plugins, no
* collections/globals to avoid loading heavy modules in a memory-constrained
* environment (the full config would OOM a small VPS via SIGKILL).
*
* Run via: NODE_OPTIONS="--experimental-strip-types --no-require-module" node src/scripts/migrate.ts
*/
import path from 'path';
import { fileURLToPath } from 'url';
import { buildConfig, getPayload } from 'payload';
import { postgresAdapter } from '@payloadcms/db-postgres';
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
// Minimal config: only the DB adapter + explicit migrations path.
// No editor, no plugins, no collections/globals needed to run raw SQL migrations.
const config = buildConfig({
secret: process.env.PAYLOAD_SECRET || 'migrate-secret',
db: postgresAdapter({
pool: { connectionString: process.env.DATABASE_URI },
migrationDir: path.resolve(dirname, '../migrations'),
}),
collections: [],
globals: [],
});
const payload = await getPayload({ config });
console.log('[migrator] Running database migrations...');
await payload.db.migrate();
console.log('[migrator] Migrations complete.');
process.exit(0);