Skip to content
AstroCraft Docs
On this theme

Work & Case Studies

The work section is two routes over one collection. /work/ lists every project in curated order; /work/<slug>/ draws a single case study from the entry’s frontmatter. Six sample projects ship with the theme.

Ordering is curated, not chronological

getAllProjects() in src/js/work.ts is the whole read layer:

export async function getAllProjects(): Promise<CollectionEntry<"work">[]> {
  const projects = await getCollection("work");
  return projects.sort((a, b) => a.data.order - b.data.order);
}

Ascending by the order field, no filter. That is an explicit frontmatter fact rather than a date sort, because the selected works are a sequence somebody chose — and it is also the order the entrance animations run in.

Duplicate order values are not detected, and there is no secondary sort key, so give each project a distinct number. The shipped set runs 1 through 6.

One thing that surprises people: the PROJECT / 01 label on a card is not the order value. It is the card’s position in the array it was rendered from, zero-padded. Reorder the list and the labels renumber to stay sequential.

The case-study page

/work/<slug>/ is built from the entry alone. Its structure is fixed by the design:

  • Hero — the project title, discipline, year, stack and role, over the thumbnail in a WebGL distortion frame.
  • Chapters — one alternating image-and-text row per chapters entry.
  • Testimonial — the shared pull quote from pageCopy.testimonial.
  • CTA — the shared closing invitation.

The route computes its own SEO, using the entry’s standfirst as the meta description and emitting a CreativeWork JSON-LD node:

const canonical = new URL(Astro.url.pathname, Astro.site).href;
const schema = getCreativeWorkSchema({
  name,
  description,
  url: canonical,
  image: new URL(thumbnail.src, Astro.site).href,
  discipline,
  authorName: siteData.author.name,
  inLanguage: siteLocale,
  publisherId: organizationId(canonical),
});

Note there is no datePublished in that node, deliberately. A work entry has a display order, not a publication date, and a date invented for the graph is a date that is wrong.

Chapters

The chapters array is the case-study body, and each entry is { title, body, image }. They render as a list of rows that alternate sides — odd chapters put the image left, even ones put it right — with a scroll-triggered entrance that comes in from the matching direction.

Any number works. The design draws three, but the loop is chapters.map(…) with index % 2 picking the side, so nine chapters alternate correctly.

Two constraints matter here, and neither is obvious from the schema.

First, title and body are plain text, not markdown. They are rendered through TextReveal, which splits the element’s textContent and rebuilds it span by span for the reveal animation. Any markdown syntax appears literally; any HTML is discarded; you cannot author a line break. If a chapter needs paragraphs, make it two chapters.

Second, the MDX body of a work entry is never rendered. The route never calls render(), so prose written below the frontmatter produces nothing. All six shipped entries end at the closing ---. Case-study copy goes in standfirst and chapters[].body.

Chapter images are the only images on the site with no explicit width and height — Astro infers each source’s own dimensions, because the sample entries mix sizes, and the 572×477 frame crops with object-cover.

The work index

/work/ renders a hero from pageCopy.work and then Sections/Work/Projects.astro, which is the one section in the theme that reads a collection directly rather than taking props:

const projects = await getAllProjects();

It draws each project as a ProjectCard in an alternating row-left / row-right layout. The home page draws the same card in its stacked two-column variant. That is the card model working as intended: one component with layout variants, because both places show the same parts in a different arrangement.

Adding a project

Create a folder under src/data/work/ named for the slug you want, put an index.mdx in it with the nine required fields, and drop the art beside it:

src/data/work/harbour-atlas/
├── index.mdx
├── harbour-atlas.jpg
├── harbour-atlas-alt-1.jpg
├── harbour-atlas-alt-2.jpg
└── harbour-atlas-alt-3.jpg

Keep the slug prefix on the filenames. Flattening them to thumbnail.jpg across six projects emits six thumbnail.<hash>.jpg files into dist/_astro/ that only the hash distinguishes — the redundancy buys legible build output.

Give the entry an order that does not collide, quote the year, and the route builds itself. Nothing else needs editing: getStaticPaths enumerates the collection, and both the home grid and the work index pick it up.

Source images should be at least 960 × 640. Astro does not upscale, and the card and hero both request that width.

Removing the work section entirely

If your project has no case studies, delete src/pages/work/, src/components/Sections/Work/, src/components/Sections/Project/, src/components/Cards/ProjectCard.astro, src/js/work.ts, src/data/work/, and the work entry in src/content.config.ts. Then remove the Work link from navData.json.ts, the work key from pageCopy.json.ts, the <Work /> section from src/pages/index.astro, and the /work/ line from src/pages/llms.txt.ts.

pnpm build will name anything you miss.

NEXT STEPBlog & RSS