Skip to content

Commit

Permalink
feat(webperf): replace google fonts by local
Browse files Browse the repository at this point in the history
  • Loading branch information
fpasquet committed Apr 25, 2024
1 parent f4e332b commit 8f25013
Show file tree
Hide file tree
Showing 23 changed files with 322 additions and 8 deletions.
1 change: 0 additions & 1 deletion .gitignore
Expand Up @@ -24,7 +24,6 @@ reports
coverage
storybook-static
public/data
public/fonts
public/imgs
public/feed.xml
.env
20 changes: 20 additions & 0 deletions 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
40 changes: 40 additions & 0 deletions README.md
Expand Up @@ -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)
128 changes: 128 additions & 0 deletions 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<string[]>(
(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();
Binary file added public/fonts/agdasima-bold-latin-ext.woff2
Binary file not shown.
Binary file added public/fonts/agdasima-bold-latin.woff2
Binary file not shown.
Binary file added public/fonts/agdasima-regular-latin-ext.woff2
Binary file not shown.
Binary file added public/fonts/agdasima-regular-latin.woff2
Binary file not shown.
Binary file added public/fonts/montserrat-medium-latin-ext.woff2
Binary file not shown.
Binary file added public/fonts/montserrat-medium-latin.woff2
Binary file not shown.
Binary file added public/fonts/montserrat-regular-latin-ext.woff2
Binary file not shown.
Binary file added public/fonts/montserrat-regular-latin.woff2
Binary file not shown.
Binary file not shown.
Binary file added public/fonts/montserrat-semi-bold-latin.woff2
Binary file not shown.
Binary file added src/assets/fonts/agdasima/agdasima-bold.ttf
Binary file not shown.
Binary file added src/assets/fonts/agdasima/agdasima-regular.ttf
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
67 changes: 67 additions & 0 deletions 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<string, string> = {
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',
};
9 changes: 3 additions & 6 deletions src/templates/HtmlTemplate/HtmlTemplate.tsx
Expand Up @@ -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;
Expand Down Expand Up @@ -54,12 +56,7 @@ export const HtmlTemplate: React.FC<HtmlTemplateProps> = ({
))}
<link rel="shortcut icon" type="image/x-icon" href={generateUrl('/favicon.ico')} />
<link rel="manifest" href={generateUrl('/web-app-manifest.json')} />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link
href="https://fonts.googleapis.com/css2?family=Agdasima:wght@400;700&family=Montserrat:wght@100;400;500;700&display=swap"
rel="stylesheet"
/>
<style dangerouslySetInnerHTML={{ __html: fontFaces }} />
{links?.map((link, index) => (
<link key={index} {...link} />
))}
Expand Down
63 changes: 63 additions & 0 deletions src/templates/HtmlTemplate/fontFaces.ts
@@ -0,0 +1,63 @@
import { fonts, FontWeightType, subsets } from '@/config/website/fonts';
import { getPathFile } from '@/helpers/assetHelper';

// Function to get numerical font weight based on the FontWeightType
const getFontWeightNumber = (fontWeight: FontWeightType): number => {
// eslint-disable-next-line default-case
switch (fontWeight) {
case 'thin':
return 100;
case 'extra-light':
return 200;
case 'light':
return 300;
case 'regular':
return 400;
case 'medium':
return 500;
case 'semi-bold':
return 600;
case 'bold':
return 700;
case 'extra-bold':
return 800;
case 'black':
return 900;
}
throw new Error(`This fontWeight "${fontWeight}" does not exist`);
};

const templateFontFace = (options: {
fontFamilyName: string;
fontPath: string;
fontWeight: FontWeightType;
unicodeRange: string;
isItalic?: boolean;
}): string =>
`@font-face {
font-family: '${options.fontFamilyName}';
font-style: ${options.isItalic ? 'italic' : 'normal'};
font-weight: ${getFontWeightNumber(options.fontWeight)};
font-display: swap;
src: url(${getPathFile(options.fontPath)}) format('woff2');
unicode-range: ${options.unicodeRange};
}`.replace(/\n|\s+(?!format)/g, '');

export const fontFaces = Object.entries(subsets)
.reduce<string[]>((currentFontFaces, [subsetName, unicodeRange]) => {
for (const font of fonts) {
for (const style of font.styles) {
currentFontFaces.push(
templateFontFace({
fontFamilyName: font.fontFamilyName,
fontPath: `/fonts/${style.fontFileName}-${subsetName}.woff2`,
fontWeight: style.fontWeight,
isItalic: style.isItalic,
unicodeRange,
})
);
}
}
return currentFontFaces;
}, [])
.join('');
2 changes: 1 addition & 1 deletion src/translations/en.translations.json
Expand Up @@ -103,7 +103,7 @@
"description": "All our tutorials, tool explanations, REX participation in community events and blog articles on various technologies in PHP web development."
},
"title": "Our articles and feedbacks about PHP programming",
"description": "<strong>PHP is a backend-oriented programming language</strong> that allows the development of dynamic and interactive applications. It is a popular language, used by a large number of web applications widely used by the general public. In this category, find all the articles, feedbacks and tutorials from our astronauts about <strong>PHP, Symfony, Laravel, the essential tools to improve your productivity, and our reports on major community events!</strong> Good reading!",
"description": "<strong>PHP is a backend-oriented programming language</strong> that allows the development of dynamic and interactive applications. It is a popular language, used by a large number of web applications widely used by the general public. In this category, find all the articles, feedbacks and tutorials from our astronauts about <strong>PHP, Symfony, Laravel, Angular, the essential tools to improve your productivity, and our reports on major community events!</strong> Good reading!",
"expertise": {
"title": "What's the value of PHP?",
"description": "With its rich ecosystem, strong community, and sturdy frameworks such as Symfony, <strong>PHP makes possible the development of all types of backend applications</strong>. Its use helps speed up the development process: its predefined structure and components allow developers to focus more on business logic. PHP's flexibility allows the creation of <strong>interactive web applications, e-commerce sites, blogs, marketplaces, media sites and even personalized back offices</strong>. PHP is therefore a quality solution for developing your tailor-made web projects.",
Expand Down

0 comments on commit 8f25013

Please sign in to comment.