Penny Black Philately

Built with Gatsby

Penny Black Philately

by John Vincent


Posted on October 1, 2023


Stamps

Stamps

Gatsby Stamps is a Stamp Catalog application.

Use Gatsby Stamps to view the stamps of selected Nations.

Live Deployment

Gatsby Stamps at Digital Ocean

Stamps

Technical

This implementation is a port from philately.johnvincent.io, which was built using React

The intention is to build the application using Gatsby and React.

Gatsby statically generated pages are ideal for performance and SEO.

Stamps

Performance

Stamps

Deploy Stamps

stamps.johnvincent.io is deployed to a Digital Ocean Droplet.

Please see Configure and Deploy to Ubuntu Droplet at Digital Ocean for deployment details.

Stamps

Maintenance

The following describe tasks required for the maintenance of stamps.johnvincent.io at Digital Ocean.

Update SSL Certificates to Ubuntu at Digital Ocean

Maintaining Droplets at Digital Ocean

Website Review

Website Validation

Client Technologies

Production Deployment Technologies

Application Calculations

To make an estimate as to how many pages/urls Gatsby would need to generate, some facts need to be gathered:

  • Since 1840, when the first stamp was issued in England, over 780 countries have issued stamps. About 250-300 of these countries still exist.
  • Probably about 500,000 different stamps have been produced worldwide.

For very rough estimation purposes, lets assume:

  • 500,000 stamps
  • 300 countries
  • 3 stamps per set

which roughly calulates to:

  • stamps per country: 1700
  • sets per country: 570

If each country, on average started issuing stamps in 1850, then:

  • Number of decades issuing stamps: 18
  • Number of years issuing stamps: 173
  • Number of sets per year: 3.5
  • Number of stamps per set: 3

The usual architecture of a catalog application would require the following navigation:

  • List of countries
  • Country, select a decade
  • Decade, select a year
  • Year, select a set
  • Set, select a stamp
  • Stamp, provide full details

Such an architecture would require the following pages be generated:

(300)*(18)*(10)*(3.5)*(3) = 567,000

There would doubtless also be many other pages to generate.

Generating such a large number of pages:

  • will take a very considerable amount of time and resources.
  • will slow development to a crawl.
  • will require the pages to be very simple to minimize memory usage.
  • may never generate due to out of memory errors.
  • will be impossible to test.

While this architecture would probably work, given sufficient resources, I chose a more practical approach.

Application Architecture

Stamps

I chose to work with 28 countries. Creating the data for more countries would require considerable time and may be implemented in the future.

For this application, I created all of the pages with gatsby-node.js. The createPages function provides excellent control over the generation of the pages.

Catalogs Section for SEO

Stamps

For SEO purposes, I created the Catalogs section which is accessible from a link in the footer. This section has the following navigation:

  • List of countries, select a country
  • Country, select a decade
  • Decade, select a year
  • Year

This architecture requires the following pages be generated:

(28)*(18)*(10) = 5040

which is very manageable.

When all countries are implemented the following pages would be generated:

(300)*(18)*(10) = 54,000

which is still very manageable.

These pages are mostly just links, although the Year pages have stamp images and details. They are not meant for a human user.

Catalog Section for Users

Stamps

The country catalogs which are displayed on the home page all link to the country catalog section, for example https://www.stamps.johnvincent.io/catalog/Australia

This page is generated for each country, thus a total of 28 pages. All of these pages are generated from the same template MainTemplate.jsx.

Snippet from gatsby-node.js

catalogs.forEach(({ data }) => {
	const { country, ckey } = data;
	const url = `/catalog/${ckey}/`;
	createPage({
		path: url,
		component: require.resolve('./src/templates/MainTemplate.jsx'),
		context: {
			pageMetaData: { permalink: url, meta_title: `Stamps of ${country}` },
			config,
			info: { type: 'catalog_main', country, ckey },
			slug: ckey
		}
	});
});

MainTemplate.jsx

import React from 'react';

import { graphql } from 'gatsby';

import Layout from '../containers/Layout';
import { SEO } from '..';
import Main from '../components/catalog/Main';

export default function MainTemplate(
	{ data: { catalogsAllJson: catalog }, pageContext: { pageMetaData, config, info }
	}
) {
	return (
		<Layout config={config}>
			<SEO config={config} pageMetaData={pageMetaData} />
			<Main config={config} info={info} catalog={catalog} />
		</Layout>
	);
}

export const data = graphql`
query ($slug: String!) {
  catalogsAllJson(ckey: { eq: $slug }) {
		ckey
		country
		data {
			years {
				sets {
					idx
					title
					year
					stamps {
						id
						description
						currency
						image
					}
				}
				year
			}
			country
			code
			idx
			ckey
			decades {
				from
				to
				list
			}
			stats {
				range {
					high
					low
				}
				counts {
					sets
					stamps
				}
			}
			ckey
		}
  }
}
`;

The key is to pass from gatsby-node.js

slug: ckey

in the page context to the template. GraphQL takes the value of $slug from the page context.

Main.jsx is a React component which provides for the dynamic nature of the application.

Catalog Search

Stamps

Search capabilities are not static and never can be. It is always dynamic. This requires a specific architecture.

MainTemplate.jsx provides the Search component which is referenced in the Header component.

The Search component allows the user to enter a query string, the application then links to the country search page, for example https://www.stamps.johnvincent.io/search/Belgium?query=leopold

Notice the use of a query string.

This page is generated for each country, thus a total of 28 pages. All of these pages are generated from the same template SearchTemplate.jsx.

Snippet from gatsby-node.js

catalogs.forEach(({ data }) => {
	const { country, ckey } = data;
	const url = `/search/${ckey}/`;
	createPage({
		path: url,
		component: require.resolve('./src/templates/SearchTemplate.jsx'),
		context: {
			pageMetaData: { permalink: url, meta_title: `Stamps of ${country}` },
			config,
			info: { type: 'catalog_search', country, ckey },
			slug: ckey
		}
	});
});

SearchTemplate.jsx

import React, { useState, useEffect } from 'react';

import { graphql } from 'gatsby';

import Layout from '../containers/Layout';
import { SEO, Header } from '..';
import Main from '../components/catalog/Main';

import search from '../components/utilities/search';

export default function SearchTemplate(props) {
	const [hydrated, setHydrated] = useState(false);
	useEffect(() => {
		setHydrated(true);
	}, []);
	if (!hydrated) {
		return null;		// Returns null on first render, so the client and server match
	}

	const { location, data: { catalogsAllJson }, pageContext: { pageMetaData, config, info }} = props;

	let noData = true;
	let catalog = {};
	const params = new URLSearchParams(location.search);
	const query = params.get('query');
	if (query && query.length > 0) {
		catalog = search(query, catalogsAllJson);
		const { data } = catalog;
		const { decades, years } = data;
		noData = !decades || decades.length < 1 || !years || years.length < 1;
	}

	if (noData) {
		return (
			<Layout config={config}>
				<SEO config={config} pageMetaData={pageMetaData} />
				<Header info={{ ...info, query }} />
			</Layout>
		);
	}

	return (
		<Layout config={config}>
			<SEO config={config} pageMetaData={pageMetaData} />
			<Main config={config} info={{ ...info, query }} catalog={catalog} />
		</Layout>
	);
}

export const data = graphql`
query ($slug: String!) {
  catalogsAllJson(ckey: { eq: $slug }) {
		ckey
		country
		data {
			years {
				sets {
					idx
					title
					year
					stamps {
						id
						description
						currency
						image
					}
				}
				year
			}
			country
			code
			idx
			ckey
			decades {
				from
				to
				list
			}
			stats {
				range {
					high
					low
				}
				counts {
					sets
					stamps
				}
			}
			ckey
		}
  }
}
`;

Again, pass from gatsby-node.js

slug: ckey

in the page context to the template. GraphQL takes the value of $slug from the page context.

Notice

export default function SearchTemplate(props) {
	const [hydrated, setHydrated] = useState(false);
	useEffect(() => {
		setHydrated(true);
	}, []);
	if (!hydrated) {
		return null;		// Returns null on first render, so the client and server match
	}
...

It is a requirement for the server and the client to initially produce identical output.

Hydration is the process of using client-side JavaScript to add application state and interactivity to server-rendered HTML. Gatsby uses hydration to transform the static HTML created at build time into a React application.

This code allows for the server and client to initially generate identical code, and then for the client to act as a React application, or as it were, it transforms static to dynamic.

Gatsby Data

All external data is added to data/catalogs and loaded into Gatsby/GraphQL using the plugin gatsby-source-filesystem.

In practice, for the data to be useful to the application via GraphQL, the data must be in a very particular format. GraphQL has it's own requirements. Violate these and will get the following error on loading the json with plugin.

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

This may be remedied with:

npm run start --node-flags --max-old-space-size=4096--no-warnings

however this error, without stating as such, is actually indicating a problem with the architecture of the data.

Loading Json hashmaps into GraphQL quickly caused out of memory errors.

However, the application also has very specific requirements of the data. It is extremely difficult to architect the data so it will quickly load into GraphQL in a format that is useful to the application and for the application pages to be generated within a reasonable timeframe.

In practice, the source data requires a complex transformation application to provide the data in a useful format.

Gatsby Images

This application is very image intensive. To maximize the value of Gatsby all images should use gatsby-plugin-image. In practice, generating the vast number of images using the plugin takes large amounts of memory and time. It is not practical for this application to load it's images using gatsby-plugin-image.

Debugger Problem

The Visual Studio Code debugger failed with

Cannot find module '../build/Release/sharp-darwin-x64.node'

The problem is in gatsby-plugin-sharp. There appears to be no solution other than to upgrade to Gatsby V5.

See Migrating Gatsby from V4 to V5/

This would force upgrades to Node V18 and to React 18, which causes upgrades to many packages that are not compatible. For example:

npm outdated

npm install gatsby@latest --legacy-peer-deps

npm install react@latest react-dom@latest --legacy-peer-deps

which caused more errors with "@lmdb/lmdb-darwin-arm64": "2.6.0-alpha6"

I concluded that migrating to Gatsby V5 was not possible at this time.

End