From a9f5ea925dcb41805a4b67415f92dc39eeec5e61 Mon Sep 17 00:00:00 2001 From: Matthias Kretschmann Date: Tue, 19 Sep 2023 19:04:02 +0100 Subject: [PATCH] more unit tests --- src/components/Search/Search.test.tsx | 4 +- src/lib/astro/getAllPosts.test.ts | 45 +++++++++++ .../astro/{astro.test.ts => index.test.ts} | 1 - src/lib/astro/loadAndFormatCollection.test.ts | 81 +++++++++++++++++++ test/__fixtures__/getCollectionArticles.json | 61 ++++++++++++++ test/__fixtures__/getCollectionLinks.json | 28 +++++++ test/__fixtures__/getCollectionPhotos.json | 70 ++++++++++++++++ 7 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 src/lib/astro/getAllPosts.test.ts rename src/lib/astro/{astro.test.ts => index.test.ts} (97%) create mode 100644 src/lib/astro/loadAndFormatCollection.test.ts create mode 100644 test/__fixtures__/getCollectionArticles.json create mode 100644 test/__fixtures__/getCollectionLinks.json create mode 100644 test/__fixtures__/getCollectionPhotos.json diff --git a/src/components/Search/Search.test.tsx b/src/components/Search/Search.test.tsx index b63c0ebfe..a0a595a4d 100644 --- a/src/components/Search/Search.test.tsx +++ b/src/components/Search/Search.test.tsx @@ -6,6 +6,7 @@ import Search from './Search' let portalRoot: HTMLDivElement let unsubscribe: () => void let fetchSpy: any +let originalFetch: any let storeState = false beforeEach(() => { @@ -19,6 +20,7 @@ beforeEach(() => { }) // Mock fetch API + originalFetch = globalThis.fetch globalThis.fetch = async () => { return { json: () => @@ -30,9 +32,9 @@ beforeEach(() => { }) afterEach(() => { - // Cleanup portalRoot.remove() unsubscribe() + globalThis.fetch = originalFetch }) test('Search component', async () => { diff --git a/src/lib/astro/getAllPosts.test.ts b/src/lib/astro/getAllPosts.test.ts new file mode 100644 index 000000000..04fe530aa --- /dev/null +++ b/src/lib/astro/getAllPosts.test.ts @@ -0,0 +1,45 @@ +import { test, expect, vi, describe, beforeEach, afterEach } from 'vitest' +import * as loadAndFormatCollectionModule from './loadAndFormatCollection' +import { getAllPosts } from './getAllPosts' +import mockArticles from '@test/__fixtures__/getCollectionArticles.json' +import mockLinks from '@test/__fixtures__/getCollectionLinks.json' +import mockPhotos from '@test/__fixtures__/getCollectionPhotos.json' + +let loadAndFormatCollectionSpy: any + +beforeEach(() => { + loadAndFormatCollectionSpy = vi.spyOn( + loadAndFormatCollectionModule, + 'loadAndFormatCollection' + ) +}) + +afterEach(() => { + loadAndFormatCollectionSpy.mockRestore() +}) + +describe('getAllPosts', () => { + test('combines and sorts all posts', async () => { + loadAndFormatCollectionSpy.mockImplementation( + async (collectionName: string) => { + switch (collectionName) { + case 'articles': + return mockArticles + case 'links': + return mockLinks + case 'photos': + return mockPhotos + default: + return [] + } + } + ) + + const result = await getAllPosts() + + expect(result).toBeDefined() + expect(result.length).toBe( + mockArticles.length + mockLinks.length + mockPhotos.length + ) + }) +}) diff --git a/src/lib/astro/astro.test.ts b/src/lib/astro/index.test.ts similarity index 97% rename from src/lib/astro/astro.test.ts rename to src/lib/astro/index.test.ts index 4d49429ee..903c192f5 100644 --- a/src/lib/astro/astro.test.ts +++ b/src/lib/astro/index.test.ts @@ -22,7 +22,6 @@ test('sortPosts sorts posts by date in descending order', () => { getAllPostsSpy.mockImplementationOnce(() => Promise.resolve(posts)) - // getAllPostsSpy = vi.spyOn(getAllPosts, async () => posts) const sortedPosts = sortPosts(posts) expect(sortedPosts[0].data.date).toStrictEqual('2022-01-03') expect(sortedPosts[1].data.date).toStrictEqual('2022-01-02') diff --git a/src/lib/astro/loadAndFormatCollection.test.ts b/src/lib/astro/loadAndFormatCollection.test.ts new file mode 100644 index 000000000..c282a8b47 --- /dev/null +++ b/src/lib/astro/loadAndFormatCollection.test.ts @@ -0,0 +1,81 @@ +import { test, expect, vi, beforeEach, afterEach, describe } from 'vitest' +import * as astroContent from 'astro:content' +import * as exifLib from '@lib/exif' +import { loadAndFormatCollection } from './loadAndFormatCollection' +import getCollectionArticles from '@test/__fixtures__/getCollectionArticles.json' +import getCollectionLinks from '@test/__fixtures__/getCollectionLinks.json' +import getCollectionPhotos from '@test/__fixtures__/getCollectionPhotos.json' + +let getCollectionSpy: any +let readOutExifSpy: any + +beforeEach(() => { + getCollectionSpy = vi.spyOn(astroContent, 'getCollection') + + readOutExifSpy = vi.spyOn(exifLib, 'readOutExif') + readOutExifSpy.mockImplementationOnce(async () => ({ + exif: 'mocked exif', + iptc: 'mocked iptc' + })) +}) + +afterEach(() => { + getCollectionSpy.mockRestore() + readOutExifSpy.mockRestore() +}) + +describe('loadAndFormatCollection', () => { + test('loads articles', async () => { + getCollectionSpy.mockImplementationOnce(async () => getCollectionArticles) + const result = await loadAndFormatCollection('articles') + expect(result).toBeDefined() + expect(result.length).toBeGreaterThan(0) + }) + + test('loads links', async () => { + getCollectionSpy.mockImplementationOnce(async () => getCollectionLinks) + const result = await loadAndFormatCollection('links') + expect(result).toBeDefined() + expect(result.length).toBeGreaterThan(0) + }) + + test('loads photos', async () => { + getCollectionSpy.mockImplementationOnce(async () => getCollectionPhotos) + const result = await loadAndFormatCollection('photos') + expect(result).toBeDefined() + expect(result.length).toBeGreaterThan(0) + }) + + test('loads photos in production', async () => { + const originalEnv = import.meta.env.PROD + import.meta.env.PROD = true + + getCollectionSpy.mockImplementationOnce(async () => getCollectionPhotos) + const result = await loadAndFormatCollection('photos') + expect(result).toBeDefined() + expect(result.length).toBeGreaterThan(0) + + import.meta.env.PROD = originalEnv + }) + + test('filters out drafts in production', async () => { + const originalEnv = import.meta.env.PROD + import.meta.env.PROD = true + + const mockDrafts = [ + { id: '/what/an/id/and/la', data: { draft: true }, otherFields: '...' }, + { id: '/what/an/id/and/le', data: { draft: false }, otherFields: '...' }, + { id: '/what/an/id/and/lu', data: { draft: true }, otherFields: '...' } + ] as any + + getCollectionSpy.mockImplementationOnce(async () => mockDrafts) + const result = await loadAndFormatCollection('articles') + expect(result).toBeDefined() + // Only one article should remain after filtering out drafts + expect(result.length).toBe(1) + // The remaining article should not be a draft + expect(result[0].data.draft).toBe(false) + + import.meta.env.PROD = originalEnv + }) +}) diff --git a/test/__fixtures__/getCollectionArticles.json b/test/__fixtures__/getCollectionArticles.json new file mode 100644 index 000000000..1c9710111 --- /dev/null +++ b/test/__fixtures__/getCollectionArticles.json @@ -0,0 +1,61 @@ +[ + { + "id": "2020-05-22-gatsby-redirect-from/index.md", + "slug": "2020-05-22-gatsby-redirect-from", + "body": "\nPlugin for [Gatsby](https://www.gatsbyjs.org) to create redirects based on a list in your Markdown frontmatter, mimicking the behavior of [jekyll-redirect-from](https://github.com/jekyll/jekyll-redirect-from).\n\n## Features\n\nBy adding a `redirect_from` list of URLs to your Markdown file's YAML frontmatter, this plugin creates client-side redirects for all of them at build time, with Gatsby's [createRedirect](https://www.gatsbyjs.org/docs/actions/#createRedirect) used under the hood.\n\nBy combining this plugin with [gatsby-plugin-meta-redirect](https://github.com/getchalk/gatsby-plugin-meta-redirect) you get simple server-side redirects from your `redirect_from` lists out of the box. You can also combine it with any other plugin picking up Gatsby `createRedirect` calls to get proper SEO-friendly [server-side redirects](#server-side-redirects) for your hosting provider.\n\n## Usage\n\nFirst, install the plugin from your project's root:\n\n```bash\ncd yourproject/\nnpm i gatsby-redirect-from gatsby-plugin-meta-redirect\n```\n\nThen add it to your `gatsby-config.js` file under `plugins`:\n\n```js title=\"gatsby-config.js\"\nplugins: [\n 'gatsby-redirect-from',\n 'gatsby-plugin-meta-redirect' // make sure this is always the last one\n]\n```\n\nThat's it for the configuration.\n\nFinally, use the key `redirect_from` followed by a list in your Markdown file's YAML frontmatter:\n\n```yaml title=\"post.md\"\n---\ntitle: Aperture File Types\nredirect_from:\n - /new-goodies-aperture-file-types-icons/\n - /goodie-updated-aperture-file-types-v11/\n - /aperture-file-types-v12-released/\n - /2008/04/aperture-file-types/\n # note: trailing slashes are required\n---\n```\n\n## Default Query\n\nPlugin assumes the default setup from [gatsby-starter-blog](https://github.com/gatsbyjs/gatsby-starter-blog), with Markdown files processed by [gatsby-transformer-remark](https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-transformer-remark), and adding a field `slug` for each markdown node. Resulting in the availability of a `allMarkdownRemark` query.\n\nHead over to `gatsby-starter-blog`'s [`gatsby-node.js`](https://github.com/gatsbyjs/gatsby-starter-blog/blob/master/gatsby-node.js#L57) file to see how this is done, or follow the [Adding Markdown Pages](https://www.gatsbyjs.org/docs/adding-markdown-pages/) tutorial.\n\nOptionally, you can pass a different query to this [plugin's configuration](#options).\n\n## Server-Side Redirects\n\nGatsby's `createRedirect` only creates client-side redirects, so further integration is needed to get SEO-friendly server-side redirects or make your redirects work when users have JavaScript disabled. Which is highly dependent on your hosting provider: if you want to have the proper HTML status codes like `301`, Apache needs `.htaccess` rules for that, Nginx needs `rewrite` rules, S3 needs `RoutingRules`, Vercel needs entries in a `vercel.json`, Netlify needs a `_redirects` file, and so on.\n\nOne simple way, as suggested by default in installation, is to use [gatsby-plugin-meta-redirect](https://github.com/getchalk/gatsby-plugin-meta-redirect) to generate static HTML files with a `` tag for every `createRedirect` call in their ``. So it works out of the box with this plugin without further adjustments.\n\nThis way is the most compatible way of handling redirects, working with pretty much every hosting provider. Only catch: it's only for usability, no SEO-friendly `301` redirect is set anywhere.\n\nFor some hosting providers additional plugins are available which will pick up the redirects created by this plugin and create server-side redirects for you. Be sure to add any of those plugins after `gatsby-redirect-from` in your `gatsby-config.js`:\n\n| Provider | Plugin |\n| -------- | ----------------------------------------------------------------------------------------------------- |\n| Netlify | [gatbsy-plugin-netlify](https://www.gatsbyjs.org/packages/gatsby-plugin-netlify/) |\n| Vercel | [gatsby-plugin-zeit-now](https://github.com/universse/gatsby-plugin-zeit-now) |\n| AWS S3 | [gatsby-plugin-s3](https://www.gatsbyjs.org/packages/gatsby-plugin-s3) |\n| Nginx | [gatsby-plugin-nginx-redirect](https://github.com/gimoteco/gatsby-plugin-nginx-redirect) |\n| Apache | [gatsby-plugin-htaccess-redirects](https://github.com/GatsbyCentral/gatsby-plugin-htaccess-redirects) |\n\n## Options\n\nPlugin does not require to be configured but additional customization options are available:\n\n| Option | Default | Description |\n| ------- | ------------------- | ------------------------------------------------------------------------------------------------ |\n| `query` | `allMarkdownRemark` | Modify the query being used to get the frontmatter data. E.g. if you use MDX, set `allMdx` here. |\n\nAdd options to the plugins's configuration object in `gatsby-config.js` like so:\n\n```js title=\"gatsby-config.js\"\nplugins: [\n {\n resolve: 'gatsby-redirect-from',\n options: {\n query: 'allMdx'\n }\n },\n 'gatsby-plugin-meta-redirect' // make sure this is always the last one\n]\n```\n\n## Check out & contribute\n\nHead over to GitHub for more documentation, take a peek into the code, or to report some bugs.\n\n

\n GitHub\n

\n", + "collection": "articles", + "data": { + "title": "Redirect plugin for Markdown Pages in Gatsby", + "date": "2020-05-22T14:08:00.367Z", + "updated": "2020-05-23T09:35:12.000Z", + "tags": ["goodies", "gatsby", "development"], + "toc": true, + "changelog": "kremalicious/gatsby-redirect-from", + "image": { + "src": "/@fs/Users/m/Code/blog/src/content/articles/2020-05-22-gatsby-redirect-from/gatsby-redirect-from-teaser.png?origWidth=1880&origHeight=600&origFormat=png", + "width": 1880, + "height": 600, + "format": "png" + }, + "githubLink": "https://github.com/kremalicious/blog/tree/main/content/articles/2020-05-22-gatsby-redirect-from/index.md" + } + }, + { + "id": "2021-07-29-ocean-makes-multi-network-even-easier/index.md", + "slug": "2021-07-29-ocean-makes-multi-network-even-easier", + "body": "\nSimplifying Multi-Network on Ocean with a Unified Interface.\n\n---\n\n> This article was originally posted [on Medium in the Ocean Protocol blog](https://blog.oceanprotocol.com/ocean-makes-multinetwork-even-simpler-c3ec6c0cbd50).\n\n---\n\n## 🦑 The New Reality\n\nWhen we launched the Ocean Market as part of v3 we had [just moved onto ETH Mainnet](https://blog.oceanprotocol.com/oceans-on-ethereum-mainnet-ba9be1aee0ce) from our own custom PoA Mainnet, so all focus for the user interface went into working against that one production network. As we deployed the Ocean Protocol contracts to more chains to escape rapidly rising transaction fees, the main interface paradigm, of basing the displayed metadata on the user’s connected network quickly became a pain to use. Hello, _Could not retrieve asset_. 🙃\n\nSo we sat down and figured out the best patterns to solve these main pain points, focusing solely on the end user perspective:\n\n- Reduce friction when following links to assets outside of ETH Mainnet\n- Retain the DID and existing URLs as the unique identifier for an asset, regardless of network\n- Increase discoverability of assets outside of ETH Mainnet\n- Increase discoverability of all networks Ocean Protocol is deployed to\n- Encourage usage of networks beyond just ETH Mainnet\n- Reduce need to switch wallet networks as much as possible when browsing the market\n- Any possible solution needs to scale easily as we continue deploying to more networks\n\n## 🧜‍♀️ Multi-Network Market\n\n![Leeloo agrees words with “multi” in front are better.](./multinetwork-01.jpeg)\n\nUltimately, we arrived at a solution tackling all this, where the main new paradigm is an interface showing assets mixed from multiple networks. All the time and on every screen where assets are listed. This detaches the metadata and financial data source from the user’s wallet network as it was before.\n\nThe displayed networks are now controlled by the new network selector.\n\n![The new network selector and revised menubar in the Ocean Market interface.](./multinetwork-02.png)\n\nBy default, we auto-select all production networks Ocean Protocol is deployed to. As soon as you interact with this new network switcher, your selection takes over and is saved in your browser so it will be the same the next time you come to the market.\n\nSelecting or de-selecting networks then modifies all Elasticsearch queries going to our new Aquarius, resulting in mixed assets on screen.\n\n![Mixed assets from multiple networks.](./multinetwork-03.png)\n\nAll assets now indicate which network they belong to, and you are prompted to switch to the asset’s network when we detect your wallet being connected to another network.\n\n![One remaining place where user wallet switching is still important.](./multinetwork-04.png)\n\nAnd in the case of using MetaMask, we added actions to switch your wallet network directly from the UI, which, as of right now, is pretty much the most streamlined user flow possible to switch networks with MetaMask from a Dapp.\n\nWith all this, wallet network switching is now only needed once you want to interact with an asset, like downloading or adding liquidity to its pool.\n\nUser wallet network also stays important for publishing an asset, so we based the whole publish form on the currently connected network to define onto which network an asset is published.\n\n![Publish form with network indicator.](./multinetwork-05.png)\n\nAs for our key market statistics in the footer, we switched it to show consolidated numbers as a sum of all production networks. In its tooltip, you can find the values split up by network.\n\n![New consolidated market statistics based on each network.](./multinetwork-06.png)\n\nMore assets on screen and more controls also led to further UI tweaks to get more space available to the actual main content. We completely refactored the main menu layout, added a global search box to it, and moved some warnings around. And, while we were at it, improved the mobile experience for it. ✨✨\n\n![Everything you need from our menu is all there in mobile viewports.](./multinetwork-07.png)\n\nAnd finally, we also automatically migrate all your existing bookmarks from all the networks and combine them into one list.\n\n### Developer Changes\n\nFor developers, there are new values in `app.config.js` controlling the networks which are displayed in the network selection popover:\n\n- `chainIdsSupported`: List of all supported chainIds. Used to populate the Networks user preferences list.\n- `chainIds`: List of chainIds which metadata cache queries will return by default. This preselects the Networks user preferences.\n\nIn the background, the code base changed drastically. We now have only one Aquarius but still multiple providers and especially subgraphs, and we had to also technically detach the wallet network from the data source. E.g. for showing prices and financial data, main refactoring work went into correlating the assets based on `ddo.chainId` with the respective subgraph and querying multiple subgraphs at the same time as needed. For this, we also simplified our GraphQL setup and switched from [Apollo Client](https://www.apollographql.com/docs/react/) to [urql](https://github.com/FormidableLabs/urql).\n\nIf you’re interested in all the detailed code changes, you can follow along with the [main Pull Request](https://github.com/oceanprotocol/market/pull/628) which has references and screenshots for all other changes done against it. This is also the best place to start if you run your own fork of the market and want to integrate the latest multi-network changes without looking at just one big change in `main`.\n\n### Check it out!\n\nHead to [market.oceanprotocol.com](https://market.oceanprotocol.com) and see assets currently mixed from 3 networks by default.\n\n- [market.oceanprotocol.com](https://market.oceanprotocol.com)\n\nYou can find all the values required to connect your wallet to the networks Ocean Protocol is deployed to in our [supported networks](https://docs.oceanprotocol.com/concepts/networks/) documentation, along with a guide on how to [set up a custom network in MetaMask](https://docs.oceanprotocol.com/tutorials/metamask-setup/#set-up-custom-network).\n\n## 🐋 Multi-Network Aquarius\n\nAquarius got a complete refactor. Besides numerous optimizations and stabilization, this new Aquarius indexes assets from multiple chains and delivers them all together in its API responses, with a new `ddo.chainId` value as part of each asset’s metadata.\n\nIn addition to making an interface with mixed assets possible, this also brings a huge maintainability advantage as now only one Aquarius instance has to be deployed and maintained instead of one for each supported network.\n\nSo multiple Aquarius instances are now reduced to one instance, where for every network a specific indexer is started. The [Aquarius API](https://docs.oceanprotocol.com/references/aquarius/) got a new endpoint exposing which chains are indexed under `/api/v1/aquarius/chains/list`.\n\n![`/chains/list` endpoint response exposing indexed chain IDs](./multinetwork-08.png)\n\n### Migration to Multi-Network Aquarius\n\nAquarius `v3.0.0+` is the one.\n\nIf you use our remote Aquarius instances, all you have to do is point your app against the new `aquarius.oceanprotocol.com` and then in your interface do things based on `ddo.chainId`, like modifying your Elasticsearch queries to include assets from specific networks.\n\nWe will keep the old dedicated network instances like `aquarius.mainnet.oceanprotocol.com` running until September 1st, 2021, and we encourage everybody to migrate to `aquarius.oceanprotocol.com` instead.\n\nWith one Aquarius indexing multiple chains it is rarely useful to return all the assets, as most likely you are only interested in production network assets when listing them in an app. So we will also remove the `GET /assets/ddo` endpoint and suggest to replace it with a specific search query to `POST /assets/ddo/query`, and include the chainId you want, like:\n\n```js\n{\n page: 1,\n offset: 1000,\n query: {\n query_string: {\n query: 'chainId:1 AND -isInPurgatory:true'\n }\n }\n}\n```\n\nIf you have your own instances deployed we suggest to deploy a new one with `v3.0.0+` to have everything reindex, and finally switch your URLs in your app to this new deployment and adapt your app interface accordingly. The readme has further information on how to exactly deploy this new Aquarius.\n\n- [oceanprotocol/aquarius](https://github.com/oceanprotocol/aquarius)\n\n---\n\n> This article was originally posted [on Medium in the Ocean Protocol blog](https://blog.oceanprotocol.com/ocean-makes-multinetwork-even-simpler-c3ec6c0cbd50).\n\n---\n", + "collection": "articles", + "data": { + "title": "Ocean Makes Multi-Network Even Easier", + "date": "2021-07-29T00:00:00.000Z", + "tags": ["oceanprotocol", "blockchain", "design", "development", "web3"], + "toc": true, + "image": { + "src": "/@fs/Users/m/Code/blog/src/content/articles/2021-07-29-ocean-makes-multi-network-even-easier/ocean-makes-multi-network-even-easier-teaser.png?origWidth=2692&origHeight=1202&origFormat=png", + "width": 2692, + "height": 1202, + "format": "png" + }, + "githubLink": "https://github.com/kremalicious/blog/tree/main/content/articles/2021-07-29-ocean-makes-multi-network-even-easier/index.md" + } + }, + { + "id": "2023-09-18-favicon-generation-with-astro/index.md", + "slug": "2023-09-18-favicon-generation-with-astro", + "body": "\nThose 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](https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs) in modern times. Those findings can be implemented quite easy in Astro.\n\nThis article outlines how to implement just that with [Astro](https://astro.build), utilizing its [Static File Endpoints](https://docs.astro.build/en/core-concepts/endpoints/) and [`getImage()`](https://docs.astro.build/en/guides/images/#generating-images-with-getimage) function to generate multiple favicon sizes.\n\nThis 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.\n\nBut you might wonder why there's a need for a dynamic approach when these images could simply be added to the `public/` directory manually.\n\nIf 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.\n\nOne 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.\n\n## Project Structure\n\nTo begin, these are the source files we will deal with, with only 2 image assets:\n\n```\nmy-astro-project/\n├── src/\n│ ├── pages/\n│ │ └── manifest.json.ts\n│ │ └── favicon.ico.ts\n│ ├── layouts/\n│ │ └── index.astro\n│ └── images/\n│ │ └── favicon.png\n│ │ └── favicon.svg\n```\n\n- `src/images/`\\\n 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.\n\n- `src/layouts/index.astro`\\\n 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.\n\n- `src/pages/manifest.json.ts`\\\n 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.\n\n### Final Generated Files\n\nAfter 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.\n\nThis should be present in your `dist/` folder after following the rest of this article:\n\n```\nmy-astro-project/\n├── dist/\n│ ├── favicon.ico\n│ ├── manifest.json\n│ ├── _astro/\n│ │ └── favicon.HASH.png\n│ │ └── favicon.HASH.png\n│ │ └── favicon.HASH.png\n│ │ └── favicon.HASH.svg\n```\n\n## Adding Favicon References to the `head`\n\nTo reference the manifest file and to generate required favicon sizes, let's update the `head` section of the site first.\n\nIn 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.\n\nIn this example layout file, let's add:\n\n```astro title=\"src/layouts/index.astro\"\n---\nimport { getImage } from 'astro:assets'\nimport faviconSrc from '../images/favicon.png'\nimport faviconSvgSrc from '../images/favicon.svg'\n\nconst appleTouchIcon = await getImage({\n src: faviconSrc,\n width: 180,\n height: 180,\n format: 'png'\n})\nconst faviconSvg = await getImage({ src: faviconSvgSrc, format: 'svg' })\n---\n\n\n \n {'...'}\n \n \n \n \n {'...'}\n \n \n {'...'}\n \n\n```\n\nAstro'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.\n\nThe SVG favicon is not generated anew but is essentially passed through the `getImage()` function to benefit from cache busting.\n\n## Generating the Web Manifest\n\nIn this setup, the [manifest file](https://developer.mozilla.org/en-US/docs/Web/Manifest) is dynamically generated using Astro's [Static File Endpoints feature](https://docs.astro.build/en/core-concepts/endpoints/).\n\nAdd the following code to `src/pages/manifest.json.ts`:\n\n```typescript title=\"src/pages/manifest.json.ts\"\nimport type { APIRoute } from 'astro'\nimport { getImage } from 'astro:assets'\nimport favicon from '../images/favicon.png'\n\nconst faviconPngSizes = [192, 512]\n\nexport const GET: APIRoute = async () => {\n const icons = await Promise.all(\n faviconPngSizes.map(async (size) => {\n const image = await getImage({\n src: favicon,\n width: size,\n height: size,\n format: 'png'\n })\n return {\n src: image.src,\n type: `image/${image.options.format}`,\n sizes: `${image.options.width}x${image.options.height}`\n }\n })\n )\n\n const manifest = {\n name: 'Your site title',\n description: 'Your site description',\n start_url: '/',\n display: 'standalone',\n id: 'some-unique-id',\n icons\n }\n\n return new Response(JSON.stringify(manifest))\n}\n```\n\nThis will generate the manifest file into `/manifest.json` with additional favicon assets being created and referenced in the newly created manifest file.\n\nThe 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.\n\nWith this, the `manifest.json` also has the minimally required keys to make your site installable as a [Progressive Web App](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/What_is_a_progressive_web_app).\n\n## Generating the Favicon.ico\n\n> Don’t get smart with the static asset folder structure and cache busters.\\\n> [Evil Martians](https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs)\n\nYup, for legacy browsers we actually _need_ a `favicon.ico` at the site's root, hence the reference to `/favicon.ico` in the `head`.\n\nThe 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.\n\nBut 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](https://docs.astro.build/en/core-concepts/endpoints/) again to generate and deliver this asset under `/favicon.ico`.\n\nAs `sharp` does not support `ico` output by default, we have to use `sharp-ico`:\n\n```bash\nnpm install sharp-ico\n```\n\nAstro uses [`sharp`](https://github.com/lovell/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.\n\nThen use `sharp` and `sharp-ico` directly in `src/pages/favicon.ico.ts` to resize and generate the final `favicon.ico` from the source image:\n\n```typescript title=\"src/pages/favicon.ico.ts\"\nimport type { APIRoute } from 'astro'\nimport sharp from 'sharp'\nimport ico from 'sharp-ico'\nimport path from 'node:path'\n\n// relative to project root\nconst faviconSrc = path.resolve('src/images/favicon.png')\n\nexport const GET: APIRoute = async () => {\n // resize to 32px PNG\n const buffer = await sharp(faviconSrc).resize(32).toFormat('png').toBuffer()\n // generate ico\n const icoBuffer = ico.encode([buffer])\n\n return new Response(icoBuffer, {\n headers: { 'Content-Type': 'image/x-icon' }\n })\n}\n```\n\nOnly 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()`:\n\n```typescript title=\"src/pages/favicon.ico.ts\"\nconst buffer32 = await sharp(faviconSrc).resize(32).toFormat('png').toBuffer()\nconst buffer16 = await sharp(faviconSrc).resize(16).toFormat('png').toBuffer()\n\nico.encode([buffer16, buffer32])\n```\n\nIn the end, this will return our dynamically generated ico file under `/favicon.ico`.\n\nWe 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!\n\n## Conclusion\n\nAll 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:\n\n- [How to Favicon in 2023: Six files that fit most needs](https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs)\n- [Astro: Static File Endpoints](https://docs.astro.build/en/core-concepts/endpoints/)\n- [Astro: `getImage()`](https://docs.astro.build/en/guides/images/#generating-images-with-getimage)\n", + "collection": "articles", + "data": { + "title": "Favicon Generation with Astro", + "date": "2023-09-18T00:47:30.000Z", + "tags": ["development", "favicon", "astro"], + "toc": true, + "image": { + "src": "/@fs/Users/m/Code/blog/src/content/articles/2023-09-18-favicon-generation-with-astro/favicon-generation-with-astro-teaser.png?origWidth=1880&origHeight=600&origFormat=png", + "width": 1880, + "height": 600, + "format": "png" + }, + "githubLink": "https://github.com/kremalicious/blog/tree/main/content/articles/2023-09-18-favicon-generation-with-astro/index.md" + } + } +] diff --git a/test/__fixtures__/getCollectionLinks.json b/test/__fixtures__/getCollectionLinks.json new file mode 100644 index 000000000..1f964f6f5 --- /dev/null +++ b/test/__fixtures__/getCollectionLinks.json @@ -0,0 +1,28 @@ +[ + { + "id": "2013-11-20-why-we-are-allowed-to-hate-silicon-valley.md", + "slug": "2013-11-20-why-we-are-allowed-to-hate-silicon-valley", + "body": "\nDon't fully agree with Evgeny Morozov's culture pessimism but he makes some good points in this article:\n\n> It might even help bury some of the myths spun by Silicon Valley. Wouldn’t it be nice if one day, told that Google’s mission is to “organize the world’s information and make it universally accessible and useful,” we would finally read between the lines and discover its true meaning: “to monetize all of the world’s information and make it universally inaccessible and profitable”? With this act of subversive interpretation, we might eventually hit upon the greatest emancipatory insight of all: Letting Google organize all of the world’s information makes as much sense as letting Halliburton organize all of the world’s oil.\n", + "collection": "links", + "data": { + "title": "Why We Are Allowed to Hate Silicon Valley", + "tags": ["silicon valley", "google"], + "linkurl": "http://www.faz.net/aktuell/feuilleton/debatten/the-internet-ideology-why-we-are-allowed-to-hate-silicon-valley-12658406.html", + "date": "2013-11-20T00:00:00.000Z", + "githubLink": "https://github.com/kremalicious/blog/tree/main/content/links/2013-11-20-why-we-are-allowed-to-hate-silicon-valley.md" + } + }, + { + "id": "2013-12-11-silicon-valley-isnt-a-meritocracy.md", + "slug": "2013-12-11-silicon-valley-isnt-a-meritocracy", + "body": "\nFrom [Silicon Valley Isn't a Meritocracy — And It's Dangerous to Hero-Worship Entrepreneurs](http://www.wired.com/opinion/2013/11/silicon-valley-isnt-a-meritocracy-and-the-cult-of-the-entrepreneur-holds-people-back/):\n\n> People in tech repeatedly portray Silicon Valley as places where the smartest, most motivated people from around the globe are changing the world for the better, and this rhetoric has been taken up and repeated often by traditional media outlets. Unlike, say, community activists, public schoolteachers, social workers, or health care providers, technologists are ultimately focused on a small slice of the population, and they are primarily looking for ideas that will prove profitable. These entrepreneurs may have a passion for better audio streaming or e-mail, but to say that such pursuits are world-changing is a bit disingenuous.\n", + "collection": "links", + "data": { + "title": "Silicon Valley Isn't a Meritocracy", + "tags": ["silicon valley"], + "linkurl": "https://www.wired.com/2013/11/silicon-valley-isnt-a-meritocracy-and-the-cult-of-the-entrepreneur-holds-people-back/", + "date": "2013-12-11T00:00:00.000Z", + "githubLink": "https://github.com/kremalicious/blog/tree/main/content/links/2013-12-11-silicon-valley-isnt-a-meritocracy.md" + } + } +] diff --git a/test/__fixtures__/getCollectionPhotos.json b/test/__fixtures__/getCollectionPhotos.json new file mode 100644 index 000000000..f95ae25b1 --- /dev/null +++ b/test/__fixtures__/getCollectionPhotos.json @@ -0,0 +1,70 @@ +[ + { + "id": "2021-11-21-assembleia-da-republica/index.md", + "slug": "2021-11-21-assembleia-da-republica", + "body": "\nIn front of the [Palácio de São Bento](https://en.wikipedia.org/wiki/São_Bento_Palace) housing the [Portuguese parliament]() in Lisbon, Portugal.\n", + "collection": "photos", + "data": { + "title": "Assembleia da República", + "date": "2021-11-21T17:08:39.000Z", + "image": { + "src": "/@fs/Users/m/Code/blog/src/content/photos/2021-11-21-assembleia-da-republica/2021-11-21-assembleia-da-republica.jpg?origWidth=2393&origHeight=3181&origFormat=jpg", + "width": 2393, + "height": 3181, + "format": "jpg", + "orientation": 1 + } + } + }, + { + "id": "2021-11-25-praca-do-comercio/index.md", + "slug": "2021-11-25-praca-do-comercio", + "body": "\nOn the [Praça do Comércio](https://en.wikipedia.org/wiki/Praça_do_Comércio) looking at the [Arco da Rua Augusta](https://en.wikipedia.org/wiki/Rua_Augusta_Arch) surrounded by the usual spectacular light in Lisbon, Portugal.\n", + "collection": "photos", + "data": { + "title": "Praça do Comércio", + "date": "2021-11-25T16:59:58.000Z", + "image": { + "src": "/@fs/Users/m/Code/blog/src/content/photos/2021-11-25-praca-do-comercio/2021-11-25-praca-do-comercio.jpg?origWidth=2735&origHeight=3646&origFormat=jpg", + "width": 2735, + "height": 3646, + "format": "jpg", + "orientation": 1 + } + } + }, + { + "id": "2021-11-26-forever-bicycles/index.md", + "slug": "2021-11-26-forever-bicycles", + "body": "\nAi Weiwei's installation [Forever Bicycles from 2015](https://artsandculture.google.com/asset/forever-bicycle/XAFbVKYmLwOI7Q?hl=en) in front of the [Cordoaria Nacional](https://en.wikipedia.org/wiki/Cordoaria_Nacional) housing the [Ai Weiwei – Rapture](https://aiweiweilisboa.pt) exhibition.\n", + "collection": "photos", + "data": { + "title": "Ai Weiwei, Forever Bicycles", + "date": "2021-11-26T15:32:28.000Z", + "image": { + "src": "/@fs/Users/m/Code/blog/src/content/photos/2021-11-26-forever-bicycles/2021-11-26-forever-bicycles.jpg?origWidth=2781&origHeight=3746&origFormat=jpg", + "width": 2781, + "height": 3746, + "format": "jpg", + "orientation": 1 + } + } + }, + { + "id": "2021-11-26-law-of-the-journey/index.md", + "slug": "2021-11-26-law-of-the-journey", + "body": "\nDetail of Ai Weiwei's installation _Law of the Journey_ from 2017, looking at a print of _Odyssey_ from 2017. Inside the [Cordoaria Nacional](https://en.wikipedia.org/wiki/Cordoaria_Nacional) housing the [Ai Weiwei – Rapture](https://aiweiweilisboa.pt) exhibition in Lisbon, Portugal.\n\n> There’s no refugee crisis, but only human crisis… In dealing with refugees we’ve lost our very basic values.\n> — [Ai Weiwei, 2017](https://www.gessato.com/law-journey-ai-weiwei/)\n", + "collection": "photos", + "data": { + "title": "Ai Weiwei, Law of the Journey", + "date": "2021-11-26T16:12:03.000Z", + "image": { + "src": "/@fs/Users/m/Code/blog/src/content/photos/2021-11-26-law-of-the-journey/2021-11-26-law-of-the-journey.jpg?origWidth=3873&origHeight=2796&origFormat=jpg", + "width": 3873, + "height": 2796, + "format": "jpg", + "orientation": 1 + } + } + } +]