I was also in your place, and I agree that it is difficult. In my case, I created custom sites for a single product for customers. While each site followed a similar layout and workflow, each of them should have had enough flexibility to have a fully customizable design, customizable rules for delivery and coupons, as well as various trading gateways and configurations.
A few years later, we ended up with something supported. First, we created libraries to host all of our common code, and placed these libraries in a TFS project, simply called Common. Then we created a new TFS project for each site (and not the client, since many clients had several products / sites) and forked the applicable projects in them from Common. Then we created the VS Template project, which contained the site’s skeleton, including the “constructive” views, controllers and their methods of action (remember that each site had the same basic flow). In addition, each site operated on its own database, which was cloned from an unused and mostly empty template database.
With each site running on its own branch and database, changes can be made to the source stream and design that was installed by the template (which should never be merged back) without affecting any other site. To customize business methods, such as delivery calculations, we could subclass the general class and override where necessary. Part of what allowed was the conversion of all our code to use Injection Dependency. In particular, each Controller introduced Services, and each Service introduced Repositories. Merchants processing was also encoded for the interface and introduced. It is also worth mentioning that this allowed us to hard code all the upsell logic for each site (you bought product X, therefore we recommend Y), which was much easier to create and maintain compared to defining complex configuration rules in our old upsell rule engine. I don’t know if you have something like that ...
Sometimes we would like to make changes to the Common Code itself, which was usually caused by a specific need for a particular site. In this case, we will make changes to this branch, combine them with Common, and then combine them with other sites at our convenience (great for “hacking” changes or changes that also required changes to the database). Similarly, for changes to the database, we need to update the template database and then write a little script to update other site databases with the same schema changes (they should be smart and careful anyway).
An additional advantage was that we also created Mock repositories that will be used / implemented in the configuration of the Design assembly, which allows designers to navigate through the application and work on screens without transferring themselves to the workflow. It also allowed them to start work on the site before something was done in the background, which was very important for those anxious customers who needed to "see something."
10+ customers are certainly not a small amount of what you are talking about. There was enough pain for me. At that time, we had more than 30 sites that were supported by three developers and two designers.
Finally, I know that this is beyond the scope of your question and a little arrogant, but, having received a “final” client’s rejection of the design before the designers actually started to implement it (and before the developers did their job), also saved us a lot of expensive improvements. I know that design is not final, but increasing efficiency at the end of the implementation gave customers less time to change their mind about the design they approved.
I hope that at least you will find several approaches to thinking.