@terrymooreii/sia 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ name: Build and GH-Page Deploy
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ - name: Checkout
17
+ uses: actions/checkout@v3
18
+ with:
19
+ persist-credentials: false
20
+
21
+ - name: Install
22
+ run: npm install
23
+
24
+ - name: Build
25
+ run: |
26
+ npm run build
27
+
28
+ - name: Deploy to GH Page
29
+ uses: JamesIves/github-pages-deploy-action@4.1.1
30
+ with:
31
+ branch: gh-pages
32
+ folder: public
33
+ ssh-key: ${{ secrets.DEPLOY_KEY }}
@@ -0,0 +1,3 @@
1
+ # Ignore artifacts:
2
+ public
3
+ node_modules
package/.prettierrc ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "printWidth": 120,
3
+ "tabWidth": 2,
4
+ "jsxBracketSameLine": true,
5
+ "singleQuote": true,
6
+ "semi": false,
7
+ "trailingComma": "es5"
8
+ }
package/lib/build.js ADDED
@@ -0,0 +1,20 @@
1
+ import config from './readconfig.js'
2
+ import { parseContent, generateBlogListPage } from './parse.js'
3
+ import { mkdir, cpdir } from './helpers.js'
4
+ import path from 'path'
5
+ import { generateRSS } from './rss.js'
6
+ const { app } = config
7
+
8
+ export const build = () => {
9
+ mkdir(path.join(app.public))
10
+
11
+ const posts = parseContent()
12
+ generateBlogListPage(posts)
13
+ generateRSS(posts, app.feed.count)
14
+
15
+ cpdir(path.join(app.src, app.css), path.join(app.public, app.css))
16
+ cpdir(path.join(app.src, app.js), path.join(app.public, app.js))
17
+ cpdir(path.join(app.src, app.images), path.join(app.public, app.images))
18
+ cpdir(path.join(app.src, app.assets), path.join(app.public, app.assets))
19
+ console.log('Build success')
20
+ }
package/lib/helpers.js ADDED
@@ -0,0 +1,37 @@
1
+ import fs from 'fs'
2
+
3
+ export const formatDate = (postDate) => {
4
+ const d = new Date(postDate)
5
+ const date = d.toLocaleDateString('en-us', {
6
+ year: 'numeric',
7
+ month: 'long',
8
+ day: 'numeric',
9
+ })
10
+ const time = d.toLocaleTimeString()
11
+ return {
12
+ datetime: `${date} ${time}`,
13
+ date,
14
+ year: d.getFullYear(),
15
+ }
16
+ }
17
+
18
+ export const mkdir = (path) => {
19
+ if (!fs.existsSync(path)) {
20
+ fs.mkdirSync(path)
21
+ return true
22
+ }
23
+ return false
24
+ }
25
+
26
+ export const cpdir = (src, dest) => {
27
+ if (fs.existsSync(src)) {
28
+ mkdir(dest)
29
+ fs.cpSync(src, dest, { recursive: true })
30
+ return true
31
+ }
32
+ return false
33
+ }
34
+
35
+ export const writefile = (file, content, options = { flags: 'w+' }) => {
36
+ fs.writeFileSync(file, content, options)
37
+ }
package/lib/index.js ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { build } from './build.js'
4
+ import { create } from './new.js'
5
+ import { init } from './init.js'
6
+
7
+ const [, , cmd, type, folder] = process.argv
8
+
9
+ switch (cmd) {
10
+ case init:
11
+ init()
12
+ break
13
+ case 'build':
14
+ build()
15
+ break
16
+ case 'new':
17
+ create(type, folder)
18
+ break
19
+ default:
20
+ console.log('Missing command')
21
+ break
22
+ }
package/lib/init.js ADDED
@@ -0,0 +1,8 @@
1
+ import path from 'path'
2
+ import { cpdir } from './helpers.js'
3
+
4
+ // copy source
5
+ export const init = () => {
6
+ // TODO Make this smarter
7
+ // cpdir(path.join('../', 'templates', 'src'), process.cwd())
8
+ }
@@ -0,0 +1,33 @@
1
+ import config from './readconfig.js'
2
+ import markdownit from 'markdown-it'
3
+ import hljs from 'highlight.js'
4
+
5
+ import javascript from 'highlight.js/lib/languages/javascript'
6
+ import bash from 'highlight.js/lib/languages/bash'
7
+ hljs.registerLanguage('javascript', javascript)
8
+ hljs.registerLanguage('bash', bash)
9
+
10
+ const md = markdownit({
11
+ html: true,
12
+ linkify: true,
13
+ typographer: true,
14
+ breaks: true,
15
+ highlight: function (str, lang) {
16
+ if (config?.app?.markdown?.highlightjs && lang && hljs.getLanguage(lang)) {
17
+ try {
18
+ return hljs.highlight(str, { language: lang }).value
19
+ } catch (__) {}
20
+ }
21
+
22
+ return ''
23
+ },
24
+ ...config?.app?.markdown?.markdownitOptions,
25
+ })
26
+
27
+ if (config?.app?.markdown?.plugins) {
28
+ config.app.markdown.plugins.forEach((plugin) => {
29
+ md.use(plugin)
30
+ })
31
+ }
32
+
33
+ export default md
package/lib/new.js ADDED
@@ -0,0 +1,46 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import config from './readconfig.js'
4
+ import { mkdir } from './helpers.js'
5
+ import slugify from 'slugify'
6
+
7
+ const { app } = config
8
+
9
+ export const create = (type, folder) => {
10
+ if (!type || !folder) {
11
+ console.log('Must specify a type and folder')
12
+ process.exit(1)
13
+ }
14
+
15
+ const slugified = slugify(folder, {
16
+ replacement: '-',
17
+ lower: true,
18
+ strict: true,
19
+ locale: 'en',
20
+ trim: true,
21
+ })
22
+
23
+ if (!['page', 'post'].includes(type)) {
24
+ console.log('Invalid type. post or page')
25
+ process.exit(1)
26
+ }
27
+
28
+ const date = new Date()
29
+ const createdAt = date.toISOString()
30
+
31
+ const template = `---
32
+ created_at: ${createdAt}
33
+ title: ${slugified}
34
+ description:
35
+ image:
36
+ template: ${type}
37
+ ---
38
+
39
+ Add ${type} content here
40
+
41
+ `
42
+
43
+ const root = path.join(app.src, app.content, slugified)
44
+ mkdir(root)
45
+ fs.writeFileSync(path.join(root, `index.md`), template)
46
+ }
package/lib/parse.js ADDED
@@ -0,0 +1,94 @@
1
+ import config from './readconfig.js'
2
+ import matter from 'gray-matter'
3
+ import fs from 'fs'
4
+ import path from 'path'
5
+ import nunjucks from 'nunjucks'
6
+ import md from './markdown.js'
7
+
8
+ import { formatDate, mkdir, writefile } from './helpers.js'
9
+
10
+ const { app, site } = config
11
+
12
+ nunjucks.configure(path.join(app.src, app.partials), {
13
+ autoescape: false,
14
+ })
15
+
16
+ const render = (srcFolder, publicFolder, file) => {
17
+ const parsed = matter.read(path.join(srcFolder, file))
18
+ const { data: page, content } = parsed
19
+ const fileName = path.parse(file).name
20
+ const fileNameHtml = `${fileName}.html`
21
+
22
+ if (!page.template) return
23
+
24
+ page.date = formatDate(page.created_at)
25
+
26
+ // technically slug is the permalink or uri
27
+ // this removes the public folder path to reveal the slug path
28
+ page.slug = path.join(publicFolder).replace(app.public, '')
29
+ // index pages will render automatically in folder but if the file
30
+ // name is not then make sure it included in the permalink
31
+ if (fileNameHtml !== 'index.html') {
32
+ page.slug = path.join(page.slug, fileNameHtml)
33
+ }
34
+
35
+ const html = md.render(content)
36
+ const rendered = nunjucks.render(`${page.template}${app.template_ext}`, {
37
+ content: html,
38
+ page,
39
+ site,
40
+ })
41
+
42
+ writefile(path.join(publicFolder, fileNameHtml), rendered)
43
+
44
+ // collect all the page attibutes for blog posts
45
+ if (page.template === 'post') {
46
+ return { ...page }
47
+ }
48
+ }
49
+
50
+ const getAllFiles = (srcPath, posts) => {
51
+ // take the source path and generate the public path equivalent
52
+ const publicPath = srcPath.replace(path.join(app.src, app.content), app.public)
53
+
54
+ mkdir(publicPath)
55
+
56
+ const files = fs.readdirSync(srcPath)
57
+
58
+ files.forEach((file) => {
59
+ const filePath = path.join(srcPath, file)
60
+
61
+ if (fs.statSync(filePath).isDirectory()) {
62
+ // recursively look at files
63
+ getAllFiles(filePath, posts)
64
+ } else {
65
+ // Process markdown files and render the nunjucks template
66
+ if (path.extname(file) === '.md') {
67
+ const post = render(srcPath, publicPath, file)
68
+ if (post) {
69
+ posts.push(post)
70
+ }
71
+ } else {
72
+ // file is not a markdown file so just copy it to its public folder
73
+ fs.copyFileSync(path.join(srcPath, file), path.join(publicPath, file))
74
+ }
75
+ }
76
+ })
77
+ return posts
78
+ }
79
+
80
+ export const parseContent = () => {
81
+ return getAllFiles(path.join(app.src, app.content), [], '/').filter((o) => o != null) // make sure there are no undefined items
82
+ }
83
+
84
+ export const generateBlogListPage = (posts) => {
85
+ const rendered = nunjucks.render(app.posts_template, {
86
+ posts,
87
+ site,
88
+ })
89
+
90
+ const blogListDirectory = path.join(app.public, app.blog_list)
91
+
92
+ mkdir(blogListDirectory)
93
+ writefile(path.join(blogListDirectory, 'index.html'), rendered)
94
+ }
@@ -0,0 +1,16 @@
1
+ import { findUp } from 'find-up'
2
+
3
+ async function getConfig() {
4
+ try {
5
+ const siarc = await findUp('siarc.js')
6
+ const { default: config } = await import(siarc)
7
+ return config
8
+ } catch (err) {
9
+ console.log('Unable to find and load the siarc.js file.')
10
+ }
11
+ }
12
+ const config = await getConfig()
13
+ export default config ?? {
14
+ app: {},
15
+ site: {},
16
+ }
package/lib/rss.js ADDED
@@ -0,0 +1,63 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { Feed } from 'feed'
4
+ import config from './readconfig.js'
5
+ const { app, site } = config
6
+
7
+ const feed = new Feed({
8
+ title: site.blog_title,
9
+ description: site.blog_description,
10
+ id: site.blog_url,
11
+ link: site.blog_url,
12
+ language: 'en', // optional, used only in RSS 2.0, possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
13
+ image: site.blog_image,
14
+ favicon: `${site.blog_url}/assets/favicon.ico`,
15
+ copyright: `All rights reserved ${new Date().getFullYear()}, ${site.author}`,
16
+ feedLinks: {
17
+ json: `${site.blog_url}/feed.json`,
18
+ atom: `${site.blog_url}/atom.xml`,
19
+ },
20
+ author: {
21
+ name: site.author,
22
+ email: site.email_address,
23
+ },
24
+ })
25
+
26
+ const writeFeed = (name, content) => {
27
+ fs.writeFileSync(path.join(app.public, name), content, {
28
+ flag: 'w+',
29
+ })
30
+ }
31
+
32
+ export const generateRSS = (posts, count = 10) => {
33
+ posts.splice(0, count).forEach((post) => {
34
+ feed.addItem({
35
+ title: post.title,
36
+ id: `${site.blog_url}${post.slug}`,
37
+ link: `${site.blog_url}${post.slug}`,
38
+ description: post.description,
39
+ content: post.content,
40
+ author: [
41
+ {
42
+ name: site.author,
43
+ email: site.email_address,
44
+ link: site.blog_url,
45
+ },
46
+ ],
47
+ date: post.created_at,
48
+ image: `${site.blog_url}/${post.image}`,
49
+ })
50
+ })
51
+
52
+ if (app.feed.rss2) {
53
+ writeFeed('rss.xml', feed.rss2())
54
+ }
55
+
56
+ if (app.feed.atom1) {
57
+ writeFeed('atom.xml', feed.atom1())
58
+ }
59
+
60
+ if (app.feed.json1) {
61
+ writeFeed('feed.json', feed.json1())
62
+ }
63
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@terrymooreii/sia",
3
+ "version": "1.0.1",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "lint": "npx prettier . --write"
8
+ },
9
+ "type": "module",
10
+ "author": "",
11
+ "license": "ISC",
12
+ "bin": {
13
+ "sia": "./lib/index.js"
14
+ },
15
+ "dependencies": {
16
+ "feed": "^4.2.2",
17
+ "find-up": "^7.0.0",
18
+ "forever": "^4.0.3",
19
+ "gray-matter": "^4.0.3",
20
+ "highlight.js": "^11.9.0",
21
+ "markdown-it": "^14.0.0",
22
+ "nunjucks": "^3.2.4",
23
+ "prettier": "3.2.1",
24
+ "slugify": "^1.6.6"
25
+ }
26
+ }
package/readme.md ADDED
@@ -0,0 +1,100 @@
1
+ # Sia Micro blog
2
+
3
+ Sia is static site generator
4
+
5
+ ## Installation
6
+
7
+ ```
8
+ npm install --save-dev @terrymooreii/sia
9
+ ```
10
+
11
+ After installation run the following command in a new node repo
12
+
13
+ ```
14
+ sia init
15
+ ```
16
+
17
+ This will install the nunjucks template files, some base css and a shell `index.html`
18
+
19
+ It also add the `siarc.js` config file to the root of your project.
20
+
21
+ The `siarc.js` file contains two sections. One for configuing your site information. The second is to configure the app's folders, numjucks template, and markdown configutation.
22
+
23
+
24
+ ## Adding pages and posts
25
+
26
+ The easist way to add a new page or a post to your site is to run the following command
27
+
28
+ ```
29
+ sia new post new-post-name
30
+ ```
31
+
32
+ This will create a new folder in the `content` folder called `new-post-name` and add a shell `index.html`
33
+
34
+ You can do the same thing to create a new page
35
+
36
+ ```
37
+ sia new post new-post-name
38
+ ```
39
+
40
+ The difference between pages and posts is that during the build process sia keeps tack of all posts and generate a post list page at the url `/blog`
41
+
42
+ New pages and post will have config area at the top that tell sia how to handle the page and post. All properties expect the image is required. The image is used to generate the page's `og:image`. If its not present we will use the `blogs_image`.
43
+
44
+ ```
45
+ ---
46
+
47
+ template: post
48
+ title: Page title
49
+ created_at: 2024-01-12 16:00:00-5:00
50
+ description: Testing out some markdown
51
+ ---
52
+ ```
53
+
54
+ - `template` is the nunjucks template located in `src/_partials`
55
+ - `title` is the post title and used in the blog posts list page
56
+ - `create_at` is the date of the post
57
+ - `description` a short description of the post
58
+
59
+
60
+ ## Build
61
+
62
+ To build the site run
63
+
64
+ ```
65
+ sia build
66
+ ```
67
+
68
+ This will parse all markdown files and then numjucks.
69
+ The default output folder is `/public`
70
+
71
+ The build command will also copy all `assets`, `js` and `css` to the `/public` folder. If a post or a page folder contain other files other than markdown then those files will also get moved to the `/public/<folder>`. This makes it easy to organize a single page or posts with custom js, css, or images.
72
+
73
+ All markdown files will get parsed with `markdown-it` and you can add additional `markdown-it` plugins in the `siarc.js` file.
74
+
75
+ ## Local developement
76
+
77
+ Currently these are the apps that I use on my blog for local development and building
78
+
79
+ ```
80
+ "scripts": {
81
+ "serve": "npx http-server public",
82
+ "watch": "forever --watchDirectory ./src -w ./node_modules/@terrymooreii/sia/lib/index.js build",
83
+ "dev": "concurrently --kill-others \"npm run watch\" \"npm run serve\"",
84
+ "clean": "rm -rf public",
85
+ "build": "sia build"
86
+ },
87
+ ```
88
+
89
+ ```
90
+ npm install forever concurrently http-server
91
+ ```
92
+
93
+ coming soon will be a simple way to run a local web server to see live updates to posts and pages as you work via the `sia` command.
94
+
95
+ ## Templates and site configuration
96
+
97
+ All `njk` files are in a simple default state to generate a simple and clean website. You can modifiy the html in these file to customer your site to look how you want.
98
+
99
+ If you make something cool please let me know.
100
+
@@ -0,0 +1,53 @@
1
+ import url from 'url'
2
+ import path from 'path'
3
+
4
+ const __filename = url.fileURLToPath(import.meta.url)
5
+ const __dirname = path.dirname(__filename)
6
+
7
+ export default {
8
+ // User Config
9
+ site: {
10
+ blog_url: process.env.BLOG_URL || '',
11
+ blog_title: '',
12
+ blog_description: '',
13
+ blog_image: '',
14
+ author: '',
15
+ email_address: '',
16
+ highlightjs_theme: 'atom-one-dark.min',
17
+ nav: [
18
+ {
19
+ title: 'Home',
20
+ href: '/',
21
+ },
22
+ {
23
+ title: 'Blog',
24
+ href: '/blog',
25
+ },
26
+ ],
27
+ },
28
+
29
+ // App config
30
+ app: {
31
+ public: `${__dirname}/public`,
32
+ src: `${__dirname}/src`,
33
+ partials: '_partials',
34
+ template_ext: '.njk',
35
+ content: 'content',
36
+ css: 'css',
37
+ js: 'js',
38
+ assets: 'assets',
39
+ images: 'imgs',
40
+ posts_template: 'posts.njk',
41
+ blog_list: 'blog',
42
+ feed: {
43
+ count: 10,
44
+ rss2: true,
45
+ atom1: true,
46
+ json1: true,
47
+ },
48
+ markdown: {
49
+ highlightjs: true,
50
+ plugins: [],
51
+ },
52
+ },
53
+ }
@@ -0,0 +1 @@
1
+ <footer>Made with Sia</footer>
@@ -0,0 +1,35 @@
1
+ <head>
2
+ <meta charset="UTF-8" />
3
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4
+ <title>{{ page.title if page.title else site.blog_title }}</title>
5
+ <meta name="author" content="{{ site.author }}" />
6
+ <meta
7
+ name="description"
8
+ content="{{ page.description if page.description else site.blog_description }}"
9
+ />
10
+ {% if page.slug %}
11
+ <meta property="og:url" content="{{site.blog_url}}{{ page.slug }}" />
12
+ {% else %}
13
+ <meta property="og:url" content="{{site.blog_url}}" />
14
+ {% endif %}
15
+ <meta property="og:title" content="{{ page.title if page.title else site.blog_title }}" />
16
+ <meta property="og:description" content="{{ page.description if page.description else site.blog_description }}" />
17
+ <meta property="og:image" content="{{ page.post_image }}" />
18
+
19
+ <link type="application/rss+xml" rel="alternate" href="{{ site.blog_url }}/rss.xml" title="{{ site.blog_title }} - RSS Feed" />
20
+ <link type="application/atom+xml" rel="alternate" href="{{ site.blog_url }}/atom.xml" title="{{ site.blog_title }} - Atom Feed" />
21
+ <link type="application/feed+json" rel="alternate" href="{{ site.blog_url }}/feed.json" title="{{ site.blog_title }} - JSON Feed" />
22
+
23
+ <link rel="icon" type="image/x-icon" href="{{ site.blog_url }}/assest/favicon.ico">
24
+ <link rel="apple-touch-icon" sizes="180x180" href="{{ site.blog_url }}/assets/apple-touch-icon.png">
25
+ <link rel="icon" type="image/png" sizes="32x32" href="{{ site.blog_url }}/assets/favicon-32x32.png">
26
+ <link rel="icon" type="image/png" sizes="16x16" href="{{ site.blog_url }}/assets/favicon-16x16.png">
27
+ <link rel="manifest" href="{{ site.blog_url }}/assets/site.webmanifest">
28
+
29
+ <link rel="stylesheet" type="text/css" href="{{ site.blog_url }}/css/theme.css" />
30
+ <link rel="stylesheet" type="text/css" href="{{ site.blog_url }}/css/markdown.css" />
31
+ <link
32
+ rel="stylesheet"
33
+ href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/{{ site.highlightjs_theme }}.css"
34
+ />
35
+ </head>
@@ -0,0 +1 @@
1
+ <!-- add content here to add it to the top of each page/post -->
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE >
2
+ <html lang="en-US">
3
+ {% include "_head.njk" %}
4
+ <body class="markdown-body" data-theme="dark">
5
+ {% include "_nav.njk" %}
6
+ <main class="content">
7
+ {% include "_header.njk" %}
8
+ {% block content %}{% endblock %}
9
+ {% include "_footer.njk" %}
10
+ </main>
11
+ </body>
12
+ </html>
@@ -0,0 +1,12 @@
1
+ <nav>
2
+ <div class="content nav-container">
3
+ <h1 class="blog-title"><a href="{{ site.blog_url }}">{{ site.blog_title }}</a></h1>
4
+ <ul>
5
+ {% for link in site.nav %}
6
+ <li>
7
+ <a href="{{ site.blog_url }}{{ link.href }}" title="{{ link.title }}">{{ link.title }}</a>
8
+ </li>
9
+ {% endfor %}
10
+ </ul>
11
+ </div>
12
+ </nav>
@@ -0,0 +1,5 @@
1
+ {%extends "_layout.njk" %}
2
+
3
+ {% block content %}
4
+ {{ content }}
5
+ {% endblock %}
@@ -0,0 +1,13 @@
1
+ {%extends "_layout.njk" %}
2
+
3
+ {% block content %}
4
+ <article>
5
+ <header>
6
+ <h1 class="post-title">{{ page.title }}</h1>
7
+ <time datetime="{{page.date.datetime}}" class="date">
8
+ {{ page.date.datetime }}
9
+ </time>
10
+ </header>
11
+ <p>{{ content }}</p>
12
+ </article>
13
+ {% endblock %}