React, in Jekyll, with dynamic pages!
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.
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.
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')
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
.
addNode
s 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.
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.
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.
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.
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.
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.
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.