11tyCMS: Major refactoring, state management overhaul, routing, site history selection and site dashboard implementation
posted on Nov 3, 2025
It's been a busy week for 11tyCMS. SO much has changed under the hood. My goal has been to refactor the code base to make things as easy as possible to work with. At the moment (until we go FOSS!), my illnesses and disabilities have gotten worse. So making this as easy to work with is vital: It will allow me to make updates with lower energy and it'll make it easier to onboard new contributors.
Here's a roundup of everything that's happened this week:
π¨ Major refactoring
As mentioned above... Refactoring has been a major priority of mine. Reducing the technical debt as much as possible, while also making the code self documenting and as easy to follow as possible. Until now? The Electron specific code has been extremely difficult to follow: registering the IPC handles with consistent channel names, and then exposing them to the contextBridge... Here, I'd frequently forget what channel name I used, not to mention the function arguments. It was a huge source of development inertia.
IPC improvements
To resolve this problem, I changed how the functions in IPC work. Previously, you'd have to add your function in the main folder's index.js file, where you'd call the IPC main's handle function, giving the function a channel name... Then, you'd have to expose that function to Electron's "browser". This involved switching to the preload folder's index.js and exposing the functions you just made. All requiring you to remember the channel name, and all the arguments.
Now? We have a function file at the root of the src folder. Here, you simply import the file that exports the object containing all your functions and put it in the functionsByChannels object. Whatever you named the import becomes the beginning of the channel name, the end of it is made up by whatever the function names are. For example, if you have a file called site.js like so:
export default {
    openFile: (filePath) => {
        ...
    },
    saveFile: (path, metadata, contents) => {
        ...
    },
}
 You'll import it in the functions.js file, and put it in the functions.js file like so:
let functionsByChannels = {
    site
}
The code will then register the functions openFile and saveFile under the channels site:openFile and site:saveFile respectively. You don't have to think about channel names, or forget which one you used... Its all defined by the function names and whatever you decided to name the import.
Is there a function you don't want exposed to the Electron's "browser"? Fine! Just suffix your function's name in your export object with an underscore. This will mean only the "backend" part of electron can access it.
This has made development so much easier, faster and less tiring.
State overhaul
After those improvements on the "backend" side of things... I then shifted to refactoring the frontend. A huge part of this was changing how state works. I was very unhappy with all these random bits of state scattered across the app in multiple nested components. The path they took was unclear, monitoring changes was even harder and working with it was just... Horrible.
Having never used any state management libraries for React, I was anxious to use something as simple but as powerful as possible. I wanted something that would empower the development of 11tyCMS. Enter: Zustand. This has allowed me to separate a lot of the state that isnt contained to one component into its own file. To use this state? You just use it as a hook, along with any actions to manipulate that data.
This has made using site data across new and existing components SO much easier. It's also made debugging a boon. If there's a data problem? I can isolate myself to the store file and figure out which function is the culprit.
If there's a new component that needs access to site info? All I need to do now is:
const selectedSiteInfo = useSiteStore(({selectedSiteInfo})=>selectedSiteInfo);
const updateSelectedSiteInfo = useSiteStore(({actions})=>actions.updateSelectedSiteInfo);
It's also meant that all the complicated operations I have to perform for these things are isolated away from the UI side of things. For example, for the updateSelectedSiteInfo function:
updateSelectedSiteInfo: async (data) => {
    await window.api.setSiteInfo(data);
    set({ selectedSiteInfo: { ...useSiteStore.getState()['selectedSiteInfo'], ...data } })
    updateSiteInSiteHistory(useSiteStore.getState().cwd, data)
}
This is used in 2 places in the source code. Before? I would have had to have written that twice. Now? Its written here once and used consistently everywhere I use the hook. Amazing stuff.
Routing
This has involved overhauling all components across 11tyCMS. Before, separation of concerns wasnt really a thing... That is to say: a lot of the React components that make up 11tyCMS were doing FAR too much. They were bulky and performing multiple functions. Ideally, you should separate your components out into small pieces that do their specific task concisely and well. We're not finishing with this just yet, but I can gladly say: a majority of components across 11tyCMS have been refactored and things are so much easier to work with now.
Refactoring the components then allowed me to implement a routing system in 11tyCMS. This is something that I've been wanting for a while, because until now? Whatever view was on the screen was controlled by state at the top level component. This was clunky, horrible to work with and very inflexible. It also meant I had to use state in places where I could have used URL params. But now? No longer. react-router-dom has been implemented and everything feels and works so much better for it.
Better yet, it also makes the web version of 11tyCMS so much easier to make if this infrastructure is already in place. It's also pathed the way for the next feature...
π°οΈ Site history

As you can see? Whenever you open up 11tyCMS now, you'll be presented with this lovely screen. When you open a site in 11tyCMS, it will remember its directory and metadata, listing every site you've opened. This saves you from having to manually navigate to your site's directory every time you want to post something. Major UX improvement!
There's still more to do, though. I want the ability to change the order of sites, and the ability to clear a site off the menu if it was just a "one off". I also need to add a check for any site you select from the list to ensure it still exists at that directory. But for now? It's a much welcomed improvement to the UX.
π Site dashboard
This is unfortunately the most ugly thing in this post... But it does work! To access it? You now click the site's pill in the top left corner, and you're lead to the dashboard:

So, a lot of 11ty sites have a metadata.js file in the _data folder. In here, the site's title, description and author is detailed. I wanted the ability to change these within 11tyCMS. It's a core functionality I wanted for the site dashboard view. It works great! However, it still needs a lot of work in the design and functionality department. I'm aware that there's some thing in a metadata.js or site.js file that people may not want showing in the dashboard. To remedy this, I'm going to make it so the only things that display by default are the title, url, language, description and author fields. Any other fields, you'll have to enable in the site's _11tycms.json file.
The other issue is, right now it expects a .json file. This needs to work with .js metadata files too. I have a solution for that, but that wont be until the next update.
π Build and publish improvements
Beforehand, building and publishing was hard coded. That is to say, every 11ty site had to use the same build and publish command. Now? I've made 11tyCMS create a 11tycms.json file in your site's root. In here, you control any site specific 11tyCMS settings. 2 of those settings are the commands used for building and publishing. Eventually, these will be configurable in the dashboard. But for now? You can change the commands for these by editing the 11tycms.json file. And it works beautifully! Finally, I can publish my personal blog on neocities and 11tyCMS's via git. In fact, this very post was published for the first time, entirely within 11tyCMS. No need for a terminal!
Conclusion
This has been a HUGE 2 weeks for 11tyCMS. Quite honestly? We're getting very VERY close to a public release of the source code and the app itself. It will be in alpha form, but it's working well enough now that I want other's to use it. That way I can act on feedback and work with others to make this the best possible experience for writing on the indie web.
These changes will mark a new era for 11tyCMS. It will hopefully mean more frequent updates, as changing the code requires WAY less energy with all the refactoring I've done.
But before this gets into the hands of the public, the UI needs a polish first. I want consistent color schemes, potentially migrate the CSS over to tailwind and just brush up a few rough edges. But we're getting very very close. Watch this space! Looks like we could be progressing through the roadmap a lot faster than planned (I'll do another post soon about 11tyCMS's roadmap!).
Thanks so much for reading, and look forward to updating y'all again on future progress π