@terrymooreii/sia 2.1.2 → 2.1.4

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 (70) hide show
  1. package/lib/assets.js +17 -20
  2. package/lib/build.js +2 -2
  3. package/lib/config.js +1 -1
  4. package/lib/content.js +24 -21
  5. package/lib/templates.js +10 -5
  6. package/package.json +1 -1
  7. package/themes/_shared/includes/meta.njk +41 -0
  8. package/themes/_shared/includes/theme-script.njk +8 -0
  9. package/themes/developer/includes/footer.njk +5 -0
  10. package/themes/developer/includes/header.njk +46 -0
  11. package/themes/developer/includes/pagination.njk +39 -0
  12. package/themes/developer/includes/sidebar.njk +60 -0
  13. package/themes/developer/includes/tag-list.njk +7 -0
  14. package/themes/developer/layouts/base.njk +29 -0
  15. package/themes/developer/layouts/note.njk +21 -0
  16. package/themes/developer/layouts/page.njk +13 -0
  17. package/themes/developer/layouts/post.njk +47 -0
  18. package/themes/developer/pages/blog.njk +41 -0
  19. package/themes/developer/pages/feed.njk +22 -0
  20. package/themes/developer/pages/index.njk +65 -0
  21. package/themes/developer/pages/notes.njk +38 -0
  22. package/themes/developer/pages/tag.njk +39 -0
  23. package/themes/developer/pages/tags.njk +21 -0
  24. package/themes/developer/styles/main.css +874 -0
  25. package/themes/magazine/includes/footer.njk +32 -0
  26. package/themes/magazine/includes/header.njk +63 -0
  27. package/themes/magazine/includes/pagination.njk +29 -0
  28. package/themes/magazine/includes/tag-list.njk +7 -0
  29. package/themes/magazine/layouts/base.njk +23 -0
  30. package/themes/magazine/layouts/note.njk +25 -0
  31. package/themes/magazine/layouts/page.njk +13 -0
  32. package/themes/magazine/layouts/post.njk +79 -0
  33. package/themes/magazine/pages/blog.njk +35 -0
  34. package/themes/magazine/pages/feed.njk +22 -0
  35. package/themes/magazine/pages/index.njk +79 -0
  36. package/themes/magazine/pages/notes.njk +33 -0
  37. package/themes/magazine/pages/tag.njk +31 -0
  38. package/themes/magazine/pages/tags.njk +19 -0
  39. package/themes/magazine/styles/main.css +1169 -0
  40. package/themes/main/layouts/base.njk +21 -0
  41. package/themes/minimal/includes/footer.njk +14 -0
  42. package/themes/minimal/includes/header.njk +71 -0
  43. package/themes/minimal/includes/pagination.njk +26 -0
  44. package/themes/minimal/includes/tag-list.njk +11 -0
  45. package/themes/minimal/layouts/base.njk +21 -0
  46. package/themes/minimal/layouts/note.njk +25 -0
  47. package/themes/minimal/layouts/page.njk +14 -0
  48. package/themes/minimal/layouts/post.njk +46 -0
  49. package/themes/minimal/pages/blog.njk +37 -0
  50. package/themes/minimal/pages/feed.njk +28 -0
  51. package/themes/minimal/pages/index.njk +61 -0
  52. package/themes/minimal/pages/notes.njk +34 -0
  53. package/themes/minimal/pages/tag.njk +42 -0
  54. package/themes/minimal/pages/tags.njk +39 -0
  55. package/defaults/layouts/base.njk +0 -41
  56. /package/{defaults → themes/main}/includes/footer.njk +0 -0
  57. /package/{defaults → themes/main}/includes/header.njk +0 -0
  58. /package/{defaults → themes/main}/includes/pagination.njk +0 -0
  59. /package/{defaults → themes/main}/includes/tag-list.njk +0 -0
  60. /package/{defaults → themes/main}/layouts/note.njk +0 -0
  61. /package/{defaults → themes/main}/layouts/page.njk +0 -0
  62. /package/{defaults → themes/main}/layouts/post.njk +0 -0
  63. /package/{defaults → themes/main}/pages/blog.njk +0 -0
  64. /package/{defaults → themes/main}/pages/feed.njk +0 -0
  65. /package/{defaults → themes/main}/pages/index.njk +0 -0
  66. /package/{defaults → themes/main}/pages/notes.njk +0 -0
  67. /package/{defaults → themes/main}/pages/tag.njk +0 -0
  68. /package/{defaults → themes/main}/pages/tags.njk +0 -0
  69. /package/{defaults → themes/main}/styles/main.css +0 -0
  70. /package/{defaults/styles/minimal.css → themes/minimal/styles/main.css} +0 -0
package/lib/assets.js CHANGED
@@ -127,8 +127,7 @@ function hasCssFiles(dir) {
127
127
  /**
128
128
  * Copy default styles to output
129
129
  */
130
- export function copyDefaultStyles(config, defaultsDir) {
131
- const defaultStylesDir = join(defaultsDir, 'styles');
130
+ export function copyDefaultStyles(config, themesDir) {
132
131
  const outputStylesDir = join(config.outputDir, 'styles');
133
132
 
134
133
  // Check if user has custom styles (must actually have CSS files)
@@ -141,24 +140,22 @@ export function copyDefaultStyles(config, defaultsDir) {
141
140
  return copied;
142
141
  }
143
142
 
144
- // Copy selected theme from the package
145
- if (existsSync(defaultStylesDir)) {
146
- const themeName = config.theme || 'main';
147
- const themeFile = join(defaultStylesDir, `${themeName}.css`);
148
-
149
- if (existsSync(themeFile)) {
150
- // Copy the selected theme as main.css so templates don't need to change
151
- ensureDir(outputStylesDir);
152
- copyFileSync(themeFile, join(outputStylesDir, 'main.css'));
153
- console.log(`🎨 Using "${themeName}" theme`);
154
- return ['main.css'];
155
- } else {
156
- // Fallback to copying all styles if theme not found
157
- console.log(`⚠️ Theme "${themeName}" not found, using default`);
158
- const copied = copyAssets(defaultStylesDir, outputStylesDir);
159
- console.log(`🎨 Copied ${copied.length} default style files`);
160
- return copied;
161
- }
143
+ // Copy styles from the selected theme
144
+ const themeName = config.theme || 'main';
145
+ const themeStylesDir = join(themesDir, themeName, 'styles');
146
+
147
+ if (existsSync(themeStylesDir)) {
148
+ const copied = copyAssets(themeStylesDir, outputStylesDir);
149
+ console.log(`🎨 Using "${themeName}" theme`);
150
+ return copied;
151
+ }
152
+
153
+ // Fallback to main theme if selected theme not found
154
+ const fallbackStylesDir = join(themesDir, 'main', 'styles');
155
+ if (existsSync(fallbackStylesDir)) {
156
+ console.log(`⚠️ Theme "${themeName}" not found, using "main" theme`);
157
+ const copied = copyAssets(fallbackStylesDir, outputStylesDir);
158
+ return copied;
162
159
  }
163
160
 
164
161
  console.log('⚠️ No styles found');
package/lib/build.js CHANGED
@@ -8,7 +8,7 @@ import { copyImages, copyDefaultStyles, copyStaticAssets, writeFile, ensureDir }
8
8
 
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = dirname(__filename);
11
- const defaultsDir = join(__dirname, '..', 'defaults');
11
+ const themesDir = join(__dirname, '..', 'themes');
12
12
 
13
13
  /**
14
14
  * Clean the output directory
@@ -244,7 +244,7 @@ export async function build(options = {}) {
244
244
 
245
245
  // Copy assets
246
246
  copyImages(config);
247
- copyDefaultStyles(config, defaultsDir);
247
+ copyDefaultStyles(config, themesDir);
248
248
  copyStaticAssets(config);
249
249
 
250
250
  const duration = ((Date.now() - startTime) / 1000).toFixed(2);
package/lib/config.js CHANGED
@@ -10,7 +10,7 @@ const defaultConfig = {
10
10
  url: 'http://localhost:3000',
11
11
  author: 'Anonymous'
12
12
  },
13
- theme: 'main', // Theme CSS file to use: 'main' or 'minimal'
13
+ theme: 'main', // Theme to use: 'main' or 'minimal'
14
14
  input: 'src',
15
15
  output: 'dist',
16
16
  layouts: '_layouts',
package/lib/content.js CHANGED
@@ -221,28 +221,31 @@ function embedGiphyGifs(html) {
221
221
  });
222
222
  }
223
223
 
224
- // Override the link renderer to handle YouTube URLs
225
- const renderer = marked.getRenderer();
226
- const originalLink = renderer.link.bind(renderer);
227
-
228
- renderer.link = (token) => {
229
- const videoId = extractYouTubeId(token.href);
230
-
231
- if (videoId) {
232
- // Return responsive YouTube embed instead of a link
233
- return `<div class="youtube-embed"><iframe src="https://www.youtube.com/embed/${videoId}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>`;
234
- }
235
-
236
- const gifId = extractGiphyId(token.href);
237
-
238
- if (gifId) {
239
- // Return responsive Giphy embed instead of a link
240
- return `<div class="giphy-embed"><iframe src="https://giphy.com/embed/${gifId}" frameBorder="0" class="giphy-embed" allowFullScreen></iframe></div>`;
224
+ // Override the link renderer to handle YouTube and Giphy URLs
225
+ marked.use({
226
+ renderer: {
227
+ link(href, s, text) {
228
+ const videoId = extractYouTubeId(href);
229
+
230
+ if (videoId) {
231
+ // Return responsive YouTube embed instead of a link
232
+ return `<div class="youtube-embed"><iframe src="https://www.youtube.com/embed/${videoId}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>`;
233
+ }
234
+
235
+ const gifId = extractGiphyId(href);
236
+
237
+ if (gifId) {
238
+ // Return responsive Giphy embed instead of a link
239
+ return `<div class="giphy-embed"><iframe src="https://giphy.com/embed/${gifId}" frameBorder="0" class="giphy-embed" allowFullScreen></iframe></div>`;
240
+ }
241
+
242
+ // Use default link rendering for non-YouTube/Giphy links
243
+ text = text || href;
244
+ const title = text ? ` title="${text}"` : '';
245
+ return `<a href="${href}"${title}>${text}</a>`;
246
+ }
241
247
  }
242
-
243
- // Use default link rendering for non-YouTube/Giphy links
244
- return originalLink(token);
245
- };
248
+ });
246
249
 
247
250
  /**
248
251
  * Generate a URL-friendly slug from a string
package/lib/templates.js CHANGED
@@ -204,11 +204,16 @@ export function createTemplateEngine(config) {
204
204
  templatePaths.push(config.includesDir);
205
205
  }
206
206
 
207
- // Default templates from the package
208
- const defaultTemplatesDir = join(__dirname, '..', 'defaults');
209
- templatePaths.push(join(defaultTemplatesDir, 'layouts'));
210
- templatePaths.push(join(defaultTemplatesDir, 'includes'));
211
- templatePaths.push(join(defaultTemplatesDir, 'pages'));
207
+ // Default templates from the selected theme
208
+ const themeName = config.theme || 'main';
209
+ const themeDir = join(__dirname, '..', 'themes', themeName);
210
+ templatePaths.push(join(themeDir, 'layouts'));
211
+ templatePaths.push(join(themeDir, 'includes'));
212
+ templatePaths.push(join(themeDir, 'pages'));
213
+
214
+ // Shared includes available to all themes
215
+ const sharedIncludesDir = join(__dirname, '..', 'themes', '_shared', 'includes');
216
+ templatePaths.push(sharedIncludesDir);
212
217
 
213
218
  // Create the environment
214
219
  const env = nunjucks.configure(templatePaths, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrymooreii/sia",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "description": "A simple, powerful static site generator with markdown, front matter, and Nunjucks templates",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -0,0 +1,41 @@
1
+ {# Core meta tags - include this in your theme's <head> #}
2
+ <meta charset="UTF-8">
3
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
4
+ <meta name="description" content="{{ page.excerpt or page.description or site.description }}">
5
+ <meta name="author" content="{{ page.author or site.author }}">
6
+ <title>{% if title %}{{ title }} | {% endif %}{{ site.title }}</title>
7
+
8
+ {# Open Graph #}
9
+ <meta property="og:title" content="{{ title or site.title }}">
10
+ <meta property="og:description" content="{{ page.excerpt or page.description or site.description }}">
11
+ <meta property="og:type" content="{% if page.collection == 'posts' %}article{% else %}website{% endif %}">
12
+ <meta property="og:url" content="{{ site.url }}{{ page.url or ('/' | url) }}">
13
+ <meta property="og:site_name" content="{{ site.title }}">
14
+ {% if page.image %}
15
+ <meta property="og:image" content="{{ site.url }}{{ page.image | url }}">
16
+ {% endif %}
17
+
18
+ {# Twitter Card #}
19
+ <meta name="twitter:card" content="{% if page.image %}summary_large_image{% else %}summary{% endif %}">
20
+ <meta name="twitter:title" content="{{ title or site.title }}">
21
+ <meta name="twitter:description" content="{{ page.excerpt or page.description or site.description }}">
22
+ {% if page.image %}
23
+ <meta name="twitter:image" content="{{ site.url }}{{ page.image | url }}">
24
+ {% endif %}
25
+
26
+ {# Article specific meta (for blog posts) #}
27
+ {% if page.collection == 'posts' %}
28
+ <meta property="article:published_time" content="{{ page.date | date('iso') }}">
29
+ <meta property="article:author" content="{{ page.author or site.author }}">
30
+ {% if page.tags %}
31
+ {% for tag in page.tags %}
32
+ <meta property="article:tag" content="{{ tag }}">
33
+ {% endfor %}
34
+ {% endif %}
35
+ {% endif %}
36
+
37
+ {# RSS Feed #}
38
+ <link rel="alternate" type="application/rss+xml" title="{{ site.title }}" href="{{ '/feed.xml' | url }}">
39
+
40
+ {# Canonical URL #}
41
+ <link rel="canonical" href="{{ site.url }}{{ page.url or ('/' | url) }}">
@@ -0,0 +1,8 @@
1
+ {# Prevent flash of incorrect theme - include in <head> before stylesheets #}
2
+ <script>
3
+ (function() {
4
+ var saved = localStorage.getItem('theme');
5
+ var theme = saved || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
6
+ document.documentElement.setAttribute('data-theme', theme);
7
+ })();
8
+ </script>
@@ -0,0 +1,5 @@
1
+ <footer class="site-footer">
2
+ <div class="footer-content">
3
+ <p>&copy; {{ "now" | date("year") }} {{ site.title }}. Built with <a href="https://github.com/terrymooreii/sia">Sia</a>.</p>
4
+ </div>
5
+ </footer>
@@ -0,0 +1,46 @@
1
+ <header class="top-header">
2
+ <button class="mobile-menu-btn" aria-label="Toggle menu">
3
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
4
+ <line x1="3" y1="12" x2="21" y2="12"></line>
5
+ <line x1="3" y1="6" x2="21" y2="6"></line>
6
+ <line x1="3" y1="18" x2="21" y2="18"></line>
7
+ </svg>
8
+ </button>
9
+
10
+ <div class="header-spacer"></div>
11
+
12
+ <div class="header-actions">
13
+ <button class="theme-toggle" aria-label="Toggle dark mode">
14
+ <svg class="sun-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
15
+ <circle cx="12" cy="12" r="5"></circle>
16
+ <line x1="12" y1="1" x2="12" y2="3"></line>
17
+ <line x1="12" y1="21" x2="12" y2="23"></line>
18
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
19
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
20
+ <line x1="1" y1="12" x2="3" y2="12"></line>
21
+ <line x1="21" y1="12" x2="23" y2="12"></line>
22
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
23
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
24
+ </svg>
25
+ <svg class="moon-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
26
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
27
+ </svg>
28
+ </button>
29
+ </div>
30
+ </header>
31
+
32
+ <script>
33
+ // Theme toggle
34
+ document.querySelector('.theme-toggle').addEventListener('click', function() {
35
+ const html = document.documentElement;
36
+ const current = html.getAttribute('data-theme');
37
+ const next = current === 'dark' ? 'light' : 'dark';
38
+ html.setAttribute('data-theme', next);
39
+ localStorage.setItem('theme', next);
40
+ });
41
+
42
+ // Mobile menu
43
+ document.querySelector('.mobile-menu-btn').addEventListener('click', function() {
44
+ document.querySelector('.sidebar').classList.toggle('open');
45
+ });
46
+ </script>
@@ -0,0 +1,39 @@
1
+ {% if pagination.totalPages > 1 %}
2
+ <nav class="pagination" aria-label="Pagination">
3
+ {% if pagination.previousUrl %}
4
+ <a href="{{ pagination.previousUrl }}" class="pagination-btn pagination-prev">
5
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
6
+ <polyline points="15 18 9 12 15 6"></polyline>
7
+ </svg>
8
+ <span>Previous</span>
9
+ </a>
10
+ {% else %}
11
+ <span class="pagination-btn pagination-prev disabled">
12
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
13
+ <polyline points="15 18 9 12 15 6"></polyline>
14
+ </svg>
15
+ <span>Previous</span>
16
+ </span>
17
+ {% endif %}
18
+
19
+ <span class="pagination-info">
20
+ Page {{ pagination.currentPage }} of {{ pagination.totalPages }}
21
+ </span>
22
+
23
+ {% if pagination.nextUrl %}
24
+ <a href="{{ pagination.nextUrl }}" class="pagination-btn pagination-next">
25
+ <span>Next</span>
26
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
27
+ <polyline points="9 18 15 12 9 6"></polyline>
28
+ </svg>
29
+ </a>
30
+ {% else %}
31
+ <span class="pagination-btn pagination-next disabled">
32
+ <span>Next</span>
33
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
34
+ <polyline points="9 18 15 12 9 6"></polyline>
35
+ </svg>
36
+ </span>
37
+ {% endif %}
38
+ </nav>
39
+ {% endif %}
@@ -0,0 +1,60 @@
1
+ <aside class="sidebar">
2
+ <div class="sidebar-header">
3
+ <a href="{{ '/' | url }}" class="sidebar-logo">
4
+ <span class="logo-icon">{{ site.title[0] }}</span>
5
+ <span class="logo-text">{{ site.title }}</span>
6
+ </a>
7
+ </div>
8
+
9
+ <nav class="sidebar-nav">
10
+ <a href="{{ '/' | url }}" class="nav-item {% if page.url == '/' %}active{% endif %}">
11
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
12
+ <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
13
+ <polyline points="9 22 9 12 15 12 15 22"></polyline>
14
+ </svg>
15
+ <span>Home</span>
16
+ </a>
17
+ <a href="{{ '/blog/' | url }}" class="nav-item {% if page.url and '/blog' in page.url %}active{% endif %}">
18
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
19
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
20
+ <polyline points="14 2 14 8 20 8"></polyline>
21
+ <line x1="16" y1="13" x2="8" y2="13"></line>
22
+ <line x1="16" y1="17" x2="8" y2="17"></line>
23
+ </svg>
24
+ <span>Posts</span>
25
+ </a>
26
+ <a href="{{ '/notes/' | url }}" class="nav-item {% if page.url and '/notes' in page.url %}active{% endif %}">
27
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
28
+ <line x1="18" y1="2" x2="22" y2="6"></line>
29
+ <path d="M7.5 20.5L19 9l-4-4L3.5 16.5 2 22z"></path>
30
+ </svg>
31
+ <span>Notes</span>
32
+ </a>
33
+ <a href="{{ '/tags/' | url }}" class="nav-item {% if page.url and '/tags' in page.url %}active{% endif %}">
34
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
35
+ <path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path>
36
+ <line x1="7" y1="7" x2="7.01" y2="7"></line>
37
+ </svg>
38
+ <span>Tags</span>
39
+ </a>
40
+ <a href="{{ '/about/' | url }}" class="nav-item {% if page.url and '/about' in page.url %}active{% endif %}">
41
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
42
+ <circle cx="12" cy="12" r="10"></circle>
43
+ <line x1="12" y1="16" x2="12" y2="12"></line>
44
+ <line x1="12" y1="8" x2="12.01" y2="8"></line>
45
+ </svg>
46
+ <span>About</span>
47
+ </a>
48
+ </nav>
49
+
50
+ <div class="sidebar-footer">
51
+ <a href="{{ '/feed.xml' | url }}" class="nav-item">
52
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
53
+ <path d="M4 11a9 9 0 0 1 9 9"></path>
54
+ <path d="M4 4a16 16 0 0 1 16 16"></path>
55
+ <circle cx="5" cy="19" r="1"></circle>
56
+ </svg>
57
+ <span>RSS Feed</span>
58
+ </a>
59
+ </div>
60
+ </aside>
@@ -0,0 +1,7 @@
1
+ {% if tags and tags.length %}
2
+ <div class="tag-list">
3
+ {% for tag in tags %}
4
+ <a href="{{ ('/tags/' + (tag | slug) + '/') | url }}" class="tag">#{{ tag }}</a>
5
+ {% endfor %}
6
+ </div>
7
+ {% endif %}
@@ -0,0 +1,29 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ {% include "meta.njk" %}
5
+ {% include "theme-script.njk" %}
6
+
7
+ <link rel="stylesheet" href="{{ '/styles/main.css' | url }}">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
11
+
12
+ {% block head %}{% endblock %}
13
+ </head>
14
+ <body>
15
+ <div class="app-layout">
16
+ {% include "sidebar.njk" %}
17
+
18
+ <div class="main-wrapper">
19
+ {% include "header.njk" %}
20
+
21
+ <main class="main-content">
22
+ {% block content %}{% endblock %}
23
+ </main>
24
+
25
+ {% include "footer.njk" %}
26
+ </div>
27
+ </div>
28
+ </body>
29
+ </html>
@@ -0,0 +1,21 @@
1
+ {% extends "base.njk" %}
2
+
3
+ {% block content %}
4
+ <article class="note-article">
5
+ <header class="note-header">
6
+ <time class="note-date" datetime="{{ page.date | date('iso') }}">
7
+ {{ page.date | date('full') }}
8
+ </time>
9
+ </header>
10
+
11
+ <div class="note-content prose">
12
+ {{ content | safe }}
13
+ </div>
14
+
15
+ {% if page.tags %}
16
+ <footer class="note-footer">
17
+ {% include "tag-list.njk" %}
18
+ </footer>
19
+ {% endif %}
20
+ </article>
21
+ {% endblock %}
@@ -0,0 +1,13 @@
1
+ {% extends "base.njk" %}
2
+
3
+ {% block content %}
4
+ <article class="page-article">
5
+ <header class="page-header">
6
+ <h1 class="page-title">{{ page.title }}</h1>
7
+ </header>
8
+
9
+ <div class="page-content prose">
10
+ {{ content | safe }}
11
+ </div>
12
+ </article>
13
+ {% endblock %}
@@ -0,0 +1,47 @@
1
+ {% extends "base.njk" %}
2
+
3
+ {% block content %}
4
+ <article class="post-article">
5
+ <header class="post-header">
6
+ {% if page.tags %}
7
+ <div class="post-tags-top">
8
+ {% for tag in page.tags | limit(2) %}
9
+ <a href="{{ ('/tags/' + (tag | slug) + '/') | url }}" class="tag-badge">{{ tag }}</a>
10
+ {% endfor %}
11
+ </div>
12
+ {% endif %}
13
+
14
+ <h1 class="post-title">{{ page.title }}</h1>
15
+
16
+ <div class="post-meta">
17
+ <div class="author-info">
18
+ <div class="author-avatar">{{ site.author[0] }}</div>
19
+ <div class="author-details">
20
+ <span class="author-name">{{ page.author or site.author }}</span>
21
+ <span class="post-date">{{ page.date | date('long') }} · {{ content | readingTime }}</span>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </header>
26
+
27
+ {% if page.image %}
28
+ <figure class="post-hero">
29
+ <img src="{{ page.image | url }}" alt="{{ page.title }}">
30
+ </figure>
31
+ {% endif %}
32
+
33
+ <div class="post-content prose">
34
+ {{ content | safe }}
35
+ </div>
36
+
37
+ {% if page.tags %}
38
+ <footer class="post-footer">
39
+ <div class="post-tags">
40
+ {% for tag in page.tags %}
41
+ <a href="{{ ('/tags/' + (tag | slug) + '/') | url }}" class="tag">#{{ tag }}</a>
42
+ {% endfor %}
43
+ </div>
44
+ </footer>
45
+ {% endif %}
46
+ </article>
47
+ {% endblock %}
@@ -0,0 +1,41 @@
1
+ {% extends "base.njk" %}
2
+
3
+ {% block content %}
4
+ <div class="blog-page">
5
+ <header class="page-header">
6
+ <h1 class="page-title">Blog</h1>
7
+ <p class="page-description">Thoughts, tutorials, and insights</p>
8
+ </header>
9
+
10
+ <div class="post-cards">
11
+ {% for post in posts %}
12
+ <article class="post-card">
13
+ <div class="card-content">
14
+ {% if post.tags %}
15
+ <div class="card-tags">
16
+ {% for tag in post.tags | limit(2) %}
17
+ <span class="tag-small">#{{ tag }}</span>
18
+ {% endfor %}
19
+ </div>
20
+ {% endif %}
21
+
22
+ <h2 class="card-title">
23
+ <a href="{{ post.url | url }}">{{ post.title }}</a>
24
+ </h2>
25
+
26
+ <p class="card-excerpt">{{ post.excerpt | excerpt(150) }}</p>
27
+
28
+ <div class="card-meta">
29
+ <span class="card-date">{{ post.date | date('short') }}</span>
30
+ <span class="card-reading-time">{{ post.content | readingTime }}</span>
31
+ </div>
32
+ </div>
33
+ </article>
34
+ {% else %}
35
+ <p class="no-posts">No posts yet.</p>
36
+ {% endfor %}
37
+ </div>
38
+
39
+ {% include "pagination.njk" %}
40
+ </div>
41
+ {% endblock %}
@@ -0,0 +1,22 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
3
+ <channel>
4
+ <title>{{ site.title }}</title>
5
+ <description>{{ site.description }}</description>
6
+ <link>{{ site.url }}</link>
7
+ <atom:link href="{{ site.url }}/feed.xml" rel="self" type="application/rss+xml"/>
8
+ <lastBuildDate>{{ buildDate }}</lastBuildDate>
9
+ {% for post in posts | limit(20) %}
10
+ <item>
11
+ <title>{{ post.title }}</title>
12
+ <link>{{ site.url }}{{ post.url }}</link>
13
+ <guid isPermaLink="true">{{ site.url }}{{ post.url }}</guid>
14
+ <pubDate>{{ post.date | date('rss') }}</pubDate>
15
+ <description><![CDATA[{{ post.excerpt | excerpt(300) }}]]></description>
16
+ {% for tag in post.tags %}
17
+ <category>{{ tag }}</category>
18
+ {% endfor %}
19
+ </item>
20
+ {% endfor %}
21
+ </channel>
22
+ </rss>
@@ -0,0 +1,65 @@
1
+ {% extends "base.njk" %}
2
+
3
+ {% block content %}
4
+ <div class="home-page">
5
+ <section class="hero-section">
6
+ <h1 class="hero-title">{{ site.title }}</h1>
7
+ <p class="hero-description">{{ site.description }}</p>
8
+ </section>
9
+
10
+ <section class="posts-section">
11
+ <div class="section-header">
12
+ <h2 class="section-title">Latest Posts</h2>
13
+ <a href="{{ '/blog/' | url }}" class="view-all-link">View all posts →</a>
14
+ </div>
15
+
16
+ <div class="post-cards">
17
+ {% for post in collections.posts | limit(6) %}
18
+ <article class="post-card">
19
+ <div class="card-content">
20
+ {% if post.tags %}
21
+ <div class="card-tags">
22
+ {% for tag in post.tags | limit(2) %}
23
+ <span class="tag-small">#{{ tag }}</span>
24
+ {% endfor %}
25
+ </div>
26
+ {% endif %}
27
+
28
+ <h3 class="card-title">
29
+ <a href="{{ post.url | url }}">{{ post.title }}</a>
30
+ </h3>
31
+
32
+ <p class="card-excerpt">{{ post.excerpt | excerpt(120) }}</p>
33
+
34
+ <div class="card-meta">
35
+ <span class="card-date">{{ post.date | date('short') }}</span>
36
+ <span class="card-reading-time">{{ post.content | readingTime }}</span>
37
+ </div>
38
+ </div>
39
+ </article>
40
+ {% else %}
41
+ <p class="no-posts">No posts yet. Create your first post!</p>
42
+ {% endfor %}
43
+ </div>
44
+ </section>
45
+
46
+ {% if collections.notes.length %}
47
+ <section class="notes-section">
48
+ <div class="section-header">
49
+ <h2 class="section-title">Recent Notes</h2>
50
+ <a href="{{ '/notes/' | url }}" class="view-all-link">View all notes →</a>
51
+ </div>
52
+
53
+ <div class="notes-list">
54
+ {% for note in collections.notes | limit(3) %}
55
+ <article class="note-card">
56
+ <time class="note-time">{{ note.date | date('short') }}</time>
57
+ <div class="note-preview">{{ note.excerpt | excerpt(150) }}</div>
58
+ <a href="{{ note.url | url }}" class="note-link">Read more →</a>
59
+ </article>
60
+ {% endfor %}
61
+ </div>
62
+ </section>
63
+ {% endif %}
64
+ </div>
65
+ {% endblock %}