@daz4126/swifty 2.11.0 → 2.12.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 CHANGED
@@ -15,13 +15,21 @@ Swifty uses convention over configuration to make it super simple to build blazi
15
15
  - **RSS feed generation** for blogs and content folders
16
16
  - **Draft mode** for work-in-progress pages (visible in dev, hidden in production)
17
17
  - **Scheduled publishing** via future dates in front matter
18
+ - **Contact forms** via third-party services (Formspree, Netlify Forms, etc.)
19
+ - **Pagination** for folders with many pages
20
+ - **Data files** - Load JSON/YAML data and use in templates
21
+ - **Open Graph tags** - Auto-generated social sharing meta tags
22
+ - **Word count & reading time** - Auto-calculated for blog posts
23
+ - **Previous/next navigation** - Auto-generated links between sibling pages
24
+ - **[Eta templating](https://eta.js.org/)** - Full JavaScript in templates with EJS syntax
18
25
  - **Optional [Turbo](https://turbo.hotwired.dev/)** for SPA-like transitions
19
26
 
20
27
  ## Quickstart
21
28
 
22
29
  ```bash
23
- npm install @daz4126/swifty
24
- npx swifty init
30
+ npm install -g @daz4126/swifty
31
+ swifty my-site
32
+ cd my-site
25
33
  npx swifty start
26
34
  ```
27
35
 
@@ -34,6 +42,7 @@ your-site/
34
42
  ├── pages/ # Markdown content (folder structure = URLs)
35
43
  ├── layouts/ # HTML layout templates
36
44
  ├── partials/ # Reusable content snippets
45
+ ├── data/ # JSON/YAML data files
37
46
  ├── css/ # Stylesheets (auto-injected)
38
47
  ├── js/ # JavaScript (auto-injected)
39
48
  ├── images/ # Images (auto-optimized to WebP)
@@ -44,16 +53,18 @@ your-site/
44
53
  ## Commands
45
54
 
46
55
  ```bash
47
- npx swifty init # Create new project structure
56
+ npx swifty my-site # Create new site in my-site/ folder
48
57
  npx swifty build # Build static site to dist/ (for production)
49
58
  npx swifty start # Build, watch, and serve at localhost:3000 (for development)
50
59
  npx swifty build --out dir # Build to custom output directory
60
+ npx swifty deploy "message" # Build, git add, commit, and push (message optional)
51
61
  ```
52
62
 
53
63
  ### Development vs Production
54
64
 
55
65
  - **`swifty start`** - For development. Includes live reload (auto-refreshes browser on file changes) and file watching with incremental builds for CSS/JS/images.
56
66
  - **`swifty build`** - For production deployment. Produces clean output without any development scripts.
67
+ - **`swifty deploy "message"`** - Builds the site and commits/pushes to git in one command.
57
68
 
58
69
  ## Documentation
59
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daz4126/swifty",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,6 +26,7 @@
26
26
  "description": "Super Speedy Static Site Generator",
27
27
  "dependencies": {
28
28
  "chokidar": "^4.0.0",
29
+ "eta": "^4.5.0",
29
30
  "fs-extra": "^11.2.0",
30
31
  "gray-matter": "^4.0.3",
31
32
  "highlight.js": "^11.11.1",
@@ -33,7 +34,7 @@
33
34
  "livereload": "^0.10.3",
34
35
  "marked": "^14.1.3",
35
36
  "marked-highlight": "^2.2.1",
36
- "serve": "^14.2.4",
37
+ "serve": "^14.2.5",
37
38
  "sharp": "^0.33.5"
38
39
  },
39
40
  "devDependencies": {
package/src/assets.js CHANGED
@@ -3,6 +3,7 @@ import fsExtra from "fs-extra";
3
3
  import path from "path";
4
4
  import sharp from "sharp";
5
5
  import { dirs, defaultConfig } from "./config.js";
6
+ import { mapLimit } from "./concurrency.js";
6
7
 
7
8
  // Get file modification timestamp for cache busting
8
9
  const getFileMtime = async (filePath) => {
@@ -13,7 +14,53 @@ const getFileMtime = async (filePath) => {
13
14
  const validExtensions = {
14
15
  css: [".css"],
15
16
  js: [".js"],
16
- images: [".png", ".jpg", ".jpeg", ".gif", ".svg", " .webp"],
17
+ images: [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"],
18
+ };
19
+
20
+ const optimizableImageExtensions = [".jpg", ".jpeg", ".png"];
21
+
22
+ const getBuildConcurrency = () => defaultConfig.build_concurrency || 16;
23
+
24
+ const isDestinationFresh = async (source, destination) => {
25
+ try {
26
+ const [sourceStats, destinationStats] = await Promise.all([
27
+ fs.stat(source),
28
+ fs.stat(destination),
29
+ ]);
30
+ return destinationStats.mtimeMs >= sourceStats.mtimeMs;
31
+ } catch (err) {
32
+ return false;
33
+ }
34
+ };
35
+
36
+ const copyIfStale = async (source, destination) => {
37
+ if (await isDestinationFresh(source, destination)) {
38
+ return false;
39
+ }
40
+
41
+ await fsExtra.copy(source, destination);
42
+ return true;
43
+ };
44
+
45
+ const optimizeImageToWebp = async (source, destination) => {
46
+ if (await isDestinationFresh(source, destination)) {
47
+ return false;
48
+ }
49
+
50
+ const image = sharp(source);
51
+ const metadata = await image.metadata();
52
+ const originalWidth = metadata.width || 0;
53
+ const maxWidth = defaultConfig.max_image_width || 800;
54
+ const imageQuality = defaultConfig.image_quality || 80;
55
+ const resizeOptions =
56
+ originalWidth > 0 ? { width: Math.min(originalWidth, maxWidth) } : {};
57
+
58
+ await image
59
+ .resize(resizeOptions)
60
+ .toFormat("webp", { quality: imageQuality })
61
+ .toFile(destination);
62
+
63
+ return true;
17
64
  };
18
65
 
19
66
  const ensureAndCopy = async (source, destination, validExts) => {
@@ -40,47 +87,47 @@ const copyAssets = async (outputDir = dirs.dist) => {
40
87
  validExtensions.css,
41
88
  );
42
89
  await ensureAndCopy(dirs.js, path.join(outputDir, "js"), validExtensions.js);
43
- await ensureAndCopy(
44
- dirs.images,
45
- path.join(outputDir, "images"),
46
- validExtensions.images,
47
- );
48
90
  };
49
91
  async function optimizeImages(outputDir = dirs.dist) {
50
92
  try {
51
- const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png"];
52
- const images_folder = path.join(outputDir, "images");
53
- const files = await fs.readdir(images_folder);
54
-
55
- await Promise.all(
56
- files.map(async (file) => {
57
- const filePath = path.join(images_folder, file);
93
+ if (!(await fsExtra.pathExists(dirs.images))) {
94
+ console.log(`No ${path.basename(dirs.images)} found in ${dirs.images}`);
95
+ return;
96
+ }
97
+
98
+ const imagesFolder = path.join(outputDir, "images");
99
+ await fsExtra.ensureDir(imagesFolder);
100
+
101
+ const files = await fs.readdir(dirs.images);
102
+
103
+ await mapLimit(
104
+ files.filter((file) =>
105
+ validExtensions.images.includes(path.extname(file).toLowerCase()),
106
+ ),
107
+ async (file) => {
108
+ const filePath = path.join(dirs.images, file);
58
109
  const ext = path.extname(file).toLowerCase();
59
110
 
60
- if (!IMAGE_EXTENSIONS.includes(ext)) return;
111
+ if (!optimizableImageExtensions.includes(ext)) {
112
+ const destination = path.join(imagesFolder, file);
113
+ const copied = await copyIfStale(filePath, destination);
114
+ if (copied) {
115
+ console.log(`Copied ${file}`);
116
+ }
117
+ return;
118
+ }
61
119
 
62
120
  const optimizedPath = path.join(
63
- images_folder,
121
+ imagesFolder,
64
122
  `${path.basename(file, ext)}.webp`,
65
123
  );
66
124
 
67
- if (filePath !== optimizedPath) {
68
- const image = sharp(filePath);
69
- const metadata = await image.metadata();
70
- const originalWidth = metadata.width || 0;
71
- const maxWidth = defaultConfig.max_image_size || 800;
72
- const resizeWidth = Math.min(originalWidth, maxWidth);
73
-
74
- await image
75
- .resize({ width: resizeWidth })
76
- .toFormat("webp", { quality: 80 })
77
- .toFile(optimizedPath);
78
-
79
- await fs.unlink(filePath);
80
-
125
+ const optimized = await optimizeImageToWebp(filePath, optimizedPath);
126
+ if (optimized) {
81
127
  console.log(`Optimized ${file} -> ${optimizedPath}`);
82
128
  }
83
- }),
129
+ },
130
+ getBuildConcurrency(),
84
131
  );
85
132
  } catch (error) {
86
133
  console.error("Error optimizing images:", error);
@@ -148,7 +195,6 @@ const copySingleAsset = async (filePath, outputDir = dirs.dist) => {
148
195
 
149
196
  // Process a single image
150
197
  const optimizeSingleImage = async (filePath, outputDir = dirs.dist) => {
151
- const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png"];
152
198
  const filename = path.basename(filePath);
153
199
  const ext = path.extname(filePath).toLowerCase();
154
200
  const imagesFolder = path.join(outputDir, "images");
@@ -156,26 +202,20 @@ const optimizeSingleImage = async (filePath, outputDir = dirs.dist) => {
156
202
  await fsExtra.ensureDir(imagesFolder);
157
203
 
158
204
  // For non-optimizable images (svg, webp, gif), just copy
159
- if (!IMAGE_EXTENSIONS.includes(ext)) {
160
- await fsExtra.copy(filePath, path.join(imagesFolder, filename));
161
- console.log(`Copied ${filename}`);
205
+ if (!optimizableImageExtensions.includes(ext)) {
206
+ const copied = await copyIfStale(filePath, path.join(imagesFolder, filename));
207
+ if (copied) {
208
+ console.log(`Copied ${filename}`);
209
+ }
162
210
  return true;
163
211
  }
164
212
 
165
213
  // Optimize jpg/jpeg/png to webp
166
214
  const optimizedPath = path.join(imagesFolder, `${path.basename(filename, ext)}.webp`);
167
- const image = sharp(filePath);
168
- const metadata = await image.metadata();
169
- const originalWidth = metadata.width || 0;
170
- const maxWidth = defaultConfig.max_image_size || 800;
171
- const resizeWidth = Math.min(originalWidth, maxWidth);
172
-
173
- await image
174
- .resize({ width: resizeWidth })
175
- .toFormat("webp", { quality: 80 })
176
- .toFile(optimizedPath);
177
-
178
- console.log(`Optimized ${filename} -> ${path.basename(optimizedPath)}`);
215
+ const optimized = await optimizeImageToWebp(filePath, optimizedPath);
216
+ if (optimized) {
217
+ console.log(`Optimized ${filename} -> ${path.basename(optimizedPath)}`);
218
+ }
179
219
  return true;
180
220
  };
181
221
 
package/src/build.js CHANGED
@@ -1,13 +1,69 @@
1
1
  import { copyAssets, optimizeImages } from "./assets.js";
2
2
  import { generatePages, createPages, addLinks } from "./pages.js";
3
3
  import { generateRssFeeds } from "./rss.js";
4
+ import { generateSeoFiles } from "./sitemap.js";
4
5
  import { dirs } from "./config.js";
5
6
 
6
- export default async function build(outputDir) {
7
+ export default async function build(outputDir = dirs.dist) {
8
+ const startTime = performance.now();
9
+ console.log("🚀 Starting build...");
10
+
11
+ const copyStart = performance.now();
7
12
  await copyAssets(outputDir);
13
+ const copyTime = performance.now() - copyStart;
14
+ console.log(`📁 Assets copied in ${(copyTime / 1000).toFixed(2)}s`);
15
+
16
+ const imageStart = performance.now();
8
17
  await optimizeImages(outputDir);
18
+ const imageTime = performance.now() - imageStart;
19
+ console.log(`🖼️ Images optimized in ${(imageTime / 1000).toFixed(2)}s`);
20
+
21
+ const pagesStart = performance.now();
9
22
  const pages = await generatePages(dirs.pages);
23
+ const pagesTime = performance.now() - pagesStart;
24
+ console.log(`📄 Pages generated in ${(pagesTime / 1000).toFixed(2)}s`);
25
+
26
+ const linksStart = performance.now();
10
27
  await addLinks(pages);
28
+ const linksTime = performance.now() - linksStart;
29
+ console.log(`🔗 Links added in ${(linksTime / 1000).toFixed(2)}s`);
30
+
31
+ const createStart = performance.now();
11
32
  await createPages(pages, outputDir);
33
+ const createTime = performance.now() - createStart;
34
+ console.log(`✨ Pages created in ${(createTime / 1000).toFixed(2)}s`);
35
+
36
+ const rssStart = performance.now();
12
37
  await generateRssFeeds(pages, outputDir);
38
+ const rssTime = performance.now() - rssStart;
39
+ console.log(`📡 RSS feeds generated in ${(rssTime / 1000).toFixed(2)}s`);
40
+
41
+ const seoStart = performance.now();
42
+ await generateSeoFiles(pages, outputDir);
43
+ const seoTime = performance.now() - seoStart;
44
+ console.log(`🧭 SEO files generated in ${(seoTime / 1000).toFixed(2)}s`);
45
+
46
+ const totalTime = performance.now() - startTime;
47
+ console.log(`\n✅ Build completed in ${(totalTime / 1000).toFixed(2)}s`);
48
+
49
+ // Performance summary
50
+ const stages = [
51
+ { name: "Assets", time: copyTime },
52
+ { name: "Images", time: imageTime },
53
+ { name: "Pages", time: pagesTime },
54
+ { name: "Links", time: linksTime },
55
+ { name: "Create", time: createTime },
56
+ { name: "RSS", time: rssTime },
57
+ { name: "SEO", time: seoTime }
58
+ ];
59
+
60
+ const slowest = stages.sort((a, b) => b.time - a.time)[0];
61
+ if (slowest.time > totalTime * 0.3) {
62
+ console.log(`⚠️ Slowest stage: ${slowest.name} (${(slowest.time / 1000).toFixed(2)}s)`);
63
+ }
64
+
65
+ // Force output to appear
66
+ if (process.stdout.isTTY) {
67
+ process.stdout.write('\n');
68
+ }
13
69
  }
package/src/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawn } from "child_process";
3
+ import { spawn, execSync } from "child_process";
4
4
 
5
5
  const args = process.argv.slice(2);
6
6
  let outDir = "dist"; // default
@@ -14,13 +14,22 @@ if (outIndex !== -1 && args[outIndex + 1]) {
14
14
  // Pass outDir as an environment variable too (optional, still useful)
15
15
  process.env.OUT_DIR = outDir;
16
16
 
17
+ const reservedCommands = ["build", "start", "watch", "deploy"];
18
+
17
19
  async function main() {
18
20
  const command = args[0];
19
21
 
22
+ // No arguments - show usage
23
+ if (!command) {
24
+ console.log(`Usage:`);
25
+ console.log(` swifty <sitename> Create a new site in <sitename> folder`);
26
+ console.log(` swifty build [--out folder] Build the site`);
27
+ console.log(` swifty start [--out folder] Build and serve with live reload`);
28
+ console.log(` swifty deploy ["message"] Build, commit, and push to git`);
29
+ return;
30
+ }
31
+
20
32
  switch (command) {
21
- case "init":
22
- await import("./init.js");
23
- break;
24
33
  case "build": {
25
34
  const build = await import("./build.js");
26
35
  if (typeof build.default === "function") {
@@ -54,9 +63,44 @@ async function main() {
54
63
  }
55
64
  break;
56
65
  }
57
- default:
58
- console.log(`Unknown command: ${command}`);
59
- console.log(`Usage: swifty [init|build|start|watch] [--out folder]`);
66
+ case "deploy": {
67
+ const commitMessage = args[1] || "Deploying latest build";
68
+
69
+ // Build the site
70
+ const build = await import("./build.js");
71
+ if (typeof build.default === "function") {
72
+ await build.default(outDir);
73
+ }
74
+
75
+ // Git operations
76
+ try {
77
+ console.log("Adding files to git...");
78
+ execSync("git add .", { stdio: "inherit" });
79
+
80
+ console.log("Committing changes...");
81
+ execSync(`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`, {
82
+ stdio: "inherit",
83
+ });
84
+
85
+ console.log("Pushing to remote...");
86
+ execSync("git push", { stdio: "inherit" });
87
+
88
+ console.log("Deployed successfully!");
89
+ } catch (error) {
90
+ console.error("Deploy failed:", error.message);
91
+ process.exit(1);
92
+ }
93
+ break;
94
+ }
95
+ default: {
96
+ // Treat as sitename for new project creation
97
+ const sitename = command;
98
+ const init = await import("./init.js");
99
+ if (typeof init.default === "function") {
100
+ await init.default(sitename);
101
+ }
102
+ break;
103
+ }
60
104
  }
61
105
  }
62
106
 
@@ -0,0 +1,25 @@
1
+ const DEFAULT_CONCURRENCY = 16;
2
+
3
+ const getConcurrencyLimit = (value = DEFAULT_CONCURRENCY) => {
4
+ const limit = Number(value);
5
+ return Number.isInteger(limit) && limit > 0 ? limit : DEFAULT_CONCURRENCY;
6
+ };
7
+
8
+ const mapLimit = async (items, mapper, concurrency = DEFAULT_CONCURRENCY) => {
9
+ const limit = Math.min(getConcurrencyLimit(concurrency), items.length || 1);
10
+ const results = new Array(items.length);
11
+ let index = 0;
12
+
13
+ const workers = Array.from({ length: limit }, async () => {
14
+ while (index < items.length) {
15
+ const currentIndex = index;
16
+ index += 1;
17
+ results[currentIndex] = await mapper(items[currentIndex], currentIndex);
18
+ }
19
+ });
20
+
21
+ await Promise.all(workers);
22
+ return results;
23
+ };
24
+
25
+ export { getConcurrencyLimit, mapLimit };
package/src/config.js CHANGED
@@ -16,6 +16,7 @@ const dirs = {
16
16
  css: path.join(baseDir, "css"),
17
17
  js: path.join(baseDir, "js"),
18
18
  partials: path.join(baseDir, "partials"),
19
+ data: path.join(baseDir, "data"),
19
20
  };
20
21
 
21
22
  async function loadConfig(dir) {
@@ -31,6 +32,25 @@ async function loadConfig(dir) {
31
32
  return {};
32
33
  }
33
34
 
34
- const defaultConfig = await loadConfig(baseDir);
35
+ // Hardcoded defaults (used if not specified in config file)
36
+ const builtInDefaults = {
37
+ default_layout_name: 'default',
38
+ // Reading time calculation
39
+ words_per_minute: 200,
40
+ // Image optimization settings
41
+ max_image_width: 800,
42
+ image_quality: 80,
43
+ // LiveReload and watcher settings
44
+ livereload_port: 35729,
45
+ watcher_delay: 100,
46
+ watcher_interval: 500,
47
+ // Pagination
48
+ default_page_count: 2,
49
+ // Build safety
50
+ build_concurrency: 16,
51
+ };
52
+
53
+ const loadedConfig = await loadConfig(baseDir);
54
+ const defaultConfig = { ...builtInDefaults, ...loadedConfig };
35
55
 
36
- export { baseDir, dirs, defaultConfig, loadConfig };
56
+ export { baseDir, dirs, defaultConfig, loadConfig };
package/src/data.js ADDED
@@ -0,0 +1,70 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import yaml from "js-yaml";
4
+ import fsExtra from "fs-extra";
5
+ import { dirs } from "./config.js";
6
+
7
+ let dataCache = null;
8
+
9
+ /**
10
+ * Load all data files from the data/ folder
11
+ * Supports .json and .yaml/.yml files
12
+ * File name becomes the key: data/team.json -> data.team
13
+ */
14
+ async function loadData() {
15
+ if (dataCache !== null) {
16
+ return dataCache;
17
+ }
18
+
19
+ const data = {};
20
+
21
+ // Check if data directory exists
22
+ if (!(await fsExtra.pathExists(dirs.data))) {
23
+ dataCache = data;
24
+ return data;
25
+ }
26
+
27
+ try {
28
+ const files = await fs.readdir(dirs.data);
29
+
30
+ for (const file of files) {
31
+ const filePath = path.join(dirs.data, file);
32
+ const stat = await fs.stat(filePath);
33
+
34
+ // Skip directories
35
+ if (stat.isDirectory()) continue;
36
+
37
+ const ext = path.extname(file).toLowerCase();
38
+ const name = path.basename(file, ext);
39
+
40
+ // Only process JSON and YAML files
41
+ if (!['.json', '.yaml', '.yml'].includes(ext)) continue;
42
+
43
+ try {
44
+ const content = await fs.readFile(filePath, 'utf-8');
45
+
46
+ if (ext === '.json') {
47
+ data[name] = JSON.parse(content);
48
+ } else {
49
+ data[name] = yaml.load(content);
50
+ }
51
+ } catch (error) {
52
+ console.warn(`Error loading data file ${file}: ${error.message}`);
53
+ }
54
+ }
55
+ } catch (error) {
56
+ console.warn(`Error reading data directory: ${error.message}`);
57
+ }
58
+
59
+ dataCache = data;
60
+ return data;
61
+ }
62
+
63
+ /**
64
+ * Clear the data cache (useful for watch mode)
65
+ */
66
+ function clearDataCache() {
67
+ dataCache = null;
68
+ }
69
+
70
+ export { loadData, clearDataCache };
package/src/init.js CHANGED
@@ -2,62 +2,52 @@
2
2
 
3
3
  import fs from "fs";
4
4
  import path from "path";
5
- import { fileURLToPath } from "url";
6
- import { dirname } from "path";
7
5
 
8
- // Needed to emulate __dirname in ES modules
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = dirname(__filename);
11
-
12
- // Define the structure
13
- const structure = {
14
- "pages/index.md": "This my home page.",
15
- "layouts/": null,
16
- "partials/": null,
17
- "css/": null,
18
- "js/": null,
19
- "images/": null,
20
- "config.yaml": `sitename: Swifty
6
+ function getStructure(sitename) {
7
+ return {
8
+ "pages/index.md": `# Welcome to ${sitename}\n\nThis is my home page.`,
9
+ "layouts/": null,
10
+ "partials/": null,
11
+ "css/": null,
12
+ "js/": null,
13
+ "images/": null,
14
+ "data/": null,
15
+ "config.yaml": `sitename: ${sitename}
21
16
  breadcrumb_separator: "&raquo;"
22
17
  breadcrumb_class: swifty_breadcrumb
23
18
  link_class: swifty_link
24
- tag_class: tag
25
- default_layout_name: site
19
+ tag_class: swifty_tag
20
+ default_layout_name: default
26
21
  default_link_name: links
27
22
  max_image_size: 800
28
23
 
29
- dateFormat:
24
+ dateFormat:
30
25
  weekday: short
31
26
  month: short
32
27
  day: numeric
33
28
  year: numeric`,
34
- "template.html": `<!DOCTYPE html>
29
+ "template.html": `<!DOCTYPE html>
35
30
  <html lang="en">
36
31
  <head>
37
32
  <meta charset="UTF-8">
38
33
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
39
- <link rel="icon" href="favicon.ico" sizes="48x48" type="image/x-icon">
40
- <link rel="icon" href="favicon-16x16.png" sizes="16x16" type="image/x-icon">
41
- <link rel="icon" href="favicon-32x32.png" sizes="32x32" type="image/png">
42
- <link rel="apple-touch-icon" href="path/to/apple-touch-icon.png">
43
- <link rel="icon" sizes="192x192" href="android-chrome-192x19.png">
44
- <link rel="icon" sizes="512x512" href="android-chrome-512x512.png">
45
- <title>{{ sitename }} || {{ title }}</title>
34
+ <title><%= sitename %> | <%= title %></title>
46
35
  </head>
47
36
  <body>
48
37
  <header>
49
- <h1>Swifty</h1>
38
+ <h1>${sitename}</h1>
50
39
  </header>
51
40
  <main>
52
- {{ content }}
41
+ <%= content %>
53
42
  </main>
54
43
  <footer>
55
44
  <p>This site was built with Swifty, the super speedy static site generator.</p>
56
45
  </footer>
57
46
  </body>
58
47
  </html>
59
- `
60
- };
48
+ `,
49
+ };
50
+ }
61
51
 
62
52
  function createStructure(basePath, structure) {
63
53
  Object.entries(structure).forEach(([filePath, content]) => {
@@ -71,7 +61,20 @@ function createStructure(basePath, structure) {
71
61
  });
72
62
  }
73
63
 
74
- const projectRoot = process.cwd();
75
- createStructure(projectRoot, structure);
64
+ export default function init(sitename) {
65
+ const projectRoot = path.join(process.cwd(), sitename);
66
+
67
+ // Check if folder already exists
68
+ if (fs.existsSync(projectRoot)) {
69
+ console.error(`Error: Folder "${sitename}" already exists.`);
70
+ process.exit(1);
71
+ }
76
72
 
77
- console.log("✅ Swifty project initialized successfully!");
73
+ const structure = getStructure(sitename);
74
+ createStructure(projectRoot, structure);
75
+
76
+ console.log(`Site "${sitename}" created successfully!`);
77
+ console.log(`\nNext steps:`);
78
+ console.log(` cd ${sitename}`);
79
+ console.log(` npx swifty start`);
80
+ }
package/src/layout.js CHANGED
@@ -40,7 +40,7 @@ const createTemplate = async () => {
40
40
  ? `<script type="module">import * as Turbo from 'https://esm.sh/@hotwired/turbo';</script>`
41
41
  : '';
42
42
  const livereloadScript = process.env.SWIFTY_WATCH
43
- ? `<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':35729/livereload.js?snipver=1"></' + 'script>')</script>`
43
+ ? `<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':${defaultConfig.livereload_port || 35729}/livereload.js?snipver=1"></' + 'script>')</script>`
44
44
  : '';
45
45
 
46
46
  // Preload local assets for faster loading
@@ -59,10 +59,10 @@ const createTemplate = async () => {
59
59
  };
60
60
 
61
61
  const applyLayoutAndWrapContent = async (page,content) => {
62
- const layoutContent = await getLayout(page.data.layout !== undefined ? page.data.layout : page.layout);
62
+ const layoutContent = await getLayout(page.meta.layout !== undefined ? page.meta.layout : page.layout);
63
63
  if (!layoutContent) return content;
64
64
  // Use function to avoid $` special replacement patterns in content
65
- return layoutContent.replace(/\{\{\s*content\s*\}\}/g, () => content);
65
+ return layoutContent.replace(/<%=\s*content\s*%>/g, () => content);
66
66
  };
67
67
 
68
68
  const getTemplate = async () => {