` component anywhere:
+
+```html
+
+ # This is rendered as markdown
+
+ **Bold text** and *italic text*
+
+```
+
+## Tips and Tricks
+
+1. **Use frame files** for consistent layouts across entries
+2. **Keep components small** and focused on one thing
+3. **Use frontmatter** for all your metadata needs
+4. **Leverage slots** for flexible component content
+
+Happy building!
diff --git a/example/src/data/Posts/getting-started.md b/example/src/data/Posts/getting-started.md
new file mode 100644
index 0000000..1bda722
--- /dev/null
+++ b/example/src/data/Posts/getting-started.md
@@ -0,0 +1,71 @@
+---
+title: Getting Started with Simple
+description: A comprehensive guide to setting up your first Simple project
+date: Jan 20 2025
+author: John Doe
+tags: tutorial, beginner
+readtime: 5 min
+---
+
+# Getting Started with Simple
+
+In this post, I'll walk you through setting up your first project with Simple.
+
+## Installation
+
+First, you'll need to install Simple. The easiest way is to use cargo:
+
+```bash
+cargo install simple-web
+```
+
+## Project Structure
+
+A typical Simple project looks like this:
+
+```
+my-project/
+├── src/
+│ ├── components/ # Reusable UI components
+│ ├── data/ # Data files (TOML, JSON, or Markdown)
+│ ├── pages/ # Your site pages
+│ ├── public/ # Static assets
+│ └── templates/ # Templates for dynamic content
+```
+
+## Creating Your First Page
+
+Create a file at `src/pages/index.html`:
+
+```html
+
+
+
+ My Site
+
+
+ Hello World!
+ <-Template{Posts} />
+
+
+```
+
+## Building Your Site
+
+Run the build command:
+
+```bash
+simple build my-project
+```
+
+Your compiled site will be in the `dist/` directory!
+
+## Development Mode
+
+For live reloading during development:
+
+```bash
+simple dev my-project
+```
+
+This starts a local server and watches for changes.
diff --git a/example/src/data/Posts/my-first-post.md b/example/src/data/Posts/my-first-post.md
new file mode 100644
index 0000000..f3f6d09
--- /dev/null
+++ b/example/src/data/Posts/my-first-post.md
@@ -0,0 +1,35 @@
+---
+title: My First Blog Post
+description: Welcome to my new blog built with Simple!
+date: Jan 15 2025
+author: John Doe
+tags: introduction, getting-started
+---
+
+# Welcome to My Blog
+
+This is my very first blog post using the Simple build tool! I'm excited to share my thoughts and experiences with you.
+
+## What is Simple?
+
+Simple is a lightweight static site generator that makes it easy to build fast, modern websites. It uses:
+
+- Components for reusable UI elements
+- Templates for dynamic content
+- Markdown for easy content authoring
+- YAML frontmatter for metadata
+
+## Getting Started
+
+To create your own blog post, simply:
+
+1. Create a new `.md` file in your `data/Posts/` directory
+2. Add YAML frontmatter with your metadata
+3. Write your content in Markdown
+4. Add the filename to your `Posts.data.toml` file
+
+That's it! Simple will automatically generate your blog post page.
+
+## Next Steps
+
+Check out my other posts to learn more about using Simple and building great websites!
diff --git a/example/src/pages/index.html b/example/src/pages/index.html
new file mode 100644
index 0000000..9c90c15
--- /dev/null
+++ b/example/src/pages/index.html
@@ -0,0 +1,80 @@
+
+
+
+
+
+ My Blog - Built with Simple
+
+
+
+
+
+
+ <-Template{Posts} />
+
+
+
+
+
diff --git a/example/src/templates/Posts.frame.html b/example/src/templates/Posts.frame.html
new file mode 100644
index 0000000..5e75742
--- /dev/null
+++ b/example/src/templates/Posts.frame.html
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+ ${title} - My Blog
+
+
+
+
+ ← Back to all posts
+
+
+
+
+ ${--content}
+
+
+
+
+
diff --git a/example/src/templates/Posts.template.html b/example/src/templates/Posts.template.html
new file mode 100644
index 0000000..265d6e3
--- /dev/null
+++ b/example/src/templates/Posts.template.html
@@ -0,0 +1,13 @@
+
+
+
+ ${title}
+
+ ${date}
+ by ${author}
+ ${readtime}
+
+ ${description}
+ ${tags}
+
+
diff --git a/src/handlers/entries.rs b/src/handlers/entries.rs
index 824247f..4a2c023 100644
--- a/src/handlers/entries.rs
+++ b/src/handlers/entries.rs
@@ -1,5 +1,6 @@
use crate::dev::{SCRIPT, WS_PORT};
use crate::error::{ErrorType, ProcessError, WithItem};
+use crate::handlers::frontmatter::extract_frontmatter;
use crate::handlers::katex_assets;
use crate::handlers::pages::page;
use crate::utils::kv_replace;
@@ -69,9 +70,18 @@ pub fn process_entry(
};
let processed_content = if entry_path.extension().and_then(|s| s.to_str()) == Some("md") {
+ // Strip frontmatter from markdown content before rendering
+ let content_without_frontmatter = match extract_frontmatter(&content) {
+ Ok((_, remaining)) => remaining,
+ Err(_) => {
+ // If frontmatter extraction fails, use content as-is (might not have frontmatter)
+ content.clone()
+ }
+ };
+
frame_content.replace(
"${--content}",
- &("\n".to_owned() + &content + ""),
+ &("\n".to_owned() + &content_without_frontmatter + ""),
)
} else {
frame_content.replace("${--content}", &content)
diff --git a/src/handlers/frontmatter.rs b/src/handlers/frontmatter.rs
new file mode 100644
index 0000000..e732ce7
--- /dev/null
+++ b/src/handlers/frontmatter.rs
@@ -0,0 +1,166 @@
+use crate::error::{ErrorType, MapProcErr, ProcessError, WithItem};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use std::collections::HashMap;
+use std::fs;
+use std::path::PathBuf;
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct FileList {
+ pub files: Vec,
+}
+
+/// Extract YAML frontmatter from markdown content
+/// Returns (frontmatter_map, remaining_content)
+pub fn extract_frontmatter(
+ content: &str,
+) -> Result<(HashMap, String), ProcessError> {
+ let content = content.trim_start();
+
+ if !content.starts_with("---") {
+ return Err(ProcessError {
+ error_type: ErrorType::Syntax,
+ item: WithItem::Data,
+ path: PathBuf::new(),
+ message: Some("Frontmatter must start with '---'".to_string()),
+ });
+ }
+
+ let after_first_delimiter = &content[3..];
+
+ if let Some(end_pos) = after_first_delimiter.find("\n---") {
+ let frontmatter_str = &after_first_delimiter[..end_pos];
+ let remaining = &after_first_delimiter[end_pos + 4..].trim_start();
+
+ // Parse YAML frontmatter
+ let yaml_value: serde_yaml::Value = serde_yaml::from_str(frontmatter_str)
+ .map_err(|e| ProcessError {
+ error_type: ErrorType::Syntax,
+ item: WithItem::Data,
+ path: PathBuf::new(),
+ message: Some(format!("Failed to parse YAML frontmatter: {}", e)),
+ })?;
+
+ // Convert YAML to HashMap
+ let mut map = HashMap::new();
+ if let serde_yaml::Value::Mapping(mapping) = yaml_value {
+ for (key, value) in mapping {
+ if let serde_yaml::Value::String(k) = &key {
+ let v = match &value {
+ serde_yaml::Value::String(s) => s.clone(),
+ serde_yaml::Value::Number(n) => n.to_string(),
+ serde_yaml::Value::Bool(b) => b.to_string(),
+ _ => String::new(),
+ };
+ map.insert(k.clone(), v);
+ }
+ }
+ }
+
+ // Validate that title exists
+ if !map.contains_key("title") {
+ return Err(ProcessError {
+ error_type: ErrorType::Syntax,
+ item: WithItem::Data,
+ path: PathBuf::new(),
+ message: Some("Frontmatter must contain a 'title' field".to_string()),
+ });
+ }
+
+ Ok((map, remaining.to_string()))
+ } else {
+ Err(ProcessError {
+ error_type: ErrorType::Syntax,
+ item: WithItem::Data,
+ path: PathBuf::new(),
+ message: Some("Frontmatter must end with '---'".to_string()),
+ })
+ }
+}
+
+/// Load data from markdown files with frontmatter based on a TOML file list
+/// Returns a JSON array value compatible with the existing template system
+pub fn load_frontmatter_data(
+ src: &PathBuf,
+ name: &str,
+) -> Result<(Value, Vec), Vec> {
+ let mut errors = Vec::new();
+
+ let toml_path = src
+ .join("data")
+ .join(name.replace(":", "/"))
+ .with_extension("data.toml");
+
+ // Read the TOML file
+ let toml_content = fs::read_to_string(&toml_path)
+ .map_proc_err(
+ WithItem::Data,
+ ErrorType::Io,
+ &toml_path,
+ Some("Failed to read data.toml file".to_string()),
+ )
+ .map_err(|e| vec![e])?;
+
+ // Parse the TOML file
+ let file_list: FileList = toml::from_str(&toml_content).map_err(|e| {
+ vec![ProcessError {
+ error_type: ErrorType::Syntax,
+ item: WithItem::Data,
+ path: toml_path.clone(),
+ message: Some(format!("Failed to parse TOML: {}", e)),
+ }]
+ })?;
+
+ let data_dir = src.join("data").join(name.replace(":", "/"));
+ let mut items = Vec::new();
+
+ for file in &file_list.files {
+ let md_path = data_dir.join(file);
+
+ let content = match fs::read_to_string(&md_path) {
+ Ok(c) => c,
+ Err(e) => {
+ errors.push(ProcessError {
+ error_type: ErrorType::Io,
+ item: WithItem::Data,
+ path: md_path.clone(),
+ message: Some(format!("Failed to read markdown file: {}", e)),
+ });
+ continue;
+ }
+ };
+
+ let (mut frontmatter, _) = match extract_frontmatter(&content) {
+ Ok((fm, remaining)) => (fm, remaining),
+ Err(mut e) => {
+ e.path = md_path.clone();
+ errors.push(e);
+ continue;
+ }
+ };
+
+ // Generate entry-path and result-path from filename
+ let file_stem = md_path
+ .file_stem()
+ .and_then(|s| s.to_str())
+ .unwrap_or("unknown");
+
+ let relative_entry_path = format!("{}/{}", name.replace(":", "/"), file);
+ let result_path = format!("content/{}.html", file_stem);
+
+ // Add the special fields
+ frontmatter.insert("--entry-path".to_string(), relative_entry_path);
+ frontmatter.insert("--result-path".to_string(), result_path.clone());
+ frontmatter.insert("link".to_string(), format!("./{}", result_path));
+
+ // Convert HashMap to serde_json::Value
+ let obj: serde_json::Map = frontmatter
+ .into_iter()
+ .map(|(k, v)| (k, Value::String(v)))
+ .collect();
+
+ items.push(Value::Object(obj));
+ }
+
+ Ok((Value::Array(items), errors))
+}
diff --git a/src/handlers/templates.rs b/src/handlers/templates.rs
index 32c9125..5ede490 100644
--- a/src/handlers/templates.rs
+++ b/src/handlers/templates.rs
@@ -1,5 +1,6 @@
use crate::error::{ErrorType, MapProcErr, ProcessError, WithItem};
use crate::handlers::entries::process_entry;
+use crate::handlers::frontmatter::load_frontmatter_data;
use crate::handlers::pages::page;
use crate::utils::kv_replace;
use crate::utils::ProcessResult;
@@ -47,37 +48,68 @@ pub fn get_template(src: &PathBuf, name: &str, mut hist: HashSet) -> Pr
.inspect_err(|e| errors.push((*e).clone()))
.unwrap_or_else(|_| String::new());
- let data = fs::read_to_string(&data_path)
- .map_proc_err(
- WithItem::Data,
- ErrorType::Io,
- &data_path,
- Some("Failed to read data file".to_string()),
- )
- .inspect_err(|e| errors.push((*e).clone()))
- .unwrap_or_else(|_| String::new());
-
- if template.is_empty() || data.is_empty() {
+ if template.is_empty() {
return ProcessResult {
output: String::new(),
errors,
};
}
- let v: Value = match serde_json::from_str(&data) {
- Ok(value) => value,
- Err(e) => {
- errors.push(ProcessError {
- error_type: ErrorType::Syntax,
- item: WithItem::Data,
- path: data_path,
- message: Some(format!("JSON decode error: {}", e)),
- });
+ // Try to load from .toml + frontmatter first, fall back to .json
+ let toml_path = src
+ .join("data")
+ .join(name.replace(":", "/"))
+ .with_extension("data.toml");
+
+ let v: Value = if toml_path.exists() {
+ // Use frontmatter-based loading
+ match load_frontmatter_data(src, name) {
+ Ok((value, fm_errors)) => {
+ errors.extend(fm_errors);
+ value
+ }
+ Err(fm_errors) => {
+ errors.extend(fm_errors);
+ return ProcessResult {
+ output: String::new(),
+ errors,
+ };
+ }
+ }
+ } else {
+ // Fall back to JSON loading
+ let data = fs::read_to_string(&data_path)
+ .map_proc_err(
+ WithItem::Data,
+ ErrorType::Io,
+ &data_path,
+ Some("Failed to read data file".to_string()),
+ )
+ .inspect_err(|e| errors.push((*e).clone()))
+ .unwrap_or_else(|_| String::new());
+
+ if data.is_empty() {
return ProcessResult {
output: String::new(),
errors,
};
}
+
+ match serde_json::from_str(&data) {
+ Ok(value) => value,
+ Err(e) => {
+ errors.push(ProcessError {
+ error_type: ErrorType::Syntax,
+ item: WithItem::Data,
+ path: data_path,
+ message: Some(format!("JSON decode error: {}", e)),
+ });
+ return ProcessResult {
+ output: String::new(),
+ errors,
+ };
+ }
+ }
};
let items = match v.as_array() {
diff --git a/src/main.rs b/src/main.rs
index 3bc08b0..66b62ad 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,7 @@
mod handlers {
pub mod components;
pub mod entries;
+ pub mod frontmatter;
pub mod katex_assets;
pub mod markdown;
pub mod pages;