The Settings API

Haha, ok ok…

Another summary of what we have discussed so far:

Schema

(Not sure if this types are correct, but the TypeScript docs are down right know, so bear with me)

type MainSettings = {
  url?: string; // Default: undefined 
  title?: string; // Default: undefined
  timezone?: number; // Default: 0
  language?: string; // Default: "en"
}

type Package = {
  name: string;
  active?: boolean; // Default: true
  namespaces?: string | string[]; // Default: ["all", "available", "namespaces"],
  settings?: object;
}

type Settings = {
  name?: string; // Default: undefined
  match?: string | string[]; // Default: undefined
  mode?: string; // Default: "html"
  settings?: MainSettings;
  packages: (string | Package)[];
}

type ExportedSettings = Settings | Settings[];

Examples

One site

export default {
  settings: {
    url: "https://site.com",
    title: "Site Title",
    timezone: 1,
    language: "en",
  },
  packages: [
    {
      name: "@frontity/theme",
      namespaces: ["theme", "h2r"],
      settings: {
        theme: { ... },
        h2r: { ... },
        // Each key corresponds to one namespace.
      }
    }
  ]
}

One site with AMP

export default [
  {
    settings: { ... },
    packages: [
      {
        name: "@frontity/theme",
        namespaces: ["theme", "h2r", "comments"],
        settings: { ... },
      }
    ]
  },
  {
    mode: "amp",
    match: "\/amp\/?$",
    settings: { ... },
    packages: [
      {
        name: "@frontity/theme",
        namespaces: ["theme", "h2r"],
        settings: { ... }
      }
    ]
  }
]

More than one site

export default [
  {
    name: "site-one",
    settings: { ... },
    packages: [
      {
        name: "@frontity/theme",
        settings: { ... }
      }
    ]
  },
  {
    name: "site-one-amp",
    match: "\/amp\/?",
    mode: "amp",
    settings: { ... },
    packages: [
      {
        name: "@frontity/theme",
        settings: { ... }
      }
    ]
  },
  {
    name: "site-two",
    settings: { ... },
    packages: [
      {
        name: "@frontity/theme",
        settings: { ... }
      }
    ]
  },
  {
    name: "site-two-amp",
    match: "\/amp\/?",
    mode: "amp",
    settings: { ... },
    packages: [
      {
        name: "@frontity/theme",
        settings: { ... }
      }
    ]
  },
]

I realised that if we use the match setting for both the site and the mode, we would end with more than one match matching the current url, and we’d need to set some kind of priority. In order to avoid that, to select some site settings we’ll use the name field, and to select some mode settings the match field. We can also use the url field instead of the name field.

I don’t have a preference here, but if using one of them would reduce the schema surface, I’d go for that one.

Also, I’d rather build a repetitive schema that looks clearer at first glance, than something more complicated to avoid duplicated settings. As the settings file is a JS file, the developer can always extract duplicated settings and reuse them when necessary:

const mainSettings = {
  url: "https://site.com",
  title: "Site Title",
  timezone: 0,
  language: "en"
}

export default [
  {
    settings: mainSettings,
    packages: [ ... ]
  },
  {
    match: "\/amp\/?$",
    settings: mainSettings,
    packages: [ ... ]
  }
]

API

So far, unless something changes, the API remains as discussed before:

  • getSettings({ name, url })
  • getPackages({ name, url })

Ideas

The frontity.settings.js file should be in the root folder of the project, at the same level than package.json.

The default settings will be defined in a frontity.settings.js file inside the file-settings package.

The queries modifying the url or name fields shouldn’t affect the settings schema, they should be used in core, either to override the value in the store, or to be used with getSettings or getPackages to retrieve different settings.

The mode defined in settings is defined only to be used within React, not to decide what settings to use.


I think I covered pretty much everything we discussed. Let me know if I’m missing something.

1 Like

Yes, I think that’s the last problem we need to solve, at least for now.

In my first designs, it worked because everything was an array and the user can order the items. That means things like “amp” must go before “html” which may not be very intuitive:

This works:

export default = [
    {
      mode: "amp",
      match: "\/amp$",
      ...
    },
    {
      mode: "html",
      // match: ".*" (the default)
      ...
    }
];

This doesn’t, because the first match catches everything:

export default = [
    {
      mode: "html",
      // match: ".*" (the default)
      ...
    },
    {
      mode: "amp",
      match: "\/amp$",
      ...
    }
];

The getPackages() should return an object with all the packages for each site:

{
  "my-site": [ ... ],
  "my-blog": [ ... ],
  "my-blog-amp": [ ... ],
}

With that we can generate the client bundles and server bundle.

The first version of file-settings has been implemented. Here is the README info:

Usage

You can install it with npm :

npm i @frontity/file-settings

Here is a small example of how to use getSettings :

import { getSettings } from "@frontity/file-settings";

const settings = await getSettings({ name: "example-name", url: "https://example.site" });

API Reference

async getSettings(options) => settings

Used to retrieve the settings from the frontity.settings.js file.

Parameters

options : { name?: string; url: string; }

Used to match the right set of settings when there is more than one.

  • options.name : string (optional)
    The name of the set of settings you want to retrieve. When provided, getSettings won’t use options.url .
  • options.url : string
    The url of the site using Frontity. The matches field of each set of settings will be tested against this url to determine which set of settings should be used.

Return

settings : Settings
An object with type Settings containing a set of settings.

async getPackages() => packages

Used to retrieve a list of names of the packages used in each settings set.

Return

packages : { [key: string]: string[] }
An object with a key for each set of settings populated with an array of packages names from that set.

If the settings file exports only one set of settings (or mono settings ), packages will have only one key named default :

{
  default: [ "theme-package", "source-package" ] 
}

If the settings file exports various sets of settings (or multi settings ), packages will have one key per set of settings named like them.

{ 
  "settings-one": [ "theme-one", "source-one" ],
  "settings-two": [ "theme-two", "source-one" ]
}

Settings File

The file must be located in the root directory of the project, it must be named frontity.settings.ts or frontity.settings.js , and it needs to export a serializable object.

The settings exported can be mono settings (only one):

{
  name?: string;
  matches?: string[];
  mode?: string; // Default: "html"
  settings?: {
    url?: string;
    title?: string;
    timezone?: number; // Default: 0
    language?: string; // Default; "en"
  },
  packages: [
    string,
    {
      name: string;
      active?: boolean; // Default: true
      namespaces?: string[];
      settings?: object;
    }
  ]
}

Or multi settings :

// An array of more than one set of settings.
[
  {
    name: string; // This time the name is mandatory and must be unique.
    matches?: string[];
    mode?: string; // Default: "html"
    settings?: { ... },
    packages: [ ... ]
  },
  {
    name: string; // This time the name is mandatory and must be unique.
    matches?: string[];
    mode?: string; // Default: "html"
    settings?: { ... },
    packages: [ ... ]
  }
]

Typescript

Some TS types are exposed to be used in development. They can be accessed like this:

import { Settings } from “@frontity/file-settings”; const settings: Settings = { … };

The following are probably the only types you will need during development:

Settings<T = Package>

Types for the imported settings object from the settings file. You’ll want to use them on your frontity.settings.ts file.

Package

Types for each package within a settings object.

2 Likes

I’m going to reverse namespace and call it excludedNamespaces. Because it’s the only use it has and makes everything much simpler. The default which is an empty array makes sense this way.

{
  name: string;
  active?: boolean;
  excludedNamespaces?: string[]; // Default []
  settings?: object;
}

With that change I think I will also be able to treat settings just like any other namespace.

Pushing our design principles even further, I’m going to get rid of settings and live only state.

Once we finished the design of Connect, state and namespaces were not the same than in our previous framework.

  • Old framework: each package lives in its own namespace.
  • New framework: each package has the opportunity to use a shared state for whatever they want, namespaced or not.

Regarding settings, this is the change I want to make

  • From: frontity.settings.js can be used to overwrite a subset of the state named settings.
  • To: frontity.settings.js can be used to overwrite any part of the state.

These are the changes in code:

  1. Packages don’t use the setting namespace, just their own:
export default {
  state: {
    theme: {
       mainColor: "#FFF",
       linkColor: "#CCC",
       featuredImage: true,
    }
  }
}
  1. Users can overwrite any state using frontity.settings.js:
export default {
  packages: [
    {
       name: "my-theme",
       active: true,
       state: {
         theme: {
           linkColor: "#AAA",
           featuredImage: false,
         }
      }
  ]
}

That’s it. Everything gets merged together when the app starts.

The pros for removing settings are:

  • Settings are not different from state anymore, it’s just a small subset of it.
  • Settings don’t provide any additional feature the state itself doesn’t.
  • Settings is an extra concept (and we want to minimize concepts).
  • If devs are able to work with state directly Frontity becomes more hackable.
  • It’s easier to launch Frontity without settings and introduce them later on, rather than the opposite.

The cons for removing settings are:

  • Package providers won’t be able to select what part of the state is a setting and what not. Everything becomes a setting. And therefore, everything becomes hackable.

Interesting things arise. For example, users will be able to add a non-existent urls to their sites:

export default {
  packages: [
    {
       name: "@frontity/wp-source",
       state: {
         source: {
           dataMap: {
             "/about": {
                type: "page",
                id: 0,
                isPage: true,
             }
           },
           page: {
             0: {
                title: "About us",
                content: "Yes, this is us!"
             }
           }
         }
      }
  ]
}

As an extra, I’m going to remove the concept of excluded namespaces as well. I think the API to do so (if it is needed at all) needs to be programmatic instead of something backed in the frontity.settings.js file.

We will keep state.frontity for the general state, but now everything is explicitly namespaced:

export default {
  name: "site-1",
  mode: "html",
  state: {
    frontity: {
      url: "https://site.com",
      language: "en"
    }
  },
  packages: [...]
}

Actually, the concept of namespaces is abstract to Frontity, so users can overwrite any state they want in any place.

export default {
  name: "site-1",
  mode: "html",
  state: {
    frontity: {
      url: "https://site.com",
      language: "en"
    },
    source: {
      api: "https://wp.site.com", // <- this works but it's not recommended.
  },
  packages: [...]
}

I’ll try to finish the refactor today.

@development-team if you feel I’m missing something, say it now :slight_smile: