- Add complete dark mode system with theme context and toggle - Implement dark mode toggle component in navigation menu - Add client-side routing with SSR-safe signal handling - Fix language selector styling for better dark mode compatibility - Add documentation system with mdBook integration - Improve navigation menu with proper external/internal link handling - Add comprehensive project documentation and configuration - Enhance theme system with localStorage persistence - Fix arena panic issues during server-side rendering - Add proper TypeScript configuration and build optimizations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
13 KiB
13 KiB
Rustelo Template System Usage Example
This guide shows how to integrate and use the Rustelo template system in your application.
Basic Integration
1. Update your main.rs
// Add to your main.rs
#[cfg(feature = "content-db")]
mod template;
use template::{TemplateService, TemplateConfig};
// In your main function or app initialization
async fn initialize_app() -> Result<(), Box<dyn std::error::Error>> {
// Initialize template service
let template_service = TemplateService::new("templates", "content/docs")?
.with_languages(vec!["en".to_string(), "es".to_string(), "fr".to_string()])
.with_default_language("en")
.with_cache(true);
// Add to your app state
let app_state = AppState {
template_service: Arc::new(template_service),
// ... other state
};
// Create router with template routes
let app = Router::new()
.merge(template::create_template_routes(app_state.template_service.clone()))
.with_state(app_state);
// Start server
let listener = tokio::net::TcpListener::bind("127.0.0.1:3030").await?;
axum::serve(listener, app).await?;
Ok(())
}
2. Update your AppState
#[derive(Clone)]
pub struct AppState {
pub leptos_options: LeptosOptions,
#[cfg(feature = "content-db")]
pub template_service: Arc<TemplateService>,
// ... other fields
}
3. Update Cargo.toml
Make sure tera is included in your content-db feature:
[features]
content-db = [
"sqlx",
"pulldown-cmark",
"syntect",
"serde_yaml",
"tempfile",
"uuid",
"chrono",
"tera" # Add this line
]
Creating Your First Template Page
Step 1: Create a Template
Create templates/product-page.html:
<!DOCTYPE html>
<html lang="{{lang | default(value='en')}}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}} - {{site_name | default(value="My Store")}}</title>
<meta name="description" content="{{description}}">
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
.product-header {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40px;
margin-bottom: 40px;
}
.product-image {
width: 100%;
max-width: 500px;
height: auto;
border-radius: 8px;
}
.product-info h1 {
color: #333;
margin-bottom: 10px;
}
.price {
font-size: 1.5em;
color: #e74c3c;
font-weight: bold;
margin: 15px 0;
}
.description {
color: #666;
margin-bottom: 30px;
}
.buy-button {
background: #27ae60;
color: white;
padding: 15px 30px;
border: none;
border-radius: 5px;
font-size: 1.1em;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
.buy-button:hover {
background: #219a52;
}
.features {
margin-top: 40px;
}
.features ul {
list-style-type: none;
padding: 0;
}
.features li {
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.features li:before {
content: "✓";
color: #27ae60;
margin-right: 10px;
}
@media (max-width: 768px) {
.product-header {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="product-header">
<div class="product-image-container">
<img src="{{image_url}}" alt="{{title}}" class="product-image">
</div>
<div class="product-info">
<h1>{{title}}</h1>
<p class="description">{{description}}</p>
<div class="price">{{currency}}{{price}}</div>
{% if available %}
<a href="{{buy_url}}" class="buy-button">{{buy_button_text | default(value="Buy Now")}}</a>
{% else %}
<button disabled class="buy-button" style="background: #95a5a6;">{{out_of_stock_text | default(value="Out of Stock")}}</button>
{% endif %}
</div>
</div>
{% if features %}
<div class="features">
<h2>{{features_title | default(value="Features")}}</h2>
<ul>
{% for feature in features %}
<li>{{feature}}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if reviews %}
<div class="reviews">
<h2>{{reviews_title | default(value="Customer Reviews")}}</h2>
{% for review in reviews %}
<div class="review" style="border: 1px solid #ddd; padding: 15px; margin: 10px 0; border-radius: 5px;">
<strong>{{review.author}}</strong>
<span style="color: #f39c12;">{{review.rating}}/5 ⭐</span>
<p>{{review.comment}}</p>
</div>
{% endfor %}
</div>
{% endif %}
</body>
</html>
Step 2: Create English Configuration
Create content/docs/en_awesome-widget.tpl.toml:
template_name = "product-page"
[values]
title = "Awesome Widget Pro"
description = "The ultimate widget for all your needs. Built with premium materials and cutting-edge technology."
price = "99.99"
currency = "$"
image_url = "/images/awesome-widget.jpg"
buy_url = "/checkout/awesome-widget"
available = true
buy_button_text = "Buy Now"
out_of_stock_text = "Out of Stock"
features_title = "Key Features"
reviews_title = "Customer Reviews"
lang = "en"
site_name = "Widget Store"
features = [
"Premium aluminum construction",
"Wireless connectivity",
"5-year warranty",
"Easy setup in under 5 minutes",
"Compatible with all major platforms"
]
[[values.reviews]]
author = "John Smith"
rating = 5
comment = "Amazing product! Works exactly as advertised."
[[values.reviews]]
author = "Sarah Johnson"
rating = 4
comment = "Great quality, fast shipping. Highly recommend."
[[values.reviews]]
author = "Mike Chen"
rating = 5
comment = "Best widget I've ever used. Worth every penny."
[metadata]
category = "products"
product_id = "awesome-widget-pro"
sku = "AWP-001"
Step 3: Create Spanish Configuration
Create content/docs/es_awesome-widget.tpl.toml:
template_name = "product-page"
[values]
title = "Widget Increíble Pro"
description = "El widget definitivo para todas sus necesidades. Construido con materiales premium y tecnología de vanguardia."
price = "99.99"
currency = "$"
image_url = "/images/awesome-widget.jpg"
buy_url = "/checkout/awesome-widget"
available = true
buy_button_text = "Comprar Ahora"
out_of_stock_text = "Agotado"
features_title = "Características Principales"
reviews_title = "Reseñas de Clientes"
lang = "es"
site_name = "Tienda de Widgets"
features = [
"Construcción premium de aluminio",
"Conectividad inalámbrica",
"Garantía de 5 años",
"Configuración fácil en menos de 5 minutos",
"Compatible con las principales plataformas"
]
[[values.reviews]]
author = "Juan Pérez"
rating = 5
comment = "¡Producto increíble! Funciona exactamente como se anuncia."
[[values.reviews]]
author = "María González"
rating = 4
comment = "Excelente calidad, envío rápido. Muy recomendado."
[[values.reviews]]
author = "Carlos Rodríguez"
rating = 5
comment = "El mejor widget que he usado. Vale cada centavo."
[metadata]
category = "productos"
product_id = "awesome-widget-pro"
sku = "AWP-001"
Step 4: Access Your Pages
Now you can access your product pages:
- English:
http://localhost:3030/page:awesome-widget?lang=en - Spanish:
http://localhost:3030/page:awesome-widget?lang=es - Default:
http://localhost:3030/page:awesome-widget
Advanced Usage Examples
Custom Route Handler
Create a custom handler that uses the template service:
use crate::template::TemplateService;
use axum::{extract::{Path, Query, State}, response::Html, http::StatusCode};
use serde::Deserialize;
#[derive(Deserialize)]
struct ProductQuery {
lang: Option<String>,
variant: Option<String>,
}
async fn product_page_handler(
Path(product_id): Path<String>,
Query(query): Query<ProductQuery>,
State(template_service): State<Arc<TemplateService>>,
) -> Result<Html<String>, StatusCode> {
let lang = query.lang.unwrap_or_else(|| "en".to_string());
// You could modify the content based on variant
let content_name = if let Some(variant) = query.variant {
format!("{}-{}", product_id, variant)
} else {
product_id
};
match template_service.render_page(&content_name, &lang).await {
Ok(rendered) => Ok(Html(rendered.content)),
Err(_) => Err(StatusCode::NOT_FOUND),
}
}
// Add to your router
let app = Router::new()
.route("/product/:id", get(product_page_handler))
.with_state(app_state);
Dynamic Content Injection
use std::collections::HashMap;
use serde_json::Value;
async fn dynamic_page_handler(
Path(page_name): Path<String>,
State(template_service): State<Arc<TemplateService>>,
) -> Result<Html<String>, StatusCode> {
// Load base configuration
let mut config = template_service
.get_page_config(&page_name, "en")
.await
.map_err(|_| StatusCode::NOT_FOUND)?;
// Add dynamic content
config.values.insert(
"current_time".to_string(),
Value::String(chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string())
);
config.values.insert(
"visitor_count".to_string(),
Value::Number(get_visitor_count().into()) // Your function
);
// Render with custom context
let context = template_service.create_context(&config.values);
match template_service.render_with_context(&config.template_name, &context).await {
Ok(html) => Ok(Html(html)),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
API Integration
// Get all products for a language
async fn api_products_list(
Path(lang): Path<String>,
State(template_service): State<Arc<TemplateService>>,
) -> Result<Json<Vec<ProductSummary>>, StatusCode> {
let content_list = template_service
.get_available_content(&lang)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let mut products = Vec::new();
for content_name in content_list {
if let Ok(config) = template_service.get_page_config(&content_name, &lang).await {
if let Some(category) = config.metadata.and_then(|m| m.get("category")) {
if category == "products" {
products.push(ProductSummary {
id: content_name,
title: config.values.get("title").unwrap_or(&Value::String("".to_string())).as_str().unwrap_or("").to_string(),
price: config.values.get("price").unwrap_or(&Value::String("0".to_string())).as_str().unwrap_or("0").to_string(),
});
}
}
}
}
Ok(Json(products))
}
#[derive(Serialize)]
struct ProductSummary {
id: String,
title: String,
price: String,
}
Development Tips
1. Hot Reload During Development
Add ?reload=true to your URLs during development:
http://localhost:3030/page:awesome-widget?lang=en&reload=true
2. Template Debugging
Use the API endpoints to debug:
# Check if template exists
curl http://localhost:3030/api/template/exists/awesome-widget?lang=en
# Get template configuration
curl http://localhost:3030/api/template/config/awesome-widget?lang=en
# Get template service stats
curl http://localhost:3030/api/template/stats
3. Performance Monitoring
// Log template rendering time
let start = std::time::Instant::now();
let result = template_service.render_page("awesome-widget", "en").await;
let duration = start.elapsed();
tracing::info!("Template rendered in {:?}", duration);
4. Error Handling
async fn safe_template_render(
template_service: &TemplateService,
content_name: &str,
lang: &str,
) -> Html<String> {
match template_service.render_page(content_name, lang).await {
Ok(rendered) => Html(rendered.content),
Err(e) => {
tracing::error!("Template render failed: {}", e);
Html(format!(
r#"<html><body><h1>Page Not Found</h1><p>The requested page "{}" is not available.</p></body></html>"#,
content_name
))
}
}
}
This template system provides a flexible, performant way to create localized content in your Rustelo application. The combination of Tera templates and TOML configuration files makes it easy to manage content while maintaining the performance benefits of Rust.