Subscribe

Get authentic content to your mailbox

In Search of a True Hero šŸ¦øā€ā™‚ļø

0:00
Log

After consolidating the article display we continue with the conversation between content, design, and feature implementation. In particular, the Mount Fyffe article would benefit from a hero image. This definitely needs to be content-driven - i.e. specified in mdx files.

There are options though. Should we do this as a frontmatter (within the hyphen-hyphen-hyphen part at the top of the file hero: ../path/to/image) or within the body of the article (by rendering a Hero component <Hero src="ā€¦" />)?

One consideration that informs the decision is that presently the articles template enforces the structure of frontmatter information display like so (pseudo code):

<!-- [slug].astro -->
<article>
  <header>
    <h1>{post.data.title}</h1>
    <p>{post.data.date}</p>
    <p>{post.data.tags}</p>
  </header>
  <Content />
</article>

This means we canā€™t render anything from the article body prior to the header, meaning we couldnā€™t render a Hero component before the title, even if we initially wanted to go with that option.

This inflexibility begs the question ā€œwhy use frontmatter at all?ā€. Why not just let articles decide what goes where by importing components directly into the article body and have the template just render <Content />? On the flipside this would certainly allow inconsistency between articles to creep in. I decide to stick with consistency. Frontmatter it is. Besides some expected layout rework, importing images in frontmatter requires some special attention - we need to rework the config.ts a little to expose an image zod helper to define an image type frontmatter field:

// config.ts
schema: ({ image }) => ({
  hero: z.optional(
    image().refine((img) => img.width >= 1080, {
      message: "Hero image must be at least 1080 pixels wide!",
    }))
  })
})

Now you can use the configured frontmatter hero field directly as the src prop value for Astroā€™s Image component:

<!-- [slug].astro -->
<Image src={post.data.hero} alt="" />

This is great. The only problem is that the hero image canā€™t be wider than the article display. Till now the Layout component had:

<main>
  <SiteWidthContainer>
    <slot /> // hero needs to break out from here
  </SiteWidthContainer>
</main>

This requires some rework to remove the global wrapper approach and add site width styling to every non image element within rendered mdx content. Hereā€™s the idea as a tiny [slug].astro file:

---
const { Content } = await post.render();
---

<header>ā€¦</header>
<div class="mdx-content">
  <Content />
</div>

<style>
  .mdx-content > *:not(img) {
    max-width: 600px;
    margin-left: auto;
    margin-right: auto;
  }
</style>

This breaks some stylings like left margin on uls etc, so requires some massaging until things are back to happy, but voila, the hero image has unrestrained size. Additionally images within articles now have the option to extend beyond the text width as well. Nifty, but not quite done.

Optimising Images With Astro

I set the max-width on the images div container to 1200px and spot that the hero image is taking a while to load. A quick scan of Astroā€™s docs gives the headline message:

ā€œUse Astroā€™s nativeĀ <Image />Ā andĀ <Picture />Ā components for optimised imagesā€

ā€¦but itā€™s not quite as straightforward as that.

How it works is that these components provide a unified props API to control the solving of two problems:

  • The generation of appropriately sized image files (e.g. on disk) ready for serving statically by a web server.
  • Figuring out which of those image files is the minimum size required given a browser window of a certain size loading the page (via putting corresponding HTML properties on the img element).

Letā€™s work this through with a few examples.

Firstly, specifying a static width like <Image width=ā€200ā€ /> will cause Astro to:

  • scale down the original source image to 200px in width and make it ready to serve at a size specific url
  • set the output img src property to that url.

For responsivity (i.e. when the displayed image width depends on the browserā€™s viewport size) you can <Image widths={[800,1200,1600]} sizes=ā€90vwā€ /> which will cause Astro to:

  • generate three image files at 800, 1200, and 1600 pixels wide and make them ready to serve at size specific urls
  • add a srcset property with three entries on the output img element with the urls of the generated images
  • pass through the sizes property as provided for the browser to determine which image to load based on the viewport size.

All in all, this is certainly a nice abstraction to skip needing to generate images and wire up their urls but you do still need to understand responsive image concepts (aka srcset & sizes properties, dpi etc) and use Astroā€™s api appropriately to get ā€œoptimised imagesā€ in responsive cases.

I slapped on sufficiently suitable widths and sizes and the Hero image network size dropped 5x resulting in a better loading experience.

Heroā€™s in articles. Yay!