├── .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 | Buy Me a Coffee at ko-fi.com 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