smirky 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.
Files changed (43) hide show
  1. package/.bashrc +1 -0
  2. package/README.md +203 -0
  3. package/content/keeping-things-simple.md +13 -0
  4. package/content/three-small-steps.md +14 -0
  5. package/content/welcome-to-the-blog.md +14 -0
  6. package/dist/about/index.html +60 -0
  7. package/dist/assets/input.css +92 -0
  8. package/dist/assets/site.css +630 -0
  9. package/dist/blog/about/index.html +88 -0
  10. package/dist/blog/building-a-static-site-generator/index.html +86 -0
  11. package/dist/blog/hello-world/index.html +86 -0
  12. package/dist/blog/index.html +125 -0
  13. package/dist/blog/keeping-things-simple/index.html +70 -0
  14. package/dist/blog/three-small-steps/index.html +70 -0
  15. package/dist/blog/welcome-to-the-blog/index.html +70 -0
  16. package/dist/blog/why-kiss-matters/index.html +86 -0
  17. package/dist/contact/index.html +83 -0
  18. package/dist/index.html +56 -0
  19. package/dist/tags/index.html +65 -0
  20. package/dist/tags/javascript/index.html +125 -0
  21. package/dist/tags/webdev/index.html +125 -0
  22. package/output/assets/input.css +92 -0
  23. package/output/assets/site.css +630 -0
  24. package/package.json +31 -0
  25. package/pages/about.md +21 -0
  26. package/pages/contact.md +44 -0
  27. package/smirky.js +391 -0
  28. package/theme/assets/input.css +92 -0
  29. package/theme/assets/site.css +630 -0
  30. package/theme/blog.html +8 -0
  31. package/theme/debug.html +5 -0
  32. package/theme/index.html +10 -0
  33. package/theme/layout.html +23 -0
  34. package/theme/navbar.html +16 -0
  35. package/theme/page.html +5 -0
  36. package/theme/partials/blog_post_card.html +12 -0
  37. package/theme/partials/footer.html +4 -0
  38. package/theme/partials/head.html +2 -0
  39. package/theme/partials/navbar.html +14 -0
  40. package/theme/partials/tag_pill.html +5 -0
  41. package/theme/post.html +10 -0
  42. package/theme/site.json +8 -0
  43. package/theme/tags.html +8 -0
@@ -0,0 +1,44 @@
1
+ ---
2
+ title: "Contact"
3
+ description: "Get in touch"
4
+ template: "page"
5
+ date: "2026-01-02"
6
+ tags:
7
+ - contact
8
+ - form
9
+ ---
10
+
11
+ # Contact
12
+
13
+ If you'd like to reach out, feel free to send a message using the form below.
14
+
15
+ <!--
16
+ This contact form uses the Fabform.io backend service.
17
+ Fabform handles form submissions without requiring a custom server.
18
+ Learn more at: https://fabform.io
19
+ Documentation: https://fabform.io/docs
20
+ -->
21
+
22
+ <form action="https://fabform.io/f/YOUR_FORM_ID_HERE" method="POST" class="space-y-4">
23
+
24
+ <div>
25
+ <label class="block mb-1 font-medium">Your Name</label>
26
+ <input type="text" name="name" required class="w-full border p-2 rounded">
27
+ </div>
28
+
29
+ <div>
30
+ <label class="block mb-1 font-medium">Your Email</label>
31
+ <input type="email" name="email" required class="w-full border p-2 rounded">
32
+ </div>
33
+
34
+ <div>
35
+ <label class="block mb-1 font-medium">Message</label>
36
+ <textarea name="message" rows="5" required class="w-full border p-2 rounded"></textarea>
37
+ </div>
38
+
39
+ <button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded">
40
+ Send Message
41
+ </button>
42
+
43
+ </form>
44
+
package/smirky.js ADDED
@@ -0,0 +1,391 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import matter from "gray-matter";
6
+ import { marked } from "marked";
7
+
8
+ // --------------------------------------------------
9
+ // Directories
10
+ // --------------------------------------------------
11
+ const CONTENT_DIR = "./content";
12
+ const PAGES_DIR = "./pages";
13
+ const THEME_DIR = "./theme";
14
+ const OUTPUT_DIR = "./dist";
15
+
16
+ // --------------------------------------------------
17
+ // Helpers
18
+ // --------------------------------------------------
19
+ function ensureDir(dir) {
20
+ if (!fs.existsSync(dir)) {
21
+ fs.mkdirSync(dir, { recursive: true });
22
+ }
23
+ }
24
+
25
+ function slugify(str) {
26
+ return String(str)
27
+ .trim()
28
+ .toLowerCase()
29
+ .replace(/[\s_]+/g, "-")
30
+ .replace(/[^a-z0-9-]/g, "")
31
+ .replace(/-+/g, "-");
32
+ }
33
+
34
+ // --------------------------------------------------
35
+ // SUPER SIMPLE TEMPLATE ENGINE
36
+ // One final pass over the entire HTML
37
+ // --------------------------------------------------
38
+ function applyTemplate(html, vars) {
39
+ let out = html;
40
+ for (const key in vars) {
41
+ const token = `{{ ${key} }}`;
42
+ out = out.split(token).join(vars[key] ?? "");
43
+ }
44
+ return out;
45
+ }
46
+
47
+ // --------------------------------------------------
48
+ // Load theme files
49
+ // --------------------------------------------------
50
+ function readTheme(file) {
51
+ return fs.readFileSync(path.join(THEME_DIR, file), "utf8");
52
+ }
53
+
54
+ const layoutTemplate = readTheme("layout.html");
55
+ const indexTemplate = readTheme("index.html");
56
+ const pageTemplate = readTheme("page.html");
57
+ const postTemplate = readTheme("post.html");
58
+ const blogTemplate = readTheme("blog.html");
59
+ const tagsTemplate = readTheme("tags.html");
60
+
61
+ // Partials
62
+ const headPartial = readTheme("partials/head.html");
63
+ const navbarPartial = readTheme("partials/navbar.html");
64
+ const footerPartial = readTheme("partials/footer.html");
65
+ const blogPostCardTemplate = readTheme("partials/blog_post_card.html");
66
+ const tagPillTemplate = readTheme("partials/tag_pill.html");
67
+
68
+ // Site config
69
+ const siteConfig = JSON.parse(
70
+ fs.readFileSync(path.join(THEME_DIR, "site.json"), "utf8")
71
+ );
72
+
73
+ // --------------------------------------------------
74
+ // Copy static assets
75
+ // --------------------------------------------------
76
+ function copyAssets() {
77
+ const src = path.join(THEME_DIR, "assets");
78
+ const dest = path.join(OUTPUT_DIR, "assets");
79
+
80
+ ensureDir(dest);
81
+
82
+ for (const file of fs.readdirSync(src)) {
83
+ fs.copyFileSync(path.join(src, file), path.join(dest, file));
84
+ }
85
+
86
+ console.log("Assets copied");
87
+ }
88
+
89
+ // --------------------------------------------------
90
+ // Navbar builder
91
+ // --------------------------------------------------
92
+ function buildNavbarLinks(currentSlug) {
93
+ const files = fs.existsSync(PAGES_DIR)
94
+ ? fs.readdirSync(PAGES_DIR).filter((f) => f.endsWith(".md"))
95
+ : [];
96
+
97
+ const links = files.map((file) => {
98
+ const raw = fs.readFileSync(path.join(PAGES_DIR, file), "utf8");
99
+ const { data } = matter(raw);
100
+
101
+ const slug = slugify(data.slug || path.basename(file, ".md"));
102
+ const title = data.title || slug;
103
+
104
+ const isActive = slug === currentSlug;
105
+ const cls = isActive
106
+ ? "text-indigo-600 font-semibold"
107
+ : "text-slate-700 hover:text-slate-900";
108
+
109
+ return `<a href="/${slug}/" class="${cls}">${title}</a>`;
110
+ });
111
+
112
+ // Blog link
113
+ links.push(
114
+ `<a href="/blog/" class="${
115
+ currentSlug === "blog"
116
+ ? "text-indigo-600 font-semibold"
117
+ : "text-slate-700 hover:text-slate-900"
118
+ }">Blog</a>`
119
+ );
120
+
121
+ // Tags link
122
+ links.push(
123
+ `<a href="/tags/" class="${
124
+ currentSlug === "tags"
125
+ ? "text-indigo-600 font-semibold"
126
+ : "text-slate-700 hover:text-slate-900"
127
+ }">Tags</a>`
128
+ );
129
+
130
+ return links.join("\n ");
131
+ }
132
+
133
+ // --------------------------------------------------
134
+ // Tag pills & blog cards
135
+ // --------------------------------------------------
136
+ function renderTagPills(tags) {
137
+ return (tags || [])
138
+ .map((tag) =>
139
+ applyTemplate(tagPillTemplate, {
140
+ tag_name: tag,
141
+ tag_slug: slugify(tag)
142
+ })
143
+ )
144
+ .join("\n");
145
+ }
146
+
147
+ function renderBlogCards(posts) {
148
+ return posts
149
+ .map((post) =>
150
+ applyTemplate(blogPostCardTemplate, {
151
+ title: post.title,
152
+ url: post.url,
153
+ date: post.date,
154
+ tags: renderTagPills(post.tags)
155
+ })
156
+ )
157
+ .join("\n");
158
+ }
159
+
160
+ // --------------------------------------------------
161
+ // NEW renderPage() — assemble first, replace last
162
+ // --------------------------------------------------
163
+ function renderPage(innerHTML, vars) {
164
+ const navbarLinks = buildNavbarLinks(vars.currentSlug || "");
165
+
166
+ // Build page/post HTML from the chosen template
167
+ const pageHtml = applyTemplate(vars.template, {
168
+ ...vars,
169
+ content: innerHTML
170
+ });
171
+
172
+ // Assemble full HTML BEFORE variable replacement
173
+ let fullHtml = layoutTemplate
174
+ .replace("{{ head }}", headPartial)
175
+ .replace("{{ navbar }}", navbarPartial)
176
+ .replace("{{ footer }}", footerPartial)
177
+ .replace("{{ content }}", pageHtml);
178
+
179
+ // Final variable pass — replaces everywhere
180
+ fullHtml = applyTemplate(fullHtml, {
181
+ ...vars,
182
+ site_title: siteConfig.site.title,
183
+ site_description: siteConfig.site.description,
184
+ site_footer: siteConfig.site.footer,
185
+ navbar_links: navbarLinks
186
+ });
187
+
188
+ return fullHtml;
189
+ }
190
+
191
+ // --------------------------------------------------
192
+ // Collect tags
193
+ // --------------------------------------------------
194
+ function collectTags(posts) {
195
+ const tagMap = {};
196
+
197
+ posts.forEach((post) => {
198
+ (post.tags || []).forEach((tag) => {
199
+ const key = slugify(tag);
200
+ if (!tagMap[key]) {
201
+ tagMap[key] = { name: tag, posts: [] };
202
+ }
203
+ tagMap[key].posts.push(post);
204
+ });
205
+ });
206
+
207
+ return tagMap;
208
+ }
209
+
210
+ // --------------------------------------------------
211
+ // Build static pages
212
+ // --------------------------------------------------
213
+ function buildPages() {
214
+ if (!fs.existsSync(PAGES_DIR)) return;
215
+
216
+ const files = fs.readdirSync(PAGES_DIR).filter((f) => f.endsWith(".md"));
217
+
218
+ files.forEach((file) => {
219
+ const raw = fs.readFileSync(path.join(PAGES_DIR, file), "utf8");
220
+ const { data, content: md } = matter(raw);
221
+
222
+ const slug = slugify(data.slug || path.basename(file, ".md"));
223
+ const html = marked(md);
224
+
225
+ const final = renderPage(html, {
226
+ title: data.title || slug,
227
+ currentSlug: slug,
228
+ template: pageTemplate
229
+ });
230
+
231
+ const outDir = path.join(OUTPUT_DIR, slug);
232
+ ensureDir(outDir);
233
+ fs.writeFileSync(path.join(outDir, "index.html"), final);
234
+
235
+ console.log(`Page → /${slug}/`);
236
+ });
237
+ }
238
+
239
+ // --------------------------------------------------
240
+ // Build blog posts
241
+ // --------------------------------------------------
242
+ function buildPosts() {
243
+ if (!fs.existsSync(CONTENT_DIR)) return [];
244
+
245
+ const files = fs.readdirSync(CONTENT_DIR).filter((f) => f.endsWith(".md"));
246
+ const posts = [];
247
+
248
+ files.forEach((file) => {
249
+ const raw = fs.readFileSync(path.join(CONTENT_DIR, file), "utf8");
250
+ const { data, content: md } = matter(raw);
251
+
252
+ const slug = slugify(data.slug || path.basename(file, ".md"));
253
+ const html = marked(md);
254
+
255
+ const title =
256
+ data.title ||
257
+ slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
258
+
259
+ const tags = data.tags || [];
260
+ const tagPills = renderTagPills(tags);
261
+
262
+ const final = renderPage(html, {
263
+ title,
264
+ currentSlug: "blog",
265
+ template: postTemplate,
266
+ tag_pills: tagPills
267
+ });
268
+
269
+ const outDir = path.join(OUTPUT_DIR, "blog", slug);
270
+ ensureDir(outDir);
271
+ fs.writeFileSync(path.join(outDir, "index.html"), final);
272
+
273
+ posts.push({
274
+ title,
275
+ url: `/blog/${slug}/`,
276
+ date: data.date || "",
277
+ tags
278
+ });
279
+
280
+ console.log(`Post → /blog/${slug}/`);
281
+ });
282
+
283
+ return posts;
284
+ }
285
+
286
+ // --------------------------------------------------
287
+ // Blog index
288
+ // --------------------------------------------------
289
+ function buildBlogIndex(posts) {
290
+ const listHtml = renderBlogCards(
291
+ posts.sort((a, b) => (a.date < b.date ? 1 : -1))
292
+ );
293
+
294
+ const final = renderPage(listHtml, {
295
+ title: "Blog",
296
+ currentSlug: "blog",
297
+ template: blogTemplate
298
+ });
299
+
300
+ const outDir = path.join(OUTPUT_DIR, "blog");
301
+ ensureDir(outDir);
302
+ fs.writeFileSync(path.join(outDir, "index.html"), final);
303
+
304
+ console.log("Blog index → /blog/");
305
+ }
306
+
307
+ // --------------------------------------------------
308
+ // Tags index
309
+ // --------------------------------------------------
310
+ function buildTagsPage(tagMap) {
311
+ const tagLinks = Object.values(tagMap)
312
+ .map((t) =>
313
+ applyTemplate(tagPillTemplate, {
314
+ tag_name: `${t.name} (${t.posts.length})`,
315
+ tag_slug: slugify(t.name)
316
+ })
317
+ )
318
+ .join("\n");
319
+
320
+ const final = renderPage(tagLinks, {
321
+ title: "Tags",
322
+ currentSlug: "tags",
323
+ template: tagsTemplate
324
+ });
325
+
326
+ const outDir = path.join(OUTPUT_DIR, "tags");
327
+ ensureDir(outDir);
328
+ fs.writeFileSync(path.join(outDir, "index.html"), final);
329
+
330
+ console.log("Tags index → /tags/");
331
+ }
332
+
333
+ // --------------------------------------------------
334
+ // Tag pages
335
+ // --------------------------------------------------
336
+ function buildTagPages(tagMap) {
337
+ Object.values(tagMap).forEach((tag) => {
338
+ const listHtml = renderBlogCards(tag.posts);
339
+
340
+ const final = renderPage(listHtml, {
341
+ title: `Posts tagged "${tag.name}"`,
342
+ currentSlug: "tags",
343
+ template: blogTemplate
344
+ });
345
+
346
+ const outDir = path.join(OUTPUT_DIR, "tags", slugify(tag.name));
347
+ ensureDir(outDir);
348
+ fs.writeFileSync(path.join(outDir, "index.html"), final);
349
+
350
+ console.log(`Tag → /tags/${slugify(tag.name)}/`);
351
+ });
352
+ }
353
+
354
+ // --------------------------------------------------
355
+ // Home page
356
+ // --------------------------------------------------
357
+ function buildHome() {
358
+ const final = renderPage("", {
359
+ title: siteConfig.site.title,
360
+ currentSlug: "home",
361
+ template: indexTemplate
362
+ });
363
+
364
+ fs.writeFileSync(path.join(OUTPUT_DIR, "index.html"), final);
365
+ console.log("Home → /");
366
+ }
367
+
368
+ // --------------------------------------------------
369
+ // Build everything
370
+ // --------------------------------------------------
371
+ function build() {
372
+ ensureDir(OUTPUT_DIR);
373
+
374
+ console.log("Building...");
375
+
376
+ copyAssets();
377
+
378
+ const posts = buildPosts();
379
+ buildPages();
380
+
381
+ const tagMap = collectTags(posts);
382
+ buildBlogIndex(posts);
383
+ buildTagsPage(tagMap);
384
+ buildTagPages(tagMap);
385
+
386
+ buildHome();
387
+ console.log("Done.");
388
+ }
389
+
390
+ build();
391
+
@@ -0,0 +1,92 @@
1
+ @import "tailwindcss";
2
+
3
+ /* ---------------------------
4
+ Global Markdown Typography
5
+ Tailwind v4 — No Config File
6
+ ---------------------------- */
7
+
8
+ /* Headings */
9
+ h1 {
10
+ @apply text-4xl font-bold leading-tight mt-8 mb-4;
11
+ }
12
+
13
+ h2 {
14
+ @apply text-3xl font-semibold leading-snug mt-6 mb-3;
15
+ }
16
+
17
+ h3 {
18
+ @apply text-2xl font-semibold leading-snug mt-5 mb-2;
19
+ }
20
+
21
+ h4 {
22
+ @apply text-xl font-semibold mt-4 mb-2;
23
+ }
24
+
25
+ h5 {
26
+ @apply text-lg font-medium mt-3 mb-2;
27
+ }
28
+
29
+ h6 {
30
+ @apply text-base font-medium mt-2 mb-2;
31
+ }
32
+
33
+ /* Paragraphs */
34
+ p {
35
+ @apply text-base leading-relaxed mb-4;
36
+ }
37
+
38
+ /* Links */
39
+ a {
40
+ @apply text-blue-600 underline hover:text-blue-800;
41
+ }
42
+
43
+ /* Images */
44
+ img {
45
+ @apply rounded-lg my-4;
46
+ }
47
+
48
+ /* Lists */
49
+ ul {
50
+ @apply list-disc pl-6 mb-4;
51
+ }
52
+
53
+ ol {
54
+ @apply list-decimal pl-6 mb-4;
55
+ }
56
+
57
+ li {
58
+ @apply mb-1;
59
+ }
60
+
61
+ /* Blockquotes */
62
+ blockquote {
63
+ @apply border-l-4 border-gray-300 pl-4 italic text-gray-700 my-4;
64
+ }
65
+
66
+ /* Code blocks */
67
+ pre {
68
+ @apply bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto my-4;
69
+ }
70
+
71
+ code {
72
+ @apply bg-gray-100 text-red-600 px-1 py-0.5 rounded;
73
+ }
74
+
75
+ /* Horizontal rule */
76
+ hr {
77
+ @apply border-gray-300 my-8;
78
+ }
79
+
80
+ /* Tables */
81
+ table {
82
+ @apply w-full border-collapse my-6;
83
+ }
84
+
85
+ th {
86
+ @apply border-b font-semibold p-2 text-left;
87
+ }
88
+
89
+ td {
90
+ @apply border-b p-2;
91
+ }
92
+