485 lines
13 KiB
Markdown
485 lines
13 KiB
Markdown
|
|
# 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
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// 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
|
||
|
|
|
||
|
|
```rust
|
||
|
|
#[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:
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[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`:
|
||
|
|
|
||
|
|
```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`:
|
||
|
|
|
||
|
|
```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`:
|
||
|
|
|
||
|
|
```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:
|
||
|
|
|
||
|
|
```rust
|
||
|
|
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
|
||
|
|
|
||
|
|
```rust
|
||
|
|
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
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// 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:
|
||
|
|
```bash
|
||
|
|
# 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
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// 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
|
||
|
|
|
||
|
|
```rust
|
||
|
|
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.
|