├── .gitignore
├── .prettierrc
├── package.json
├── README.md
└── cli.js
/.gitignore:
--------------------------------------------------------------------------------
1 | yarn-error.log
2 | teeny-*.tgz
3 | node_modules/
4 | pages/
5 | public/
6 | templates/
7 | static/
8 | test/
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 4,
4 | "semi": false,
5 | "singleQuote": true,
6 | "printWidth": 120
7 | }
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "teeny-cli",
3 | "version": "0.1.0-beta",
4 | "description": "A very simple static site generator",
5 | "author": "Yakko Majuri",
6 | "main": "cli.js",
7 | "license": "MIT",
8 | "bin": {
9 | "teeny": "cli.js"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/yakkomajuri/teeny.git"
14 | },
15 | "scripts": {
16 | "dev:build": "npm run dev && teeny build",
17 | "dev:develop": "npm run dev && teeny develop",
18 | "dev:init": "npm run dev && teeny init",
19 | "dev": "rm teeny-* || true && npm pack && npm i -g teeny-*.tgz -f",
20 | "lint": "prettier --write ."
21 | },
22 | "devDependencies": {
23 | "prettier": "^2.4.1"
24 | },
25 | "dependencies": {
26 | "chokidar": "^3.5.2",
27 | "front-matter": "^4.0.2",
28 | "fs-extra": "^10.0.0",
29 | "jsdom": "^18.0.0",
30 | "marked": "^4.0.10"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Teeny: A simple static site generator
2 |
3 | > **⚠️ Disclaimer:** This is a tool I built in a couple of hours to generate my personal blog [yakkomajuri.github.io](https://yakkomajuri.github.io). It doesn't do much right now and probably won't ever.
4 |
5 |
6 |
7 | ## 🏃 Quick start
8 |
9 | ```shell
10 | npm i -g teeny-cli # yarn global add teeny-cli
11 | teeny init && teeny develop
12 | ```
13 |
14 | For an example of a project using Teeny, check out my [personal blog's repo](https://github.com/yakkomajuri/yakkomajuri.github.io).
15 |
16 | ## 📖 Backstory
17 |
18 | You can read about my motivation for building Teeny on [this blog post titled "Why I built my own static site generator"](https://yakkomajuri.github.io/blog/teeny).
19 |
20 | ## 💻 Supported commands
21 |
22 | **Initialize a Teeny project in the current directory**
23 |
24 | ```shell
25 | teeny init
26 | ```
27 |
28 | **Build the static HTML files and add them to `public/`**
29 |
30 | ```shell
31 | teeny build
32 | ```
33 |
34 | **Start a local Teeny server that listens for file changes**
35 |
36 | ```shell
37 | teeny develop
38 | ```
39 |
40 | ## 📄 How it works
41 |
42 | Teeny is a super simple static site generator built to suit my needs and my needs only.
43 |
44 | All it does is generate pages based on HTML templates and Markdown content.
45 |
46 | It does very little and is strongly opinionated (_read: I was too lazy to build customization/conditional handlers_), but has allowed me to build a blog I'm happy with extremely quickly.
47 |
48 | Essentially, there are really only 2 concepts you need to think about: templates and pages.
49 |
50 | **Templates**
51 |
52 | Templates are plain HTML and should be added to a `templates/` subdirectory.
53 |
54 | They can contain an element with the id `page-content`, which is where Teeny adds the HTML generated by parsing the Markdown content.
55 |
56 | **Pages**
57 |
58 | Markdown is a first-class citizen in Teeny, so all of your website's pages are defined by a Markdown file.
59 |
60 | The file need not have any actual content though, so if you want a page to be defined purely in HTML you just need to create a template that is referenced from a page file.
61 |
62 | To specify what template a page should use, you can specify it in the frontmatter of the page, like so:
63 |
64 | ```
65 | ---
66 | template: blog
67 | ---
68 | ```
69 |
70 | In the above example, Teeny will look for a template called `blog.html`. If no template is specified, Teeny looks for a `default.html` file in `templates/` and uses that.
71 |
72 | ## 💡 Example usage
73 |
74 | Here's an example of Teeny at work.
75 |
76 | To start a Teeny project, run `teeny init`. This will create the following in your current directory:
77 |
78 | ```
79 | .
80 | ├── pages
81 | │ └── index.md
82 | ├── static
83 | │ └── main.js
84 | └── templates
85 | ├── default.html
86 | └── homepage.html
87 | ```
88 |
89 | If you then run `teeny build`, you'll end up with this:
90 |
91 | ```
92 | .
93 | ├── pages
94 | │ └── index.md
95 | ├── public
96 | │ ├── index.html
97 | │ └── main.js
98 | ├── static
99 | │ └── main.js
100 | └── templates
101 | ├── default.html
102 | └── homepage.html
103 | ```
104 |
105 | `index.md` uses the `homepage` template, and together they generate `index.html`. As is standard with other SSGs, static files are served from `public/`.
106 |
107 | You'll also notice `main.js` got moved to `public/` too. Teeny will actually take all non-template and non-page files from `pages/`, `templates/`, and `static/` and copy them to `public/`, following the same structure from the origin directory.
108 |
109 | The reason for this is that I actually didn't want to have "magic" imports, where you have to import static assets from paths that do not correspond to the actual directory structure. As a result, I decided that static files would just live inside `templates/` and `pages/` as necessary.
110 |
111 | Later I did surrender to the `static/` directory approach though, as there may be assets both pages and templates want to use. Imports from `static/` are "magic", meaning you need to think about the output of `teeny build` for them to work.
112 |
113 | The last command that Teeny supports is `teeny develop`. This creates an HTTP server to server files from the `public/` subdirectory.
114 |
115 | It listens for changes to the files and updates the static files on the fly (naively, by just rebuilding everything each time it detects a change).
116 |
117 | ## 🔮 Potential future improvements
118 |
119 | I want to keep Teeny as tiny as possible. I deliberately put all the code in one file as a reminder to myself that this is supposed to just be a simple tool for me to build simple static blogs quickly.
120 |
121 | However, it could use a few "developer experience" upgrades, like an optimized approach to `teeny develop` instead of naively rebuilding everything, as well as some better customization options.
122 |
--------------------------------------------------------------------------------
/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const { JSDOM } = require('jsdom')
4 | const fs = require('fs-extra')
5 | const marked = require('marked')
6 | const http = require('http')
7 | const chokidar = require('chokidar')
8 | const fm = require('front-matter')
9 |
10 | // attributes: { template: "custom.html" }
11 | // body: "# My normal markdown ..."
12 | const scriptArgs = process.argv.slice(2)
13 | const command = scriptArgs[0]
14 |
15 | switch (command) {
16 | case 'build':
17 | build()
18 | break
19 | case 'develop':
20 | develop(scriptArgs[1] ? Number(scriptArgs[1]) : 8000)
21 | break
22 | case 'init':
23 | init()
24 | break
25 | default:
26 | console.log(`Command 'teeny ${command}' does not exist.`)
27 | process.exit(1)
28 | }
29 |
30 | async function build() {
31 | await fs.emptyDir('public/')
32 |
33 | await safeExecute(
34 | async () =>
35 | await fs.copy('templates/', 'public/', { filter: (f) => !f.startsWith('.') && !f.endsWith('.html') })
36 | )
37 | await safeExecute(
38 | async () => await fs.copy('pages/', 'public/', { filter: (f) => !f.startsWith('.') && !f.endsWith('.md') })
39 | )
40 | await safeExecute(async () => await fs.copy('static/', 'public/'), { filter: (f) => !f.startsWith('.') })
41 |
42 | await processDirectory('pages')
43 | }
44 |
45 | async function processDirectory(directoryPath) {
46 | let contents = await fs.readdir(`${directoryPath}/`)
47 | const processPagePromises = []
48 | for (const element of contents) {
49 | const isDirectory = (await fs.lstat(`${directoryPath}/${element}`)).isDirectory()
50 | if (isDirectory) {
51 | await processDirectory(`${directoryPath}/${element}`, processPagePromises)
52 | continue
53 | }
54 | processPagePromises.push(processPage(`${directoryPath}/${element}`))
55 | }
56 | await Promise.all(processPagePromises)
57 | }
58 |
59 | async function develop(port) {
60 | await build()
61 | const server = startServer(port)
62 | const watcher = chokidar.watch(['pages/', 'static/', 'templates/']).on('change', async (path, _) => {
63 | console.log(`Detected change in file ${path}. Restarting development server.`)
64 | server.close()
65 | await watcher.close()
66 | await develop(port)
67 | })
68 | }
69 |
70 | async function init() {
71 | await safeExecute(async () => await fs.mkdir('pages/'))
72 | await safeExecute(async () => await fs.mkdir('static/'))
73 | await safeExecute(async () => await fs.mkdir('templates/'))
74 |
75 | const examplePage = `---\ntemplate: homepage\n---\n# Hello World`
76 | const exampleTemplate = `
My first Teeny page