Skip to content

Commit

Permalink
feat: Approaching Initial Release
Browse files Browse the repository at this point in the history
  • Loading branch information
emmyoh committed Jun 11, 2024
1 parent 5170ba6 commit e30340b
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 236 deletions.
93 changes: 93 additions & 0 deletions site/blog/pain_points.vox
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
title = "Fixing Static Site Generators"
date = 2024-06-11
layout = "post"
permalink = "date"
---

{% markdown %}

Static site generators (SSGs) are a class of software that evaluate template files at runtime to output new files.
Many of these tools are intended for creating Web sites, used when you want to **publish your own content and syndicate it elsewhere**.

SSGs are described as 'simple', with minimal installation & configuration burden, human-readable content files, and freedom from databases.
This makes creating & updating sites relatively painless in many circumstances, and makes deployment fairly straightforward.

Despite the advantages over traditional content management systems (CMSes), there are some disadvantages common to many SSGs:
* Intended for software developers
* Difficult to iterate with and scale
* Templates are too limited for many use cases

Vox aims to address these issues to expand the use-cases of SSGs over CMSes:
* When you want to **own** your content long-term
* When you want to **distribute** your content to many places
* When you want to **control** the infrastructure hosting your content

---

## Content Management Pipeline

The process of writing content, rendering it, and then deploying it, sounds straightforward.
Unfortunately, each of these steps involves domain-specific knowledge that can harm the user experience.

### Prerequisite Knowledge

To write content, you likely need to know:
* the notation used by frontmatter
* the templating language
* the markup language

For many people without technical backgrounds, the writing process alone forces them toward a traditional CMS.
In fact, I'd argue no SSG---including Vox---can or *should* solve this problem. This is exactly what the role of a static CMS should be.
The role of Vox here is to be interoperable with a static CMS that simplifies writing and deployment.

### Reducing New Concepts

Many SSGs introduce dozens of concepts relating to their use & operation, while Vox tries to remain relatively simple.

* The **layout-partial paradigm**: site consistency is maintained with <a href="{{ global.url | append: "/guide/Data Model.html#layouts" }}">layouts</a>, elements are reused with <a href="{{ global.url | append: "/guide/Data Model.html#include" }}">includes</a>
* Simple <a href="{{ global.url | append: "/guide/Data Model.html" }}">data model</a> that draws only upon the aforementioned pre-requisite knowledge
* Straightforward <a href="{{ global.url | append: "/guide" }}">user guide</a>

### Providing Feedback

When the SSG is unfamiliar or a site has particularly high cognitive complexity, unexpected rendering or build errors can become frustrating rather quickly.
Vox provides logging messages that aid users in debugging their sites, both during the initial build and incremental rebuilds.

---

## Scale & Iteration

A major source of pain for too many SSGs is build performance; many sites should take fractions of a second to build on a laptop, not minutes.
Even then, once a site reaches a certain size, rebuilding from scratch when changes are made is not feasible. Incremental rebuilds should be done intelligently.
Vox enables rapid iteration and unrestricted scaling of sites with its <a href="{{ global.url | append: "/guide/Rendering Pipeline.html" }}">unique rendering pipeline</a>, rebuilding only what it needs to while ensuring the entire site is up-to-date.

---

## Emergent Capabilities

As a consequence of Vox's abstractions, various capabilities emerge without the cost of added software complexity.

### Content Traversal

<a href="{{ global.url | append: "/guide/Data Model.html#layouts" }}">Layout inheritance</a> and <a href="{{ global.url | append: "/guide/Frontmatter.html#collections" }}">collections</a> enable content traversal with a content hierarchy.
A single page can reference pieces of content elsewhere in the site with ease, making content management simple by introducing relations between pages.

### Generalising Beyond Markup

Vox pages have only three elements: the frontmatter, the body, and the templating within the body.
As such, content can resemble anything; the body is not restricted to Markdown and the output is not restricted to HTML.
Pages aren't relegated to being single chunk of markup with some metadata on top.

### Simple Theming

Due to the relational nature of pages, a Vox site is free to have its own schema of sorts, as established by the layouts & includes.
Consequently, 'themes' can be written which involve configuration of `global.toml` and nothing more, provided pages are written in accordance to the theme's 'schema'.

---

Vox is opinionated, but with its uniqueness comes distinct advantages.
In the future, I'll write about extending Vox with a bespoke static CMS to replace traditional CMSes in countless more use-cases.
Thanks for reading!

{% endmarkdown %}
65 changes: 65 additions & 0 deletions site/diary/almost_done.vox
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
title = "Approaching Initial Release"
date = 2024-06-11
layout = "post"
permalink = "date"
---

{% markdown %}

## Goals
- Modify log output to be more helpful for end-users.
- Removing code that was commented out.
- Parallelising as much as possible.
- Creating a logo for Vox.

### Logging

Logging is done at the following levels:
* **Error**: Recoverable runtime errors.
* **Warning**: Warnings that an unexpected situation has occurred at runtime.
* **Information**: Provides helpful information to the end user.
* **Debug**: Messages that aid the end user in debugging build issues with their site.
* **Trace**: Logging that indicates notable events during runtime.

### Cleanup of Removed Code

This made the code easier to read.

### Paralellisation

In `builds.rs`, there are ten sequential loops:

1. Setting the appearance of nodes in the DAG visualisation.
2. Recursively getting the descendants of a page in the DAG.
3. Recursively getting the ancestors of a page in the DAG.
4. Recursively getting the non-layout ancestors of a page in the DAG.
5. Recursively constructing the Liquid contexts of the ancestors of a page in the DAG.
6. Rendering all pages.
7. Iterating over all parent pages when rendering a page.
8. Noting parent collection pages when rendering a page.
9. Generating dependent collection contexts when rendering a page.
10. Recursively rendering a page's children.

The sixth and tenth loops cannot be parallelised as rendering order matters (must be done in topological order), while the rest cannot be parallelised as they involve mutating data outside the loop (causing a race condition if done in parallel).

In `page.rs`, there is one sequential loop when iteratively determining the collections from a page's path; this is inherently iterative and cannot be done in parallel.

In `templates.rs`, there is one sequential loop when determining the snippets in the `snippets` directory, but this would introduce race conditions if done in parallel.

In `main.rs`, upon initial inspection, the following loops may be parallelised:
1. The fourth step (DAG merging) of the re-rendering pipeline.
2. Deleting the output of removed pages in the fifth step of the re-rendering pipeline.
3. Recursively ascending a layout hierarchy to obtain the first non-layout page URL.

### Logo

---

## Future Goals

- Move as much CLI code as possible into library.
- Incorporating `global.toml` into DAG construction.
- Incorporating snippets into DAG construction.

{% endmarkdown %}
11 changes: 9 additions & 2 deletions site/diary/collections_pagination.vox
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ permalink = "date"
- Supporting pages in multiple collections at once with path nesting.
- If a build has failed, don't immediately retry.
- Upgrading all direct dependencies.
- Rebuild if `global.toml` file or `snippets` subdirectory have changed.
- Implement pagination.
- Modify log output to be more helpful for end-users.

### Collections

Expand All @@ -31,6 +31,10 @@ This was always the intended behaviour. The mistake was in accidentally checking

Notably, this brought my implementation of Jekyll's sorting filter, which was merged into the Liquid Rust implementation.

### Rebuild Global or Snippet Changes

Since neither `global.toml` or snippets are represented in the DAG (yet), the best that can be done is simply rebuilding everything if either change.

### Pagination

Pages may have a `pagination` frontmatter value, containing:
Expand All @@ -45,13 +49,16 @@ The `pagination.page_number` is used in a page's `permalink`, and can be used wi
- to calculate the remaining number of pages
- This requires the length of the paginated collection as well

This is not feasible to implement; pages are added to the DAG in an unpredictable order, so calculating `pagination.page_number` for each copy of a page is not possible, as the length of the collection being paginated is not yet known.

---

## Future Goals
- Parallelising as much as possible.
- Removing code that was commented out.
- Documenting the CLI code.
- Creating a logo for Vox.
- Incorporating `global.toml` into DAG construction.
- Incorporating snippets into DAG construction.
- Move as much CLI code as possible into library.

{% endmarkdown %}
7 changes: 5 additions & 2 deletions site/guide/data_model.vox
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,13 @@ The `page` context is composed of several fields. Refer to [the developer docume
The `layouts` context is a list where each item is a layout page's context, but with the final item being the context of the page above the layouts; items are in ascending order.
If you need a better intuition of how this list is ordered, visualise the DAG of your site; for each layout page, the first page in this list is the page directly above it.

The `layout` context is the context of a specific layout page being rendered.

Additionally, there is the `layout` context, being the specific layout page being rendered.
The `page` context is the context of the page above any layouts.

As a reminder, the terminal pages of the DAG are the pages which are output by Vox; rendering is done in topological order (from the root pages down to the terminal pages).

---

As an example, suppose the following layouts:

{% raw %}
Expand Down Expand Up @@ -177,6 +178,8 @@ Each page would be rendered as follows:
</html>
```

---

## Include

The `include` context is used to pass parameters to snippets.
Expand Down
2 changes: 1 addition & 1 deletion site/pages/guide_index.vox
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ layout = "default"
depends = ["guide"]
permalink = "guide/index.html"
---
{% assign posts = guide | sort: "data.title" | reverse %}
{% assign posts = guide | sort: "data.title" %}
{% include index.voxs posts = posts minimal = true %}
54 changes: 8 additions & 46 deletions src/builds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use liquid::{to_object, Object, Parser};
use liquid_core::to_value;
use miette::IntoDiagnostic;
use std::{env, fs, path::PathBuf};
use tracing::{debug, info, trace, warn};
use tracing::{debug, trace, warn};

/// Information held in memory while performing a build.
#[derive(Clone, Default)]
Expand Down Expand Up @@ -58,7 +58,6 @@ impl Build {
format!("label = \"{}\"", label)
},
);
debug!("DAG: {:#?}", dag_graphviz);
let mut parser = DotParser::new(&format!("{:?}", dag_graphviz));
let tree = parser.process();
if let Ok(tree) = tree {
Expand Down Expand Up @@ -212,23 +211,11 @@ impl Build {
///
/// A list of all nodes that were rendered.
pub fn render_all(&mut self, visualise_dag: bool) -> miette::Result<Vec<NodeIndex>> {
info!("Rendering all pages … ");
trace!("Rendering all pages … ");
if visualise_dag {
self.visualise_dag()?;
}
let mut rendered_indices = Vec::new();
// let root_indices = self.find_root_indices();
// debug!("Root indices: {:?}", root_indices);
// info!(
// "Rendering root pages: {:#?} … ",
// root_indices
// .iter()
// .map(|index| self.dag.graph()[*index].to_path_string())
// .collect::<Vec<_>>()
// );
// for root_index in root_indices {
// self.render_recursively(root_index, &mut rendered_indices)?;
// }
let indices = toposort(&self.dag.graph(), None).unwrap_or_default();
for index in indices {
self.render_page(index, false, &mut rendered_indices)?;
Expand Down Expand Up @@ -262,17 +249,16 @@ impl Build {
let root_path_difference = root_path
.strip_prefix(&current_directory)
.into_diagnostic()?;
info!("Rendering page: {:?}", root_path_difference);
debug!("{:#?}", root_page);
debug!("Rendering page: {:?}", root_path_difference);
let mut root_contexts = self.contexts.clone();
if root_path_difference.starts_with(PathBuf::from("layouts/")) {
trace!("Page is a layout page … ");
debug!("Page is a layout page … ");
let layout_object =
liquid_core::Value::Object(to_object(&root_page).into_diagnostic()?);
root_contexts.insert("layout".into(), layout_object.clone());
self.insert_layout_ancestor_contexts(root_index, &mut root_contexts)?;
} else {
trace!("Page is not a layout page … ");
debug!("Page is not a layout page … ");
let page_object = liquid_core::Value::Object(to_object(&root_page).into_diagnostic()?);
root_contexts.insert("page".into(), page_object.clone());
}
Expand All @@ -283,28 +269,16 @@ impl Build {
.parents(root_index)
.iter(&self.dag)
.collect::<Vec<_>>();
debug!("Parents: {:?}", parents);
for parent in parents {
let parent_page = &self.dag.graph()[parent.1];
let edge = self.dag.edge_weight(parent.0).unwrap();
match edge {
// // If the parent page is using this page as a layout, add its context as `page`.
EdgeType::Layout => {
// info!(
// "Page (`{}`) is a layout page (of `{}`) … ",
// root_page.to_path_string(),
// parent_page.to_path_string()
// );
// debug!("{:#?}", parent_page);
// let parent_object =
// liquid_core::Value::Object(to_object(&parent_page).into_diagnostic()?);
// root_contexts.insert("page".into(), parent_object.clone());
}
EdgeType::Layout => {}
// If the parent page is in a collection this page depends on, make note of it.
EdgeType::Collection => {
let parent_path = parent_page.to_path_string();
let collection_names = parent_page.get_collections()?.unwrap();
info!(
debug!(
"Parent page ({:?}) is in collections: {:?}",
parent_path, collection_names
);
Expand All @@ -322,7 +296,7 @@ impl Build {
}
}
// Add the collection pages to the root page's contexts.
info!("Adding any collections to page's contexts … ");
trace!("Adding any collections to page's contexts … ");
for (collection_name, collection) in collection_pages.iter_mut() {
let collection_pages: Vec<liquid::Object> = collection
.iter()
Expand All @@ -332,23 +306,12 @@ impl Build {
.unwrap()
})
.collect();
debug!("`{}` pages: {:#?}", collection_name, collection_pages);
let collection_object = to_value(&collection_pages).into_diagnostic()?;
root_contexts.insert(collection_name.clone().into(), collection_object.clone());
}
debug!(
"Contexts for `{}`: {:#?}",
root_page.to_path_string(),
root_contexts
);
let root_page = self.dag.node_weight_mut(root_index).unwrap();
if root_page.render(&root_contexts, &self.template_parser)? {
rendered_indices.push(root_index);
debug!(
"After rendering `{}`: {:#?}",
root_page.to_path_string(),
root_page
);
}

if recursive {
Expand All @@ -357,7 +320,6 @@ impl Build {
.children(root_index)
.iter(&self.dag)
.collect::<Vec<_>>();
debug!("Children: {:?}", children);
for child in children {
self.render_page(child.1, recursive, rendered_indices)?;
}
Expand Down
Loading

0 comments on commit e30340b

Please sign in to comment.