I considered all this options, but in my opinion, there are not a good answer to what we want to do.
Here is why:
For a "classical" website, I would use "page load rules" triggered at "page bottom" (so when we fire the code "_satellite.pageBottom()". The big advantage that I see is that I can create many page load rules with different conditions to trigger them. When a page is loaded, DTM will determine which of those rules are triggered or not according to the triggering conditions. If multiple rules are triggered, all info will be sent into one (and only one) hit.
As far as I understand the 3 solutions you propose ("Direct call rules", "Push state or hash change" or "Custom Event Based rule"), it doesn't allow to have different rules with different triggering conditions firing one single page load hit.
Solution that I try to put in place: fire a Page Load Rule using _satellite.pageBottom(); on everything considered as a page view.
1) as we are not running the header code, some prop variables and events can keep their value. We have to refresh it. So I actually fire a direct call rule in which I use s.clearVars() and then fire the _satellite.pageBottom();...
2) Beware of not trigger many _satellite.pageBottom(); because, by default, even if you don't put the footer code, it seems that a _satellite.pageBottom(); will be implicitely triggered! So it forces you to check if the page is a "fresh new DOM" or not.
NB: I tried to use the "return false" option that's mentionned at the end of the article, but it prevents to use ANY page load rule, so that's not the spirit here.
To conclude, I would simply add that I find it very sad to have to find so many workarounds to track SPA sites.
SPA are becoming more common, so Adobe and DTM should adapt instead of proposing (bad) workarounds like those presented in the article.