React

29th March 2016

React, in Jekyll, with dynamic pages!

Why?

Most of my recent work on this site has been around using pushState, ajax and lunr to create a static site that you can search with pages for every category etc… that didn’t require me to create a page for each one. This resulted in some code duplication and data bloat mainly in the post list. This include was the post details box used on the blog page, the search results and category list. Only 1 out of those 3 pages was actually being generated by Jekyll and to remove the need to update 2 templates at once I put the rendered output of that include for each post into a JSON file.

React for those of you that don’t know is a Javascript rendering engine that runs in browser to create your HTML. It is normally used along side an API of some kind to interact with a database and read/write data, I have used it slightly differently.

Using React has let me create a dynamic site hosted on Github pages where search etc… is integrated seamlessly instead of bolted on to the side after page load.

How?

To do this I needed to get all my sites content into a format that could be read by Javascript which although JSON is designed for this very job I opted to use XML. From my earlier work I found that Jekyll’s ability to generate JSON is not brilliant, liquid has an xml_escape function which I was using in a long chain to create mostly valid JSON. With that using XML made more sense, I could just pipe everything through xml_escape as it goes into my XML and I get a valid document.

To make the XML more database like I put all the entries in a SODB letting me search for content based on the current url. This is separate to the lunr search that is also on the site, SODB isn’t a linguistic search and can’t rank based on relevancy etc…

I used React-Router to keep the URL up to date and handle page-transitions. I’d never used it before but will be doing after this it is the perfect solution to the problem I had here and has lots of potential. That being said I did have a few issues working stuff out as I don’t use JSX unlike most examples.

Lets run through how loading this post in your browser worked.

  1. Your browser gets the HTML
  2. Your browser requests the stylesheet & JS assets
  3. React-Router starts up and renders the Layout with the Loader component
  4. The Loader component triggers a request for /content.xml
  5. Whilst waiting for the response a loading spinner is shown
  6. The response is received, parsed and put into the SODB and the components state is updated with the database
  7. Now that the data is there the state change causes the Post Component to be loaded

The time to readable content is a bit longer than before which is usually to be avoided but in this case the site becomes so much faster and a lot more functional once loading is complete.

I abuse the ways Github Pages uses 404.html to create pages on the fly in React which is coincidentally why this PR on static-server exists.

All pages are created using a layout that doesn’t output their contents but instead loads a bunch of Javascript files. This means that although /code.html does actually exist it only loads up React same as /404.html.

I created a browserify bundle to load up all the requirements the file I build into my bundle is just a list of requires.

React = require('react')
ReactDOM = require('react-dom')
ReactRouter = require('react-router')
ReactGA = require('react-ga')
ReactDisqusThread = require('react-disqus-thread')

Marked = require('marked')

SODB = require('sodb')

The Code

XML Parsing

My XML Parsing a bit quick and dirty but I was having issues with other implementations not parsing correctly or refusing to parse it.

arseData = (jQueryData) ->
    data = {}

    children = jQueryData.children()

    children.each (index, child) ->
        if child.childElementCount > 0
            data = addNode(data, child.nodeName, parseData($(child)))
        else
            data[child.nodeName] = child.innerHTML

    return data

addNode = (data, nodeName, value) ->
    if data[nodeName]
        if data[nodeName] instanceof Array
            data[nodeName].push value
        else
            data[nodeName] = [ value ].concat data[nodeName]
    else
        data[nodeName] = value

    return data

## from another function
jQueryData = $($.parseXML(data))
parsedData = parseData(jQueryData)

parseData takes jQueries attempt at parsing the XML and for each child at the current level it checks if it has children, if it does it calls addNode to add a sub node to data this line recurses downwards until it reaches a childless node. If a node is childless its content is put directly into data at the nodeName.

addNodes main purpose is to create arrays if needed. The only draw back to this approach is the array creation which doesn’t quite work how I would like. My XML has a structure like this:

<site>
    <posts>
        <post>
            <title>Post 1</title>
        </post>
        <post>
            <title>Post 2</title>
        </post>
    </posts>
</site>

I was aiming to put the array at site.posts but instead it appears at site.posts.post as that is the conflicting key. I considered running another pass to move arrays one up if the parent node only had 1 child but adding more loops seemed a waste for something that had no impact on the end user and ultimately ended up being passed into another structure.

The Router

ArcathNetRouter = (
    Router(
        {history: browserHistory}
        Route(
            {name: 'Layout', path: '/', component: Layout}
            IndexRoute({name: 'index', component: Loader, params: {component: Static}})
            Route({name: 'Blog', path: 'blog.html', component: Loader, params: {component: Blog}})
            Route({name: 'Categories', path: 'category.html', component: Loader, params: {component: Categories}})
            Route({name: 'Category', path: 'category/:name', component: Loader, params: {component: Category}})
            Route({name: 'CV', path: 'cv.html', component: Loader, params: {component: Static}})
            Route({name: 'Code', path: 'code.html', component: Loader, params: {component: Static}})

            Route({name: 'Post', path: ':year/:month/:day/:title.html', component: Loader, params: {component: Post}})
            Route({name: 'Search', path: 'search/:term', component: Loader, params: {component: Search}})
        )

        Route({name: 'NotFound', path: '*', component: NotFound})
    )
)

The Router defines that the Layout component should be used for everything under /. All the routes from the old system are defined here as well as a 404 error page as I have abused the behavior of Github pages to use a provided 404.html to allow these dynamic pages to exist.

All the content routes pass through a loader component that ensures that the sites content has been fetched before displaying the intended component.

The Loader Component

Loader = React.createClass({
    getInitialState: ->
        { loaded: false }

    componentDidMount: ->
        _ = @
        getData((content) ->
            _.setState({ loaded: true, content: content })
        )

    render: ->
        React.DOM.div {},
            if @state.loaded
                React.createElement(@props.route.params.component, {
                    content: @state.content,
                    location: @props.location,
                    routeParams: @props.routeParams
                })
            else
                React.createElement(Loading, {})
})

The Loader is used by every route to ensure the sites /content.xml has been loaded and parsed before trying to render a component that needs data.

Initially state.loaded is false which causes render to render the Loading component, once the data is received its state changes and actual component is rendered instead.

The Static Component

Static = React.createClass({
    render: ->
        page = @props.content.findOne({link: @props.location.pathname})

        setTitle(page.title)

        if page.markdown
            content = Marked(page.content)
        else
            content = htmlDecode(page.content)

        React.DOM.div {},
            React.DOM.div {dangerouslySetInnerHTML: {__html: content}}
})

This component is the static content renderer which find the page for the given URL and spouts its HTML straight out to the user. It can parse markdown if needed, I found an odd case where my CV had un-parsed content when creating /content.xml which was messing up my rendering.

The Blog Component

Blog = React.createClass({
    blogYears: ->
        posts = @props.content.where({type: 'post'})
        years = []
        for post in posts
            if years.indexOf(post.year) == -1
                years.push post.year

        return years

    render: ->
        setTitle('Blog')

        React.DOM.div {},
            React.DOM.h1 {}, 'Blog'
            for year in @blogYears()
                React.DOM.div {key: year},
                React.DOM.h2 {}, year
                    for post in @props.content.where({type: 'post'}, {year: year})
                        React.createElement(PostDetails, {key: post.___id, post: post})
})

This is one of the more dynamic components in the site. It loops through each year which has posts and creates a PostDetails component for each post in each year.

And More

A lot went into this change all of which can be seen in the commit, going though all of it here would be a very a long post.

In Conclusion

This has been a worthwhile move as far as I am concerned, I have ironed out all the kinks I could find and every page renders successfully (not all of them did to begin with).

I recommend trying this out on your sites, and I hope it makes you think of a static site as much more than static HTML.

Other Posts

Jekyll Atom 1.2.0

1st March 2016

A new version of Jekyll Atom is here!

RADIUS With Windows Server 2012R2

22nd July 2016

RADIUS authentication for wireless networks using NAP in Windows Server 2012R2.

Disclaimer

All content is my own unless otherwise stated.

My content is licensed under the CC-BY-NC-SA 4.0 license

cc by-nc-sa