basebrick-bricklayer 1.0.0
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.
- package/README.md +90 -0
- package/bin/bricklayer.js +6 -0
- package/history.md +10 -0
- package/index.js +254 -0
- package/package.json +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Bricklayer
|
|
2
|
+
|
|
3
|
+
Bricklayer is the core static site generator (JamBrick) for BaseBrick. It is designed to consume Nunjucks templates and Markdown with frontmatter, fetch content from a remote CMS (like Sonic.js), and generate a fully static, modular JAMstack build.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Eleventy-Style Build Pipeline**: Uses Nunjucks templating and gray-matter frontmatter for modular UI rendering.
|
|
8
|
+
- **Dynamic Content Fetching**: Automatically fetches content from a headless CMS based on configuration (`components/generic.json`) and generates individual pages for each content item with clean, SEO-friendly slugs.
|
|
9
|
+
- **Production Optimization**: In production mode (`--prod`), it minifies HTML and CSS, and compresses image assets using `sharp`.
|
|
10
|
+
- **Tailwind Integration**: Seamlessly builds Tailwind CSS using `@tailwindcss/cli`, supporting development and production (minified) builds.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Since Bricklayer is a local module within the BaseBrick ecosystem, you can link it directly:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
cd bricklayer
|
|
18
|
+
npm install
|
|
19
|
+
npm link
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
You can use Bricklayer via its CLI interface or import it directly as a Node module in your build scripts.
|
|
25
|
+
|
|
26
|
+
### CLI
|
|
27
|
+
|
|
28
|
+
To run a standard development build (compiles templates, copies assets, builds Tailwind without minification):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bricklayer
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
To run a production build (minifies HTML/CSS, compresses images):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bricklayer --prod
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Module
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
import { buildSite } from 'bricklayer';
|
|
44
|
+
|
|
45
|
+
buildSite({
|
|
46
|
+
isProd: process.env.NODE_ENV === 'production',
|
|
47
|
+
cwd: process.cwd(),
|
|
48
|
+
// Optional overrides for directories:
|
|
49
|
+
// srcDir: 'src',
|
|
50
|
+
// includesDir: 'src/_includes',
|
|
51
|
+
// publicDir: 'public',
|
|
52
|
+
// cssInput: 'src/assets/tailwind/input.css',
|
|
53
|
+
// cssOutput: 'public/assets/css/style.css',
|
|
54
|
+
// assetsSrc: 'src/assets',
|
|
55
|
+
// assetsDest: 'public/assets'
|
|
56
|
+
}).catch(console.error);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## CMS Configuration
|
|
60
|
+
|
|
61
|
+
To enable remote CMS fetching, place a `generic.json` configuration file in `src/components/generic.json`:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"name": "sonic.js",
|
|
66
|
+
"apiUrl": "/v1/posts",
|
|
67
|
+
"productionUrl": "https://cms.basebrick.xyz",
|
|
68
|
+
"locaLUrl": "http://localhost:3018",
|
|
69
|
+
"indexPage": "blog",
|
|
70
|
+
"postPage": "post"
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- `indexPage`: The template (e.g., `blog.njk`) where an array of posts will be injected under the `posts` variable.
|
|
75
|
+
- `postPage`: The template (e.g., `post.njk`) that will be used to generate individual pages for each item fetched from the API. The generated HTML will be placed in a directory matching the `postPage` name (e.g., `public/post/my-slug.html`).
|
|
76
|
+
|
|
77
|
+
## Directory Structure
|
|
78
|
+
|
|
79
|
+
Bricklayer expects the following default structure (overridable via options):
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
├── src/
|
|
83
|
+
│ ├── _includes/ # Nunjucks layouts
|
|
84
|
+
│ ├── assets/ # Static assets (images, fonts, tailwind)
|
|
85
|
+
│ ├── components/ # Component config (e.g., generic.json)
|
|
86
|
+
│ ├── index.njk # Pages
|
|
87
|
+
│ └── post.njk
|
|
88
|
+
├── public/ # Build output
|
|
89
|
+
└── package.json
|
|
90
|
+
```
|
package/history.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Version History
|
|
2
|
+
|
|
3
|
+
## 1.0.0 (May 2026)
|
|
4
|
+
|
|
5
|
+
- **Initial Extraction**: Extracted the build pipeline from the BaseBrick frontend into a standalone, reusable Node module (`bricklayer`).
|
|
6
|
+
- **JAMstack Architecture**: Transitioned from client-side dynamic fetching to a fully static build process.
|
|
7
|
+
- **Dynamic Slugs**: Implemented Eleventy-style frontmatter-driven slug generation for SEO-friendly URLs.
|
|
8
|
+
- **Asset Optimization**: Added conditional production build steps including HTML/CSS minification and image compression via `sharp`.
|
|
9
|
+
- **CMS Integration**: Standardized remote CMS connectivity via `generic.json`, supporting dynamic generation of listing and detail pages from a remote source.
|
|
10
|
+
- **CLI Tooling**: Added a `bricklayer` bin script to run builds easily from the terminal.
|
package/index.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import nunjucks from 'nunjucks';
|
|
5
|
+
import matter from 'gray-matter';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
|
|
8
|
+
export async function buildSite(options = {}) {
|
|
9
|
+
const cwd = options.cwd || process.cwd();
|
|
10
|
+
const isProd = options.isProd || false;
|
|
11
|
+
|
|
12
|
+
// Conditionally import minifiers so build doesn't fail if they aren't installed yet
|
|
13
|
+
let minifyHtml;
|
|
14
|
+
let sharp;
|
|
15
|
+
try {
|
|
16
|
+
if (isProd) {
|
|
17
|
+
const htmlMinifier = await import('html-minifier');
|
|
18
|
+
minifyHtml = htmlMinifier.minify;
|
|
19
|
+
sharp = (await import('sharp')).default;
|
|
20
|
+
}
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.warn('Compression libraries not installed. Run `npm install` in frontend to enable production compression.');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ==========================================
|
|
26
|
+
// BUILD CONFIGURATION
|
|
27
|
+
// ==========================================
|
|
28
|
+
const config = {
|
|
29
|
+
srcDir: path.join(cwd, options.srcDir || 'src'),
|
|
30
|
+
includesDir: path.join(cwd, options.includesDir || 'src/_includes'),
|
|
31
|
+
publicDir: path.join(cwd, options.publicDir || 'public'),
|
|
32
|
+
css: {
|
|
33
|
+
input: path.join(cwd, options.cssInput || 'src/assets/tailwind/input.css'),
|
|
34
|
+
output: path.join(cwd, options.cssOutput || 'public/assets/css/style.css')
|
|
35
|
+
},
|
|
36
|
+
assets: {
|
|
37
|
+
src: path.join(cwd, options.assetsSrc || 'src/assets'),
|
|
38
|
+
dest: path.join(cwd, options.assetsDest || 'public/assets')
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Ensure output directories exist
|
|
43
|
+
if (!fs.existsSync(config.publicDir)) {
|
|
44
|
+
fs.mkdirSync(config.publicDir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
const cssOutputDir = path.dirname(config.css.output);
|
|
47
|
+
if (!fs.existsSync(cssOutputDir)) {
|
|
48
|
+
fs.mkdirSync(cssOutputDir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Copy and optionally compress static assets (e.g. images)
|
|
52
|
+
if (fs.existsSync(config.assets.src)) {
|
|
53
|
+
if (isProd && sharp) {
|
|
54
|
+
console.log('Compressing images...');
|
|
55
|
+
const copyAndCompress = async (src, dest) => {
|
|
56
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
57
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
58
|
+
const promises = [];
|
|
59
|
+
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const srcPath = path.join(src, entry.name);
|
|
62
|
+
const destPath = path.join(dest, entry.name);
|
|
63
|
+
|
|
64
|
+
if (entry.isDirectory()) {
|
|
65
|
+
promises.push(copyAndCompress(srcPath, destPath));
|
|
66
|
+
} else {
|
|
67
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
68
|
+
if (['.jpg', '.jpeg', '.png', '.webp', '.avif'].includes(ext)) {
|
|
69
|
+
promises.push(
|
|
70
|
+
sharp(srcPath)
|
|
71
|
+
.jpeg({ quality: 80, force: false })
|
|
72
|
+
.png({ quality: 80, force: false })
|
|
73
|
+
.webp({ quality: 80, force: false })
|
|
74
|
+
.avif({ quality: 80, force: false })
|
|
75
|
+
.toFile(destPath)
|
|
76
|
+
.then(info => {
|
|
77
|
+
const originalSize = fs.statSync(srcPath).size;
|
|
78
|
+
const saved = ((originalSize - info.size) / 1024).toFixed(2);
|
|
79
|
+
console.log(` ↳ Compressed ${entry.name} (Saved ${saved} KB)`);
|
|
80
|
+
})
|
|
81
|
+
.catch(err => {
|
|
82
|
+
console.error(`Error compressing ${entry.name}:`, err);
|
|
83
|
+
fs.copyFileSync(srcPath, destPath);
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
} else {
|
|
87
|
+
fs.copyFileSync(srcPath, destPath);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
await Promise.all(promises);
|
|
92
|
+
};
|
|
93
|
+
await copyAndCompress(config.assets.src, config.assets.dest);
|
|
94
|
+
} else {
|
|
95
|
+
fs.cpSync(config.assets.src, config.assets.dest, { recursive: true });
|
|
96
|
+
console.log('Copied static assets.');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Setup Nunjucks environment
|
|
101
|
+
const env = nunjucks.configure([config.srcDir, config.includesDir], {
|
|
102
|
+
autoescape: false,
|
|
103
|
+
noCache: true
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Fetch remote content based on generic.json
|
|
107
|
+
const genericJsonPath = path.join(config.srcDir, 'components', 'generic.json');
|
|
108
|
+
let cmsConfig = null;
|
|
109
|
+
let remoteData = [];
|
|
110
|
+
|
|
111
|
+
if (fs.existsSync(genericJsonPath)) {
|
|
112
|
+
try {
|
|
113
|
+
cmsConfig = JSON.parse(fs.readFileSync(genericJsonPath, 'utf-8'));
|
|
114
|
+
const baseUrl = isProd ? cmsConfig.productionUrl : cmsConfig.locaLUrl;
|
|
115
|
+
const apiUrl = `${baseUrl.replace(/\/$/, '')}/${cmsConfig.apiUrl.replace(/^\//, '')}`;
|
|
116
|
+
|
|
117
|
+
console.log(`Fetching remote content from ${apiUrl}...`);
|
|
118
|
+
const response = await fetch(apiUrl);
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
|
|
121
|
+
}
|
|
122
|
+
const data = await response.json();
|
|
123
|
+
|
|
124
|
+
if (cmsConfig.name === 'sonic.js') {
|
|
125
|
+
remoteData = data.data || [];
|
|
126
|
+
} else {
|
|
127
|
+
remoteData = Array.isArray(data) ? data : (data.data || []);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Extract URL pattern from post template frontmatter if available
|
|
131
|
+
let urlFormat = 'post.title'; // default
|
|
132
|
+
const postPagePath = path.join(config.srcDir, `${cmsConfig.postPage}.njk`);
|
|
133
|
+
if (fs.existsSync(postPagePath)) {
|
|
134
|
+
const postPageData = fs.readFileSync(postPagePath, 'utf-8');
|
|
135
|
+
const parsed = matter(postPageData);
|
|
136
|
+
if (parsed.data.url) {
|
|
137
|
+
urlFormat = parsed.data.url;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Ensure each post has a clean slug for URL generation based on the url format
|
|
142
|
+
remoteData.forEach(post => {
|
|
143
|
+
let slugSource = post.title || post.id || 'post';
|
|
144
|
+
if (urlFormat.startsWith('post.')) {
|
|
145
|
+
const key = urlFormat.replace('post.', '');
|
|
146
|
+
slugSource = post[key] || slugSource;
|
|
147
|
+
} else {
|
|
148
|
+
// If they provide a nunjucks string, evaluate it
|
|
149
|
+
slugSource = env.renderString(urlFormat, { post });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
post.slug = slugSource.toString()
|
|
153
|
+
.toLowerCase()
|
|
154
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
155
|
+
.replace(/(^-|-$)+/g, '');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
console.log(`Successfully fetched ${remoteData.length} items from remote source.`);
|
|
159
|
+
} catch (e) {
|
|
160
|
+
console.error('Error fetching remote content:', e);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Build HTML pages
|
|
165
|
+
const files = fs.readdirSync(config.srcDir).filter(file => file.endsWith('.njk'));
|
|
166
|
+
|
|
167
|
+
for (const file of files) {
|
|
168
|
+
try {
|
|
169
|
+
const filePath = path.join(config.srcDir, file);
|
|
170
|
+
const data = fs.readFileSync(filePath, 'utf-8');
|
|
171
|
+
|
|
172
|
+
// Parse the front matter
|
|
173
|
+
const parsed = matter(data);
|
|
174
|
+
const content = parsed.content;
|
|
175
|
+
const frontMatter = parsed.data;
|
|
176
|
+
|
|
177
|
+
// Helper to render and output a single page
|
|
178
|
+
const renderAndSave = (templateContent, templateData, outputFilename) => {
|
|
179
|
+
const renderedContent = env.renderString(templateContent, templateData);
|
|
180
|
+
let finalContent = renderedContent;
|
|
181
|
+
|
|
182
|
+
if (frontMatter.layout) {
|
|
183
|
+
const layoutPath = path.join(config.includesDir, `${frontMatter.layout}.njk`);
|
|
184
|
+
if (fs.existsSync(layoutPath)) {
|
|
185
|
+
const layoutData = fs.readFileSync(layoutPath, 'utf-8');
|
|
186
|
+
finalContent = env.renderString(layoutData, {
|
|
187
|
+
...templateData,
|
|
188
|
+
content: renderedContent
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
console.warn(`Layout ${frontMatter.layout} not found for ${file}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (isProd && minifyHtml) {
|
|
196
|
+
const originalLength = finalContent.length;
|
|
197
|
+
finalContent = minifyHtml(finalContent, {
|
|
198
|
+
collapseWhitespace: true,
|
|
199
|
+
removeComments: true,
|
|
200
|
+
minifyCSS: true,
|
|
201
|
+
minifyJS: true
|
|
202
|
+
});
|
|
203
|
+
const savedBytes = originalLength - finalContent.length;
|
|
204
|
+
console.log(`Built HTML: ${outputFilename} (Minified by ${(savedBytes / 1024).toFixed(2)} KB)`);
|
|
205
|
+
} else {
|
|
206
|
+
console.log(`Built HTML: ${outputFilename}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const outPath = path.join(config.publicDir, outputFilename);
|
|
210
|
+
const outDir = path.dirname(outPath);
|
|
211
|
+
if (!fs.existsSync(outDir)) {
|
|
212
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
213
|
+
}
|
|
214
|
+
fs.writeFileSync(outPath, finalContent);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
if (cmsConfig && file === `${cmsConfig.postPage}.njk`) {
|
|
218
|
+
// Generate multiple static post pages
|
|
219
|
+
console.log(`Generating ${remoteData.length} static pages for ${file}...`);
|
|
220
|
+
for (const post of remoteData) {
|
|
221
|
+
renderAndSave(content, { ...frontMatter, post }, `${cmsConfig.postPage}/${post.slug}.html`);
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
// Generate normal static page
|
|
225
|
+
const outputFilename = file.replace('.njk', '.html');
|
|
226
|
+
const templateData = { ...frontMatter };
|
|
227
|
+
|
|
228
|
+
if (cmsConfig && file === `${cmsConfig.indexPage}.njk`) {
|
|
229
|
+
templateData.posts = remoteData;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
renderAndSave(content, templateData, outputFilename);
|
|
233
|
+
}
|
|
234
|
+
} catch (err) {
|
|
235
|
+
console.error(`Error processing ${file}:`, err);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log(`Successfully finished processing ${files.length} templates.`);
|
|
240
|
+
|
|
241
|
+
// ==========================================
|
|
242
|
+
// BUILD CSS (Tailwind)
|
|
243
|
+
// ==========================================
|
|
244
|
+
console.log('Building Tailwind CSS...');
|
|
245
|
+
try {
|
|
246
|
+
const tailwindArgs = isProd ? '--minify' : '';
|
|
247
|
+
execSync(`npx @tailwindcss/cli -i "${config.css.input}" -o "${config.css.output}" ${tailwindArgs}`, { stdio: 'inherit' });
|
|
248
|
+
console.log('Tailwind CSS built successfully!');
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error('Failed to build Tailwind CSS:', error);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "basebrick-bricklayer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "BaseBrick static site generator (JamBrick)",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"bricklayer": "./bin/bricklayer.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"jamstack",
|
|
12
|
+
"static-site-generator",
|
|
13
|
+
"ssg",
|
|
14
|
+
"basebrick",
|
|
15
|
+
"nunjucks",
|
|
16
|
+
"cms"
|
|
17
|
+
],
|
|
18
|
+
"author": "Chris <hello@basebrick.xyz>",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"gray-matter": "^4.0.3",
|
|
22
|
+
"nunjucks": "^3.2.4"
|
|
23
|
+
}
|
|
24
|
+
}
|