Building a Future-Ready Website for Ghanaian and International Markets in 2026
Learn how to build a website that grows your business in Ghana and globally in 2026: scalable architecture, security, global payments, SEO, and easy content management.

COPTI — the Conference of Principals of Technical Institutions — is Ghana's umbrella body for technical and vocational schools. Before this project, their web presence was fragmented and invisible to search engines. Students looking for technical schools in Ghana or TVET programmes Ghana couldn't find member institutions through organic search.
They needed a centralised, fast-loading website that could showcase all 60+ member schools, publish news and events, and rank on Google for students actively searching for technical education across Ghana's regions.
For an educational directory with 60+ pages of structured content, we needed static generation for speed, a CMS non-technical staff could actually use, and a deployment pipeline that updates instantly when content changes. Next.js 15's App Router with Incremental Static Regeneration (ISR) and Sanity's webhook-triggered revalidation was the right combination.
Sanity CMS content layer
Custom schemas for schools, staff, news, and events. Portable Text for rich editorial content with no lock-in.
Static generation + ISR
All 60+ school pages pre-rendered at build time. Sanity webhooks trigger revalidation on content edits — no manual redeploys.
Full technical SEO system
Dynamic metadata, Open Graph images, sitemap.xml, robots.txt, and JSON-LD structured data on every page.
Staff training documentation
Delivered a full PDF content management guide so the COPTI team can publish and update independently.
The schema needed to be strict enough to keep 60+ records consistent, but flexible enough for editors who aren't developers. Using a list on region and validation rules on required fields prevented data entry errors that would have broken filtering on the frontend.
1
2TypeScript — schemas/school.ts
3import { defineField, defineType } from 'sanity'
4
5export default defineType({
6 name: 'school',
7 title: 'Member School',
8 type: 'document',
9 fields: [
10 defineField({
11 name: 'name',
12 type: 'string',
13 validation: Rule => Rule.required()
14 }),
15 defineField({
16 name: 'slug',
17 type: 'slug',
18 options: { source: 'name', maxLength: 96 }
19 }),
20 defineField({
21 name: 'region',
22 type: 'string',
23 options: {
24 list: [
25 'Greater Accra', 'Volta', 'Ashanti',
26 'Northern', 'Western', 'Central'
27 ]
28 }
29 }),
30 defineField({ name: 'programmes', type: 'array', of: [{ type: 'string' }] }),
31 defineField({ name: 'principal', type: 'string' }),
32 defineField({ name: 'phone', type: 'string' }),
33 defineField({ name: 'logo', type: 'image', options: { hotspot: true } }),
34 defineField({ name: 'description', type: 'array', of: [{ type: 'block' }] }),
35 ]
36})GROQ — Sanity's native query language — lets you project only the fields you need and filter server-side. This keeps frontend bundle sizes small and makes region-based school filtering fast, with no extra API layer required.
1
2TypeScript — lib/queries.ts
3import { client } from '@/sanity/client'
4
5// Fetch schools, optionally filtered by region
6export async function getSchools(region?: string) {
7 const filter = region
8 ? `_type == "school" && region == $region`
9 : `_type == "school"`
10
11 return client.fetch(
12 `*[${filter}] | order(name asc) {
13 _id, name, region, principal,
14 "slug": slug.current,
15 "logo": logo.asset->url,
16 programmes
17 }`,
18 region ? { region } : {}
19 )
20}
21
22// Single school page — cached with a revalidation tag
23export async function getSchoolBySlug(slug: string) {
24 return client.fetch(
25 `*[_type == "school" && slug.current == $slug][0]`,
26 { slug },
27 { next: { tags: [`school-${slug}`], revalidate: 3600 } }
28 )
29}For a school directory to rank on Google, every page needs unique metadata and structured data that signals what the institution offers. We used Next.js's generateMetadata for dynamic page titles and Open Graph tags, combined with EducationalOrganization JSON-LD schema so Google can surface programme information directly in search results — especially valuable for queries like technical schools in Volta Region Ghana.
1
2TypeScript — app/schools/[slug]/page.tsx
3// Dynamic metadata for each school page
4export async function generateMetadata({ params }: Props): Promise<Metadata> {
5 const school = await getSchoolBySlug(params.slug)
6
7 return {
8 title: `${school.name} | Technical School in Ghana — COPTI`,
9 description: `${school.name} is a COPTI member technical institution
10 in ${school.region}, Ghana, offering ${school.programmes.join(', ')}.`,
11 openGraph: {
12 title: school.name,
13 images: [school.logo],
14 type: 'website',
15 },
16 alternates: {
17 canonical: `https://copti.org.gh/schools/${school.slug}`
18 }
19 }
20}
21
22// EducationalOrganization JSON-LD for Google rich results
23function SchoolJsonLd({ school }: { school: School }) {
24 const jsonLd = {
25 '@context': 'https://schema.org',
26 '@type': 'EducationalOrganization',
27 name: school.name,
28 address: {
29 '@type': 'PostalAddress',
30 addressRegion: school.region,
31 addressCountry: 'GH'
32 },
33 telephone: school.phone,
34 hasOfferCatalog: {
35 '@type': 'OfferCatalog',
36 name: 'Programmes Offered',
37 itemListElement: school.programmes.map(p => ({
38 '@type': 'Offer', name: p
39 }))
40 }
41 }
42
43 return <script type="application/ld+json"
44 dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
45}SEO note
The description field is written as a natural sentence that includes the school name, region, and programme names — all terms students actually search for. Keyword stuffing descriptions hurt rankings; this pattern integrates them naturally while remaining under Google's ~160 character display limit.
Fetching school data at request time would mean slower page loads and more Sanity API calls. By using generateStaticParams, all 60+ school pages are pre-built at deploy time and served as static HTML — which Vercel's CDN delivers globally in milliseconds. Sanity webhooks then call a revalidation endpoint to refresh only the updated page, not the whole site.
1
2TypeScript — static params + revalidation webhook
3// Pre-render all school pages at build time
4export async function generateStaticParams() {
5 const slugs: string[] = await client.fetch(
6 `*[_type == "school"].slug.current`
7 )
8 return slugs.map(slug => ({ slug }))
9}
10
11// Sanity webhook → revalidate only the updated school page
12export async function POST(req: Request) {
13 const { slug } = await req.json()
14 await revalidateTag(`school-${slug}`)
15 return Response.json({ revalidated: true, slug })
16}
17
1860+ technical school records structured, migrated, and published in Sanity CMS100/100 Lighthouse SEO score with unique metadata and JSON-LD on every pageAll school pages statically generated — sub-second load times globally via Vercel CDNContent editors publish in Sanity; Vercel revalidates automatically via webhooksStaff training PDF delivered — COPTI team manages content with no developer dependencyIndexed and ranking for "technical schools Ghana" and related queries within weeks of launch
Need a fast, SEO-ready website for your institution or NGO in Ghana?
Celestial Web Solutions builds Next.js websites that rank, load fast, and are easy for your team to manage.
Learn how to build a website that grows your business in Ghana and globally in 2026: scalable architecture, security, global payments, SEO, and easy content management.
Stay ahead with the newest web development tools and frameworks.
Confused about choosing between WordPress and a custom website? This comprehensive guide helps Ghanaian businesses make the right decision based on budget, features, and long-term goals.