Tastic the Fantastic, Semi-Automatic React Static site generator
npm install tasticTastic is a Static-Site Generator built around building simple sites from markdown content. Relative to other Static-Site generators, tastic should
- Be simple to get started with
- Be simple to build on top of
- Minimize layout and focus on the content itself
The space of static-site generators is pretty wide, and probably doesn't need another entry, so I thought I'd make my own. I tried Jekyll, and with minimal Ruby struggled to write good plugins, and eventually a gem went out of date. I tried Hugo and it was fine, but again plugins were a bit annoying. I did not try Astro or Gatsby, which might be more my speed - I spend a lot of time in React, so those might be sound starting points.
(Originally it was rendered with React via Mardown-To-JSX, and I'm planning on using React for making the frontend reactive in the future, but for now it is just _inspired_ by React and JSX)
1. Put markdown content in a directory.
2. Run tastic watch .
3. Profit
tastic watch will build the contents into a static site in .tastic/out, set up listeners to watch the directory and trigger a rebuild on file changes, and then run a local server for the contents of the directory. For serving files on a real website you should just do tastic build and have some real server serving those files
The build process for tastic is to iterate through relevant directories, looking for files.
- First it will iterate through any configured themes you have installed under /.tastic/themes/. If no explicit themes have been configured but there are folders there, they will be iterated through in alphabetic order
- Then it will iterate through your layout folder in /.tastic/layout. This is broadly where you should put any files relevant to the _layout_ of your website that is not content
- Then it will iterate through the directory, ignoring the .tastic subdir if it exists
Each of these directories are fundamentally treated the same other than the fact that the _priority_ of files align with the above iteration order - so between the files /.tastic/themes/myCoolTheme/index.html, /.tastic/layout/index.html and /index.html, the first will be overwritten by the second and the third will override both the previous.
Within these directories,
- files with any extension _other_ than .html, .md, and .tastic will be copied to the out directory at their location relative to their dir (so /.tastic/themes/myCoolTheme/foo/bar/baz.png will be copied to /.tastic/out/foo/bar/baz.png)
- .html, .md, and .tastic files will be processed as _TasticFiles_, which can go in a few different directions with a lot of overlap. A frontmatter section is parsed from each of these files containing metadata about the file, and in particular the tastic field is used to determine how exactly the file will be handled going forwards. There are 4 types of _TasticFiles_:
1. _post_ files (and any .md file will default to this) will be rendered in 5 stages:
1. Template expansion
2. Conversion to HTML
3. Evaluation of components in HTML
4. Wrapping with the special component wrapper
5. Optionally prettified, and renamed to end in .html
2. _html_ files (and any .html file will default to this) will be treated the same without wrapping:
1. Template expansion
2. Evaluation of components in HTML
3. Optionally prettified
3. _other_ files will not be assumed to be HTML, and will only be template expanded
4. _component_ files will be treated similarly to React components, will not produce a file of their own but can be referenced by other tastic files - e.g. might be used to render a component defined in the foo.tastic file. These files will also undergo:
1. Template expansion
2. Conversion to HTML
3. Evaluation of components in HTML
Frontmatter is parsed from the beginning of .html, .md, and .tastic files using the front-matter npm library, essentially just extracting any YAML written between ---\n bounds at the top of the file (and ignoring it if there is none). Arbitrary attributes can be passed, and the attributes will be accessible within the post variable in the Template String evaluation step, so for example:
```
---
foo: bar
---
${ post.foo }
will evaluate to
bar
This information is also accessible within the
site variable in contexts which have access to it, so your wrapper could do`html
${ site.postList.map(({foo}) => foo) }
`to get a list of the foo frontmatter of all files (more about template evaluation in the next section).
Lastly, frontmatter can be used to specify options for how tastic will treat the given file by using the
tastic key. This can contain an object with optional fields type to specify which of the four tastic file types this is and out to override the default path of the output file. For example, if you have a file foo.tastic`
---
tastic:
type: other
out: foo.json
---
${ site.postList.map(({foo}) => foo) }
`then the file foo.json will be created from the evaluated template string. As a shorthand, you can just pass the type instead of a full object, as in
`
---
tastic: component
---
`$3
The bodies of all Tastic files are evaluated as JS template literals. This means you can put arbitrary javascript in between
${ and } and it will be evaluated, e.g. hello ${ 1+2 } => hello 3- Is this terribly secure? Probably not? Make sure you are only running code that you trust! But it is at least simple to understand
- The code will be evaluated in the context of a Function declaration, in strict mode, but that doesn't do much
- If the value between the curly braces evaluates to
null, undefined, and false, it will just be ignored instead of being rendered as the literal strings "null", "undefined", or "false" - both for JSX parity and because you probably don't want that
- If the value between the curly braces evaluates to an array, it will be joined with spaces instead of commas
- If the value between the curly braces evaluates to a function, it will be executed without arguments and the returned result will be cast to a string and output
- Different tastic file types will have access to different variables
- _post_ files have access to the variable post of type InitialPostMetadata. This will include all of the metadata passed in the frontmatter, as well as a _file field containing metadata about the file
- _html_ and _other_ files will not have access to a post variable, but will have access to the site variable of type SiteMetada, which contains a traversable tree of all of the parsed posts across the entire site
- _component_ files will have access to whatever variables are exposed in the thing that includes them, so it depends on whether they are referenced by a _post_ or not. They also have access to the props variable, which will include any string attributes passed by the parent, and the children variable, which will evaluate to the compiled string of any child elements that might be passed it
- The wrapper component and any component descendent will have access to both the site variable and an expanded post variable of type PostMetadata which includes the fully rendered post, it's table-of-contents, and helper navigation functions$3
All _post_ files will be converted to HTML by first parsing their entirety with the node-html-parser package, and then any non-empty top-level text-nodes will be treated as markdown and converted into html via marked. This means something like this
`md
---
some: ["front", "matter"]
---h1
h2
_italics_
`Will be converted into the HTML
`html
h1
h2
italics
`$3
Any component referenced in the output of the HTML conversion step or in a given HTML file will be evaluated more or less as if it was included in the parent, with the additional
props and children variables.A component like foo.html:
`md
${ post.title } - ${props.label}
${ children }
The end
`when included by a given post:
`html
---
title: Title
---Lorem Ipsem
`would be evaluated as
`html
Title - label
Lorem Ipsem
The end
`If a component is declared at a given relative path, it will only be valid for files at that relative path or below, so
/post.md would not see the existence of /docs/component.html, while /docs/post.md would.If in the evaluation of a file, a component is valid from multiple locations, the component is overridden with the same priorities as regular files are overridden, as discussed at the top of the file - so
/.tastic/layout/component.html would override /.tastic/themes/foo/component.html$3
The wrapper component (just a regular component with the name of _wrapper_) is a bit of a special case, in that instead of manually being referenced, if defined (and it almost certainly should be defined) it ends up being the wrapper provided around each _post_ file. It should looks something like
`html
${ children }
`It can and should reference other components to remain simple
TODO
Things I would like to and likely will do:
- [ ] Theme-management as part of CLI
- [ ] Dynamic theme and param management within
watch command, involving something like
- including some sort of process.env variable in the site variable which will let a component evaluate whether it is being run in the watch environment
- using that to load a JS file in the default wrapper only when running in the watch environment
- using that to add a little UI that can pop out and be used to send a few small API calls to the watch server
- which will then adjust settings and rebuild
- [ ] Include a default theme as part of the release?
- [ ] Just generally more themes, tastic-themes repo
- [ ] Get markdown with inline html to render appropriately (right now, html breaks markdown into chunks)
- [ ] Allow for turning off katex, mermaid, and syntax highlighting
- [ ] Expose more options for adding hooks to marked and options for highlight.js and katex
- [ ] Nice ergonomics around getting post lists, filtered, sorted, nested, paginated
- [ ] Get top-heading (first html element a heading?) and excerpt (first ~80 words of first paragraph?)
- [ ] Allow meta components that do things like declare other output files ( ?) or remove this one ( ?)
- [ ] Render as React app
- [ ] tastic init` command to add .tastic directory, install themes, optionally configure github action