RustyCMS: file-based headless CMS — API, Admin UI (content, types, assets), Docker/Caddy, image transform; only demo type and demo content in version control

Made-with: Cursor
This commit is contained in:
Peter Meier
2026-03-12 14:21:49 +01:00
parent aad93d145f
commit 7795a238e1
278 changed files with 15551 additions and 4072 deletions

View File

@@ -8,7 +8,7 @@ use tokio::sync::RwLock;
use axum::http::header::HeaderValue;
use tower_http::cors::{AllowOrigin, CorsLayer};
use tower_http::trace::{DefaultOnResponse, TraceLayer};
use tracing::Level;
use tracing::{info_span, Level};
use tracing_subscriber::EnvFilter;
use rustycms::api::handlers::AppState;
@@ -19,11 +19,11 @@ use rustycms::store::{filesystem::FileStore, sqlite::SqliteStore, ContentStore};
#[command(name = "rustycms", about = "A file-based headless CMS written in Rust")]
struct Cli {
/// Path to the directory containing type definitions (*.json5)
#[arg(long, default_value = "./types")]
#[arg(long, default_value = "./types", env = "RUSTYCMS_TYPES_DIR")]
types_dir: PathBuf,
/// Path to the directory containing content files
#[arg(long, default_value = "./content")]
#[arg(long, default_value = "./content", env = "RUSTYCMS_CONTENT_DIR")]
content_dir: PathBuf,
/// Port to listen on
@@ -93,17 +93,11 @@ async fn main() -> anyhow::Result<()> {
.unwrap_or_else(|_| "sqlite:content.db".into());
tracing::info!("Using SQLite store: {}", url);
let s = SqliteStore::new(&url).await?;
for name in registry.collection_names() {
s.ensure_collection_dir(&name).await?;
}
std::sync::Arc::new(s)
}
_ => {
tracing::info!("Using file store: {}", cli.content_dir.display());
let s = FileStore::new(&cli.content_dir);
for name in registry.collection_names() {
s.ensure_collection_dir(&name).await?;
}
std::sync::Arc::new(s)
}
}
@@ -140,6 +134,13 @@ async fn main() -> anyhow::Result<()> {
tracing::info!("Multilingual: locales {:?} (default: {})", locs, &locs[0]);
}
let assets_dir = cli.content_dir.join("assets");
// RUSTYCMS_BASE_URL is the public URL of the API (e.g. https://api.example.com).
// Used to expand relative /api/assets/ paths to absolute URLs in responses.
// Falls back to the local server_url (http://host:port).
let base_url = std::env::var("RUSTYCMS_BASE_URL").unwrap_or_else(|_| server_url.clone());
let state = Arc::new(AppState {
registry: Arc::clone(&registry),
store,
@@ -150,6 +151,8 @@ async fn main() -> anyhow::Result<()> {
transform_cache,
http_client,
locales,
assets_dir,
base_url,
});
// Hot-reload: watch types_dir and reload schemas on change
@@ -203,11 +206,25 @@ async fn main() -> anyhow::Result<()> {
Err(_) => CorsLayer::permissive(),
};
let trace = TraceLayer::new_for_http().on_response(
DefaultOnResponse::new()
.level(Level::INFO)
.latency_unit(tower_http::LatencyUnit::Millis),
);
let trace = TraceLayer::new_for_http()
.make_span_with(|request: &axum::http::Request<axum::body::Body>| {
let method = request.method().as_str();
let uri = request
.uri()
.path_and_query()
.map(|pq| pq.as_str().to_string())
.unwrap_or_else(|| request.uri().path().to_string());
info_span!(
"request",
method = %method,
uri = %uri,
)
})
.on_response(
DefaultOnResponse::new()
.level(Level::INFO)
.latency_unit(tower_http::LatencyUnit::Millis),
);
let app = rustycms::api::routes::create_router(state)
.layer(cors)
.layer(trace);