I'm looking for a way to generate all possible Google Fonts URLs for a specific font. I want to include all available styles ( italic, normal) and weights (e.g 400–800).
I'm working on an image editor, and I need to load every font from a font-family when a user selects a font.
Currently, I'm using the fontsource API to retrieve data on the available fonts and I try to generate the google CSS links from that data,
E.g
https://fonts.googleapis.com/css2?family=Open%20Sans:ital,wght@0,300..800;1,300..800
but the function I wrote sometimes generate invalid links and I'm not sure it includes all possibles styles/weights.
Example API result:
{
"id": "open-sans",
"family": "Open Sans",
"subsets": [
"cyrillic",
"cyrillic-ext",
"greek",
"greek-ext",
"hebrew",
"latin",
"latin-ext",
"vietnamese"
],
"weights": [
300,
400,
500,
600,
700,
800
],
"styles": [
"italic",
"normal"
],
"defSubset": "latin",
"variable": true,
"lastModified": "2022-09-22",
"category": "sans-serif",
"license": "OFL-1.1",
"type": "google"
}
Here is my current code, do you know a better solution for this? Thanks
function generateGoogleFontURL(font) {
const formattedFontFamily = font.family.replace(/\s+/g, "+")
if (font.weights.length == 0) {
return [`https://fonts.googleapis.com/css2?family=${formattedFontFamily}`]
}
if (!font.variable) {
if (font.styles.includes("italic")) {
return font.weights.map(x => `https://fonts.googleapis.com/css2?family=${formattedFontFamily}:ital,wght@0,${x};1,${x}`)
}
return [`https://fonts.googleapis.com/css2?family=${formattedFontFamily}:wght@${font.weights.join(';')}`]
}
if (font.weights.length > 1) {
const minWeight = font.weights[0]
const maxWeight = font.weights[font.weights.length - 1]
if (font.styles.includes("italic")) {
return [`https://fonts.googleapis.com/css2?family=${formattedFontFamily}:ital,wght@0,${minWeight}..${maxWeight};1,${minWeight}..${maxWeight}`]
}
else {
return [`https://fonts.googleapis.com/css2?family=${formattedFontFamily}:wght@${minWeight}..${maxWeight}`]
}
}
if (font.styles.includes("italic")) {
return [`https://fonts.googleapis.com/css2?family=${formattedFontFamily}:ital,wght@0,${font.weights[0]};1,${font.weights[0]}`]
}
return [`https://fonts.googleapis.com/css2?family=${formattedFontFamily}:wght@${font.weights[0]}`]
}
The current function should actually work.
But there are some issues:
wdth
(width) axis.Here's a revised version concatenating all styles and weights in one URL
let fontAPiJsonFS = "https://api.fontsource.org/v1/fonts";
let fontFamily = "Open Sans";
fontFamily = "Roboto";
(async() => {
let fetched = await fetch(fontAPiJsonFS);
let fontItems = await (await await fetch(fontAPiJsonFS)).json();
// filter family name and google fonts
let fontItem = fontItems.filter(
(item) => item.family === fontFamily && item.type === "google"
)[0];
let url = generateGoogleFontURL(fontItem);
a_css.href = url;
a_css.textContent = url;
})();
function generateGoogleFontURL(font) {
let styles = font.styles;
let weights = font.weights;
let hasItalics = styles.includes("italic");
// very uncommon but might be used for handwriting fonts
let italicOnly = styles.length === 1 && styles[0] === "italic";
const formattedFontFamily = font.family.replace(/\s+/g, "+");
const baseUrl = `https://fonts.googleapis.com/css2?family=`;
let url = baseUrl + formattedFontFamily;
// concatenate prefix like: ":ital,wght@"
let query_prefix = "";
/**
* 1. static fonts
*/
if (!font.variable) {
let tuples = [];
if (hasItalics) {
query_prefix += ":ital";
tuples.push(weights.map((x) => `0,${x}`));
}
// italics and regular
if (!italicOnly) {
query_prefix += ",wght";
tuples.push(weights.map((x) => `1,${x}`));
}
// only regular
else {
query_prefix = ":wght";
}
// concatenate URL
url += query_prefix + "@" + tuples.flat().join(";");
return url;
} else {
/**
* 2. variable fonts
*/
if (font.weights.length > 1) {
const minWeight = font.weights[0];
const maxWeight = font.weights[font.weights.length - 1];
if (hasItalics) {
return `https://fonts.googleapis.com/css2?family=${formattedFontFamily}:ital,wght@0,${minWeight}..${maxWeight};1,${minWeight}..${maxWeight}`;
} else {
return `https://fonts.googleapis.com/css2?family=${formattedFontFamily}:wght@${minWeight}..${maxWeight}`;
}
}
}
}
<p>
<strong>Google font URL: </strong>
<a id="a_css" href=""></a>
</p>
As commented by Mike 'Pomax' Kamermans: google's developer API is currently the better option – at least when it comes to variable fonts.
You'll need to require an API key.
An API call would look like something this:
https://www.googleapis.com/webfonts/v1/webfonts?capability=VF&capability=WOFF2&sort=style&key=${apiKey}
It is crucial to add the capability parameter capability=VF
to the query – otherwise the response will omit axes data.
This parameter won't filter the output to variable fonts only!
So you might need to include an extra filter returning only items with an axes property.
Google's open font API is unforgiving about the order of tuples (simplified: query chunks).
google error message
Axes must be listed alphabetically (e.g. a,b,c,A,B,C)
They must be sorted in alphabetical and case-sensitive order
let axes = [
{ tag: "wght", start: 300, end: 800 },
{ tag: "wdth", start: 75, end: 100 },
{ tag: "GRAD", start: -200, end: 150 },
{ tag: "slnt", start: -10, end: 0 }
];
axes = alphanumeric_sort(axes, 'tag');
console.log(axes);
function alphanumeric_sort(object, key) {
// sort keys alphabetically
object = object.sort((a, b) => a[key].localeCompare(b[key]));
// sort keys case sensitive
object = [
object.filter((item) => item[key].toLowerCase() === item[key]),
object.filter((item) => item[key].toUpperCase() === item[key])
].flat();
return object;
}
Now, we can assemble valid CSS URLs including all axes:
let baseUrl = `https://fonts.googleapis.com/css2?family=`;
let fontAPiJson =
"https://raw.githubusercontent.com/herrstrietzel/fonthelpers/main/json/gfontsAPI.json";
let fontFamily = "Roboto Flex";
// init
(async () => {
let fetched = await fetch(fontAPiJson);
let fontItems = await (await fetched.json()).items;
let fontItem = fontItems.filter((item) => item.family === fontFamily)[0];
//console.log(fontItem)
let url = baseUrl + getGoogleFontQueryAPI(fontItem, true);
a_var.href=url;
a_var.textContent=url;
})();
/**
* parse API Json
*/
function getGoogleFontQueryAPI(fontItem, variable = true) {
let fontFamily = fontItem.family;
// sanitize whitespace in font family name
let fontfamilyQuery = fontFamily.replaceAll(" ", "+");
let axes = fontItem.axes;
let isVF = axes && axes.length ? true : false;
// prepended tuple keys like ital, wght etc
let queryPre = [];
let query = "";
// count weights in variants
let styles = fontItem.variants;
// sanitize styles
styles = styles.map((style) => {
style = style === "italic" ? "400i" : style;
return style.replaceAll("italic", "i").replaceAll("regular", "400");
});
let weightsItalic = [];
let weightsRegular = [];
styles.forEach((style) => {
if (style.includes("i")) {
weightsItalic.push(parseFloat(style));
} else {
weightsRegular.push(parseFloat(style));
}
});
// italic and regular
if (weightsItalic.length) {
queryPre.push("ital");
}
if (weightsRegular.length && !isVF) {
queryPre.push("wght");
}
// is variable
if (isVF) {
// sort axes alphabetically - case sensitive ([a-z],[A-Z])!!!
axes = alphanumeric_sort(axes, 'tag');
let ranges = axes.map((val) => {
return val.start + ".." + val.end;
});
// italic and regular
if (weightsItalic.length && weightsRegular.length) {
//queryPre.push("ital");
rangeArr = [];
for (let i = 0; i < 2; i++) {
rangeArr.push(`${i},${ranges.join(",")}`);
}
}
// only italic
else if (weightsItalic.length && !weightsRegular.length) {
//queryPre.push("ital");
rangeArr = [];
rangeArr.push(`1,${ranges.join(",")}`);
}
// only regular
else {
rangeArr = [];
rangeArr.push(`${ranges.join(",")}`);
}
// add axes tags to pre query string
axes.map((val) => {
return queryPre.push(val.tag);
});
query =
fontfamilyQuery +
":" +
queryPre.join(",") +
"@" +
rangeArr.join(";") +
"&display=swap";
return query;
}
/**
* 2. get static
*/
query = fontfamilyQuery + ":" + queryPre.join(",") + "@";
// italic and regular
if (weightsItalic.length && weightsRegular.length) {
query +=
weightsRegular
.map((val) => {
return "0," + val;
})
.join(";") +
";" +
weightsItalic
.map((val) => {
return "1," + val;
})
.join(";");
}
// only italic
else if (weightsItalic.length && !weightsRegular.length) {
query += weightsItalic
.map((val) => {
return "1," + val;
})
.join(";");
}
// only regular
else {
query += weightsRegular
.map((val) => {
return val;
})
.join(";");
}
return query;
}
function alphanumeric_sort(object, key) {
// sort keys alphabetically
object = object.sort((a, b) => a[key].localeCompare(b[key]));
// sort keys case sensitive
object = [
object.filter((item) => item[key].toLowerCase() === item[key]),
object.filter((item) => item[key].toUpperCase() === item[key])
].flat();
return object;
}
<p><strong>Variable font URL: </strong><a id="a_var" href=""></a></p>
In the above example I'm using a static copy of the output JSON. Some sort of caching is probably a good idea to prevent too many API requests.
However you should update this static copy once in a while to include recently added fonts.
Since fontsource is still quite new (state 2023), you should consider contributing pull requests or suggesting new features in the discussion section.