1
0
mirror of https://github.com/kremalicious/blog.git synced 2025-01-05 11:25:07 +01:00
blog/content/articles/2023-09-18-favicon-generation-with-astro/index.md
Matthias Kretschmann 299084de93
Gatsby → Astro (#829)
* basic astro setup, kick out all gatsby configs

* move content folder

* src/pages setup

* more file reorg

* more config updates

* more reorgs

* refactor

* refactor

* bump astro

* refactor

* svg icon build system, theme switch

* remark plugin for lead paragraph, more refactor

* make images work

* post meta

* custom Picture component

* Pagination, More component, 404 fixes

* linking fixes

* add table of contents

* post actions fixes

* tag fixes

* content changes

* content changes: move media files to their posts

* more content moving, remove media folder

* refactor remark-lead-paragraph

* link css file when defined in post frontmatter

* move content up again

* kbd post update

* allow js

* downloads solution

* add astro check

* redirect_from solution

* githubLink solution

* reorg

* exif solution as prebuild step

* exif solution on each post during build

* isolate lead paragraph extraction to articles

* restore Exif components

* deploy script update

* fix redirects

* xml & json feeds

* build fix

* fix exif readout in production

* head and seo tweaks, add feeds

* tweak image display

* archive pages with single layout

* restore tags archive

* sitemap setup

* restore thanks page functionality

* reorg

* cleanup

* parallel scripts during prebuild

* restore jest setup

* remove jest, switch to vitest as test runner

* adapt CI

* test refactor

* typescript tweaks

* avatar fixes

* typings

* restore search functionality

* theme switch with nanostores

* fixes

* test fixes

* restore changelog functionality

* umami script

* border color tweak

* related posts with fuse.js

* plawright e2e testing setup

* search tweaks

* simplify typekit loading

* photo fix

* e2e tests

* related posts fix

* fix tags archive

* tweaks

* tweaks

* linux snapshots

* fix header test

* new symlink tactic

* fix dev server in codespaces

* fix yaml

* ci fixes

* changelog loading tweaks

* e2e against dev server on ci

* changelog tweaks

* ci tweaks

* ci tweaks

* ci tweaks

* docs updates

* ci tweaks

* refactor photo creation script

* package updates

* refactor search

* ci tweak

* ci tweaks

* test tweaks, more unit tests

* more unit tests

* post creation script tweaks

* refactor new scripts, test them for real life

* more tests

* refactor

* codeclimate-action update

* uses update

* limit ci runs

* fix theme toggle, test it

* more tests

* favicon files cleanup

* icon components location change

* type checking through ci

* command fixes

* ci fix

* search tweaks

* ci tweaks

* revised favicons, write post draft about it

* drafts filtering fix

* lint fix, favicon fixes

* copy changes

* fix related search images

* content updates

* new codeblock styles, copy tweaks, fixes

* package updates

* typing fixes

* lint fix

* content updates

* restore link posts

* faster theme switching

* split up astro utils

* related posts fixes

* fix

* refactor

* fixes

* copy tweaks

* fixes

* picture tweaks

* image fixes

* feed fixes, adapt for json feed v1.1

* e2e test updates

* layout tweaks

* update snaphots

* migrate to createMarkdownProcessor

* ci cache tweaks

* activate more browsers for e2e testing

* switch to macos-13 images

* build caching tweaks

* markdown fix

* set image quality

* remove avif generation

* picture tweaks

* head fixes

* add og:image:alt

* create-icons test

* new post: Favicon Generation with Astro
2023-09-18 02:16:53 +01:00

10 KiB
Raw Blame History

date title image tags toc
2023-09-18T00:47:30.000Z Favicon Generation with Astro ./favicon-generation-with-astro-teaser.png
development
favicon
astro
true

Those small but impactful icons displayed next to a website's title in a browser tab seem like a minor detail, yet implementing favicons involves various considerations for different formats and sizes to fit a range of devices and browsers. Luckily, we can always count on Evil Martians to tell us which files are needed in modern times. Those findings can be implemented quite easy in Astro.

This article outlines how to implement just that with Astro, utilizing its Static File Endpoints and getImage() function to generate multiple favicon sizes.

This procedure assumes you are fine with all sizes being generated from one big size. If you require more control e.g. over the smaller sizes you can use the following walkthrough as a starting point.

But you might wonder why there's a need for a dynamic approach when these images could simply be added to the public/ directory manually.

If you're fine with never changing your favicon assets, the most simple approach would be to generate all files manually into the public/ folder, including the manifest.json. And then reference them with their absolute path in your head as described further down, skipping the dynamic image generation and manifest creation.

One significant advantage of generating favicons dynamically is cache busting. When you update your favicon, browsers might still serve the old one from cache. By generating favicons dynamically, you can ensure that the latest version is served, as, if they have changed, each build will create new, uniquely named files that bypass the cache.

Project Structure

To begin, these are the source files we will deal with, with only 2 image assets:

my-astro-project/
├── src/
│ ├── pages/
│ │ └── manifest.json.ts
│ │ └── favicon.ico.ts
│ ├── layouts/
│ │ └── index.astro
│ └── images/
│ │ └── favicon.png
│ │ └── favicon.svg
  • src/images/
    Housing the original favicon images. favicon.png is a large-sized image (512px) that will be resized dynamically, whereas favicon.svg can be a SVG file that adapts to the user's light or dark mode settings.

  • src/layouts/index.astro
    This can be any layout template or page that contains your HTML head content, as we will add the links to the favicons and the manifest file in there.

  • src/pages/manifest.json.ts
    This is an Astro Static File Endpoint that dynamically generates the /manifest.json file, referencing the generated favicons. This file uses Astro's getImage() function to create various sizes of PNG icons from a single source image, and then includes these in the generated manifest.

Final Generated Files

After building the project, the generated favicon files will be placed in the dist/_astro/ directory (dist/_image/ during development) with dynamic filenames, and correctly referenced in your head and /manifest.json. This happens automatically during the site build, so there's no need to keep track of these files manually.

This should be present in your dist/ folder after following the rest of this article:

my-astro-project/
├── dist/
│   ├── favicon.ico
│   ├── manifest.json
│   ├── _astro/
│   │   └── favicon.HASH.png
│   │   └── favicon.HASH.png
│   │   └── favicon.HASH.png
│   │   └── favicon.HASH.svg

Adding Favicon References to the head

To reference the manifest file and to generate required favicon sizes, let's update the head section of the site first.

In this example, we do this in a src/layouts/index.astro file, assuming this is then used as a shared layout in one of your src/pages/ files. But do this wherever your head info gets populated in your site.

In this example layout file, let's add:

---
import { getImage } from 'astro:assets'
import faviconSrc from '../images/favicon.png'
import faviconSvgSrc from '../images/favicon.svg'

const appleTouchIcon = await getImage({
  src: faviconSrc,
  width: 180,
  height: 180,
  format: 'png'
})
const faviconSvg = await getImage({ src: faviconSvgSrc, format: 'svg' })
---

<html>
  <head>
    {'...'}
    <link rel="icon" href="/favicon.ico" sizes="32x32" />
    <link rel="icon" href={faviconSvg.src} type="image/svg+xml" />
    <link rel="apple-touch-icon" href={appleTouchIcon.src} />
    <link rel="manifest" href="/manifest.json" />
    {'...'}
  </head>
  <body>
    {'...'}
  </body>
</html>

Astro's getImage() function is used to generate an Apple Touch Icon (180x180 PNG) on build time for static builds, or during server-side rendering. Astro will then reference those generated images in the respective head tags added above.

The SVG favicon is not generated anew but is essentially passed through the getImage() function to benefit from cache busting.

Generating the Web Manifest

In this setup, the manifest file is dynamically generated using Astro's Static File Endpoints feature.

Add the following code to src/pages/manifest.json.ts:

import type { APIRoute } from 'astro'
import { getImage } from 'astro:assets'
import favicon from '../images/favicon.png'

const faviconPngSizes = [192, 512]

export const GET: APIRoute = async () => {
  const icons = await Promise.all(
    faviconPngSizes.map(async (size) => {
      const image = await getImage({
        src: favicon,
        width: size,
        height: size,
        format: 'png'
      })
      return {
        src: image.src,
        type: `image/${image.options.format}`,
        sizes: `${image.options.width}x${image.options.height}`
      }
    })
  )

  const manifest = {
    name: 'Your site title',
    description: 'Your site description',
    start_url: '/',
    display: 'standalone',
    id: 'some-unique-id',
    icons
  }

  return new Response(JSON.stringify(manifest))
}

This will generate the manifest file into /manifest.json with additional favicon assets being created and referenced in the newly created manifest file.

The code above is written in TypeScript but you can use trusty old JavaScript by using a .js file ending and removing the : APIRoute type annotation.

With this, the manifest.json also has the minimally required keys to make your site installable as a Progressive Web App.

Generating the Favicon.ico

Dont get smart with the static asset folder structure and cache busters.
Evil Martians

Yup, for legacy browsers we actually need a favicon.ico at the site's root, hence the reference to /favicon.ico in the head.

The most simple way is to generate that ico file once with one of the many online or cli tools available, put it in public/ and be done with it.

But to accomplish this without dealing with another source file and without having to worry about future favicon changes, we can make use of Astro's Static File Endpoints again to generate and deliver this asset under /favicon.ico.

As sharp does not support ico output by default, we have to use sharp-ico:

npm install sharp-ico

Astro uses sharp under the hood so it should be installed already but if you get errors, you might have to add it to your dependencies too.

Then use sharp and sharp-ico directly in src/pages/favicon.ico.ts to resize and generate the final favicon.ico from the source image:

import type { APIRoute } from 'astro'
import sharp from 'sharp'
import ico from 'sharp-ico'
import path from 'node:path'

// relative to project root
const faviconSrc = path.resolve('src/images/favicon.png')

export const GET: APIRoute = async () => {
  // resize to 32px PNG
  const buffer = await sharp(faviconSrc).resize(32).toFormat('png').toBuffer()
  // generate ico
  const icoBuffer = ico.encode([buffer])

  return new Response(icoBuffer, {
    headers: { 'Content-Type': 'image/x-icon' }
  })
}

Only one size in the final ico should be fine for most use cases. If you want to get more sizes into the final ico, you can pass more buffers to that array passed to ico.encode():

const buffer32 = await sharp(faviconSrc).resize(32).toFormat('png').toBuffer()
const buffer16 = await sharp(faviconSrc).resize(16).toFormat('png').toBuffer()

ico.encode([buffer16, buffer32])

In the end, this will return our dynamically generated ico file under /favicon.ico.

We have to work around Astro's native asset handling here, I could not get sharp to work with astro:assets generated urls, or with the raw old ?url import way. Which is why a Node.js native module path is used, which might lead to problems during SSR depending on your setup so be aware. Would love to know a way of passing Astro-generated image URLs so sharp understands them, if you know a way, do let me know!

Conclusion

All the required favicon assets are now integrated in an Astro project, covering most modern browsers and devices. Make sure to look through all he other tidbits in the Evil Martian post and explore the Astro docs: