Compare commits
7 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b6cfaa5c2 | ||
|
|
0f8de6479d | ||
|
|
dc1b1efbab | ||
|
|
1ba1349607 | ||
|
|
96ab1c5da6 | ||
|
|
3f6dfe36b1 | ||
|
|
2966dfb518 |
53 changed files with 11043 additions and 994 deletions
|
|
@ -32,7 +32,28 @@
|
|||
"Bash(node --input-type=module:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(ls:*)",
|
||||
"WebFetch(domain:axilaccountants.co.uk)"
|
||||
"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:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
11
.github/workflows/deploy.yml
vendored
11
.github/workflows/deploy.yml
vendored
|
|
@ -32,10 +32,17 @@ jobs:
|
|||
git -C "$PROJECT_DIR" reset --hard origin/main
|
||||
|
||||
echo "▶ Building Docker image..."
|
||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" build --pull
|
||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" build
|
||||
|
||||
echo "▶ Restarting containers..."
|
||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d --remove-orphans
|
||||
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
|
||||
|
||||
echo "▶ Pruning unused images..."
|
||||
docker image prune -f
|
||||
|
|
|
|||
|
|
@ -18,11 +18,16 @@ ENV WATCHPACK_POLLING=true
|
|||
EXPOSE 3000
|
||||
CMD ["pnpm", "dev"]
|
||||
|
||||
# --- Migrator (deps + src only, no Next.js build — used for pnpm payload migrate) ---
|
||||
# --- Migrator (deps + src only — runs migrations before app starts) ---
|
||||
FROM base AS migrator
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
CMD ["pnpm", "payload", "migrate"]
|
||||
# --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"]
|
||||
|
||||
# --- Build ---
|
||||
FROM base AS build
|
||||
|
|
|
|||
|
|
@ -1,4 +1,19 @@
|
|||
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: .
|
||||
|
|
@ -20,6 +35,8 @@ services:
|
|||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
migrator:
|
||||
condition: service_completed_successfully
|
||||
|
||||
db:
|
||||
image: postgres:17-alpine
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
"@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
34
pnpm-lock.yaml
generated
|
|
@ -66,6 +66,9 @@ 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
|
||||
|
|
@ -1497,6 +1500,11 @@ 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==}
|
||||
|
||||
|
|
@ -1974,6 +1982,11 @@ 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==}
|
||||
|
||||
|
|
@ -3350,6 +3363,10 @@ 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}
|
||||
|
|
@ -3965,6 +3982,9 @@ 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
|
||||
|
|
@ -5348,6 +5368,11 @@ 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':
|
||||
|
|
@ -5832,6 +5857,8 @@ snapshots:
|
|||
|
||||
crypt@0.0.2: {}
|
||||
|
||||
cssesc@3.0.0: {}
|
||||
|
||||
cssfilter@0.0.10: {}
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
|
@ -7484,6 +7511,11 @@ 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
|
||||
|
|
@ -8135,6 +8167,8 @@ snapshots:
|
|||
|
||||
utf8-byte-length@1.0.5: {}
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
uuid@10.0.0: {}
|
||||
|
||||
uuid@9.0.0: {}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { Header } from '@/components/layout/Header';
|
||||
import { Footer } from '@/components/layout/Footer';
|
||||
import { ServerHeader } from '@/components/layout/ServerHeader';
|
||||
import { ServerFooter } from '@/components/layout/ServerFooter';
|
||||
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 (
|
||||
<>
|
||||
<Header />
|
||||
<ServerHeader />
|
||||
<main>
|
||||
{/* Hero */}
|
||||
<section className="bg-bg relative overflow-hidden pt-32 pb-20">
|
||||
|
|
@ -275,7 +275,7 @@ export default function AboutPage() {
|
|||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
<ServerFooter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
236
src/app/(site)/blog/[slug]/page.tsx
Normal file
236
src/app/(site)/blog/[slug]/page.tsx
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
130
src/app/(site)/blog/page.tsx
Normal file
130
src/app/(site)/blog/page.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { Header } from '@/components/layout/Header';
|
||||
import { Footer } from '@/components/layout/Footer';
|
||||
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';
|
||||
|
|
@ -96,7 +96,7 @@ const CONTACT_DETAILS = [
|
|||
export default function ContactPage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<ServerHeader />
|
||||
<main>
|
||||
{/* Hero */}
|
||||
<section className="bg-bg relative overflow-hidden pt-32 pb-16">
|
||||
|
|
@ -196,7 +196,7 @@ export default function ContactPage() {
|
|||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
<ServerFooter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Metadata } from 'next';
|
||||
import Image from 'next/image';
|
||||
import { Header } from '@/components/layout/Header';
|
||||
import { Footer } from '@/components/layout/Footer';
|
||||
import { ServerHeader } from '@/components/layout/ServerHeader';
|
||||
import { ServerFooter } from '@/components/layout/ServerFooter';
|
||||
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 (
|
||||
<>
|
||||
<Header />
|
||||
<ServerHeader />
|
||||
<main>
|
||||
{/* Hero */}
|
||||
<section className="bg-bg relative overflow-hidden pt-32 pb-20">
|
||||
|
|
@ -325,7 +325,7 @@ export default function CoursesPage() {
|
|||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
<ServerFooter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
45
src/app/(site)/layout.tsx
Normal file
45
src/app/(site)/layout.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
46
src/app/(site)/page.tsx
Normal file
46
src/app/(site)/page.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,24 +1,25 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { Header } from '@/components/layout/Header';
|
||||
import { Footer } from '@/components/layout/Footer';
|
||||
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 { CheckCircleIcon } from '@/components/ui/icons';
|
||||
import { getPublishedServices, getServiceBySlugCached } from '@/lib/payload';
|
||||
import { convertLexicalToPlaintext } from '@payloadcms/richtext-lexical/plaintext';
|
||||
|
||||
const SERVICES: Record<
|
||||
string,
|
||||
{
|
||||
title: string;
|
||||
tagline: string;
|
||||
intro: string;
|
||||
includes: string[];
|
||||
benefits: { title: string; desc: string }[];
|
||||
faq: { q: string; a: string }[];
|
||||
}
|
||||
> = {
|
||||
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> = {
|
||||
bookkeeping: {
|
||||
title: 'Bookkeeping',
|
||||
tagline: 'Accurate records, zero stress',
|
||||
|
|
@ -32,18 +33,23 @@ const SERVICES: Record<
|
|||
'Monthly management accounts',
|
||||
'Real-time financial dashboard access',
|
||||
],
|
||||
benefits: [
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
title: 'Save 10+ hours per month',
|
||||
desc: 'Stop wrestling with spreadsheets. We handle every transaction so you never have to.',
|
||||
description:
|
||||
'Stop wrestling with spreadsheets. We handle every transaction so you never have to.',
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: 'Always audit-ready',
|
||||
desc: 'Clean, HMRC-compliant records mean no panic when a filing deadline arrives.',
|
||||
description: 'Clean, HMRC-compliant records mean no panic when a filing deadline arrives.',
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: 'Real-time visibility',
|
||||
desc: 'Log in any time to see exactly how your business is performing, with no surprises.',
|
||||
description:
|
||||
'Log in any time to see exactly how your business is performing, with no surprises.',
|
||||
},
|
||||
],
|
||||
faq: [
|
||||
|
|
@ -74,18 +80,24 @@ const SERVICES: Record<
|
|||
'HMRC correspondence management',
|
||||
'Tax planning & year-end strategy',
|
||||
],
|
||||
benefits: [
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
title: 'Never miss a deadline',
|
||||
desc: "We track every HMRC deadline and file well in advance — you'll never pay a late filing penalty.",
|
||||
description:
|
||||
"We track every HMRC deadline and file well in advance — you'll never pay a late filing penalty.",
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: 'Maximum allowances',
|
||||
desc: 'Our tax specialists review every available allowance and relief to legally minimise your tax bill.',
|
||||
description:
|
||||
'Our tax specialists review every available allowance and relief to legally minimise your tax bill.',
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: 'HMRC as a proxy',
|
||||
desc: 'We handle all correspondence with HMRC. You never have to speak to them directly unless you want to.',
|
||||
description:
|
||||
'We handle all correspondence with HMRC. You never have to speak to them directly unless you want to.',
|
||||
},
|
||||
],
|
||||
faq: [
|
||||
|
|
@ -116,18 +128,24 @@ const SERVICES: Record<
|
|||
'Payslips for every employee',
|
||||
'P60s, P45s and P11Ds',
|
||||
],
|
||||
benefits: [
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
title: 'Zero errors',
|
||||
desc: 'Manual payroll errors are costly. Our systems double-check every calculation before submission.',
|
||||
description:
|
||||
'Manual payroll errors are costly. Our systems double-check every calculation before submission.',
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: 'Full compliance',
|
||||
desc: 'RTI submissions, auto-enrolment, NMW compliance — all handled on time, every time.',
|
||||
description:
|
||||
'RTI submissions, auto-enrolment, NMW compliance — all handled on time, every time.',
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: 'Scales with you',
|
||||
desc: 'Whether you have 1 employee or 100, our payroll service scales effortlessly as your team grows.',
|
||||
description:
|
||||
'Whether you have 1 employee or 100, our payroll service scales effortlessly as your team grows.',
|
||||
},
|
||||
],
|
||||
faq: [
|
||||
|
|
@ -158,18 +176,24 @@ const SERVICES: Record<
|
|||
'HMRC VAT correspondence',
|
||||
'VAT registration & deregistration',
|
||||
],
|
||||
benefits: [
|
||||
steps: [
|
||||
{
|
||||
step: 1,
|
||||
title: 'Never miss a VAT deadline',
|
||||
desc: 'Late VAT returns trigger automatic penalties. We file every return well before the deadline.',
|
||||
description:
|
||||
'Late VAT returns trigger automatic penalties. We file every return well before the deadline.',
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: 'Right scheme for your business',
|
||||
desc: "Flat Rate, Cash Accounting or Standard — we make sure you're on the scheme that minimises your VAT bill.",
|
||||
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',
|
||||
desc: "HMRC's Making Tax Digital requirements are completely managed by us. No software headaches.",
|
||||
description:
|
||||
"HMRC's Making Tax Digital requirements are completely managed by us. No software headaches.",
|
||||
},
|
||||
],
|
||||
faq: [
|
||||
|
|
@ -189,8 +213,16 @@ const SERVICES: Record<
|
|||
},
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
return Object.keys(SERVICES).map((slug) => ({ slug }));
|
||||
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 async function generateMetadata({
|
||||
|
|
@ -199,22 +231,64 @@ export async function generateMetadata({
|
|||
params: Promise<{ slug: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const svc = SERVICES[slug];
|
||||
if (!svc) return {};
|
||||
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 {};
|
||||
return {
|
||||
title: `${svc.title} — Axil Accountants`,
|
||||
description: svc.intro,
|
||||
title: `${fallback.title} — Axil Accountants`,
|
||||
description: fallback.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 svc = SERVICES[slug];
|
||||
if (!svc) notFound();
|
||||
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;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<ServerHeader />
|
||||
<main>
|
||||
{/* Hero */}
|
||||
<section className="bg-bg relative overflow-hidden pt-32 pb-20">
|
||||
|
|
@ -243,83 +317,91 @@ export default async function ServicePage({ params }: { params: Promise<{ slug:
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* What'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>
|
||||
<p className="text-emerald mb-3 text-sm font-semibold tracking-widest uppercase">
|
||||
What'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>
|
||||
{/* 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'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}
|
||||
{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">
|
||||
<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>
|
||||
<p className="text-muted text-sm leading-relaxed">{b.desc}</p>
|
||||
</SpotlightCard>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</FadeIn>
|
||||
</div>
|
||||
</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>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* CTA */}
|
||||
<section className="bg-charcoal py-20">
|
||||
|
|
@ -348,7 +430,7 @@ export default async function ServicePage({ params }: { params: Promise<{ slug:
|
|||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
<ServerFooter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { Header } from '@/components/layout/Header';
|
||||
import { Footer } from '@/components/layout/Footer';
|
||||
import { ServerHeader } from '@/components/layout/ServerHeader';
|
||||
import { ServerFooter } from '@/components/layout/ServerFooter';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { BeamButton } from '@/components/ui/BeamButton';
|
||||
import { FadeIn } from '@/components/ui/FadeIn';
|
||||
|
|
@ -13,6 +13,9 @@ 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',
|
||||
|
|
@ -20,7 +23,16 @@ export const metadata: Metadata = {
|
|||
'Bookkeeping, tax returns, payroll and VAT returns for UK businesses. Fixed monthly fees, dedicated account manager.',
|
||||
};
|
||||
|
||||
const SERVICES = [
|
||||
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 = [
|
||||
{
|
||||
slug: 'bookkeeping',
|
||||
icon: BookkeepingIcon,
|
||||
|
|
@ -61,17 +73,38 @@ const SERVICES = [
|
|||
icon: VATIcon,
|
||||
title: 'VAT Returns',
|
||||
tagline: 'MTD-compliant filing',
|
||||
desc: 'Making Tax Digital-compliant VAT returns filed on time, every quarter. We also advise on the right VAT scheme for your business.',
|
||||
desc: 'Making Tax Digital-compliant VAT returns filed on time, every quarter.',
|
||||
perks: ['Quarterly VAT returns', 'MTD compliance', 'VAT scheme advice', 'HMRC correspondence'],
|
||||
color: 'bg-blue-mist',
|
||||
accent: 'text-blue',
|
||||
},
|
||||
];
|
||||
|
||||
export default function ServicesPage() {
|
||||
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;
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<ServerHeader />
|
||||
<main>
|
||||
{/* Hero */}
|
||||
<section className="bg-bg relative overflow-hidden pt-32 pb-20">
|
||||
|
|
@ -178,7 +211,7 @@ export default function ServicesPage() {
|
|||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
<ServerFooter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,438 +0,0 @@
|
|||
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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
@import 'tailwindcss';
|
||||
@plugin '@tailwindcss/typography';
|
||||
|
||||
/* ============================================================
|
||||
DESIGN TOKENS — Axil Accountants
|
||||
|
|
|
|||
|
|
@ -1,45 +1,8 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { Inter, DM_Mono } from 'next/font/google';
|
||||
import { SmoothScrollProvider } from '@/components/providers/SmoothScrollProvider';
|
||||
import './globals.css';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
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>
|
||||
);
|
||||
// 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,20 +1,32 @@
|
|||
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';
|
||||
|
||||
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' },
|
||||
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 COMPANY = [
|
||||
{ label: 'About Us', href: '/about' },
|
||||
{ label: 'Our Team', href: '/about#team' },
|
||||
{ label: 'Blog', href: '/blog' },
|
||||
{ label: 'Contact', href: '/contact' },
|
||||
];
|
||||
const LEGAL = [
|
||||
|
||||
const LEGAL_FALLBACK: LinkItem[] = [
|
||||
{ label: 'Privacy Policy', href: '/privacy-policy' },
|
||||
{ label: 'Cookie Policy', href: '/cookie-policy' },
|
||||
{ label: 'Terms of Use', href: '/terms' },
|
||||
|
|
@ -48,7 +60,21 @@ function FooterCol({
|
|||
);
|
||||
}
|
||||
|
||||
export function Footer() {
|
||||
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';
|
||||
|
||||
return (
|
||||
<footer className="bg-charcoal relative overflow-hidden text-white">
|
||||
{/* Radial gradient overlay */}
|
||||
|
|
@ -81,7 +107,7 @@ export function Footer() {
|
|||
|
||||
<div className="flex gap-3">
|
||||
<a
|
||||
href="https://linkedin.com"
|
||||
href={linkedIn}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="LinkedIn"
|
||||
|
|
@ -93,7 +119,7 @@ export function Footer() {
|
|||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://facebook.com"
|
||||
href={facebook}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Facebook"
|
||||
|
|
@ -106,9 +132,10 @@ export function Footer() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<FooterCol heading="Services" links={SERVICES} />
|
||||
<FooterCol heading="Company" links={COMPANY} />
|
||||
<FooterCol heading="Legal" links={LEGAL} />
|
||||
{columns.map((col) => (
|
||||
<FooterCol key={col.heading} heading={col.heading} links={col.links} />
|
||||
))}
|
||||
<FooterCol heading="Legal" links={legalLinks} />
|
||||
</div>
|
||||
|
||||
{/* Bottom bar */}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,12 @@ 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';
|
||||
|
||||
const NAV_ITEMS = [
|
||||
type NavChild = { label: string; href: string; desc: string };
|
||||
type NavItem = { label: string; href: string; dropdown?: NavChild[] };
|
||||
|
||||
const NAV_ITEMS_FALLBACK: NavItem[] = [
|
||||
{ label: 'Home', href: '/' },
|
||||
{
|
||||
label: 'Services',
|
||||
|
|
@ -33,6 +37,18 @@ const NAV_ITEMS = [
|
|||
{ 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' },
|
||||
|
|
@ -57,11 +73,13 @@ 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;
|
||||
|
|
@ -72,7 +90,7 @@ function NavLinks({
|
|||
|
||||
return (
|
||||
<>
|
||||
{NAV_ITEMS.map((item) =>
|
||||
{items.map((item) =>
|
||||
item.dropdown ? (
|
||||
<div
|
||||
key={item.label}
|
||||
|
|
@ -132,7 +150,8 @@ function NavLinks({
|
|||
);
|
||||
}
|
||||
|
||||
export function Header() {
|
||||
export function Header({ navData }: { navData?: Navigation | null }) {
|
||||
const navItems = buildNavItems(navData);
|
||||
const lastScrollY = useRef(0);
|
||||
const [atTop, setAtTop] = useState(true);
|
||||
const [floatVisible, setFloatVisible] = useState(false);
|
||||
|
|
@ -179,6 +198,7 @@ export function Header() {
|
|||
|
||||
<nav className="hidden items-center gap-1 lg:flex">
|
||||
<NavLinks
|
||||
items={navItems}
|
||||
pathname={pathname}
|
||||
dropdownOpen={dropdownOpen}
|
||||
setDropdownOpen={setDropdownOpen}
|
||||
|
|
@ -226,6 +246,7 @@ export function Header() {
|
|||
{/* Nav links */}
|
||||
<nav className="hidden items-center lg:flex">
|
||||
<NavLinks
|
||||
items={navItems}
|
||||
pathname={pathname}
|
||||
dropdownOpen={dropdownOpenF}
|
||||
setDropdownOpen={setDropdownOpenF}
|
||||
|
|
@ -246,7 +267,7 @@ export function Header() {
|
|||
{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">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
|
|
|
|||
7
src/components/layout/ServerFooter.tsx
Normal file
7
src/components/layout/ServerFooter.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { getFooter } from '@/lib/payload';
|
||||
import { Footer } from './Footer';
|
||||
|
||||
export async function ServerFooter() {
|
||||
const footerData = await getFooter();
|
||||
return <Footer footerData={footerData} />;
|
||||
}
|
||||
7
src/components/layout/ServerHeader.tsx
Normal file
7
src/components/layout/ServerHeader.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { getNavigation } from '@/lib/payload';
|
||||
import { Header } from './Header';
|
||||
|
||||
export async function ServerHeader() {
|
||||
const navData = await getNavigation();
|
||||
return <Header navData={navData} />;
|
||||
}
|
||||
|
|
@ -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 = [
|
||||
const AUDIENCES_FALLBACK = [
|
||||
{
|
||||
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,7 +14,6 @@ const AUDIENCES = [
|
|||
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.',
|
||||
|
|
@ -24,7 +23,6 @@ const AUDIENCES = [
|
|||
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.',
|
||||
|
|
@ -35,22 +33,34 @@ const AUDIENCES = [
|
|||
},
|
||||
];
|
||||
|
||||
export function AudienceSection() {
|
||||
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;
|
||||
|
||||
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">
|
||||
Built for businesses like yours
|
||||
{overline}
|
||||
</p>
|
||||
<h2 className="font-display text-charcoal text-4xl font-bold sm:text-5xl">
|
||||
Who we work with
|
||||
</h2>
|
||||
<h2 className="font-display text-charcoal text-4xl font-bold sm:text-5xl">{headline}</h2>
|
||||
</FadeIn>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-3">
|
||||
{AUDIENCES.map((a, i) => (
|
||||
<FadeIn key={a.label} delay={i * 0.1}>
|
||||
{audiences.map((a, i) => (
|
||||
<FadeIn key={a.title} 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'}`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,27 @@ 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';
|
||||
|
||||
const POSTS = [
|
||||
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 = [
|
||||
{
|
||||
category: 'Tax Tips',
|
||||
title: 'How to Legally Reduce Your Tax Bill in 2026',
|
||||
|
|
@ -14,7 +33,7 @@ const POSTS = [
|
|||
readTime: '5 min read',
|
||||
slug: 'reduce-tax-bill-2026',
|
||||
gradient: 'from-emerald/12 to-emerald-mist',
|
||||
tagVariant: 'green' as const,
|
||||
tagVariant: 'green' as TagVariant,
|
||||
},
|
||||
{
|
||||
category: 'HMRC Updates',
|
||||
|
|
@ -25,7 +44,7 @@ const POSTS = [
|
|||
readTime: '4 min read',
|
||||
slug: 'making-tax-digital-2026',
|
||||
gradient: 'from-blue/10 to-blue-mist',
|
||||
tagVariant: 'blue' as const,
|
||||
tagVariant: 'blue' as TagVariant,
|
||||
},
|
||||
{
|
||||
category: 'Payroll Guide',
|
||||
|
|
@ -36,11 +55,39 @@ const POSTS = [
|
|||
readTime: '6 min read',
|
||||
slug: 'first-employee-payroll-guide',
|
||||
gradient: 'from-emerald/8 to-bg',
|
||||
tagVariant: 'green' as const,
|
||||
tagVariant: 'green' as TagVariant,
|
||||
},
|
||||
];
|
||||
|
||||
export function BlogPreviewSection() {
|
||||
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;
|
||||
|
||||
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">
|
||||
|
|
@ -61,7 +108,7 @@ export function BlogPreviewSection() {
|
|||
</FadeIn>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-3">
|
||||
{POSTS.map((post, i) => (
|
||||
{items.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">
|
||||
|
|
@ -81,8 +128,12 @@ export function BlogPreviewSection() {
|
|||
<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>
|
||||
<span>·</span>
|
||||
<span>{post.readTime}</span>
|
||||
{post.readTime && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span>{post.readTime}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<ArrowRightIcon
|
||||
size={13}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,27 @@ 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 = [
|
||||
const REASSURANCES_FALLBACK = [
|
||||
'4.9/5 on Google',
|
||||
'No lock-in contracts',
|
||||
'ICAEW & ACCA Certified',
|
||||
'Free 30-min consultation',
|
||||
];
|
||||
|
||||
export function FinalCTASection() {
|
||||
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;
|
||||
|
||||
return (
|
||||
<section className="bg-charcoal relative overflow-hidden py-28 lg:py-36">
|
||||
{/* Background decoration */}
|
||||
|
|
@ -21,23 +33,18 @@ export function FinalCTASection() {
|
|||
<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">
|
||||
Ready to start?
|
||||
{overline}
|
||||
</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">
|
||||
Take the stress out of
|
||||
<br />
|
||||
your finances — today.
|
||||
{headline}
|
||||
</h2>
|
||||
|
||||
<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>
|
||||
<p className="mx-auto mb-10 max-w-xl text-lg leading-relaxed text-white/50">{body}</p>
|
||||
|
||||
<div className="mb-10 flex flex-wrap items-center justify-center gap-4">
|
||||
<BeamButton size="lg" variant="light" trailingArrow href="/contact">
|
||||
Book a Free Consultation
|
||||
{ctaPrimary}
|
||||
</BeamButton>
|
||||
<Button
|
||||
size="lg"
|
||||
|
|
@ -45,13 +52,13 @@ export function FinalCTASection() {
|
|||
href="/services"
|
||||
className="border-white/20 text-white hover:border-white/40 hover:bg-white/8"
|
||||
>
|
||||
See Our Services
|
||||
{ctaSecondary}
|
||||
</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}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,22 @@ 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 = [
|
||||
const TRUST_ITEMS_FALLBACK = [
|
||||
'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];
|
||||
|
||||
|
|
@ -125,7 +133,22 @@ function DashboardPreview() {
|
|||
);
|
||||
}
|
||||
|
||||
export function HeroSection() {
|
||||
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;
|
||||
|
||||
return (
|
||||
<section className="bg-bg relative mt-18 flex min-h-[calc(100vh-4.5rem)] flex-col overflow-x-hidden">
|
||||
{/* Background */}
|
||||
|
|
@ -152,29 +175,24 @@ export function HeroSection() {
|
|||
{/* 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">
|
||||
Trusted by 500+ UK Businesses
|
||||
</span>
|
||||
<span className="text-emerald-dark text-xs font-semibold">{eyebrow}</span>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<p className="text-muted mb-7 max-w-sm text-base leading-relaxed">{body}</p>
|
||||
|
||||
<div className="mb-7 flex flex-wrap gap-3">
|
||||
<BeamButton size="lg" trailingArrow href="/contact">
|
||||
Book a Free Consultation
|
||||
{ctaPrimary}
|
||||
</BeamButton>
|
||||
<Button size="lg" variant="secondary" href="/services">
|
||||
See Our Services
|
||||
{ctaSecondary}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<StarRating rating={5} size="sm" />
|
||||
<span className="text-charcoal text-sm font-semibold">4.9/5</span>
|
||||
<span className="text-muted text-sm">Google Reviews</span>
|
||||
<span className="text-charcoal text-sm font-semibold">{statsRating}</span>
|
||||
<span className="text-muted text-sm">{statsSource}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
|
|
@ -243,7 +261,7 @@ export function HeroSection() {
|
|||
for British Business
|
||||
</p>
|
||||
<div className="mt-6 flex flex-col gap-2">
|
||||
{TRUST_ITEMS.map((item) => (
|
||||
{trustItems.map((item) => (
|
||||
<span key={item} className="text-muted flex items-center gap-2 text-sm">
|
||||
<CheckCircleIcon size={13} color="var(--emerald)" />
|
||||
{item}
|
||||
|
|
@ -257,13 +275,19 @@ export function HeroSection() {
|
|||
{/* 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">
|
||||
<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>
|
||||
{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];
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,33 @@
|
|||
import { FadeIn } from '@/components/ui/FadeIn';
|
||||
import type { HomePage } from '@/payload-types';
|
||||
|
||||
const PAINS = [
|
||||
const PAINS_FALLBACK = [
|
||||
{
|
||||
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() {
|
||||
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;
|
||||
|
||||
return (
|
||||
<section className="bg-charcoal relative overflow-hidden py-24 lg:py-32">
|
||||
{/* Subtle dot grid */}
|
||||
|
|
@ -28,45 +37,46 @@ export function PainPointsSection() {
|
|||
{/* Header */}
|
||||
<FadeIn className="mb-16 max-w-3xl">
|
||||
<p className="text-emerald mb-4 text-sm font-semibold tracking-widest uppercase">
|
||||
The problem
|
||||
{overline}
|
||||
</p>
|
||||
<h2 className="font-display text-4xl leading-tight font-bold text-white sm:text-5xl lg:text-[3.5rem]">
|
||||
Most businesses are losing
|
||||
<br />
|
||||
money to <span className="text-emerald">bad accounting.</span>
|
||||
{headline.includes('bad accounting') ? (
|
||||
<>
|
||||
Most businesses are losing
|
||||
<br />
|
||||
money to <span className="text-emerald">bad accounting.</span>
|
||||
</>
|
||||
) : (
|
||||
headline
|
||||
)}
|
||||
</h2>
|
||||
</FadeIn>
|
||||
|
||||
{/* Editorial 73% stat */}
|
||||
{/* Editorial 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]">
|
||||
73<span className="text-5xl sm:text-6xl">%</span>
|
||||
{statValue}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<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>
|
||||
</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.number} delay={i * 0.08} className="h-full">
|
||||
{pains.map((p, i) => (
|
||||
<FadeIn key={p.title} 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">
|
||||
{p.number}
|
||||
{String(i + 1).padStart(2, '0')}
|
||||
</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">
|
||||
|
|
|
|||
|
|
@ -3,6 +3,14 @@ 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;
|
||||
|
|
@ -10,7 +18,7 @@ interface ProcessItem {
|
|||
description: string;
|
||||
}
|
||||
|
||||
const PROCESS_ITEMS: ProcessItem[] = [
|
||||
const PROCESS_ITEMS_FALLBACK: ProcessItem[] = [
|
||||
{
|
||||
icon: Phone,
|
||||
title: 'Free Consultation',
|
||||
|
|
@ -61,35 +69,46 @@ function ProcessCard({ icon: Icon, title, description }: ProcessItem) {
|
|||
);
|
||||
}
|
||||
|
||||
export function ProcessSection() {
|
||||
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;
|
||||
|
||||
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">
|
||||
How we do it
|
||||
{overline}
|
||||
</span>
|
||||
<h2 className="font-display text-charcoal mb-4 text-3xl font-bold tracking-tight md:text-4xl">
|
||||
From sign-up to sorted in days
|
||||
{headline}
|
||||
</h2>
|
||||
<p className="text-muted mb-6 text-base leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
<p className="text-muted mb-6 text-base leading-relaxed">{body}</p>
|
||||
<Button
|
||||
size="lg"
|
||||
href="/contact"
|
||||
className="transition-transform duration-300 hover:scale-[1.04]"
|
||||
>
|
||||
Get started
|
||||
{ctaLabel}
|
||||
<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">
|
||||
{PROCESS_ITEMS.map((item, i) => (
|
||||
{items.map((item, i) => (
|
||||
<FadeIn key={item.title} delay={i * 0.1}>
|
||||
<ProcessCard {...item} />
|
||||
</FadeIn>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,28 @@ 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';
|
||||
|
||||
const SERVICES = [
|
||||
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 = [
|
||||
{
|
||||
icon: BookkeepingIcon,
|
||||
title: 'Bookkeeping',
|
||||
|
|
@ -17,7 +37,7 @@ const SERVICES = [
|
|||
description:
|
||||
'Bank reconciliation, expense categorisation, monthly management accounts — all handled so you always know where you stand.',
|
||||
href: '/services/bookkeeping',
|
||||
accent: 'emerald' as const,
|
||||
accent: 'emerald' as AccentKey,
|
||||
},
|
||||
{
|
||||
icon: TaxIcon,
|
||||
|
|
@ -26,7 +46,7 @@ const SERVICES = [
|
|||
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 const,
|
||||
accent: 'blue' as AccentKey,
|
||||
},
|
||||
{
|
||||
icon: PayrollIcon,
|
||||
|
|
@ -35,7 +55,7 @@ const SERVICES = [
|
|||
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 const,
|
||||
accent: 'emerald' as AccentKey,
|
||||
},
|
||||
{
|
||||
icon: VATIcon,
|
||||
|
|
@ -44,7 +64,7 @@ const SERVICES = [
|
|||
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 const,
|
||||
accent: 'blue' as AccentKey,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -63,7 +83,26 @@ const accentMap = {
|
|||
},
|
||||
};
|
||||
|
||||
export function ServicesSection() {
|
||||
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;
|
||||
|
||||
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">
|
||||
|
|
@ -86,9 +125,9 @@ export function ServicesSection() {
|
|||
|
||||
{/* Card grid */}
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{SERVICES.map((s, i) => {
|
||||
{items.map((s, i) => {
|
||||
const Icon = s.icon;
|
||||
const colors = accentMap[s.accent];
|
||||
const colors = accentMap[s.accent as AccentKey];
|
||||
return (
|
||||
<FadeIn key={s.title} delay={i * 0.08}>
|
||||
<Link href={s.href} className="group block h-full">
|
||||
|
|
|
|||
|
|
@ -3,26 +3,24 @@
|
|||
import { useState } from 'react';
|
||||
import { FadeIn } from '@/components/ui/FadeIn';
|
||||
import { CheckCircleIcon } from '@/components/ui/icons';
|
||||
import type { HomePage } from '@/payload-types';
|
||||
|
||||
const FEATURES = [
|
||||
const FEATURES_FALLBACK = [
|
||||
{
|
||||
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 = [
|
||||
const CHECKLIST_FALLBACK = [
|
||||
'Onboarded within 48 hours',
|
||||
'We handle the HMRC transition',
|
||||
'MTD-compliant from day one',
|
||||
|
|
@ -48,9 +46,25 @@ const STACK = [
|
|||
},
|
||||
];
|
||||
|
||||
export function SolutionSection() {
|
||||
export function SolutionSection({ data }: { data?: HomePage['solution'] | null }) {
|
||||
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">
|
||||
|
|
@ -58,20 +72,14 @@ export function SolutionSection() {
|
|||
{/* Left — text */}
|
||||
<FadeIn x={-20} y={0}>
|
||||
<p className="text-emerald mb-4 text-sm font-semibold tracking-widest uppercase">
|
||||
The Axil way
|
||||
{overline}
|
||||
</p>
|
||||
<h2 className="font-display text-charcoal mb-6 text-4xl leading-tight font-bold sm:text-5xl">
|
||||
We found a<br />
|
||||
<span className="gradient-text">better way</span>
|
||||
<br />
|
||||
to do accounting.
|
||||
{headline}
|
||||
</h2>
|
||||
<p className="text-muted mb-8 text-lg leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
<p className="text-muted mb-8 text-lg leading-relaxed">{body}</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>
|
||||
|
|
@ -83,7 +91,7 @@ export function SolutionSection() {
|
|||
{/* 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];
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ 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: TestimonialItem[] = [
|
||||
const TESTIMONIALS_FALLBACK: TestimonialItem[] = [
|
||||
{
|
||||
name: 'Sarah T.',
|
||||
role: 'Limited Company Director',
|
||||
|
|
@ -56,11 +57,18 @@ const TESTIMONIALS: TestimonialItem[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const col1 = TESTIMONIALS.slice(0, 3);
|
||||
const col2 = TESTIMONIALS.slice(3, 6);
|
||||
const col3 = [...TESTIMONIALS.slice(6, 8), TESTIMONIALS[0]];
|
||||
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 };
|
||||
}
|
||||
|
||||
export function TestimonialsSection() {
|
||||
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[];
|
||||
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">
|
||||
|
|
|
|||
|
|
@ -1,65 +1,83 @@
|
|||
'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 STATS = [
|
||||
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 = [
|
||||
{ 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 = [
|
||||
const USPS_FALLBACK = [
|
||||
{
|
||||
Icon: ShieldCheckIcon,
|
||||
icon: 'shield-check',
|
||||
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: ReceiptIcon,
|
||||
icon: 'receipt',
|
||||
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: PersonCircleIcon,
|
||||
icon: 'person-circle',
|
||||
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: CloudIcon,
|
||||
icon: 'cloud',
|
||||
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() {
|
||||
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;
|
||||
|
||||
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">
|
||||
Why choose Axil
|
||||
{overline}
|
||||
</p>
|
||||
<h2 className="font-display text-charcoal text-4xl font-bold sm:text-5xl">
|
||||
Numbers that
|
||||
<br />
|
||||
speak for themselves
|
||||
</h2>
|
||||
<h2 className="font-display text-charcoal text-4xl font-bold sm:text-5xl">{headline}</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}
|
||||
|
|
@ -73,15 +91,24 @@ export function WhyAxilSection() {
|
|||
|
||||
{/* USP grid */}
|
||||
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{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>
|
||||
))}
|
||||
{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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
// Runs on Next.js startup (Node.js runtime only) before any requests are handled.
|
||||
// Uses compiled/bundled code — no tsx/ESM resolution issues.
|
||||
// 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.
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
const { getPayload } = await import('payload');
|
||||
const { default: config } = await import('@payload-config');
|
||||
|
||||
const payload = await getPayload({ config });
|
||||
|
||||
console.log('[startup] Running database migrations...');
|
||||
await payload.db.migrate();
|
||||
console.log('[startup] Migrations complete.');
|
||||
// Initialize Payload (connects to DB, registers collections/globals)
|
||||
await getPayload({ config });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
171
src/lib/payload.ts
Normal file
171
src/lib/payload.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
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 },
|
||||
);
|
||||
765
src/lib/seed.ts
Normal file
765
src/lib/seed.ts
Normal file
|
|
@ -0,0 +1,765 @@
|
|||
/**
|
||||
* 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 3–4 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);
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres';
|
||||
import { sql, type MigrateUpArgs, type MigrateDownArgs } from '@payloadcms/db-postgres';
|
||||
|
||||
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
|
|
|
|||
6575
src/migrations/20260223_204039.json
Normal file
6575
src/migrations/20260223_204039.json
Normal file
File diff suppressed because it is too large
Load diff
182
src/migrations/20260223_204039.ts
Normal file
182
src/migrations/20260223_204039.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
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";`);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import * as migration_20260223_152652 from './20260223_152652';
|
||||
import * as migration_20260223_204039 from './20260223_204039';
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
|
|
@ -6,4 +7,9 @@ export const migrations = [
|
|||
down: migration_20260223_152652.down,
|
||||
name: '20260223_152652',
|
||||
},
|
||||
{
|
||||
up: migration_20260223_204039.up,
|
||||
down: migration_20260223_204039.down,
|
||||
name: '20260223_204039',
|
||||
},
|
||||
];
|
||||
|
|
|
|||
1696
src/payload-types.ts
Normal file
1696
src/payload-types.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -20,6 +20,7 @@ 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);
|
||||
|
|
@ -46,7 +47,7 @@ export default buildConfig({
|
|||
|
||||
collections: [Users, Media, Services, Categories, Posts, TeamMembers, Testimonials, FAQs],
|
||||
|
||||
globals: [Navigation, Footer, SiteSettings],
|
||||
globals: [Navigation, Footer, SiteSettings, HomePage],
|
||||
|
||||
upload: {
|
||||
limits: {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,43 @@
|
|||
import type { CollectionConfig } from 'payload';
|
||||
import type { CollectionAfterChangeHook } from 'payload';
|
||||
import type {
|
||||
CollectionConfig,
|
||||
CollectionAfterChangeHook,
|
||||
CollectionAfterDeleteHook,
|
||||
} from 'payload';
|
||||
|
||||
const calculateReadingTime: CollectionAfterChangeHook = async ({ doc, req: { payload } }) => {
|
||||
if (!doc.content) return doc;
|
||||
// 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 } });
|
||||
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 */
|
||||
}
|
||||
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: {
|
||||
|
|
@ -20,7 +48,8 @@ export const Posts: CollectionConfig = {
|
|||
read: () => true,
|
||||
},
|
||||
hooks: {
|
||||
afterChange: [calculateReadingTime],
|
||||
afterChange: [calculateReadingTime, revalidatePosts],
|
||||
afterDelete: [revalidatePostsOnDelete],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,29 @@
|
|||
import type { CollectionConfig } from 'payload';
|
||||
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 */
|
||||
}
|
||||
};
|
||||
|
||||
export const Services: CollectionConfig = {
|
||||
slug: 'services',
|
||||
|
|
@ -9,6 +34,10 @@ export const Services: CollectionConfig = {
|
|||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
hooks: {
|
||||
afterChange: [revalidateServices],
|
||||
afterDelete: [revalidateServicesOnDelete],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,27 @@
|
|||
import type { CollectionConfig } from 'payload';
|
||||
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 */
|
||||
}
|
||||
};
|
||||
|
||||
export const Testimonials: CollectionConfig = {
|
||||
slug: 'testimonials',
|
||||
|
|
@ -7,6 +30,10 @@ 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' },
|
||||
|
|
|
|||
|
|
@ -6,6 +6,13 @@ 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',
|
||||
|
|
|
|||
|
|
@ -1,8 +1,19 @@
|
|||
import type { GlobalConfig } from 'payload';
|
||||
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;
|
||||
};
|
||||
|
||||
export const Footer: GlobalConfig = {
|
||||
slug: 'footer',
|
||||
access: { read: () => true },
|
||||
hooks: { afterChange: [revalidateFooter] },
|
||||
fields: [
|
||||
{
|
||||
name: 'columns',
|
||||
|
|
|
|||
249
src/payload/globals/HomePage.ts
Normal file
249
src/payload/globals/HomePage.ts
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
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 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -1,8 +1,19 @@
|
|||
import type { GlobalConfig } from 'payload';
|
||||
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;
|
||||
};
|
||||
|
||||
export const Navigation: GlobalConfig = {
|
||||
slug: 'navigation',
|
||||
access: { read: () => true },
|
||||
hooks: { afterChange: [revalidateNavigation] },
|
||||
fields: [
|
||||
{
|
||||
name: 'items',
|
||||
|
|
|
|||
36
src/scripts/migrate.ts
Normal file
36
src/scripts/migrate.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* 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);
|
||||
Loading…
Add table
Reference in a new issue