google-fonts

How does the Google Fonts CSS v2 API work exactly, in the case of Crimson+Pro:ital,wght@0,700;1,700?


I am trying to figure out how to take this Google Font API JSON metadata, which lists all the fonts like this:

{
  "family": "ABeeZee",
  "id": "abeezee",
  "subsets": ["latin", "latin-ext"],
  "weights": [400],
  "styles": ["italic", "normal"],
  "unicodeRange": {
    "latin-ext": "U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF",
    "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"
  },
  "variants": {
    "400": {
      "italic": {
        "latin-ext": {
          "url": {
            "woff2": "https://fonts.gstatic.com/s/abeezee/v22/esDT31xSG-6AGleN2tCUnJ8DKpE.woff2",
            "woff": "https://fonts.gstatic.com/s/abeezee/v22/esDT31xSG-6AGleN2tCUnJ8FOJKuGPLB.woff",
            "truetype": "https://fonts.gstatic.com/s/abeezee/v22/esDT31xSG-6AGleN2tCklQ.ttf"
          }
        },
        "latin": {
          "url": {
            "woff2": "https://fonts.gstatic.com/s/abeezee/v22/esDT31xSG-6AGleN2tCUkp8D.woff2",
            "woff": "https://fonts.gstatic.com/s/abeezee/v22/esDT31xSG-6AGleN2tCUkp8FOJKuGA.woff",
            "truetype": "https://fonts.gstatic.com/s/abeezee/v22/esDT31xSG-6AGleN2tCklQ.ttf"
          }
        }
      },
      "normal": {
        "latin-ext": {
          "url": {
            "woff2": "https://fonts.gstatic.com/s/abeezee/v22/esDR31xSG-6AGleN2tukkIcH.woff2",
            "woff": "https://fonts.gstatic.com/s/abeezee/v22/esDR31xSG-6AGleN2tuklpUEGpCeGQ.woff",
            "truetype": "https://fonts.gstatic.com/s/abeezee/v22/esDR31xSG-6AGleN6tI.ttf"
          }
        },
        "latin": {
          "url": {
            "woff2": "https://fonts.gstatic.com/s/abeezee/v22/esDR31xSG-6AGleN2tWkkA.woff2",
            "woff": "https://fonts.gstatic.com/s/abeezee/v22/esDR31xSG-6AGleN2tWklpUEGpA.woff",
            "truetype": "https://fonts.gstatic.com/s/abeezee/v22/esDR31xSG-6AGleN6tI.ttf"
          }
        }
      }
    }
  },
  "defSubset": "latin",
  "lastModified": "2022-09-22",
  "version": "v22",
  "category": "sans-serif"
}

And convert it into a URL request using the CSS2 API, like this (e.g. for Crimson Pro Bold & Bold Italic):

https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,700;1,700

What I don't get is the exact meaning of the 0 and 1 in that URL, which is not present in the JSON metadata I linked to. How do I figure out what 0 or 1 value I need from the JSON?

This link https://fonts.google.com/knowledge/glossary/italic_axis says the spec defines the "ital" italic axis as defaulting to 0 (the minimum), maxing out at 1, with a step of 0.1. What does that even mean? Is 0 no italics and 1 is full italics?

If so, what does ital,wght@0,700;1,700 mean?

How do I figure out when I should add a 0 or 1 to the URL I'm going to generate from this JSON metadata?

It seems to get more complicated too, as per this example:

Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1

So basically, I would like to know how to fetch any font properly using the CSS2 API, given the JSON metadata above. I only care about getting the 4 main fonts: regular, bold, italic, and bold italic, when they are available FYI.


Solution

  • ital(ic) axis

    In fact, the ital query parameter can be considered a boolean value.

    You can't interpolate between upright designs and true italics – so the documentation claiming there are intermediate steps of 0.1 is wrong – it is not a proper design axis like wght, wdth or slnt. Variable fonts also allow more fine grained controls (if meticulously defined by the font designer) to get a intermediate design: so it's not just simple linear interpolation (... but most of the time it is based on control point interpolation).

    "static" (one style/weight per file) queries

    ital,wght@0,700;1,700
    

    translates to load font in:

    Variable font queries

    Your second example queries a variable font – note the .. separators to define a axis range

    Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1
    

    You can interpolate in a "slant" axis which has a similar effect (also commonly referred to as "oblique" – the main geometry is retained of a glyph). True italics however are separate font files - that's why you need separate tuples.

    You may have a look at this example generating google font API queries for variable (if available) and static fonts:

    const baseUrl = `https://fonts.googleapis.com/css2?family=`;
    let fontAPiJson =
      "https://cdn.jsdelivr.net/gh/herrstrietzel/fonthelpers@main/json/gfontsMeta.json";
    // default selection
    let fontFamily = "Open Sans";
    
    (async() => {
      let fetched = await fetch(fontAPiJson);
      // convert tp parsable JSON
      let metaTxt = await (await fetched).text();
      let fontItems = JSON.parse("[" + metaTxt + "]")[0].familyMetadataList;
      //console.log(fontItems)
    
      /**
       * populate font list
       */
      populateFontList(fontItems);
    
      function populateFontList(fontItems) {
        let options = "";
        fontItems.forEach((font) => {
          options += `<option>${font.family}</option>`;
        });
        fontsDataOptions.innerHTML = options;
      }
    
      /**
       * update fonts
       */
      inputFont.addEventListener("input", (e) => {
        fontFamily = e.currentTarget.value;
        getFontItemUrls(fontItems, fontFamily);
      });
    
      /**
       * init
       */
      inputFont.value = fontFamily;
      inputFont.dispatchEvent(new Event("input"));
    
      //console.log(fontItems  )
      function getFontItemUrls(fontItems, fontFamily) {
        let fontItem = fontItems.filter((item) => item.family === fontFamily)[0];
        let googleQueryParams = getGoogleFontQueryMeta(fontItem);
        let urlStatic = googleQueryParams.static ?
          baseUrl + googleQueryParams.static :
          "";
        let urlVariable = googleQueryParams.variable ?
          baseUrl + googleQueryParams.variable :
          "";
        a_meta.href = urlVariable;
        a_meta.textContent = urlVariable;
        a_meta_static.href = urlStatic;
        a_meta_static.textContent = urlStatic;
      }
    })();
    
    /**
     * compatible with
     * https://fonts.google.com/metadata/fonts
     */
    
    function getGoogleFontQueryMeta(fontItem, variable = true) {
      //console.log(item);
      let fontFamily = fontItem.family;
      let fontfamilyQuery = fontFamily.replaceAll(" ", "+");
      // prepended tuples
      let queryPre = [];
      let queryParams = {
        variable: "",
        static: ""
      };
      // count weights
      let styles = Object.keys(fontItem.fonts);
      let weightsItalic = [];
      let weightsRegular = [];
      styles.forEach((style) => {
        if (style.includes("i")) {
          weightsItalic.push(parseFloat(style));
        } else {
          weightsRegular.push(parseFloat(style));
        }
      });
      // is variable
      let axes = fontItem.axes;
      let isVF = axes && axes.length ? true : false;
      if (isVF) {
        //console.log(axes, ranges)
        // sort axes alphabetically - case sensitive ([a-z],[A-Z])
        axes = [
          axes.filter((item) => item.tag.toLowerCase() === item.tag),
          axes.filter((item) => item.tag.toUpperCase() === item.tag)
        ].flat();
        let ranges = axes.map((val) => {
          return val.min + ".." + val.max;
        });
        //  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);
        });
        queryParams.variable =
          fontfamilyQuery +
          ":" +
          queryPre.join(",") +
          "@" +
          rangeArr.join(";") +
          "&display=swap";
      }
      /**
       * get static
       */
      queryPre = [];
      if (weightsItalic.length) {
        queryPre.push("ital");
        queryPre.push("wght");
      } else if (!weightsItalic.length && weightsRegular.length) {
        queryPre.push("wght");
      }
      let query = 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(";");
      }
      // generate URL query
      queryParams.static = fontfamilyQuery + ":" + query + "&display=swap";
      return queryParams;
    }
    body {
      font-family: sans-serif
    }
    
    legend {
      font-weight: bold;
    }
    
    input {
      width: 100%
    }
    
    a {
      word-break: break-all
    }
    <fieldset>
      <legend>Get font CSS </legend>
      <input id="inputFont" type="text" list="fontsDataOptions" placeholder="Search for google font">
      <datalist id="fontsDataOptions"></datalist>
    
    
    </fieldset>
    <p><strong>Variable font URL: </strong>
      <a id="a_meta" href=""></a>
    </p>
    <p><strong>Static font URL:</strong>
      <a id="a_meta_static" href=""></a>
    </p>

    In the above example I'm using a static copy of the meta json data - so it's not up-to-date.

    Parameter sorting

    Worth noting axis identifiers must be ordered alpha-numerically so [a-z][A-Z] [0-9]

    https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,400;0,700;1,400;1,700
    

    works!

    https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,700;0,400;1,400;1,700
    

    doesn't work!

    So the API query structure is quite unforgiving.

    Frankly, I prefer the quite similar approach using the developers API. as it also gives you the ability to get truetype fonts or ignore variable fonts.