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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { allPosts } from "@/lib/blog";
import { BLOG_PAGE_SIZE, pageCount } from "@/lib/blog/pagination";
import { prisma } from "@/lib/db";
import { PUBLIC_HACKATHON_WHERE } from "@/lib/events/visibility";
import { coverFor } from "@/lib/events/covers";
import { INDEXABLE_DEVELOPER_WHERE, isRealAccountEmail } from "@/lib/seo/indexable";
import { RENAMED_IMAGES } from "@/lib/seo/renamed-images";
import { appUrl } from "@/lib/url";

/**
Expand All @@ -15,11 +17,29 @@ import { appUrl } from "@/lib/url";
*/
export const revalidate = 3600;

/** The current image paths in one marketing folder, as absolute URLs. */
function imagesIn(...folders: string[]): string[] {
return Object.values(RENAMED_IMAGES)
.filter((path) => folders.some((folder) => path.startsWith(`/marketing/${folder}/`)))
.map((path) => appUrl(path));
}

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// Image entries help Google Images find the photos each page actually shows.
const entries: MetadataRoute.Sitemap = [
{ url: appUrl("/"), changeFrequency: "daily", priority: 1 },
{
url: appUrl("/"),
changeFrequency: "daily",
priority: 1,
images: imagesIn("hero/kenya", "journey", "trust"),
},
{ url: appUrl("/hackathons"), changeFrequency: "daily", priority: 0.9 },
{ url: appUrl("/how-it-works"), changeFrequency: "weekly", priority: 0.8 },
{
url: appUrl("/how-it-works"),
changeFrequency: "weekly",
priority: 0.8,
images: imagesIn("process"),
},
{ url: appUrl("/how-escrow-works"), changeFrequency: "weekly", priority: 0.8 },
{ url: appUrl("/for-organizers"), changeFrequency: "weekly", priority: 0.8 },
{ url: appUrl("/blog"), changeFrequency: "daily", priority: 0.8 },
Expand All @@ -35,6 +55,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
lastModified: new Date(meta.publishedAt),
changeFrequency: "monthly",
priority: 0.7,
images: [appUrl(meta.cover)],
});
}
// Paginated blog views are indexable with self-canonicals, so list them too.
Expand All @@ -55,7 +76,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
prisma.event.findMany({
// Demo hackathons are noindex, so they don't belong in the sitemap.
where: { ...PUBLIC_HACKATHON_WHERE, isDemo: false },
select: { slug: true, updatedAt: true },
select: { slug: true, updatedAt: true, coverUrl: true, categories: true },
orderBy: { startsAt: "desc" },
}),
prisma.user.findMany({
Expand All @@ -69,6 +90,12 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
lastModified: event.updatedAt,
changeFrequency: "daily",
priority: 0.9,
images: [
(() => {
const cover = coverFor(event);
return cover.startsWith("/") ? appUrl(cover) : cover;
})(),
],
});
}
for (const developer of developers.filter((user) => isRealAccountEmail(user.email))) {
Expand Down
2 changes: 1 addition & 1 deletion components/patterns/auth-visual.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function AuthVisual() {
return (
<aside className="auth-visual" aria-label="About HackVillage">
<Image
src="/marketing/auth/welcome.webp"
src="/marketing/auth/hackathon-builders-welcome.webp"
alt=""
fill
priority
Expand Down
2 changes: 1 addition & 1 deletion components/patterns/blog-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function BlogCard({ post }: { post: BlogPostMeta }) {
<div className="relative aspect-[21/9] overflow-hidden bg-brand/10">
<Image
src={post.cover}
alt=""
alt={post.coverAlt}
fill
sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
className="object-cover transition-transform duration-500 group-hover:scale-[1.04]"
Expand Down
10 changes: 5 additions & 5 deletions components/patterns/builder-journey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ export function BuilderJourney() {
</Link>
</div>
<div className="journey-pair journey-pair-left">
<PillPhoto name="build" alt="Kenyan software engineer building her hackathon project" />
<PillPhoto name="kenyan-developer-building-hackathon-project" alt="Kenyan software engineer building her hackathon project" />
<PillPhoto
name="ship"
name="developer-demoing-mobile-app"
alt="Kenyan developer demonstrating the mobile app he built"
/>
</div>
Expand All @@ -73,7 +73,7 @@ export function BuilderJourney() {
</svg>
<div className="journey-pill journey-pill-center">
<Image
src="/marketing/journey/win.webp"
src="/marketing/journey/hackathon-winner-with-trophy-and-laptop.webp"
alt="Kenyan hackathon winner holding her trophy and laptop"
fill
sizes="(max-width: 1023px) 80vw, 26vw"
Expand All @@ -85,11 +85,11 @@ export function BuilderJourney() {
<div className="journey-column journey-column-right">
<div className="journey-pair journey-pair-right">
<PillPhoto
name="paid"
name="developer-receiving-hackathon-prize-payout"
alt="Kenyan developer checking his phone after receiving a prize payout"
/>
<PillPhoto
name="hired"
name="engineer-presenting-portfolio-to-hiring-manager"
alt="Kenyan engineer presenting her project portfolio to a hiring manager"
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion components/patterns/cta-banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function CtaBanner({ photoSide = "right" }: { photoSide?: "left" | "right

<div className="cta-banner-photo">
<Image
src="/marketing/newsletter/community.webp"
src="/marketing/newsletter/developers-exploring-next-hackathon.webp"
alt="Two Kenyan developers exploring their next hackathon together"
fill
sizes="(max-width: 1023px) 100vw, 50vw"
Expand Down
8 changes: 4 additions & 4 deletions components/patterns/how-it-works.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { HOST_HACKATHON_HREF } from "@/lib/auth/signup-links";
const STEPS = [
{
number: "01",
image: "launch",
image: "organizers-planning-hackathon-at-laptop",
intro: "Set the challenge, put up the money, and give great ideas somewhere to start.",
title: "The Prize Is Already There",
description:
Expand All @@ -17,7 +17,7 @@ const STEPS = [
},
{
number: "02",
image: "build",
image: "developers-building-at-nairobi-hackathon",
intro: "Bring the right people together and turn one idea into a working project.",
title: "Builders Team Up",
description:
Expand All @@ -26,7 +26,7 @@ const STEPS = [
},
{
number: "03",
image: "judge",
image: "developer-demoing-project-to-hackathon-judges",
intro: "Give every project a fair review and feedback they can actually use.",
title: "Everyone Gets Judged the Same Way",
description:
Expand All @@ -35,7 +35,7 @@ const STEPS = [
},
{
number: "04",
image: "reward",
image: "hackathon-winners-checking-payout-beside-trophy",
intro: "Celebrate the work, reward the winners, and let anyone check the receipts.",
title: "Winners Get Paid Instantly",
description:
Expand Down
14 changes: 7 additions & 7 deletions components/patterns/landing-hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,35 +62,35 @@ export function LandingHero() {
Real rewards.
</p>
<Photo
name="coding-focus"
name="kenyan-software-engineer-coding-on-laptop"
alt="Kenyan software engineer concentrating on her laptop in a sunlit workspace"
/>
</div>
<div className="hero-column hero-column-left">
<Photo name="speaker" alt="Kenyan tech speaker sharing ideas at a hackathon" />
<Photo name="kenyan-tech-speaker-at-hackathon" alt="Kenyan tech speaker sharing ideas at a hackathon" />
<Photo
name="team-build"
name="kenyan-developers-collaborating-at-hackathon"
alt="Two Kenyan developers collaborating on a hackathon project"
/>
</div>
<Photo
name="center-developer"
name="kenyan-software-engineer-holding-laptop"
alt="Kenyan software engineer in a blue overshirt holding her laptop"
center
/>
<div className="hero-column hero-column-right">
<Photo
name="hackathon-pair"
name="kenyan-engineers-coding-together"
alt="Two Kenyan engineers sharing a laugh while coding together"
/>
<Photo
name="community"
name="nairobi-tech-community-gathering"
alt="Developers exchanging ideas around a table at a Nairobi tech gathering"
/>
</div>
<div className="hero-column hero-column-edge-right">
<Photo
name="event-arrival"
name="kenyan-developer-arriving-at-hackathon"
alt="Kenyan developer arriving at a hackathon with his laptop"
/>
<p className="hero-side-note">
Expand Down
8 changes: 4 additions & 4 deletions components/patterns/trust-impact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function TrustImpact() {
<div className="trust-impact-frame">
<div className="trust-impact-photo">
<Image
src="/marketing/trust/escrow.webp"
src="/marketing/trust/organizer-reviewing-hackathon-prize-funding.webp"
alt="Kenyan organizer reviewing hackathon prize funding at her laptop"
fill
sizes="(max-width: 1023px) 80vw, 460px"
Expand All @@ -72,7 +72,7 @@ export function TrustImpact() {
<div className="trust-impact-strip">
<div className="trust-strip-photo">
<Image
src="/marketing/trust/deposit.webp"
src="/marketing/trust/organizer-funding-hackathon-prize-pool.webp"
alt="Kenyan organizer funding a prize pool from his phone"
fill
sizes="200px"
Expand All @@ -81,7 +81,7 @@ export function TrustImpact() {
</div>
<div className="trust-strip-photo">
<Image
src="/marketing/trust/payout.webp"
src="/marketing/trust/developer-celebrating-prize-payout.webp"
alt="Kenyan developer celebrating a prize payout on her phone"
fill
sizes="200px"
Expand All @@ -90,7 +90,7 @@ export function TrustImpact() {
</div>
<div className="trust-strip-photo">
<Image
src="/marketing/trust/record.webp"
src="/marketing/trust/engineer-checking-payout-record.webp"
alt="Kenyan engineer checking a transaction record on his laptop"
fill
sizes="200px"
Expand Down
26 changes: 13 additions & 13 deletions db/seeds/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ async function main(): Promise<void> {
maxTeams: 20,
rolesWanted: ["frontend", "fintech", "mobile", "design"],
categories: ["fintech", "mobility"],
coverUrl: "/marketing/hackathons/fintech.webp",
coverUrl: "/marketing/hackathons/fintech-hackathon-kenya.webp",
status: "LIVE",
prizeVerifiedAt: new Date(now - 5 * DAY),
publishedAt: new Date(now - 6 * DAY),
Expand Down Expand Up @@ -220,7 +220,7 @@ async function main(): Promise<void> {
maxTeams: 15,
rolesWanted: ["ai", "backend", "design"],
categories: ["ai", "health"],
coverUrl: "/marketing/hackathons/ai.webp",
coverUrl: "/marketing/hackathons/ai-hackathon-kenya.webp",
status: "PENDING_DEPOSIT",
publishedAt: new Date(now - 1 * DAY),
},
Expand Down Expand Up @@ -271,7 +271,7 @@ async function main(): Promise<void> {
verified: true,
rolesWanted: ["data", "backend", "mobile"],
categories: ["climate", "agritech"],
coverUrl: "/marketing/hackathons/climate.webp",
coverUrl: "/marketing/hackathons/climate-tech-hackathon-kenya.webp",
prizes: [
[1, "1st place", 200_000],
[2, "2nd place", 100_000],
Expand All @@ -291,7 +291,7 @@ async function main(): Promise<void> {
verified: true,
rolesWanted: ["mobile", "design", "backend"],
categories: ["agritech", "fintech"],
coverUrl: "/marketing/hackathons/agritech.webp",
coverUrl: "/marketing/hackathons/agritech-hackathon-kenya.webp",
prizes: [
[1, "1st place", 150_000],
[2, "2nd place", 75_000],
Expand All @@ -312,7 +312,7 @@ async function main(): Promise<void> {
verified: true,
rolesWanted: ["frontend", "backend", "civic"],
categories: ["civic"],
coverUrl: "/marketing/hackathons/civic.webp",
coverUrl: "/marketing/hackathons/civic-tech-hackathon-kenya.webp",
prizes: [
[1, "1st place", 150_000],
[2, "2nd place", 75_000],
Expand All @@ -332,7 +332,7 @@ async function main(): Promise<void> {
verified: true,
rolesWanted: ["hardware", "mobile", "fintech"],
categories: ["climate"],
coverUrl: "/marketing/hackathons/climate.webp",
coverUrl: "/marketing/hackathons/climate-tech-hackathon-kenya.webp",
prizes: [
[1, "1st place", 200_000],
[2, "2nd place", 100_000],
Expand All @@ -352,7 +352,7 @@ async function main(): Promise<void> {
verified: true,
rolesWanted: ["backend", "data", "mobile"],
categories: ["agritech", "web3"],
coverUrl: "/marketing/hackathons/agritech.webp",
coverUrl: "/marketing/hackathons/agritech-hackathon-kenya.webp",
prizes: [
[1, "1st place", 250_000],
[2, "2nd place", 120_000],
Expand All @@ -373,7 +373,7 @@ async function main(): Promise<void> {
verified: false,
rolesWanted: ["frontend", "design", "education"],
categories: ["edtech"],
coverUrl: "/marketing/hackathons/edtech.webp",
coverUrl: "/marketing/hackathons/edtech-hackathon-kenya.webp",
prizes: [
[1, "1st place", 80_000],
[2, "2nd place", 40_000],
Expand All @@ -393,7 +393,7 @@ async function main(): Promise<void> {
verified: false,
rolesWanted: ["fintech", "frontend", "design"],
categories: ["web3", "fintech"],
coverUrl: "/marketing/hackathons/web3.webp",
coverUrl: "/marketing/hackathons/web3-hackathon-kenya.webp",
prizes: [
[1, "1st place", 120_000],
[2, "2nd place", 60_000],
Expand All @@ -413,7 +413,7 @@ async function main(): Promise<void> {
verified: false,
rolesWanted: ["mobile", "maps", "backend"],
categories: ["mobility", "civic"],
coverUrl: "/marketing/hackathons/mobility.webp",
coverUrl: "/marketing/hackathons/mobility-hackathon-kenya.webp",
prizes: [
[1, "1st place", 90_000],
[2, "2nd place", 45_000],
Expand All @@ -433,7 +433,7 @@ async function main(): Promise<void> {
verified: true,
rolesWanted: ["security", "fintech", "data"],
categories: ["security", "fintech"],
coverUrl: "/marketing/hackathons/security.webp",
coverUrl: "/marketing/hackathons/cybersecurity-hackathon-kenya.webp",
prizes: [
[1, "1st place", 300_000],
[2, "2nd place", 150_000],
Expand All @@ -453,7 +453,7 @@ async function main(): Promise<void> {
verified: true,
rolesWanted: ["data", "frontend", "civic"],
categories: ["civic"],
coverUrl: "/marketing/hackathons/civic.webp",
coverUrl: "/marketing/hackathons/civic-tech-hackathon-kenya.webp",
prizes: [
[1, "1st place", 100_000],
[2, "2nd place", 50_000],
Expand All @@ -473,7 +473,7 @@ async function main(): Promise<void> {
verified: true,
rolesWanted: ["ai", "data", "backend"],
categories: ["ai"],
coverUrl: "/marketing/hackathons/ai.webp",
coverUrl: "/marketing/hackathons/ai-hackathon-kenya.webp",
prizes: [
[1, "1st place", 250_000],
[2, "2nd place", 120_000],
Expand Down
2 changes: 1 addition & 1 deletion lib/blog/posts/forming-a-team-that-ships.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const post: BlogPost = {
publishedAt: "2026-09-08",
author: "HackVillage Team",
readingMinutes: 4,
cover: "/marketing/blog/team.webp",
cover: "/marketing/blog/kenyan-developers-building-together.webp",
coverAlt: "Three Kenyan developers planning and building a project together",
},
Body,
Expand Down
2 changes: 1 addition & 1 deletion lib/blog/posts/hackvillage-is-open-source.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const post: BlogPost = {
publishedAt: "2026-09-04",
author: "HackVillage Team",
readingMinutes: 3,
cover: "/marketing/blog/open-source.webp",
cover: "/marketing/blog/open-source-contributors-reviewing-code.webp",
coverAlt: "Two Kenyan open-source contributors reviewing code together",
},
Body,
Expand Down
2 changes: 1 addition & 1 deletion lib/blog/posts/how-winners-get-paid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export const post: BlogPost = {
publishedAt: "2026-09-25",
author: "HackVillage Team",
readingMinutes: 4,
cover: "/marketing/blog/payouts.webp",
cover: "/marketing/blog/hackathon-winners-checking-prize-payout.webp",
coverAlt: "Kenyan hackathon winners checking a prize payout beside their trophy",
},
Body,
Expand Down
Loading
Loading