diff --git a/.gitignore b/.gitignore index c7286c993..60edaed34 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,6 @@ reports coverage storybook-static public/data -public/fonts public/imgs public/feed.xml .env diff --git a/Dockerfile-fonttools b/Dockerfile-fonttools new file mode 100644 index 000000000..d85906f14 --- /dev/null +++ b/Dockerfile-fonttools @@ -0,0 +1,20 @@ +FROM node:18.16-alpine3.16 + +# Install Python +RUN apk add --no-cache python3 + +# Update package and install necessary build tools +RUN apk update && apk add --no-cache build-base python3 py3-pip + +# Upgrade setuptools +RUN pip install -U setuptools + +# Install brotli and fonttools +RUN pip install brotli fonttools + +# Install glyphhanger +RUN npm install -g glyphhanger tsx + +WORKDIR /var/www/app + +USER node:node diff --git a/README.md b/README.md index 75e0a54c0..ad1696a3d 100644 --- a/README.md +++ b/README.md @@ -288,3 +288,43 @@ git checkout -b feat/add-tutorial-slug ``` Once your tutorial is finished and you want it to be published and add the label `publication` to your pull request. + +## Creating font subsets for web performance + +To optimize font file sizes, it is recommended to create subsets of fonts. This process involves breaking down fonts into subsets, ensuring that web browsers only load the necessary parts of the font. The provided Docker commands utilize the fonttools tool to generate font subsets. The TypeScript files responsible for this process are located in the `src/themes/fonts` directory. Below are the Docker commands and their results for font subset creation: + +``` +# Build the fonttools Docker image +docker build -t fonttools -f Dockerfile-fonttools . + +# Run the fonttools container to create font subsets +docker run -it -v ./:/var/www/app --rm fonttools tsx bin/optimize-fonts.ts +``` + +Result of the command: + +``` +Subsetting agdasima-bold.ttf to agdasima-bold-latin-ext.woff2 (was 23.012 kB, now 1.544 kB) +Subsetting agdasima-regular.ttf to agdasima-regular-latin-ext.woff2 (was 23.152 kB, now 1.56 kB) +Subsetting agdasima-regular.ttf to agdasima-regular-latin.woff2 (was 23.152 kB, now 10.416 kB) +Subsetting agdasima-bold.ttf to agdasima-bold-latin.woff2 (was 23.012 kB, now 10.304 kB) +Subsetting montserrat-medium.ttf to montserrat-medium-latin-ext.woff2 (was 197.756 kB, now 20.4 kB) +Subsetting montserrat-semi-bold.ttf to montserrat-semi-bold-latin-ext.woff2 (was 197.964 kB, now 20.512 kB) +Subsetting montserrat-regular.ttf to montserrat-regular-latin-ext.woff2 (was 197.624 kB, now 20.28 kB) +Subsetting montserrat-semi-bold.ttf to montserrat-semi-bold-latin.woff2 (was 197.964 kB, now 24.02 kB) +Subsetting montserrat-regular.ttf to montserrat-regular-latin.woff2 (was 197.624 kB, now 23.916 kB) +Subsetting montserrat-medium.ttf to montserrat-medium-latin.woff2 (was 197.756 kB, now 24.044 kB) +... +``` + +This output demonstrates the reduction in file size achieved by creating subsets for various fonts. The original font files are listed with their corresponding subset names and sizes before and after optimization. + +Sources: + +- [Article to creating font subsets](https://markoskon.com/creating-font-subsets/) +- [FontTools is a library for manipulating fonts, written in Python](https://fonttools.readthedocs.io/en/latest/subset/) +- [Characters table by language](https://character-table.netlify.app/) +- [To analyze a font (number of characters, glyphs, language support, layout features, etc.](https://wakamaifondue.com/) +- [Site listing all unicodes by range, alphabetically, type ...](https://symbl.cc/en/unicode/blocks/basic-latin/) +- [Wikipedia: List of Unicode characters](https://en.wikipedia.org/wiki/List_of_Unicode_characters) +- [Other site listing unicodes](https://www.unicode.org/charts/nameslist/index.html) diff --git a/bin/optimize-fonts.ts b/bin/optimize-fonts.ts new file mode 100644 index 000000000..eed930c17 --- /dev/null +++ b/bin/optimize-fonts.ts @@ -0,0 +1,128 @@ +#!/usr/bin/env node + +import chalk from 'chalk'; +import { exec } from 'node:child_process'; +import { statSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { fonts, subsets } from '../src/config/website/fonts'; + +// Define the directory for fonts +const fontsDir = resolve(process.cwd(), 'src/assets/fonts'); +const fontsOutdir = resolve(process.cwd(), 'public/fonts'); + +const formatUnicode = (unicode: string): string => unicode.padStart(4, '0').toUpperCase(); +const formatUnicodeFromNumber = (unicodeNumber: number, includePrefix: boolean = true): string => { + const formattedUnicode = formatUnicode(unicodeNumber.toString(16)); + + return includePrefix ? `U+${formattedUnicode}` : formattedUnicode; +}; + +const getUnicodeTableByUnicodeRange = ( + unicodeRangeString: string +): { range: string; codePoints: number[]; unicodes: string[]; characters: string[] }[] => + unicodeRangeString + .replace(/U\+/g, '') + .split(',') + .reduce<{ range: string; codePoints: number[]; unicodes: string[]; characters: string[] }[]>( + (unicodeTable, currentRange) => { + if (currentRange.includes('-')) { + const [start, end] = currentRange.split('-').map((i) => parseInt(i, 16)); + const codePoints: number[] = Array.from({ length: end - start + 1 }, (_, index) => start + index); + + return [ + ...unicodeTable, + { + range: currentRange, + codePoints, + unicodes: codePoints.map((codePoint) => formatUnicodeFromNumber(Number(codePoint))), + characters: codePoints.map((codePoint) => String.fromCharCode(codePoint)), + }, + ]; + } + + const codePoint: number = parseInt(currentRange, 16); + + return [ + ...unicodeTable, + { + range: currentRange, + codePoints: [codePoint], + unicodes: [currentRange], + characters: [String.fromCharCode(codePoint)], + }, + ]; + }, + [] + ); + +// Define supported font formats +const formats: string[] = ['woff2']; + +// Function to get file size in kilobytes +const getFileSizeInBytes = (filePath: string): string => { + const stats = statSync(filePath); + + return `${stats.size / 1000} kB`; +}; + +const optimizeFonts = (): void => { + const unicodesInSubsets: string[] = []; + for (const [subset, unicodeRange] of Object.entries(subsets)) { + const unicodeTable = getUnicodeTableByUnicodeRange(unicodeRange); + + const unicodes = unicodeTable.reduce( + (currentUnicodes, item) => [...currentUnicodes, ...item.unicodes], + [] + ); + unicodesInSubsets.push(...unicodes); + console.log( + `Here are the characters you selected in the \`${chalk.blue.bold(subset)}\` subset: ${unicodeTable + .map((item) => item.characters.map((character) => `\`${chalk.yellow(character)}\``)) + .join(', ')}.\n` + ); + } + + // Loop through each font configuration + for (const { fontDirectoryName, styles } of fonts) { + // Define source and optimized directories + const sourcesDir = resolve(fontsDir, fontDirectoryName); + + // Loop through each font style + for (const { fontFileName } of styles) { + const sourceFontFileNameWithExtension = `${fontFileName}.ttf`; + const sourceFontPath = resolve(sourcesDir, sourceFontFileNameWithExtension); + const sourceFontFileSize = getFileSizeInBytes(sourceFontPath); + + for (const [subset, unicodeRange] of Object.entries(subsets)) { + for (const format of formats) { + const optimizedFontFileName = `${fontFileName}-${subset}.${format}`; + const optimizedFontPath = resolve(fontsOutdir, optimizedFontFileName); + + const args = [ + sourceFontPath, + `--output-file="${optimizedFontPath}"`, + `--flavor=${format}`, + '--layout-features="*"', + `--unicodes="${unicodeRange}"`, + ]; + exec(`pyftsubset ${args.join(' \\\n')}`, (error): void => { + if (error) { + console.error(`Error: ${error}`); + + return; + } + const optimizedFontFileSize = getFileSizeInBytes(optimizedFontPath); + console.log( + `Subsetting ${chalk.bold(sourceFontFileNameWithExtension)} to ${chalk.bold( + optimizedFontFileName + )} (was ${chalk.red(sourceFontFileSize)}, now ${chalk.green(optimizedFontFileSize)})` + ); + }); + } + } + } + } +}; + +optimizeFonts(); diff --git a/public/fonts/agdasima-bold-latin-ext.woff2 b/public/fonts/agdasima-bold-latin-ext.woff2 new file mode 100644 index 000000000..9437dd68c Binary files /dev/null and b/public/fonts/agdasima-bold-latin-ext.woff2 differ diff --git a/public/fonts/agdasima-bold-latin.woff2 b/public/fonts/agdasima-bold-latin.woff2 new file mode 100644 index 000000000..7f82ddb18 Binary files /dev/null and b/public/fonts/agdasima-bold-latin.woff2 differ diff --git a/public/fonts/agdasima-regular-latin-ext.woff2 b/public/fonts/agdasima-regular-latin-ext.woff2 new file mode 100644 index 000000000..fa655e3c4 Binary files /dev/null and b/public/fonts/agdasima-regular-latin-ext.woff2 differ diff --git a/public/fonts/agdasima-regular-latin.woff2 b/public/fonts/agdasima-regular-latin.woff2 new file mode 100644 index 000000000..46c75e94d Binary files /dev/null and b/public/fonts/agdasima-regular-latin.woff2 differ diff --git a/public/fonts/montserrat-medium-latin-ext.woff2 b/public/fonts/montserrat-medium-latin-ext.woff2 new file mode 100644 index 000000000..754da471c Binary files /dev/null and b/public/fonts/montserrat-medium-latin-ext.woff2 differ diff --git a/public/fonts/montserrat-medium-latin.woff2 b/public/fonts/montserrat-medium-latin.woff2 new file mode 100644 index 000000000..205173c04 Binary files /dev/null and b/public/fonts/montserrat-medium-latin.woff2 differ diff --git a/public/fonts/montserrat-regular-latin-ext.woff2 b/public/fonts/montserrat-regular-latin-ext.woff2 new file mode 100644 index 000000000..a5ada0bf8 Binary files /dev/null and b/public/fonts/montserrat-regular-latin-ext.woff2 differ diff --git a/public/fonts/montserrat-regular-latin.woff2 b/public/fonts/montserrat-regular-latin.woff2 new file mode 100644 index 000000000..33c36dcb4 Binary files /dev/null and b/public/fonts/montserrat-regular-latin.woff2 differ diff --git a/public/fonts/montserrat-semi-bold-latin-ext.woff2 b/public/fonts/montserrat-semi-bold-latin-ext.woff2 new file mode 100644 index 000000000..7f75d48a8 Binary files /dev/null and b/public/fonts/montserrat-semi-bold-latin-ext.woff2 differ diff --git a/public/fonts/montserrat-semi-bold-latin.woff2 b/public/fonts/montserrat-semi-bold-latin.woff2 new file mode 100644 index 000000000..d047f487f Binary files /dev/null and b/public/fonts/montserrat-semi-bold-latin.woff2 differ diff --git a/src/assets/fonts/agdasima/agdasima-bold.ttf b/src/assets/fonts/agdasima/agdasima-bold.ttf new file mode 100644 index 000000000..b9b5dbe33 Binary files /dev/null and b/src/assets/fonts/agdasima/agdasima-bold.ttf differ diff --git a/src/assets/fonts/agdasima/agdasima-regular.ttf b/src/assets/fonts/agdasima/agdasima-regular.ttf new file mode 100644 index 000000000..af4400941 Binary files /dev/null and b/src/assets/fonts/agdasima/agdasima-regular.ttf differ diff --git a/src/assets/fonts/montserrat/montserrat-medium.ttf b/src/assets/fonts/montserrat/montserrat-medium.ttf new file mode 100644 index 000000000..4012225c0 Binary files /dev/null and b/src/assets/fonts/montserrat/montserrat-medium.ttf differ diff --git a/src/assets/fonts/montserrat/montserrat-regular.ttf b/src/assets/fonts/montserrat/montserrat-regular.ttf new file mode 100644 index 000000000..f4a266dd3 Binary files /dev/null and b/src/assets/fonts/montserrat/montserrat-regular.ttf differ diff --git a/src/assets/fonts/montserrat/montserrat-semi-bold.ttf b/src/assets/fonts/montserrat/montserrat-semi-bold.ttf new file mode 100644 index 000000000..189ce9d0c Binary files /dev/null and b/src/assets/fonts/montserrat/montserrat-semi-bold.ttf differ diff --git a/src/config/website/fonts.ts b/src/config/website/fonts.ts new file mode 100644 index 000000000..ae179282e --- /dev/null +++ b/src/config/website/fonts.ts @@ -0,0 +1,67 @@ +// Define the type for font weight +export type FontWeightType = + | 'thin' + | 'extra-light' + | 'light' + | 'regular' + | 'medium' + | 'semi-bold' + | 'bold' + | 'extra-bold' + | 'black'; + +export const fonts: { + fontFamilyName: string; + fontFamily: string; + fontDirectoryName: string; + styles: { + fontFileName: string; + fontWeight: FontWeightType; + isItalic?: boolean; + isPreload?: boolean; + }[]; +}[] = [ + { + fontFamilyName: 'Montserrat', + fontFamily: 'Montserrat, helvetica neue, helvetica, arial, sans-serif', + fontDirectoryName: 'montserrat', + styles: [ + { + fontFileName: 'montserrat-regular', + fontWeight: 'regular', + }, + { + fontFileName: 'montserrat-medium', + fontWeight: 'medium', + }, + { + fontFileName: 'montserrat-semi-bold', + fontWeight: 'semi-bold', + }, + ], + }, + { + fontFamilyName: 'Agdasima', + fontFamily: 'Agdasima', + fontDirectoryName: 'agdasima', + styles: [ + { + fontFileName: 'agdasima-regular', + fontWeight: 'regular', + }, + { + fontFileName: 'agdasima-bold', + fontWeight: 'bold', + }, + ], + }, +]; + +// Define subsets for different languages +// Keep this order, because the one in first position will be used for the preload +export const subsets: Record = { + latin: + 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD', + 'latin-ext': + 'U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF', +}; diff --git a/src/templates/HtmlTemplate/HtmlTemplate.tsx b/src/templates/HtmlTemplate/HtmlTemplate.tsx index 1aabcd19d..f115634aa 100644 --- a/src/templates/HtmlTemplate/HtmlTemplate.tsx +++ b/src/templates/HtmlTemplate/HtmlTemplate.tsx @@ -4,6 +4,8 @@ import * as React from 'react'; import { GTM_ID } from '@/constants'; import { generateUrl } from '@/helpers/assetHelper'; +import { fontFaces } from './fontFaces'; + export interface HtmlTemplateProps { lang: string; i18nStore: ResourceStore; @@ -54,12 +56,7 @@ export const HtmlTemplate: React.FC = ({ ))} - - - +