javascripthtmlwebfontsgoogle-fonts

How do I add all the font-faces available for a specific Google Font?(in JS)


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]}`]
}

Solution

  • The current function should actually work.
    But there are some issues:

    Static fonts

    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>

    Variable fonts

    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.

    Common pitfalls: sorting the tuples

    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.