@terrymooreii/sia 2.1.6 → 2.1.8

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/bin/cli.js CHANGED
@@ -14,6 +14,7 @@ import { devCommand } from '../lib/server.js';
14
14
  import { buildCommand } from '../lib/build.js';
15
15
  import { newCommand } from '../lib/new.js';
16
16
  import { initCommand } from '../lib/init.js';
17
+ import { themeCommand } from '../lib/theme.js';
17
18
 
18
19
  program
19
20
  .name('sia')
@@ -47,5 +48,11 @@ program
47
48
  .option('-d, --draft', 'Save as draft (posts only)')
48
49
  .action(newCommand);
49
50
 
51
+ program
52
+ .command('theme <name>')
53
+ .description('Create a new Sia theme package')
54
+ .option('-q, --quick', 'Skip prompts and use defaults')
55
+ .action(themeCommand);
56
+
50
57
  program.parse();
51
58
 
package/docs/README.md CHANGED
@@ -54,8 +54,10 @@ npm run build
54
54
  - **Tags & Categories** - Organize content with tags and auto-generated tag pages
55
55
  - **Pagination** - Built-in pagination for listing pages
56
56
  - **Image Support** - Automatic image copying and organization
57
+ - **Static Assets** - Support for favicons, fonts, and other static files
57
58
  - **Live Reload** - Development server with hot reloading
58
59
  - **Multiple Themes** - Built-in themes (main, minimal, developer, magazine) with light/dark mode
60
+ - **Custom Theme Packages** - Create and share themes as npm packages (`sia-theme-*`)
59
61
  - **RSS Feed** - Automatic RSS feed generation
60
62
  - **SEO Ready** - Open Graph and Twitter Card meta tags included
61
63
 
@@ -69,6 +71,10 @@ my-site/
69
71
  │ ├── pages/ # Static pages
70
72
  │ ├── notes/ # Short notes/tweets
71
73
  │ └── images/ # Images
74
+ ├── assets/ # Static assets (optional)
75
+ ├── static/ # Static assets (optional)
76
+ ├── public/ # Static assets (optional)
77
+ ├── favicon.ico # Site favicon (optional)
72
78
  ├── _layouts/ # Custom layouts (optional)
73
79
  ├── _includes/ # Custom includes (optional)
74
80
  ├── styles/ # Custom CSS (optional)
@@ -87,7 +93,8 @@ site:
87
93
  author: "Your Name"
88
94
 
89
95
  theme:
90
- name: main # Options: main, minimal, developer, magazine
96
+ name: main # Built-in: main, minimal, developer, magazine
97
+ # Or use external: my-theme (loads sia-theme-my-theme)
91
98
 
92
99
  input: src
93
100
  output: dist
@@ -116,6 +123,58 @@ server:
116
123
  showDrafts: false
117
124
  ```
118
125
 
126
+ ## Static Assets
127
+
128
+ Sia automatically copies static assets during the build process. You can place static files in any of these locations:
129
+
130
+ - **`assets/`** - Place files in `assets/` at the project root
131
+ - **`static/`** - Place files in `static/` at the project root
132
+ - **`public/`** - Place files in `public/` at the project root
133
+ - **Root directory** - Place `favicon.ico` directly in the project root
134
+
135
+ All files from these directories will be copied to the `dist/` folder during build, preserving their directory structure.
136
+
137
+ ### Supported File Types
138
+
139
+ Static assets include:
140
+ - **Favicons** - `.ico` files (favicon.ico can be in root or asset directories)
141
+ - **Fonts** - `.woff`, `.woff2`, `.ttf`, `.eot`
142
+ - **Documents** - `.pdf`, `.txt`, `.json`, `.xml`
143
+ - **Scripts** - `.js` files
144
+ - **Stylesheets** - `.css` files (though custom CSS is better placed in `styles/`)
145
+ - **Images** - All image formats (though images are better placed in `src/images/`)
146
+
147
+ ### Example Structure
148
+
149
+ ```
150
+ my-site/
151
+ ├── assets/
152
+ │ ├── favicon.ico
153
+ │ ├── robots.txt
154
+ │ ├── manifest.json
155
+ │ └── fonts/
156
+ │ └── custom-font.woff2
157
+ ├── static/
158
+ │ └── documents/
159
+ │ └── resume.pdf
160
+ └── favicon.ico # Also supported in root
161
+ ```
162
+
163
+ During build, these will be copied to:
164
+ ```
165
+ dist/
166
+ ├── assets/
167
+ │ ├── favicon.ico
168
+ │ ├── robots.txt
169
+ │ ├── manifest.json
170
+ │ └── fonts/
171
+ │ └── custom-font.woff2
172
+ ├── static/
173
+ │ └── documents/
174
+ │ └── resume.pdf
175
+ └── favicon.ico
176
+ ```
177
+
119
178
  ## CLI Commands
120
179
 
121
180
  | Command | Description |
@@ -126,6 +185,7 @@ server:
126
185
  | `sia new post "Title"` | Create a new blog post |
127
186
  | `sia new page "Title"` | Create a new page |
128
187
  | `sia new note "Content"` | Create a new note |
188
+ | `sia theme <name>` | Create a new theme package |
129
189
 
130
190
  ## License
131
191
 
@@ -1,6 +1,6 @@
1
1
  # Creating Themes
2
2
 
3
- This guide explains how to create custom themes for Sia and how to customize existing themes.
3
+ This guide explains how to create custom themes for Sia, distribute them as npm packages, and customize existing themes.
4
4
 
5
5
  ## Table of Contents
6
6
 
@@ -13,6 +13,9 @@ This guide explains how to create custom themes for Sia and how to customize exi
13
13
  - [Styles](#styles)
14
14
  - [Shared Includes](#shared-includes)
15
15
  - [Dark Mode Support](#dark-mode-support)
16
+ - [External Theme Packages](#external-theme-packages)
17
+ - [Creating a Theme Package](#creating-a-theme-package)
18
+ - [Publishing Your Theme](#publishing-your-theme)
16
19
  - [Customizing Existing Themes](#customizing-existing-themes)
17
20
  - [Built-in Themes](#built-in-themes)
18
21
  - [Best Practices](#best-practices)
@@ -649,6 +652,218 @@ toggle.addEventListener('click', () => {
649
652
 
650
653
  ---
651
654
 
655
+ ## External Theme Packages
656
+
657
+ Sia supports distributing themes as npm packages, making it easy to share themes with the community.
658
+
659
+ ### How Theme Resolution Works
660
+
661
+ When Sia loads a theme, it follows this resolution order:
662
+
663
+ 1. **Built-in themes** - First checks the `themes/` folder in the Sia package
664
+ 2. **npm packages** - If not found, looks for `sia-theme-{name}` in your `package.json` dependencies
665
+ 3. **Fallback** - Falls back to the "main" theme if nothing is found
666
+
667
+ ### Using an External Theme
668
+
669
+ To use an external theme in your Sia site:
670
+
671
+ ```bash
672
+ # Install the theme package
673
+ npm install sia-theme-awesome
674
+ ```
675
+
676
+ Then configure it in your `_config.yml`:
677
+
678
+ ```yaml
679
+ theme:
680
+ name: awesome # Sia will look for sia-theme-awesome
681
+ ```
682
+
683
+ That's it! Sia automatically detects and uses the theme from `node_modules`.
684
+
685
+ ### Theme Package Requirements
686
+
687
+ External theme packages must:
688
+
689
+ 1. **Follow naming convention** - Package name must be `sia-theme-{name}`
690
+ 2. **Export theme directory** - Include an `index.js` that exports the theme path
691
+ 3. **Include required files** - Have `layouts/` and `pages/` directories (minimum)
692
+ 4. **Follow theme structure** - Match the same structure as built-in themes
693
+
694
+ ---
695
+
696
+ ## Creating a Theme Package
697
+
698
+ ### Using the Theme Generator
699
+
700
+ The easiest way to create a new theme is with the built-in generator:
701
+
702
+ ```bash
703
+ sia theme my-awesome-theme
704
+ ```
705
+
706
+ This creates a complete theme package with all necessary files:
707
+
708
+ ```
709
+ sia-theme-my-awesome-theme/
710
+ ├── package.json # npm package configuration
711
+ ├── index.js # Exports theme directory path
712
+ ├── README.md # Theme documentation
713
+ ├── layouts/
714
+ │ ├── base.njk # Base HTML template
715
+ │ ├── post.njk # Blog post layout
716
+ │ ├── page.njk # Static page layout
717
+ │ └── note.njk # Note layout
718
+ ├── includes/
719
+ │ ├── header.njk # Site header/navigation
720
+ │ ├── footer.njk # Site footer
721
+ │ ├── hero.njk # Homepage hero section
722
+ │ ├── pagination.njk # Pagination component
723
+ │ └── tag-list.njk # Tag cloud component
724
+ ├── pages/
725
+ │ ├── index.njk # Homepage
726
+ │ ├── blog.njk # Blog listing
727
+ │ ├── notes.njk # Notes listing
728
+ │ ├── tags.njk # All tags page
729
+ │ ├── tag.njk # Single tag page
730
+ │ └── feed.njk # RSS feed
731
+ └── styles/
732
+ └── main.css # Theme styles
733
+ ```
734
+
735
+ ### Generator Options
736
+
737
+ ```bash
738
+ # Interactive mode (prompts for details)
739
+ sia theme my-theme
740
+
741
+ # Quick mode (skip prompts, use defaults)
742
+ sia theme my-theme --quick
743
+ sia theme my-theme -q
744
+ ```
745
+
746
+ ### package.json Structure
747
+
748
+ The generated `package.json` includes:
749
+
750
+ ```json
751
+ {
752
+ "name": "sia-theme-my-theme",
753
+ "version": "1.0.0",
754
+ "description": "My Theme theme for Sia static site generator",
755
+ "main": "index.js",
756
+ "type": "module",
757
+ "keywords": [
758
+ "sia",
759
+ "sia-theme",
760
+ "static-site",
761
+ "theme"
762
+ ],
763
+ "author": "Your Name",
764
+ "license": "MIT",
765
+ "peerDependencies": {
766
+ "@terrymooreii/sia": ">=2.0.0"
767
+ }
768
+ }
769
+ ```
770
+
771
+ ### index.js Structure
772
+
773
+ The `index.js` exports the theme directory for Sia to locate:
774
+
775
+ ```javascript
776
+ import { fileURLToPath } from 'url';
777
+ import { dirname } from 'path';
778
+
779
+ const __filename = fileURLToPath(import.meta.url);
780
+ const __dirname = dirname(__filename);
781
+
782
+ // Export the theme directory path for Sia to use
783
+ export const themeDir = __dirname;
784
+ export default themeDir;
785
+ ```
786
+
787
+ ### Manual Theme Creation
788
+
789
+ If you prefer to create a theme manually:
790
+
791
+ 1. Create a new directory: `mkdir sia-theme-my-theme`
792
+ 2. Initialize npm: `npm init`
793
+ 3. Set the package name to `sia-theme-{name}`
794
+ 4. Create the required directory structure
795
+ 5. Add an `index.js` that exports the directory path
796
+ 6. Add all required template files
797
+
798
+ ---
799
+
800
+ ## Publishing Your Theme
801
+
802
+ ### Preparing for Publication
803
+
804
+ 1. **Test locally** - Link your theme and test with a Sia site:
805
+
806
+ ```bash
807
+ # In your theme directory
808
+ npm link
809
+
810
+ # In a Sia site directory
811
+ npm link sia-theme-my-theme
812
+ ```
813
+
814
+ 2. **Update package.json** - Add repository, bugs, and homepage URLs:
815
+
816
+ ```json
817
+ {
818
+ "repository": {
819
+ "type": "git",
820
+ "url": "https://github.com/username/sia-theme-my-theme.git"
821
+ },
822
+ "bugs": {
823
+ "url": "https://github.com/username/sia-theme-my-theme/issues"
824
+ },
825
+ "homepage": "https://github.com/username/sia-theme-my-theme#readme"
826
+ }
827
+ ```
828
+
829
+ 3. **Write documentation** - Update the README with:
830
+ - Screenshots of the theme
831
+ - Installation instructions
832
+ - Configuration options
833
+ - Customization tips
834
+
835
+ ### Publishing to npm
836
+
837
+ ```bash
838
+ # Login to npm (if not already)
839
+ npm login
840
+
841
+ # Publish the package
842
+ npm publish
843
+
844
+ # Or publish with public access if scoped
845
+ npm publish --access public
846
+ ```
847
+
848
+ ### Versioning
849
+
850
+ Follow semantic versioning:
851
+
852
+ - **Patch** (1.0.1) - Bug fixes, minor style tweaks
853
+ - **Minor** (1.1.0) - New features, backward-compatible changes
854
+ - **Major** (2.0.0) - Breaking changes to templates or configuration
855
+
856
+ ### Theme Discovery
857
+
858
+ To help users find your theme:
859
+
860
+ 1. Use `sia-theme` in your npm keywords
861
+ 2. Add a clear description
862
+ 3. Include screenshots in your README
863
+ 4. Consider creating a demo site
864
+
865
+ ---
866
+
652
867
  ## Customizing Existing Themes
653
868
 
654
869
  You don't need to create a full theme to customize your site.
Binary file
Binary file
Binary file
package/lib/assets.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  rmdirSync
11
11
  } from 'fs';
12
12
  import { join, dirname, relative, extname } from 'path';
13
+ import { resolveTheme, getBuiltInThemesDir } from './theme-resolver.js';
13
14
 
14
15
  // Supported asset extensions
15
16
  const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.ico', '.avif'];
@@ -126,8 +127,11 @@ function hasCssFiles(dir) {
126
127
 
127
128
  /**
128
129
  * Copy default styles to output
130
+ *
131
+ * @param {object} config - Site configuration
132
+ * @param {object} [resolvedTheme] - Pre-resolved theme info from resolveTheme()
129
133
  */
130
- export function copyDefaultStyles(config, themesDir) {
134
+ export function copyDefaultStyles(config, resolvedTheme = null) {
131
135
  const outputStylesDir = join(config.outputDir, 'styles');
132
136
 
133
137
  // Check if user has custom styles (must actually have CSS files)
@@ -140,20 +144,23 @@ export function copyDefaultStyles(config, themesDir) {
140
144
  return copied;
141
145
  }
142
146
 
143
- // Copy styles from the selected theme
147
+ // Resolve theme if not already resolved
144
148
  const themeName = config.theme?.name || 'main';
145
- const themeStylesDir = join(themesDir, themeName, 'styles');
149
+ const theme = resolvedTheme || resolveTheme(themeName, config.rootDir);
150
+ const themeStylesDir = join(theme.themeDir, 'styles');
146
151
 
147
152
  if (existsSync(themeStylesDir)) {
148
153
  const copied = copyAssets(themeStylesDir, outputStylesDir);
149
- console.log(`🎨 Using "${themeName}" theme`);
154
+ if (!theme.isExternal) {
155
+ console.log(`🎨 Using "${theme.themeName}" theme`);
156
+ }
150
157
  return copied;
151
158
  }
152
159
 
153
- // Fallback to main theme if selected theme not found
154
- const fallbackStylesDir = join(themesDir, 'main', 'styles');
160
+ // Fallback to main theme if theme styles not found
161
+ const fallbackStylesDir = join(getBuiltInThemesDir(), 'main', 'styles');
155
162
  if (existsSync(fallbackStylesDir)) {
156
- console.log(`⚠️ Theme "${themeName}" not found, using "main" theme`);
163
+ console.log(`⚠️ Theme "${themeName}" styles not found, using "main" theme styles`);
157
164
  const copied = copyAssets(fallbackStylesDir, outputStylesDir);
158
165
  return copied;
159
166
  }
package/lib/build.js CHANGED
@@ -5,10 +5,10 @@ import { loadConfig } from './config.js';
5
5
  import { buildSiteData, paginate, getPaginationUrls } from './collections.js';
6
6
  import { createTemplateEngine, renderTemplate } from './templates.js';
7
7
  import { copyImages, copyDefaultStyles, copyStaticAssets, writeFile, ensureDir } from './assets.js';
8
+ import { resolveTheme } from './theme-resolver.js';
8
9
 
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = dirname(__filename);
11
- const themesDir = join(__dirname, '..', 'themes');
12
12
 
13
13
  /**
14
14
  * Clean the output directory
@@ -215,11 +215,15 @@ export async function build(options = {}) {
215
215
  cleanOutput(config);
216
216
  }
217
217
 
218
+ // Resolve theme once for both templates and assets
219
+ const themeName = config.theme?.name || 'main';
220
+ const resolvedTheme = resolveTheme(themeName, config.rootDir);
221
+
218
222
  // Build site data (collections, tags, etc.)
219
223
  const siteData = buildSiteData(config);
220
224
 
221
- // Create template engine
222
- const env = createTemplateEngine(config);
225
+ // Create template engine with resolved theme
226
+ const env = createTemplateEngine(config, resolvedTheme);
223
227
 
224
228
  // Render all content items
225
229
  let itemCount = 0;
@@ -244,7 +248,7 @@ export async function build(options = {}) {
244
248
 
245
249
  // Copy assets
246
250
  copyImages(config);
247
- copyDefaultStyles(config, themesDir);
251
+ copyDefaultStyles(config, resolvedTheme);
248
252
  copyStaticAssets(config);
249
253
 
250
254
  const duration = ((Date.now() - startTime) / 1000).toFixed(2);
package/lib/init.js CHANGED
@@ -87,7 +87,7 @@ Welcome to my new blog! This is my first post.
87
87
 
88
88
  ## About This Site
89
89
 
90
- This site is built with [Sia](https://github.com/sia/sia), a simple and powerful static site generator.
90
+ This site is built with [Sia](https://github.com/terrymooreii/sia), a simple and powerful static site generator.
91
91
 
92
92
  ## What's Next?
93
93
 
@@ -114,7 +114,7 @@ Hello! I'm ${author}. Welcome to my corner of the internet.
114
114
 
115
115
  ## About This Site
116
116
 
117
- This site is built with [Sia](https://github.com/sia/sia), a simple static site generator that supports:
117
+ This site is built with [Sia](https://github.com/terrymooreii/sia), a simple static site generator that supports:
118
118
 
119
119
  - Markdown with front matter
120
120
  - Blog posts, pages, and notes
@@ -216,7 +216,7 @@ Then upload the \`dist/\` folder to any static hosting.
216
216
 
217
217
  return `# ${title}
218
218
 
219
- A static site built with [Sia](https://github.com/sia/sia).
219
+ A static site built with [Sia](https://github.com/terrymooreii/sia).
220
220
 
221
221
  ## Getting Started
222
222
 
package/lib/templates.js CHANGED
@@ -2,6 +2,7 @@ import nunjucks from 'nunjucks';
2
2
  import { join, dirname } from 'path';
3
3
  import { existsSync } from 'fs';
4
4
  import { fileURLToPath } from 'url';
5
+ import { resolveTheme, getBuiltInThemesDir } from './theme-resolver.js';
5
6
 
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = dirname(__filename);
@@ -190,8 +191,11 @@ function createUrlFilter(basePath) {
190
191
 
191
192
  /**
192
193
  * Create and configure the Nunjucks environment
194
+ *
195
+ * @param {object} config - Site configuration
196
+ * @param {object} [resolvedTheme] - Pre-resolved theme info from resolveTheme()
193
197
  */
194
- export function createTemplateEngine(config) {
198
+ export function createTemplateEngine(config, resolvedTheme = null) {
195
199
  // Set up template paths - user layouts first, then defaults
196
200
  const templatePaths = [];
197
201
 
@@ -205,15 +209,18 @@ export function createTemplateEngine(config) {
205
209
  templatePaths.push(config.includesDir);
206
210
  }
207
211
 
208
- // Default templates from the selected theme
212
+ // Resolve theme if not already resolved
209
213
  const themeName = config.theme?.name || 'main';
210
- const themeDir = join(__dirname, '..', 'themes', themeName);
214
+ const theme = resolvedTheme || resolveTheme(themeName, config.rootDir);
215
+ const themeDir = theme.themeDir;
216
+
217
+ // Add theme template paths
211
218
  templatePaths.push(join(themeDir, 'layouts'));
212
219
  templatePaths.push(join(themeDir, 'includes'));
213
220
  templatePaths.push(join(themeDir, 'pages'));
214
221
 
215
222
  // Shared includes available to all themes
216
- const sharedIncludesDir = join(__dirname, '..', 'themes', '_shared', 'includes');
223
+ const sharedIncludesDir = join(getBuiltInThemesDir(), '_shared', 'includes');
217
224
  templatePaths.push(sharedIncludesDir);
218
225
 
219
226
  // Create the environment
@@ -0,0 +1,175 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { createRequire } from 'module';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ // Built-in themes directory
10
+ const builtInThemesDir = join(__dirname, '..', 'themes');
11
+
12
+ /**
13
+ * Resolve the path to a theme directory
14
+ *
15
+ * Resolution order:
16
+ * 1. Check built-in themes folder (sia's themes/)
17
+ * 2. Check for npm package sia-theme-{name}
18
+ * 3. Fall back to 'main' theme
19
+ *
20
+ * @param {string} themeName - The theme name from config
21
+ * @param {string} rootDir - The user's project root directory
22
+ * @returns {{ themeDir: string, themeName: string, isExternal: boolean }}
23
+ */
24
+ export function resolveTheme(themeName, rootDir = process.cwd()) {
25
+ // 1. Check built-in themes folder
26
+ const builtInThemeDir = join(builtInThemesDir, themeName);
27
+ if (existsSync(builtInThemeDir)) {
28
+ return {
29
+ themeDir: builtInThemeDir,
30
+ themeName,
31
+ isExternal: false
32
+ };
33
+ }
34
+
35
+ // 2. Check for npm package sia-theme-{name}
36
+ const packageName = `sia-theme-${themeName}`;
37
+ const externalThemeDir = resolveExternalTheme(packageName, rootDir);
38
+
39
+ if (externalThemeDir) {
40
+ console.log(`🎨 Using external theme package: ${packageName}`);
41
+ return {
42
+ themeDir: externalThemeDir,
43
+ themeName,
44
+ isExternal: true
45
+ };
46
+ }
47
+
48
+ // 3. Fall back to 'main' theme
49
+ if (themeName !== 'main') {
50
+ console.log(`⚠️ Theme "${themeName}" not found, falling back to "main" theme`);
51
+ }
52
+
53
+ return {
54
+ themeDir: join(builtInThemesDir, 'main'),
55
+ themeName: 'main',
56
+ isExternal: false
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Attempt to resolve an external theme package
62
+ *
63
+ * @param {string} packageName - The npm package name (sia-theme-{name})
64
+ * @param {string} rootDir - The user's project root directory
65
+ * @returns {string|null} The theme directory path or null if not found
66
+ */
67
+ function resolveExternalTheme(packageName, rootDir) {
68
+ // First, check if the package is in the user's package.json
69
+ const packageJsonPath = join(rootDir, 'package.json');
70
+
71
+ if (!existsSync(packageJsonPath)) {
72
+ return null;
73
+ }
74
+
75
+ try {
76
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
77
+ const allDeps = {
78
+ ...packageJson.dependencies,
79
+ ...packageJson.devDependencies
80
+ };
81
+
82
+ // Check if the theme package is listed as a dependency
83
+ if (!allDeps[packageName]) {
84
+ return null;
85
+ }
86
+
87
+ // Try to resolve the package path
88
+ // Use createRequire to resolve from the user's project directory
89
+ const require = createRequire(join(rootDir, 'package.json'));
90
+
91
+ try {
92
+ // Try to resolve the package's main entry point
93
+ const packageMainPath = require.resolve(packageName);
94
+ const packageDir = dirname(packageMainPath);
95
+
96
+ // The theme directory is typically the package root
97
+ // Check if it has the expected theme structure
98
+ if (isValidThemeDirectory(packageDir)) {
99
+ return packageDir;
100
+ }
101
+
102
+ // Sometimes the main entry is in a subdirectory, try parent
103
+ const parentDir = dirname(packageDir);
104
+ if (isValidThemeDirectory(parentDir)) {
105
+ return parentDir;
106
+ }
107
+
108
+ // Try node_modules directly
109
+ const nodeModulesPath = join(rootDir, 'node_modules', packageName);
110
+ if (existsSync(nodeModulesPath) && isValidThemeDirectory(nodeModulesPath)) {
111
+ return nodeModulesPath;
112
+ }
113
+
114
+ return null;
115
+ } catch (resolveErr) {
116
+ // Package might not be installed yet
117
+ // Try node_modules directly as fallback
118
+ const nodeModulesPath = join(rootDir, 'node_modules', packageName);
119
+ if (existsSync(nodeModulesPath) && isValidThemeDirectory(nodeModulesPath)) {
120
+ return nodeModulesPath;
121
+ }
122
+ return null;
123
+ }
124
+ } catch (err) {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Check if a directory has the expected theme structure
131
+ *
132
+ * @param {string} dir - Directory to check
133
+ * @returns {boolean}
134
+ */
135
+ function isValidThemeDirectory(dir) {
136
+ if (!existsSync(dir)) {
137
+ return false;
138
+ }
139
+
140
+ // A valid theme must have at least layouts and pages directories
141
+ const hasLayouts = existsSync(join(dir, 'layouts'));
142
+ const hasPages = existsSync(join(dir, 'pages'));
143
+
144
+ return hasLayouts && hasPages;
145
+ }
146
+
147
+ /**
148
+ * Get the built-in themes directory path
149
+ *
150
+ * @returns {string}
151
+ */
152
+ export function getBuiltInThemesDir() {
153
+ return builtInThemesDir;
154
+ }
155
+
156
+ /**
157
+ * Get list of available built-in themes
158
+ *
159
+ * @returns {string[]}
160
+ */
161
+ export function getBuiltInThemes() {
162
+ return readdirSync(builtInThemesDir)
163
+ .filter(name => {
164
+ if (name.startsWith('_')) return false; // Skip _shared
165
+ const themePath = join(builtInThemesDir, name);
166
+ return statSync(themePath).isDirectory();
167
+ });
168
+ }
169
+
170
+ export default {
171
+ resolveTheme,
172
+ getBuiltInThemesDir,
173
+ getBuiltInThemes
174
+ };
175
+