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 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
+ ```
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { buildSite } from '../index.js';
4
+
5
+ const isProd = process.argv.includes('--prod');
6
+ buildSite({ isProd, cwd: process.cwd() }).catch(console.error);
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
+ }