How to set Frontity "homepage" independent of WordPress home page

Basically, I want to have /page/ be my / route home page for a site without also setting it at all in the WordPress back end.

The reason being, I want to use a multisite Frontity project to create multiple Frontity themes on different subdomains, each taking content from a custom post type, e.g.:

const settings = [
  {
    "name": "microsite-1",
    "match": ['https://microsite-1.domain.com'],
    "state": {
      "frontity": {
        "url": 'https://microsite-1.domain.com',
        ...
      }
    },
    "packages": [
      ...,
      {
        "name": "@frontity/wp-source",
        "state": {
          "source": {
            api: "http://wpadmin.domain.com/wp-json",
            homepage: "/microsite/microsite-1/",
            postTypes: [
              {
                type: "microsite",
                endpoint: "microsite",
                archive: '/microsites'
              }
            ]
          }
        }
      },
      ...
    ]
  }, {
    "name": "microsite-2",
    "match": ['https://microsite-2.domain.com'],
    "state": {
      "frontity": {
        "url": 'https://microsite-2.domain.com',
        ...
      }
    },
    "packages": [
      ...,
      {
        "name": "@frontity/wp-source",
        "state": {
          "source": {
            api: "http://wpadmin.domain.com/wp-json",
            homepage: "/microsite/microsite-2/",
            postTypes: [
              {
                type: "microsite",
                endpoint: "microsite",
                archive: '/microsites'
              }
            ]
          }
        }
      },
      ...
    ]
  } // etc.
]

export default settings;

The problem is, when I set it like that, I get the following error:

ServerError: You have tried to access content at route: / but it does not exist
    at Object.eval (webpack-internal:///./node_modules/@frontity/wp-source/src/libraries/handlers/postType.ts:38:146)
    at processTicksAndRejections (internal/process/task_queues.js:97:5)
    at eval (webpack-internal:///./node_modules/@frontity/wp-source/src/actions.ts:25:1) {
  status: 404,
  statusText: 'You have tried to access content at route: / but it does not exist'
}

  TypeError: Cannot read property 'undefined' of undefined
      at Root (webpack-internal:///./packages/microsites-theme/src/components/index.js:7:105)
      at runAsReaction (webpack-internal:///./node_modules/@frontity/connect/src/reactionRunner.js:16:45)
      at reaction (webpack-internal:///./node_modules/@frontity/connect/src/observer.js:7:131)
      at eval (webpack-internal:///./node_modules/@frontity/connect/src/connect.js:21:16)
      at processChild (webpack-internal:///./node_modules/react-dom/cjs/react-dom-server.node.development.js:397:2319)
      at resolve (webpack-internal:///./node_modules/react-dom/cjs/react-dom-server.node.development.js:396:122)
      at ReactDOMServerRenderer.render (webpack-internal:///./node_modules/react-dom/cjs/react-dom-server.node.development.js:433:1199)
      at ReactDOMServerRenderer.read (webpack-internal:///./node_modules/react-dom/cjs/react-dom-server.node.development.js:433:55)
      at renderToString (webpack-internal:///./node_modules/react-dom/cjs/react-dom-server.node.development.js:470:116)
      at eval (webpack-internal:///./node_modules/@frontity/core/src/server/index.tsx:68:243)

When I set the WordPress backend “home page” to any page and go to the “/” route, I get that page, and if I navigate to the microsite’s permalink e.g., “/microsites/microsite-1/”, I get that custom post type.

What I want is to be able to set the homepage property for each site in frontity.settings.js to a custom post type and when I navigate to https://localhost:3000/?frontity_name=microsite-1, I’ll get the custom post returned.

Should I just set homepage to / and then set the custom post’s ID in the state to pass it to the component? What would be the most logical way to do this? Thanks!

OK so I figured it out today. First of all, props to @luisherranz for his answer here that led me to my solution.

To reiterate, the ultimate goal was to be able to separate multiple sites by subdomains and be able to have a main or “hub” site that would act just like a regular Frontity website. The way I approached it was to start in frontity.settings.js and for each separate site, also add a state.theme.microsite property that could be either a slug matching that of a microsite (custom post type) in the WordPress back end or be false:

const settings = [
  {
    name: 'microsite-1',
    match: ['https://microsite-1.domain.com'],
    state: {
      frontity: {
        url: 'https://microsite-1.domain.com',
        ...
      }
    },
    packages: [
      {
        name: 'my-theme',
        state: {
          theme: {
            microsite: 'microsite-1'
          }
        }
      }, {
        name: '@frontity/wp-source',
        state: {
          source: {
            api: 'http://www.wpadmin.domain.com/wp-json',
            postTypes: [
              {
                type: 'microsite',
                endpoint: 'microsite',
                archive: '/microsites'
              }
            ]
          }
        }
      },
      ...
    ]
  }, {
    name: 'hub',
    match: ['https://domain.com'],
    state: {
      frontity: {
        url: 'https://domain.com',
        ...
      }
    },
    packages: [
      {
        name: 'my-theme',
        state: {
          theme: {
            microsite: false
          }
        }
      }, {
        name: '@frontity/wp-source',
        state: {
          source: {
            api: 'http://www.wpadmin.domain.com/wp-json',
            postTypes: [
              {
                type: 'microsite',
                endpoint: 'microsite',
                archive: '/microsites'
              }
            ]
          }
        }
      },
      ...
    ]
  }, {
    name: 'microsite-2',
    match: ['https://microsite-2.domain.com'],
    state: {
      frontity: {
        url: 'https://microsite-2.domain.com',
        ...
      }
    },
    packages: [
      {
        name: 'my-theme',
        state: {
          theme: {
            microsite: 'microsite-2'
          }
        }
      }, {
        name: '@frontity/wp-source',
        state: {
          source: {
            api: 'http://www.wpadmin.domain.com/wp-json',
            postTypes: [
              {
                type: 'microsite',
                endpoint: 'microsite',
                archive: '/microsites'
              }
            ]
          }
        }
      },
      ...
    ]
  }
]

export default settings

What’s also important to note is that I’ve declared the postTypes for my microsite custom post type on each so we have access to them in each site (I’m not sure how necessary it will be for the “hub” site but we’ll see).

Then, I’ve conditionally fetched the microsite in beforeSSR in my theme’s index.js so we have access to it if we are on a microsite:

export default {
  name: 'my-theme',
  ...
  actions: {
    theme: {
      beforeSSR: async ({ libraries, actions, state }) => {
        if(!!state.theme.microsite) {
          await actions.source.fetch(`/microsite/${state.theme.microsite}`)
        }
    }
  },
  ...
}

In the Root, there is a switch to detect if it is a microsite or not and render the correct component:

import React from 'react'
import {connect} from 'frontity'
import Switch from '@frontity/components/switch'

import Microsite from './microsite'

const Root = ({state}) => {
  return (
    <Switch>
      <Microsite when={!!state.theme.microsite} />
      <div>Hub site</div>
    </Switch>
  )
}

export default connect(Root)

Then in the <Microsite /> component, we can actually just use the microsite’s slug to grab the data we need and render if the microsite’s slug matches a component we’ve made for that microsite:

import React from 'react'
import {connect} from 'frontity'
import Switch from '@frontity/components/switch'

const Microsite = ({state}) => {
  // Go ahead and grab the data and "post" or "microsite" values (content, acf fields, etc.),
  // which could also be done in the microsite components because all of this is in the state
  const micrositesData = state.source.get(`/microsite/${state.theme.microsite}`)
  const microsite = state.source[micrositesData.type][micrositesData.id]

  return (
    <Switch>
      <MicrositeOne when={state.theme.microsite === 'microsite-1'} microsite={microsite}/>
      <div>Error!</div>
    </Switch>
  )
}

export default connect(Microsite)

Keep in mind that for developing, you won’t have access to the subdomains and you have to fake it using localhost:3000?frontity_name=microsite-1 but so far it looks like this works for detecting subdomains etc.

Also, I found that this catch errors if you have a microsite defined, like microsite-2, but no microsite custom post with that slug in the back end. That will also throw a console error when the actions.source.fetch is called in beforeSSR. Is there any way to ignore that error or handle it some way?

What I would say is great about this is that I believe all of the SSR should work as well as caching/pre-fetching that Frontity gives you already but sort of between subdomains (not sure if it will actually work like that but we’ll see). At the very least, multiple microsites and a hub site can be kept together so they can share components and theme definitions.

2 Likes

Wow, awesome work @david1 :slightly_smiling_face:

I didn’t understand fully what you mean by your last paragraph. Could you please set up a codesanbox with this configuration so we can take a deeper look and understand how all the pieces work together?

You can start from this one: https://githubbox.com/frontity/frontity-codesandbox

Hey @luisherranz, thanks for the complement! And for bearing with me in getting back to you since I was slammed on Friday and was busy over the weekend.

Regarding that last paragraph, I meant to assume (and I’m not sure if it is the case or not) that the <Link> element could work between microsite-1.domain.com and domain.com so that the browser should stay within the same project and render the site via Frontity/React rather than query the server for the whole project all over again. Does that make sense? Sort of like if I were navigating between domain.com/page-1 and domain.com/page-2, I could stay within the SPA and Frontity would render the navigation instead of the browser requesting the site from the server in the case that I navigate between a multi-site Frontity project between microsite-1.domain.com and domain.com. Is that even possible through Vercel (or any other technology)?

As far as deployment goes, I was hoping that my configuration would work if you assign the subdomains and the domain to the same Vercel project that Frontity would be able to detect the domain you are trying to access and switch automatically. For example, I have now made the sites live (works in progress) at https://annihilyzer.microbeninja.com and https://microbeninja.com, and assigned those sites using the match property in frontity.settings.js, but when you go to either of those, you’ll only get the first match (the Annihilyzer site). If you go to https://microbeninja.com?frontity_name=hub, you’ll get the second site, but I’d love it if I could make sure the match property gets executed in Vercel and shows the correct website. Is there a way to do this?

One idea I had, and maybe this is how it is designed to work, is to actually deploy the same project twice. Once to Vercel using https://annihilyzer.microbeninja.com as the domain and again but assigning https://microbeninja.com to the same code. Would that resolve the issue and force the sites to be correctly matched? I might test it on my own but if you happen to get back to me before then, maybe you can let me know if it should work how I have it set up since it’ll take me a little effort to redeploy.

In case you’re wondering what the usefulness of this multisite functionality is in my case (and hopefully many others) is to use a single WordPress back end and be able to spin up landing pages very quickly using custom post types etc. That’s what we are using it for currently and I was impressed Frontity was able to get me to the point where I could develop the sites in parallel in the same project but it feels like I’m either missing some configuration or that it doesn’t fully work.

1 Like

If you switch from site to site, the whole code needs to be loaded again. Sites are independent of each other.

So maybe different sites are not what you are looking for, but a single site that can be accessed by different subdomains.

I am sure that can be fixed. We use match all the time in Vercel.

But again, for your description, I think that maybe having multiple sites is not what you are looking for.

You don’t need to redeploy the code twice. You can just deploy once and add two alias. BTW, I think alias are now called simply “domains”:


So I guess what you may be looking for is a single deployment with multiple domains poiting to it.

Inside Frontity we are not storing the hostname, but I think you can get it using the init action in the server. Maybe something like this (not tested):

const myPackage = {
  actions: {
    myPackage: {
      beforeSSR: ({ state }) => ({ ctx }) => {
        state.frontity.hostname = ctx.hostname;
      },
    },
  },
};

ctx is the Koa context of the server: https://koajs.com/

Then, use state.frontity.hostname wherever you need to check for the domain:

const Home = ({ state }) => {
  if (state.frontity.hostname === "subdomain.domain.com")
    return <SubdomainHome />;
  return <MainHome />;
};

@luisherranz it looks like you are right that multisite is not what I’m looking for. I can get the hostname nicely from Koa via the ctx variable; however, I already have a bunch of fetching and setting going on in my beforeSSR function that is asynchronous and ctx is undefined if I try to re-finagle the function to pass ctx there. I guess this is something I should try to figure out on my own but how do I add the code you provided to my current beforeSSR asynchronous function?

const myPackage = {
  actions: {
    myPackage: {
      beforeSSR: async ({libraries, actions, state}) => { // I tried adding ({ctx}) here but it didn't work
        await actions.source.fetch('acf-options-page')

        console.log(ctx) // This returns "undefined"

        libraries.html2react.processors.push(image)

        libraries.source.handlers.push({
          name: 'nameAndDescription',
          priority: 10,
          pattern: 'nameAndDescription',
          func: async ({ route, state, libraries }) => {
            const response = await libraries.source.api.get({
              endpoint: '/'
            })
      
            const { name, description } = await response.json();
      
            state.source.data[route].name = name;
            state.source.data[route].description = description;
          }
        })

        await actions.source.fetch('nameAndDescription')
      },
      beforeCSR: async({libraries}) => {
        libraries.html2react.processors.push(image)
      }
    }
  }
}

By the way, if I move that libraries.html2react.processors.push(image) and libraries.source.handlers.push(...) code into my libraries property, outside of the beforeSSR property function, will they still be called beforeSSR or do they have to be pushed to the arrays in beforeSSR (and for the image processor, in beforeCSR)? For example, compare the above to the below, are they essentially the same?

const nameAndDescriptionHandler = {
  name: 'nameAndDescription',
  priority: 10,
  pattern: 'nameAndDescription',
  func: async ({ route, state, libraries }) => {
    const response = await libraries.source.api.get({
      endpoint: '/'
    })

    const { name, description } = await response.json();

    state.source.data[route].name = name;
    state.source.data[route].description = description;
  }
}

const myPackage = {
  actions: {
    myPackage: {
      beforeSSR: async ({libraries, actions, state}) => {
        await actions.source.fetch('nameAndDescription')
        await actions.source.fetch('acf-options-page')
        
        if(!!state.theme.microsite) {
          await actions.source.fetch(`/microsite/${state.theme.microsite}`)
        }
      },
      beforeCSR: async({libraries}) => {
        libraries.html2react.processors.push(image)
      }
    }
  },
  libraries: {
    html2react: {
      processors: [image]
    },
    source: {
      handlers: [nameAndDescriptionHandler]
    }
  }
}

I’m just not sure if the libraries.source.handlers.push() and libraries.html2react.processors.push() is important in beforeSSR and beforeCSR or if they are redundant. I just don’t understand how it’s queued…

The async function needs to be the “last one”.

So either this:

const beforeSSR = async ({ state }) => {
  // Do stuff...
};

Or this if you need access to ctx:

const beforeSSR = ({ state }) => async ({ ctx }) {
  // Do stuff...
  }

If you don’t need to apply any logic to your handlers or processors, it is best to add them to the libraries in your package definition:

libraries: {
    html2react: {
      processors: [image]
    },
    source: {
      handlers: [nameAndDescriptionHandler]
    }
  }

But if you need some logic, use the init action instead of beforeSSR/beforeCSR. It runs on both the server and the client:

const myPackage = {
  actions: {
    myPackage: {
      init: ({ libraries }) => {
        libraries.html2react.processors.push(image);
        // ...
      },
    },
  },
};

The init action doesn’t have access to ctx right now, but if you need that let us know and we will add it.

@luisherranz thanks again for putting the time into helping me figure all this out. I’m annoyed I didn’t trust the error code and that I didn’t try ({actions, state}) => async ({ctx}) => { but I believe that that’s what happened. That suggestion resulted in a much more streamlined frontity.settings.js file (just the single site, no more multisite) and a pretty clean index.js at the root of my components directory (thanks to you again for clarifying pushing to libraries etc.). Here’s how my theme is exported now:

export default {
  name: 'my-theme',
  actions: {
    theme: {
      beforeSSR: ({actions, state}) => async ({ctx}) => {
        await actions.source.fetch('nameAndDescription')
        await actions.source.fetch('acf-options-page')

        state.frontity.hostname = ctx.hostname

        // state.frontity.developSite can be added as an OR rule
        // to <Switch /> components or conditionals just to
        // quickly set the development site URL to localhost.
        state.frontity.developSite = ctx.hostname === 'localhost'

        // This now has to be hard-coded but that's fine
        await actions.source.fetch('/microsite/annihilyzer')
      }
    }
  }, ...
}

So what was nice about my previous version was detecting the multisite from the frontity.settings.js's state.theme property, which is now not so possible with the ctx.hostname so I am currently just manually fetching the custom post type posts, but that’s OK because it probably doesn’t need to be dynamically linked. I just like to do that whenever possible.

I also added a state.frontity.developSite variable, which can just be tagged onto a <Switch /> component to enable development on a specific site. It basically just returns true if the site is localhost. So, let’s say I’m working on a site, I can just quickly do the following:

<Switch>
        <Microsite when={state.frontity.hostname === 'annihilyzer.microbeninja.com' || state.frontity.developSite} />

When I want to develop a specific site.

If you check out https://microbeninja.com/ and https://annihilyzer.microbeninja.com/ now, it looks like everything is working and both “sites” were deployed with the same Vercel project. It also looks like it loads pretty quickly, though I’m not quite sure it’s yet as fast as it can be but I’ll work on it.

Thanks again!

Awesome, I am glad it’s working great :clap: :slightly_smiling_face:

1 Like