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
74 changes: 46 additions & 28 deletions packages/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Link from "next/link"
import type { ReactNode } from "react"

import { Button } from "@/components/ui/button"
import { footerNav } from "@/lib/nav"
import { company } from "@/lib/site"

import "@/styles/globals.css"
Expand All @@ -17,11 +18,6 @@ export const metadata: Metadata = {
"A London software consultancy. We build and repair systems that cannot afford to break, with nine years inside broadcast and streaming platforms behind it.",
}

const nav = [
{ href: "/services", label: "Services" },
{ href: "/services", label: "Work" },
]

const RootLayout = ({ children }: { children: ReactNode }) => (
<html lang="en">
<body className="overflow-x-hidden">
Expand All @@ -41,17 +37,12 @@ const RootLayout = ({ children }: { children: ReactNode }) => (
Atlantic<span className="text-accent"> Blue</span>
</Link>
<nav aria-label="Main" className="flex items-center gap-5 text-[0.9375rem]">
{/* The text links go below the small breakpoint. Four items plus a
button needs about 438px, and the narrowest phone is 320. */}
{nav.map((item) => (
<Link
key={item.label}
href={item.href}
className="hidden text-dim no-underline hover:text-ink sm:inline"
>
{item.label}
</Link>
))}
{/* One link, visible at every width. Two hidden links left a
phone with no navigation at all, and the second of them
pointed at the services page while calling itself Work. */}
<Link href="/services" className="text-dim no-underline hover:text-ink">
Services
</Link>
<Button asChild>
<a href={`mailto:${company.email}`}>Book a call</a>
</Button>
Expand All @@ -61,19 +52,46 @@ const RootLayout = ({ children }: { children: ReactNode }) => (

<main id="main">{children}</main>

<footer className="mt-16 border-t border-line bg-surface py-10 sm:mt-24">
<div className="mx-auto grid max-w-[1180px] gap-8 px-4 sm:px-6 md:grid-cols-[1.4fr_1fr] lg:px-8">
<div>
<p className="font-display text-[1.0625rem] font-bold tracking-[-0.02em]">
Atlantic Blue
</p>
<p className="mt-3 text-sm">
<a href={`mailto:${company.email}`} className="text-accent">
{company.email}
</a>
</p>
<footer className="mt-16 border-t border-line bg-surface sm:mt-24">
<div className="mx-auto max-w-[1180px] px-4 py-10 sm:px-6 lg:px-8">
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
<div>
<p className="font-display text-[1.0625rem] font-bold tracking-[-0.02em]">
Atlantic Blue
</p>
<p className="mt-3 max-w-[28ch] text-sm text-dim">
A London software consultancy. We build software that cannot afford
to break.
</p>
<p className="mt-4 text-sm">
<a href={`mailto:${company.email}`} className="text-accent">
{company.email}
</a>
</p>
</div>

{footerNav.map((column) => (
<nav key={column.heading} aria-label={column.heading}>
<p className="font-mono text-xs uppercase tracking-[0.12em] text-dim">
{column.heading}
</p>
<ul className="mt-4 flex flex-col gap-2 text-sm">
{column.links.map((link) => (
<li key={link.href}>
<Link
href={link.href}
className="text-ink no-underline hover:text-accent hover:underline"
>
{link.label}
</Link>
</li>
))}
</ul>
</nav>
))}
</div>
<div className="text-sm leading-relaxed text-dim">

<div className="mt-10 border-t border-line pt-6 text-sm leading-relaxed text-dim">
<p>{company.legalName}</p>
<p>
Registered in England and Wales, company number{" "}
Expand Down
29 changes: 9 additions & 20 deletions packages/web/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import type { Metadata } from "next"
import Image from "next/image"
import Link from "next/link"

import { Button } from "@/components/ui/button"
import { Eyebrow, Heading, Lede, Section, Title } from "@/components/ui/section"
import { servicesInGroup } from "@/lib/services"
import whiteboard from "@/images/whiteboard.jpg"
import { clients, company } from "@/lib/site"

export const metadata: Metadata = {
Expand All @@ -17,7 +15,10 @@ const ServiceList = ({ group }: { group: "specialism" | "practice" }) => (
{servicesInGroup(group).map((service) => (
<li key={service.slug}>
<h3 className="text-lg font-semibold tracking-[-0.01em] sm:text-xl">
<Link href={`/services/${service.slug}`} className="text-ink">
<Link
href={`/services/${service.slug}`}
className="text-accent underline decoration-line underline-offset-4 hover:decoration-accent"
>
{service.name}
</Link>
</h3>
Expand Down Expand Up @@ -49,23 +50,11 @@ const HomePage = () => (
<Section className="border-t border-line">
<Eyebrow>The specialism</Eyebrow>
<Heading className="mt-3">Broadcast and streaming</Heading>
<div className="mt-6 grid items-start gap-8 md:grid-cols-[1.1fr_0.9fr]">
<p className="max-w-[60ch] text-dim">
Where we contract most, and the reason the reliability work below is not a
claim. A fault on a streaming platform is visible to millions of people at once,
so it gets found and fixed to a standard most systems never need.
</p>
{/* Deliberately out of focus. It is a real whiteboard from a real
engagement, and the client's architecture is theirs, not ours to
publish. See DESIGN.md, "Confidentiality". */}
<Image
src={whiteboard}
alt="A whiteboard covered in a hand drawn system architecture, deliberately out of focus"
className="w-full border border-line"
sizes="(min-width: 768px) 40vw, 100vw"
placeholder="blur"
/>
</div>
<p className="mt-4 max-w-[60ch] text-dim">
Where we contract most, and the reason the reliability work below is not a claim. A
fault on a streaming platform is visible to millions of people at once, so it gets
found and fixed to a standard most systems never need.
</p>
<ServiceList group="specialism" />
</Section>

Expand Down
5 changes: 4 additions & 1 deletion packages/web/src/app/services/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ const ServicesPage = () => (
{servicesInGroup(group.key).map((service) => (
<li key={service.slug} className="border border-line bg-surface p-5 sm:p-6">
<h2 className="text-lg font-semibold tracking-[-0.01em] sm:text-xl">
<Link href={`/services/${service.slug}`} className="text-ink">
<Link
href={`/services/${service.slug}`}
className="text-accent underline decoration-line underline-offset-4 hover:decoration-accent"
>
{service.name}
</Link>
</h2>
Expand Down
Binary file removed packages/web/src/images/whiteboard.jpg
Binary file not shown.
25 changes: 25 additions & 0 deletions packages/web/src/lib/nav.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { services } from "./services"

/**
* The whole site in one place, used by the footer so every page is reachable
* from every page. A consultancy site is judged partly on depth, and depth that
* cannot be found is the same as no depth.
*/
export const footerNav = [
{
heading: "Specialism",
links: services
.filter((service) => service.group === "specialism")
.map((service) => ({ href: `/services/${service.slug}`, label: service.name })),
},
{
heading: "Practice",
links: services
.filter((service) => service.group === "practice")
.map((service) => ({ href: `/services/${service.slug}`, label: service.name })),
},
{
heading: "Company",
links: [{ href: "/services", label: "All services" }],
},
] as const
53 changes: 31 additions & 22 deletions packages/web/test/pages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ describe("an address that does not exist", () => {
it("serves the not found page, not the home page", async () => {
const { body } = await get("/nonsense-xyz")
expect(body).toMatch(/<h1[^>]*>Page not found<\/h1>/)
expect(body).not.toContain("cannot afford to break")
// The footer now carries the tagline on every page, so check the heading.
expect(body).not.toMatch(/<h1[^>]*>We build software/)
})

// A canonical link set on the layout is inherited by every page beneath it.
Expand Down Expand Up @@ -179,11 +180,13 @@ describe("the service pages", () => {
describe("mobile layout", () => {
// The header needed about 438px before this: a wordmark, two text links and
// a button, all unconditional. The narrowest phone in common use is 320.
it("hides the text navigation below the small breakpoint", async () => {
// Two links were hidden below 640px and nothing replaced them, so a phone had
// no navigation at all. Whatever is in the header has to be reachable.
it("keeps the navigation visible at every width", async () => {
const { body } = await get("/")
const header = body.match(/<header[\s\S]*?<\/header>/)?.[0] ?? ""
expect(header).toContain(">Services<")
expect(header).toMatch(/class="[^"]*hidden[^"]*sm:inline[^"]*"[^>]*>Services</)
expect(header).not.toMatch(/class="[^"]*\bhidden\b[^"]*"[^>]*>Services</)
})

it("keeps the call to action visible at every width", async () => {
Expand All @@ -208,30 +211,36 @@ describe("mobile layout", () => {
})
})

describe("imagery", () => {
it("serves the image through the optimiser, in a modern format", async () => {
describe("finding the pages", () => {
// A heading that is a link has to look like one. Every service title was set
// in the body colour with no underline, so the eight pages behind them were
// invisible even though the links were there.
it("marks a service link as a link", async () => {
const { body } = await get("/")
const src = body.match(/src="(\/_next\/image[^"]*)"/)?.[1]?.replace(/&amp;/g, "&")
expect(src, "no optimised image on the page").toBeDefined()

const response = await fetch(`${baseUrl}${src}`, {
headers: { accept: "image/avif,image/webp,image/*" },
})
expect(response.status).toBe(200)
expect(response.headers.get("content-type")).toMatch(/avif|webp/)
const anchors = [...body.matchAll(/<a[^>]*href="\/services\/[^"]+"[^>]*>/g)].map(
(m) => m[0],
)
expect(anchors.length).toBeGreaterThan(5)
for (const anchor of anchors) {
expect(anchor, `no visual affordance: ${anchor}`).toMatch(/underline|text-accent/)
}
})

it("gives the image dimensions, so nothing shifts as it loads", async () => {
const { body } = await get("/")
const tag = body.match(/<img[^>]*_next\/image[^>]*>/)?.[0] ?? ""
expect(tag).toMatch(/width="\d+"/)
expect(tag).toMatch(/height="\d+"/)
it("reaches every service page from the footer of every page", async () => {
for (const path of ["/", "/services", `/services/${services[0]!.slug}`]) {
const { body } = await get(path)
const footer = body.match(/<footer[\s\S]*?<\/footer>/)?.[0] ?? ""
for (const service of services) {
expect(footer, `${service.slug} missing from the footer on ${path}`).toContain(
`/services/${service.slug}`,
)
}
}
})

it("describes the image for somebody who cannot see it", async () => {
it("does not offer a link that goes somewhere other than it says", async () => {
const { body } = await get("/")
const tag = body.match(/<img[^>]*_next\/image[^>]*>/)?.[0] ?? ""
const alt = tag.match(/alt="([^"]*)"/)?.[1] ?? ""
expect(alt.length).toBeGreaterThan(20)
const header = body.match(/<header[\s\S]*?<\/header>/)?.[0] ?? ""
expect(header).not.toContain(">Work<")
})
})