Menu Search
Jump to the content X X
Smashing Conf New York

We use ad-blockers as well, you know. We gotta keep those servers running though. Did you know that we publish useful books and run friendly conferences — crafted for pros like yourself? E.g. upcoming SmashingConf Barcelona, dedicated to smart front-end techniques and design patterns.

Reimagining Single-Page Applications With Progressive Enhancement

What is the difference between a web page and a web application? Though we tend to identify documents with reading and applications with interaction, most web-based applications are of the blended variety: Users can consume information and perform tasks in the same place. Regardless, the way we approach building web applications usually dispenses with some of the simple virtues of the readable web.

Further Reading on SmashingMag: Link

Single-page applications tend to take the form of runtimes, JavaScript executables deployed like popup shops into vacant <body> elements. They’re temporary, makeshift and not cURL-able7: Their content is not really there without a script being executed. They’re also brittle and underperforming because, in service of architectural uniformity and convenience, they make all of their navigation, data handling and even the basic display of content the responsibility of one thing: client-side JavaScript.

Recently, there’s been a move towards “isomorphic” (or “universal8”) applications — applications that can run the same code on the client and the server, sending prerendered HTML from the server before delegating to client-side code. This approach (possible using Express as the server and React as the rendering engine9, for instance) is a huge step towards a more performant and robust web application architecture.

But isomorphism is surely not the only way to go about progressive enhancement for single-page applications. I’m looking into something more flexible and with less configuration, a new philosophy that capitalizes on standard browser behavior and that can blend indexable, static prose with JavaScript-embellished interactivity, rather than just “handing off” to JavaScript.

This little exposition amounts to no more than the notion of doing things The Web Way™ with a few loosely confederated concepts and techniques, but I think you could take it and make it something special.

Writing Views Link

In your typical single-page app, rendering views (i.e. individual screens) and routing between them is made the concern of JavaScript. That is, locations are defined, evaluated and brought into existence entirely by what was, until recent years, a technology considered supplementary to this kind of behavior. Call me a Luddite10, but I’m not going to use JavaScript for this at all. Heretically, I’m going to let HTML and the browser take care of it instead.

I’ll start by creating an HTML page and making the <main> part of that page my views container:

<main role="main">
    /* Views go here. */
</main>

Then, I’ll begin constructing individual views, placing each as a child element of <main>. Each view must bear an id. This will be used as part of our “routing solution.” It should also have a first-level heading: Views will be displayed one at a time, as the only perceivable content of the page, so this is preferable for screen-reader accessibility.

<div id="some-view">
    <h1>Some view</h1>
    <!-- the static view content, enhanceable with JavaScript -->
</div>

For brevity and to underline the importance of working directly in HTML, I’m hand-coding my views. You may prefer to compile your views from data using, say, Handlebars11 and a Node.js script, in which case each view within your {{#each}} block might look like the following. Notice that I’m using a Handlebars helper to dynamically create the id by slugifying12 the view’s title property.

<div id="{{slugify title}}">
    <h1>{{title}}</h1>
    {{{content}}}
</div>

Maybe using PHP to generate the content from a MySQL database is more your thing? It’s really not important how you compile your views so long as content is served precompiled to the client. Some content and functionality should be available in the absence of client-side scripting. Then, we can progressively enhance it, only in cases where we actually want to progressively enhance it. As I shall explain, my method will preserve static content within the app as just that: static content.

Not being interested in breaking with convention, I think my single-page app would benefit from a navigation block, allowing users to traverse between the views. Above the <main> view area, I might provide something like this:

<nav role="navigation">
    <ul>
        <li><a href="#the-default-view">the default view</a></li>
        <li><a href="#some-view">some view</a></li>
        <li><a href="#another-view">another view</a></li>
    </ul>
</nav>

My views are document fragments, identified by their ids, and can be navigated to using links13 bearing this identifier, or “hash.” So, when users click on the first link, pointing at #the-default-view, they’ll be transported to that view. If it is not currently visible in the viewport, the browser will scroll it into visibility. Simultaneously, the URL will update to reflect the new location. To determine where you are in the application, you need only query the URL:

http://my-app-thing.com#the-default-view

As you might imagine, leveraging standard browser behavior to traverse static content is really rather performant. It can be expected to work unencumbered by JavaScript and will even succeed where JavaScript errs. Although my “app” is more akin to a Wikipedia page than the kind of thing you’re familiar seeing built with AngularJS, the navigation part of my routing is now complete.

Note: Because conforming browsers send focus to page fragments14, keyboard accessibility is already taken care of here. I can enhance keyboard accessibility when JavaScript is, eventually, employed. More on that later.

One View At A Time Link

Being an accessibility consultant, a lot of my work revolves around reconciling state and behavior with the appearance of these things. At this point, the behavior of changing routes within our app is already supported, but the app does not look or feel like a single-page application because each view is ever present, rather than mutually exclusive. We should only ever show the view to which the user has navigated.

Is this the turning point at which I begin to progressively enhance with JavaScript? No, not yet. In this case, I’ll be harnessing CSS’ :target pseudo-class. Progressive enhancement does not just mean “adding JavaScript”: Our web page should work OK without JavaScript or CSS.

main > * {
    display: none;
}

main > *:target {
    display: block;
}

The :target pseudo-class relates to the element matching the fragment identifier in the URL. In other words, if the URL is http://my-app-thing.com#some-view, then only the element with the id of some-view will have display: block applied. To “load” that view (and hide the other views), all one has to do is click a link with the corresponding href. Believe it or not, I’m using links as links, not hijacking them and suppressing their default functionality, as most single-page apps (including client-rendered isomorphic apps) would do.

<a href="#some-view">some view</a>

This now feels more like a single-page application (which, in turn, is designed to feel like you’re navigating between separate web pages). Should I so desire, I could take this a step further by adding some animation.

main > * {
    display: none;
}

@keyframes pulse {
    0% { transform: scale(1) }
    50% { transform: scale(1.05) }
    100% { transform: scale(1) }
}

main > *:target {
    display: block;
    animation: pulse 0.5s linear 1;
}

Fancy! And, admittedly, somewhat pointless, but there is something to be said for a visual indication that the context has changed — especially when switching views is instantaneous. I’ve set up a Codepen15 for you to see the effect. Note that the browser’s “back” button works as expected, because no JavaScript has hijacked or otherwise run roughshod over it. Pleasingly, the animation triggers either via an in-page link or with the “back” and “forward” buttons.

Everything works great so far, except that no view is displayed upon http://my-app-thing.com being hit for the first time. We can fix this! No, not with JavaScript, but with a CSS enhancement again. If we used JavaScript here, it would make our whole routing system dependent on it and all would be lost.

The Default View Link

Because I can’t rely on users navigating to http://my-app-thing.com#the-default-view according to my saying so, and because :target needs the fragment identifier #the-default-view to work, I’ll need to try something else to display that default view.

As it turns out, this is achievable by controlling the source order and being a bit of a monster with CSS selectors16. First, I’ll make my default view the last of the sibling view elements in the markup. This is perfectly acceptable accessibility-wise because views are “loaded” one at a time, with the others hidden from assistive technology using display: none. Order is not pertinent.

<main role="main">
    <div id="some-view">
        <h1>some view</h1>
        <!-- … -->
    </div>
    <div id="another-view">
        <h1>another view</h1>
        <!-- … -->
    </div>
    <div id="the-default-view">
        <h1>the default view</h1>
        <!-- … -->
    </div>
</main>

Putting the default view last feels right to me. It’s like a fallback. Now, we can adapt the CSS:

main > * {
    display: none;
}

main > *:last-child {
    display: block;
}

@keyframes pulse {
    0% { transform: scale(1) }
    50% { transform: scale(1.05) }
    100% { transform: scale(1) }
}

main > *:target {
    display: block;
    animation: pulse 0.5s linear 1;
}

main > *:target ~ * {
    display: none;
}

There are two new declaration blocks: the second and final. The second overrules the first to show our default > *:last-child view. This will now be visible when the user hits http://my-app-thing.com. The final block, using the general sibling combinator, applies display: none to any element following the :target element. Because our default view comes last, this rule will always apply to it, but only if a :target element exists. (Because CSS doesn’t work backwards, a :first-child default element would not be targetable from a sibling :target element that appears after it.)

Try reloading the Codepen17 with just the root URL (no hash in the address bar) to see this in practice.

It’s Time Link

We’ve come a long way without using JavaScript. The trick now is to add JavaScript behavior judiciously, enhancing what’s been achieved so far without replacing it. We should be able to react to view changes with JavaScript without causing those view changes to fall in the realm of JavaScript. Anything short of this would be overengineering, thereby diminishing performance and reliability.

I’m going to use a modicum of plain, well-supported JavaScript, not jQuery or any other helper library: The skeleton of the app should remain small but extensible.

The hashchange Event Link

As stated, popular web application frameworks tend to render views with JavaScript. They then allow callback hooks, like Meteor’s Template.my-template.rendered, for augmenting the view at the point it’s made available. Even isomorphic apps like to use script-driven routing and rendering if they get the chance. My little app does not render views so much as reveal them. However, it’s entirely likely that, in some cases, I’ll want to act upon a newly revealed view with JavaScript, upon its arrival.

Fortuitously, the Web API affords us the extremely well-supported18 (from Internet Explorer 8 and up) hashchange event type, which fires when the URL’s fragment identifier changes. This has a similar effect but, crucially, does not rely on JavaScript rendering the view (from which it would emit a custom event) to provide us with a hook.

In the following script (demoed in another Codepen19), I use the hashchange event to log the identity of the current view, which doubles as the id of that view’s parent element. As you might imagine, it works no matter how you change that URL, including by using the “back” button.

window.addEventListener('hashchange', function() {
    console.log('this view\'s id is ', location.hash.substr(1));
});

We can scope DOM operations to our view by setting a variable inside this event handler, such as viewElem, to signify the view’s root element. Then, we can target view-specific elements with expressions such as viewElem.getElementsByClassName('button')[0] and so on.

window.addEventListener('hashchange', function() {
    var viewID = location.hash.slice(1);
    var viewElem = document.getElementById(viewID);
    viewElem.innerHTML = '<p>View loaded!</p>';
});

Abstraction Link

I’m wary of abstraction because it can become its own end, making program logic opaque in the process. But things are going to quickly turn into a mess of ugly if statements if I carry on in this vein and begin to support different functionality for individual views. I should also be addressing the issue of filling the global scope. So, I’m going to borrow a common singleton20 pattern: defining an object with our functionality inside of a self-executing function that then attaches itself to the window. This is where I’ll define my routes and application-scope methods.

In the following example, my app object contains four properties: routes for defining each route by name, default for defining the default (first-shown) root, routeChange for handling a change of route (a hash change), and init to be fired once to start the app (when JavaScript is available) using app.init().

(function() {
    var app = {
        // routes (i.e. views and their functionality) defined here
        'routes': {
            'some-view': {
                'rendered': function() {
                    console.log('this view is "some-view"');
                }
            },
            'another-view': {
                'rendered': function() {
                    console.log('this view is "another-view"');
                    app.routeElem.innerHTML = '<p>This JavaScript content overrides the static content for this view.</p>';
                }
             }
        },
        // The default view is recorded here. A more advanced implementation
        // might query the DOM to define it on the fly.
        'default': 'the-default-view',
        'routeChange': function() {
            app.routeID = location.hash.slice(1);
            app.route = app.routes[app.routeID];
            app.routeElem = document.getElementById(app.routeID);
            app.route.rendered();
        },
        // The function to start the app
        'init': function() {
            window.addEventListener('hashchange', function() {
                app.routeChange();
            });
            // If there is no hash in the URL, change the URL to
            // include the default view's hash.
            if (!window.location.hash) {
                window.location.hash = app.default;
            } else {
                // Execute routeChange() for the first time
                app.routeChange();
            }
        }
    };
    window.app = app;
})();

app.init();

Notes Link

  • The context for the current route is set within app.routeChange, using the syntax app.routes[app.routeID], where app.routeID is equal to window.location.hash.substr(1).
  • Each named route has its own rendered function, which is executed within app.routeChange with app.route.rendered().
  • The hashchange listener is attached to the window during init.
  • So that any JavaScript that should run on the default view when loading http://my-app-thing.com is run, I force that URL with window.location.hash = app.default, thereby triggering hashchange to execute app.routeChange(), including the default route’s rendered() function.
  • If the user first hits the app at a specific hashed URL (like http://my-app-thing.com#a-certain-view), then this view’s rendered function will execute if one is associated with it.
  • If I comment out app.init(), my views will still “render,” will still be navigable, styled and animated, and will contain my static content.

One thing you could use the rendered function for would be to improve keyboard and screen-reader accessibility by focusing the <h1>. When the <h1> is focused, it announces in screen readers which view the user is in and puts keyboard focus in a convenient position at the top of that view’s content.

'rendered': function() {
        app.routeElem.querySelector('h1').setAttribute('tabindex', '-1');
        app.routeElem.querySelector('h1').focus();                                          
}

Another Codepen is available21 using this tiny app “framework.” There are probably neater and even terser(!) ways to write this, but all of the fundamentals are there to explore and rearrange. I’d welcome any suggestions for enhancement, too. Perhaps something could be achieved with hashchange’s oldURL property, which (for our purposes) references the previous route.

app.prevRoute = app.routes[e.oldURL.split("#")[1]];

Then, each route, in place of the singular rendered function, could have entered and exited functions. Among other things, both adding and removing event listeners would then be possible.

app.prevRoute.exited();

Completely Static Views Link

The eagle-eyed among you will have noticed that the default view, identified in app.default as the-default-view, is not, in this case, listed in the app.routes object. This means that our app will throw an error when it tries to execute its nonexistent rendered function. The view will still appear just fine, but we can remove the error anyway by checking for the existence of the route first:

if (app.route) {
    app.route.rendered();
}

The implication is that completely static “views” can exist, error-free, side by side with views that are (potentially) highly augmented by JavaScript. This breaks from single-page app normality, wherein you would forfeit the ability to serve static prerendered content by generating all of the content from scratch in the client — well, unless JavaScript fails and you render just a blank page. A lot of examples of that unfortunate behavior can be found on Sigh, JavaScript22.

(Note: Because I actually have static content to share, I’ll want to add my app script after the content at the bottom of the page, so that it doesn’t block its rendering… But you knew that already.)

Static Views With Enhanced Functionality Link

You could, of course, mix static and JavaScript-delivered content within the same view, too. As part of the rendered function of a particular view, you could insert new DOM nodes and attach new event handlers, for instance. Maybe throw in some AJAX to fetch some fresh data before compiling a template in place of the server-rendered HTML. You could include a form that runs a PHP script on the server when JavaScript is unavailable and that returns the user to the form’s specific view with header('Location: http://my-app-thing.com#submission-form'). You could also handle query parameters, using URLs like http://my-app-thing.com/?foo=bar#some-view.

It’s entirely flexible, allowing you to combine any build tasks, server technologies, HTML structures and JavaScript libraries you wish. All that this approach does “out of the box” is to keep things on one web page in a responsible, progressive way.

Whatever you want to achieve, you have the option of attaching functions, data and other properties on either the global app scope (app.custom()) or on specific views (app.routes['route-name'].custom()), just like in a “real” single-page application. Your responsibility, then, is to blend static content and enhanced functionality as seamlessly as possible, and to avoid relegating your static content to being just a perfunctory fallback.

Conclusion Link

In this article, I’ve introduced a solution for architecting progressive single-page applications using little more than a couple of CSS tricks, less than 0.5 KB of JavaScript and, importantly, some static HTML. It is not a perfect or complete solution, just a modest skeleton, but it testifies to the notion that performant, robust and indexable single-page applications are achievable: You can embrace web standards while reaping the benefits of sharing data and functionality between different interface screens on a single web page. That is all that makes a single-page app a single-page app, really. Everything else is an add-on.

If you have any suggestions for improvements or want to raise any questions or concerns, please leave a comment. I’m not interested in building a “mature” (read: overengineered) framework, but I am interested in solving important problems in the simplest possible ways. Above all, I want us to help each other to make applications that are not just on the web, but of the web, too.

If you’re not sure what I mean by that or you’re wondering why it excites me so much, I recommend reading Aaron Gustafson’s Adaptive Web Design23. If that’s too much for the moment, do yourself a favour and read the short article, “Where to Start24” by Jeremy Keith.

(jb, ml, al)

Footnotes Link

  1. 1 https://www.smashingmagazine.com/2015/09/why-performance-matters-the-perception-of-time/
  2. 2 https://www.smashingmagazine.com/2015/11/why-performance-matters-part-2-perception-management/
  3. 3 https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/
  4. 4 https://www.smashingmagazine.com/2016/02/getting-ready-for-http2/
  5. 5 https://www.smashingmagazine.com/2016/02/everything-about-google-accelerated-mobile-pages/
  6. 6 https://www.smashingmagazine.com/2014/09/improving-smashing-magazine-performance-case-study/
  7. 7 https://indiewebcamp.com/curlable
  8. 8 https://medium.com/@mjackson/universal-javascript-4761051b7ae9
  9. 9 /2015/04/react-to-the-future-with-isomorphic-apps/
  10. 10 http://www.oxforddictionaries.com/definition/english/luddite
  11. 11 http://handlebarsjs.com/
  12. 12 https://gist.github.com/Heydon/12fd8b4947b2a05fe5f3
  13. 13 http://www.w3.org/TR/html5/browsers.html#scroll-to-fragid
  14. 14 https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation-starting-point
  15. 15 http://codepen.io/heydon/debug/JGoaVw
  16. 16 http://alistapart.com/article/quantity-queries-for-css
  17. 17 http://codepen.io/heydon/debug/JGoaVw
  18. 18 http://caniuse.com/#feat=hashchange
  19. 19 http://codepen.io/heydon/pen/BjNXba
  20. 20 http://robdodson.me/javascript-design-patterns-singleton/
  21. 21 http://codepen.io/heydon/pen/YwyPZv
  22. 22 http://sighjavascript.tumblr.com/
  23. 23 http://www.peachpit.com/store/adaptive-web-design-crafting-rich-experiences-with-9780134216140
  24. 24 https://adactio.com/journal/9963
SmashingConf New York

Hold on, Tiger! Thank you for reading the article. Did you know that we also publish printed books and run friendly conferences – crafted for pros like you? Like SmashingConf Barcelona, on October 25–26, with smart design patterns and front-end techniques.

↑ Back to top Tweet itShare on Facebook

Heydon is a UX designer and writer interested in web application accessibility and intelligent layout systems. He gesticulates a lot and is terrible at computer games. His acclaimed book on accessibility has been published with Smashing Magazine.

  1. 1

    Great stuff! Most of this is common sense, but sadly, it would appear to be not all that common.

    3
  2. 2

    this is great but my client is using IE8 (NHS in the UK) so the :target css selector wont work and I’m stuck with jQuery/BackboneJS style SPA :-(

    2
    • 3

      I love the NHS, but that makes me sad. Oh well :(

      1
    • 7

      If they are using IE8 at the NHS they are probably still on XP. Especially with the (rather private health) data they use it always makes me cringe. Use an outdated unsafe OS and browser (IE8 support goes away in January too…) for such delicate data. :(

      But of course I know these problems, at work we have such clients too. Luckily it seems most are (albeit slowly) updating at least to IE11, hopefully to Edge or Firefox or Chrome.

      I always wonder if there is a law for any organization to keep user data safe, NHS (and many more which way not be as delicate but still) are certainly not…

      0
  3. 8

    I can haz question? I used to use :target for all sorts of goodies, but found using it for displays none and such caused issues because inevitably there’d be someone *inside* waiting to become the new :target. If your views are only plain text and/or all focusables are things like external links or another view, that’s cool, but what did you do when someone else might accidentally take your :target?

    (oh and just for you: I stopped reading at “What is the difference between a web page and a web application?” but then I came back for the rest because, coffee : )

    0
    • 9

      I’m glad you brought that up, Mallory. We actually discussed it before publishing.

      My feeling is that this approach is quite idiomatic: In it’s current form you’d have to accept the convention that a view is equal to a page fragment. I’m a big believer in reducing code quantity by following certain agreed conventions rather than coding for every possible use case, no matter how unusual.

      It seemed reasonable not to address this as part of the basic proof of concept because, in my experience, single-page apps tend not to use in-page links so much — they tend to use views instead.

      Nonetheless, you could code around this by catching links to nested fragments and managing focus with javascript. When someone insists on these “same view” links using smooth scrolling, you’d be doing that anyway ¯_(ツ)_/¯

      5
  4. 10

    Funny since I just wrote an almost word for word copy of your javascript for a small internal site. But using handlebars for the templates, so maybe there is more overhead there on page load compared to straight JS?

    Where I am working we are thinking about the next version of our site and this is essentially the way we are going. Although I am starting to wonder if I need to have core css and javascript, and after page load we add in the rest of the style. Or if we need a mixed approach where we load css related to that page. But we have not had good experience with that but a change to full javascript front end may help.

    We also maybe have the added problem of some html needs to be loaded remotely, some of which is the “front end” and others will be static html which has been edited by the marketing department.

    1
  5. 11

    Why do you think there is a need for progressive design to begin with? I mean, all browsers support JavaScript and JavaScript is the assembly language of the web. No one of us is interested in how a desktop app works (even though it also uses web resources in the same way that browser apps use them), why would you be interested in how these work/are written. I mean nobody uses lynx anymore and IE8 and lower are pretty much dead as well. Why not just treat JS as the language it was intended to be: assembly for the web?

    -16
    • 12

      Frederik:
      This is where the point of view is coming from:
      http://alistapart.com/article/interaction-is-an-enhancement

      And we all wish IE8 and lower were dead, yet my government is spending my tax money to pay Microsoft to have a special support contract for all those machines still running XP that somehow 10 years wasn’t long enough for them to prepare for upgrade. It appears the UK is equally slow. It’s legacy systems all the way down and some of us are still drowning in it :/

      Also, regarding this: “why would you be interested in how these work/are written”
      Some of us are interested, curiosity in how things work and building those things is part of our craft. Not everyone’s, sure. But maybe more than you think. If it’s not for you, that’s fine too. In your case, think of this article as an “interest piece.”

      6
      • 13

        My guess here is that the “assembly” being referred to is the ultra-low-level programming language of processors. All code that runs on your computer is eventually turned into assembly which is represented in binary that the processor can directly run. Languages like C, C++, PHP, Perl, Python, etc are all, ultimately, abstractions of assembly (or assembler) code.

        I guess the point being made is that JavaScript is to web browsers what assembler is to computer processors: it’s the lowest level of code that the browser can directly implement. You can then build languages and frameworks on top of that: React/Angular/CoffeeScript/jQuery, etc.

        0
        • 14

          I’d dispute that and say HTML is the lowest level. Without HTML, javascript and CSS are nothing. Though enhanced by CSS and javascript, HTML can stand alone.

          6
          • 15

            I think he was talking about programming languages… so I would agree with him. You could go on and eventually talk about TypeScript and so on… By the way, HTML is not a programming language as we all know and JS nowadays is more than a scripting one.

            0
    • 16

      Could you explain how you think javascript is “assembly for the web”? I’m not very familiar with assembly.

      0
    • 18

      Can you guarantee that every single one of your user’s browsers, on every single request, will have JavaScript functioning optimally? What happens if, for whatever reason, your user’s browser craps out on their first visit to your site, with the DOM loading but no JavaScript? Or, what happens if, for whatever reason, your user has disabled JavaScript? That is what progressive enhancement is for, not to hate on JavaScript, but to care more about users than what’s convenient to develop. It’s a way to guarantee the user sees content as best as possible.

      I will say, however, that between the use of :target and :last-child, the JavaScript-less SPA won’t function as expected in IE8 or below.

      -1
    • 19

      Bartek Igielski

      December 26, 2015 10:31 am

      IMO nowadays building page for non-javascript users is weird… What about disabling CSS – should we design pages to looks good without CSS? It’s ridiculous for me.
      Page should work without any third party software that user have to download / install / enable (like Flash – R.I.P), but if someone disable core part of browser (CSS/JS), it’s their choice to have “handicapped” page.
      It’s not related to any screen readers or things like this – they parse CSS and JS as well as pure HTML.

      -6
      • 20

        Completely agree, this guy is getting down-voted for saying what most of us are thinking: People who switch off Javascript get what they deserve and we shouldn’t give them our time.

        Whilst this article was an interesting experiment in what can be achieved without JS in an SPA, it’s too limited for any real world application except perhaps a very basic brochure website.

        -12
  6. 21

    My biggest question on this is scalability. SPA can scale specifically because they don’t include the entirety of the content in the page at once. While I agree that this approach has great benefit in some cases, I would be wary of trying to apply it in realms where large growth or great amounts of content are anticipated. Either that, or design a really great loading screen for users to play with while client downloads one super-massive page in the background.

    Alternatively, you could use a hybrid approach. Develop it so that only the top level views — the default and the ones immediately accessible from your main navigation — are written statically and displayed as described above, but also include the AJAX functionality as well to replace them with dynamic content, including the original static content, from the server as needed.

    8
    • 22

      Yeah, with the hybrid approach, you can HIJAX it– if JS for whatever reason didn’t load, the users can click links or interact with simple forms to do new page requests, but when JS does load, it removes those elements and just substitutes ajax for loading more content from the server.

      2
    • 23

      I’m specifically suggesting that static content is sent from the server, not built on the client, and that javascript should only embellish views where the content needs to be augmented. This is generally more performant and robust. However, if there is a _lot_ of content, perhaps a solution might involve sending the first page (or so) statically then instate a script that fetches further content with AJAX. At least then, the user gets some (the latest available on the server) where javascript fails.

      5
      • 24

        A different approach for larger-scale apps is to serve the pages from separete urls. Non-JS users would always have to load the entire page, and with JS you can hijack the links and load only the actual view content. Using the location API, you could then even update the URL in the address bar, freeing up the fragment identifier for in-page targets.

        2
        • 25

          Exactly Alexis, the web already has a way of doing what Heydon is trying to do, it’s called using seperate URLs for non JS users.

          We often do pagination this way but only because we have to pander to search engines if we want our content indexed.

          -1
  7. 26

    “The Web Way™”

    I like that label.

    3
  8. 27

    I have real difficulty in seeing the static HTML version as being a “single page application” – it’s
    what I’d see as a “single page website”. To me, an application means interaction, view reuse, lots of data. Not a handful of static screens.

    Sure, this can be a suitable minimum experience – but the vast leap to Actually An App isn’t progressive enhancement.

    6
    • 28

      I have real difficulty in seeing the static HTML version as being a “single page application” – it’s
      what I’d see as a “single page website”.

      Correct. A gracefully degraded single-page application should be a single-page website. Or, by the same token, a progressively enhanced single-page website could be a single-page application. That’s the point.

      Sure, this can be a suitable minimum experience – but the vast leap to Actually An App isn’t progressive enhancement.

      So a big enhancement isn’t an enhancement? How so? Some web properties can offer little without the enhancement of interaction, sure, but there’s no reason why they can’t offer anything. In my experience, SPAs actually contain a lot of static content. With my approach you can ensure that the static content can be provided, even in the absence / failure of javascript.

      5
      • 29

        You probably weren’t responsible for the title “Reimagining Single Page Applications with Progressive Enhancement”, and my beef is mainly with that title – I guess “A Non-JS Basis for Single Page Applications To Make Core Static Content Accessible and Indexable” isn’t as exciting.

        As there’s an upper limit to the amount of static content people can risk putting into that initial page load before it becomes too weighty, at a certain scale (what I would call actual app scale) it becomes a two-tier approach.

        A big step change in scope and functionality isn’t a _progressive_ enhancement by my understanding.

        I can certainly see that something is better than nothing being available to non-JS users. But why would the single page experience ever be more important than having all functionality and content available to non-JS users?

        Further, the article touches on the valid point that critical non-JS users include cURL and web crawlers – but ignores the fact that every #section would resolve to the same URL in cURL, and the same content.

        I’m much happier with a SPA that degrades to a multi-page experience where the JS is unavailable, and uses real URLs where content should be indexed separately. This means that URLs are cURLable not just as a box-ticking exercise but anything that might really use cURL would get useful and relevant content – eg open graph readers.

        Which really _is_ The Web Way.

        6
        • 30

          You probably weren’t responsible for the title “Reimagining Single Page Applications with Progressive Enhancement”, and my beef is mainly with that title – I guess “A Non-JS Basis for Single Page Applications To Make Core Static Content Accessible and Indexable” isn’t as exciting.

          No, it isn’t, but it would essentially mean the same thing. I think you’re splitting hairs.

          A big step change in scope and functionality isn’t a _progressive_ enhancement by my understanding.

          Then you’re not understanding the term in any way I’ve ever encountered. In progressive enhancement, you take HTML content, then enhance with style (CSS) and behavior (JS). I didn’t address a big “step change”. I don’t know where that idea comes from or what you mean by it exactly.

          I can certainly see that something is better than nothing being available to non-JS users. But why would the single page experience ever be more important than having all functionality and content available to non-JS users?

          You’re putting words in my mouth. I never said “a single page experience [is] more important than having all functionality and content available to non-JS users” or anything like it. Your grievance seems to be that single page apps / documents are inherently bad things, whereas the article was about making them (a given, in the context) more progressive in their construction. If you’re talking about a “single-page app” which devolves to a huge amount of static content, which should be indexable properly, then why are you making a single-page app at all? You should just be making a website.

          Further, the article touches on the valid point that critical non-JS users include cURL and web crawlers – but ignores the fact that every #section would resolve to the same URL in cURL, and the same content.

          Indeed, you’d be cURLing a single page, because it’s a single page. Several pages may be optimal, but my method is about making something not cURLable at all, cURLable — which, for the purposes of a single-page app, would make that single page indexable. Again, if you’re dealing in vast amounts of static content, then a single-page app (using my method or anything like Angular or Meteor) is really not what you want to do in the first place. You’d make a dynamic or statically generated website, with separate pages.

          I’m much happier with a SPA that degrades to a multi-page experience where the JS is unavailable, and uses real URLs where content should be indexed separately.

          Fine, you do that :-) It’ll just be an order of magnitude more complex to implement and maintain. Surely a single-page app that becomes (or was) a multi-page site is a bigger “step change” than what I’m suggesting?

          3
  9. 31

    rtjtrhztthergrtgrthzjtzntzjztjzt

    December 21, 2015 5:42 pm

    Great idea. Unfortunately the scrolling issue of the target selector makes it unusable in most cases even with all the available workarounds.

    0
  10. 37

    So wish I had time to read and respond to this now. Timing is perfect. I’ve been grappling with these issues for a while. Fat web clients vs restful, PE thin clients.

    I found the 3 yr old http://roca-style.org/yesterday. It needs a few tweaks but it and https://github.com/defunkt/jquery-pjax have much to offer.

    I look at react and riot server rendering, but the reasons are all about speed and SEO not embracing the web. Also currently too complex even using webpack server tools.

    Jquery mobile is only thing I feel happy with for a SPAs but there seem to be communication problems from the project. Plus I don’t really want to suck in Jquery.

    Anyway thanks for the Christmas present! Have a happy holiday.

    1
  11. 38

    Haydon, after scanning quickly I thought it worth mentioning that I believe jQueryMobile does have a similar approach – http://demos.jquerymobile.com/1.4.5/pages/
    It supports single page docs or multiple pages in a doc with internal JS navigation. it also provides lifecycle events. I think it uses a form of HIJAX.

    I like your use of advanced :target selector but my question is is it supported in devices being used in the global south or is the javascript of jQM more likely to be available?

    http://caniuse.com/#search=%3Atarget
    http://jquerymobile.com/browser-support/1.4/

    Anyway I’m definitely going to have a play with this. Thanks again.

    3
    • 39

      Interesting. I haven’t used jQuery Mobile for years.

      Global south?

      1
      • 40

        I think he means the big circle on the world map centered around SouthEast Asia, where the label for that circle is something like “there are more internet users inside this circle than outside it” and majority are using mobiles, possibly still with one of the Operas as the most popular browser (not sure anymore).

        1
        • 41

          @Stomme poes Yes – that definition will do nicely.

          Now I search for it seems it is more an Anglican term used in [Human] Development works.

          0
  12. 42

    Michael Matyus

    December 21, 2015 7:56 pm

    Do you know if there are any SEO drawbacks to marking up multiple pages in one html file like this? Besides maybe, “Page weight”…

    0
    • 43

      It’s a single page, so there may be single page related issues. But at least it’s a cURLable single page with actual content in it. So, I guess it would perform better SEO-wise than an entirely client-rendered single-page application.

      -1
    • 44

      I wonder if google, bing and others distinguish between multiple HTML5 `article` elements with their own `h1` element.

      I know I’ve read in many places that multiple `h1`s on a single page is permissible with HTML5 so long as each `h1` is within its own `article`. I’d be interested to hear about anyone’s real world SEO experience with that.

      0
      • 45

        That was an idea that died stillborn– the HTML5 outline.

        It’s recommended to continue the usual heading-levels because no browser implements the Outline (which would have allowed h1 to imitate the idea from XHTML of an h tag that got its level from its surroundings, letting syndication maintain correct heading levels). Google knows some people are doing the multiple h1’s thinking the Outline exists, but for non-Googlebots using headings (like users of Assistive Technology), please don’t do that.

        1
  13. 46

    I have to admit I was skeptical at first. I’ve read and reread the article and I think this could be a really interesting approach. With some of the new template string and expression interpolation functionality available in ES6, you wouldn’t even need to use a templating library. My biggest concern would be having to modify what will undoubtedly grow into a huge html document during development, but I suppose some sort of html preprocessor could take care of that. I’m especially excited about how this approach could encourage the use of small libraries instead of monolithic frameworks. Thank you!

    2
    • 47

      Thanks! Yes, that’s what I had in mind: use a tiny, progressive approach for the routing, then add in libraries as needed.

      Personally, in development I would probably use individual files for each view, then just concatenate them into the index.html document. The build process is how you’d make the static index file, then link in js resources.

      0
  14. 48

    You know ‘performant’ means ‘one who performs’, right?

    -1
  15. 52

    Heydon – The title and article caught my imagination. I made a couple SPAs a few years ago, and each time I came away disappointed. The applications did there job and yes, I had superhuman control over the user experience, but when I compared the amount of work that went into making that possible vs. just using server side MVC tools I found it wasn’t worth it.

    In scenarios where SPA makes sense (think data app, not content site) I find that most of the UI is still static, follows the same basic-server-side-friendly patterns, and can operate admirably without a bit of JavaScript. Only occasionally does getting fancy with JS significantly improve the user’s experience.

    All that to say, I really appreciate a different perspective on SPAs, and even though I don’t expect to go down the SPA `route` any time soon, your article has helped me refine my own thinking about SPAs, and how I might incorporate and benefit from SPA-like behavior in my non-SPA apps.

    1
  16. 53

    Thank you for sharing the code. It will help a lot for single page applications.

    0
  17. 54

    I might take the progressive enhancement a bit further and hide the non-targeted views with:

    main > *:not(:target)

    So that the truly old browsers, like IE8, still get all the information.

    6
  18. 55

    Being that this handles routing. What are your thoughts of integrating a library like knockout? It is a MVVM lib without a router. It too is lightweight and could enhance this even more if you were to want a more traditional approach without the weight of a full in framework leaving choice open and flexible for technology integration. Just my two cents. I know that this would go away from this a good bit but I get the feeling that could be so me of the enhancement left open and optional.

    1
  19. 56

    Petri Hänninen

    December 27, 2015 9:04 pm

    Hi!

    Thank you for this great example that illustrates the execution of progressive enhancement. I’ve been struggling to actually see how you go from a wikipedia page to a full-fledged web app, and you make it surprisingly clear. I’ll definitely test this out more.

    I just wanted to ask, why are you using the * selector in your css when defining the display settings? As you are in full control of your html, you can be certain there will only be div elements inside the main block, right? Then couldn’t you just replace those all with div in order to improve the performance? With this project there’s obviously no visible difference but if the project grows, wouldn’t div be much more efficient?

    0
  20. 57

    Hi,
    May I suggest one performance improvement?
    Instead of using the frightening * CSS selector you could add a class to all your view s and refer to this class. I suggest this because of the way the browser resolves CSS selectors. It reads them backwards which in your case means it will first ‘grab’ all elements on the page with *, then narrow the selection down to children of all elements.

    0

↑ Back to top