@jsnchn/buntastic 0.0.12 → 0.1.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/AGENTS.md CHANGED
@@ -64,6 +64,7 @@ draft: false # Set true to exclude from production build
64
64
  | `{{ description }}` | Page description |
65
65
  | `{{ url }}` | Current page URL |
66
66
  | `{{ collection }}` | Array of posts in current folder (for index pages) |
67
+ | `{{ head \| safe }}` | Head content from child layouts (appended to parent) |
67
68
 
68
69
  ## Layout System
69
70
 
@@ -82,6 +83,25 @@ extends: base.html
82
83
 
83
84
  The `{{ content | safe }}` placeholder is where child content gets injected.
84
85
 
86
+ ## Head Block
87
+
88
+ Layouts can inject content into the `<head>` section using `{{ head | safe }}`. Content BEFORE the marker goes into `<head>`, content AFTER the marker is treated as the layout body:
89
+
90
+ ```html
91
+ <!-- layouts/post.html -->
92
+ ---
93
+ extends: base.html
94
+ ---
95
+ <link rel="stylesheet" href="/post.css">
96
+ {{ head | safe }}
97
+ <article class="post">
98
+ <h1>{{ title }}</h1>
99
+ {{ content | safe }}
100
+ </article>
101
+ ```
102
+
103
+ The `{{ head | safe }}` marker is where additional head content can be injected (from child layouts or page frontmatter). Content before the marker (e.g., the post.css link) is automatically placed in the parent's `<head>` section. Child layout head content is appended to parent head content (parent's head first, then child's).
104
+
85
105
  ## Development
86
106
 
87
107
  1. Add content to `content/` folder
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsnchn/buntastic",
3
- "version": "0.0.12",
3
+ "version": "0.1.0",
4
4
  "description": "A simple static site generator built with Bun",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -94,21 +94,55 @@ async function readLayout(layoutName: string): Promise<string> {
94
94
  return await readFile(layoutPath, "utf-8");
95
95
  }
96
96
 
97
- async function resolveLayout(frontmatter: Frontmatter): Promise<string> {
97
+ async function resolveLayout(frontmatter: Frontmatter): Promise<{ template: string; head: string }> {
98
98
  const layoutName = frontmatter.layout || frontmatter.extends || "page";
99
99
  let template = await readLayout(layoutName);
100
100
 
101
101
  const extendsMatch = template.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
102
+
103
+ let childHead = "";
104
+ let childBody = template;
105
+
102
106
  if (extendsMatch) {
103
107
  const parentLayout = extendsMatch[1].match(/extends:\s*(\w+)/);
104
108
  if (parentLayout) {
105
- const parentTemplate = await resolveLayout({ extends: parentLayout[1] } as Frontmatter);
106
- const childContent = extendsMatch[2];
107
- return parentTemplate.replace(/\{\{\s*content\s*\|\s*safe\s*\}\}/g, childContent);
109
+ childBody = extendsMatch[2];
110
+ const headPlaceholderMatch = childBody.match(/(\{\{\s*head\s*\|\s*safe\s*\}\})/);
111
+
112
+ if (headPlaceholderMatch) {
113
+ const placeholderIdx = headPlaceholderMatch.index || 0;
114
+ const beforePlaceholder = childBody.substring(0, placeholderIdx).trim();
115
+ const afterPlaceholder = childBody.substring(placeholderIdx + headPlaceholderMatch[0].length).trim();
116
+ childHead = beforePlaceholder;
117
+ childBody = afterPlaceholder;
118
+ }
119
+
120
+ const parentResult = await resolveLayout({ extends: parentLayout[1] } as Frontmatter);
121
+
122
+ let contentReplaced = parentResult.template.replace(
123
+ /\{\{\s*content\s*\|\s*safe\s*\}\}/g,
124
+ childBody
125
+ );
126
+
127
+ const mergedHead = parentResult.head + (childHead ? "\n" + childHead : "");
128
+
129
+ contentReplaced = contentReplaced.replace(
130
+ /\{\{\s*head\s*\|\s*safe\s*\}\}/g,
131
+ mergedHead
132
+ );
133
+
134
+ return { template: contentReplaced, head: mergedHead };
108
135
  }
109
136
  }
110
137
 
111
- return template;
138
+ let ownHead = "";
139
+ if (extendsMatch) {
140
+ const bodyMatch = extendsMatch[2];
141
+ const headMatch = bodyMatch.match(/\{\{\s*head\s*\|\s*safe\s*\}\}/);
142
+ ownHead = headMatch ? headMatch[0] : "";
143
+ }
144
+
145
+ return { template, head: ownHead };
112
146
  }
113
147
 
114
148
  function applyTemplate(template: string, page: Page, collection?: CollectionItem[]): string {
@@ -213,7 +247,7 @@ async function build(includeDrafts = false): Promise<void> {
213
247
  }
214
248
 
215
249
  for (const page of pages) {
216
- let template = await resolveLayout(page.frontmatter);
250
+ let { template } = await resolveLayout(page.frontmatter);
217
251
  template = template.replace(/\{\{\s*content\s*\|\s*safe\s*\}\}/g, page.html);
218
252
 
219
253
  let collection: CollectionItem[] | undefined;
@@ -266,7 +300,7 @@ async function build(includeDrafts = false): Promise<void> {
266
300
  if (await exists(join(CONTENT_DIR, "404.md"))) {
267
301
  const page404 = await buildPage(join(CONTENT_DIR, "404.md"), includeDrafts);
268
302
  if (page404) {
269
- let template = await resolveLayout(page404.frontmatter);
303
+ let { template } = await resolveLayout(page404.frontmatter);
270
304
  template = template.replace(/\{\{\s*content\s*\|\s*safe\s*\}\}/g, page404.html);
271
305
  const outputHtml = applyTemplate(template, page404);
272
306
  await writeFile(join(DIST_DIR, "404.html"), outputHtml);
@@ -6,6 +6,7 @@
6
6
  <title>{{ title }}</title>
7
7
  <meta name="description" content="{{ description }}">
8
8
  <link rel="stylesheet" href="/style.css">
9
+ {{ head | safe }}
9
10
  </head>
10
11
  <body>
11
12
  <header>
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  extends: base.html
3
3
  ---
4
+ <link rel="stylesheet" href="/post.css">
5
+ {{ head | safe }}
4
6
  <article class="post">
5
7
  <header>
6
8
  <h1>{{ title }}</h1>