Enhance documentation and admin UI: Add detailed implementation guidelines in CLAUDE.md, introduce a referrer index in README.md, and update admin UI translations for improved user experience. Update package dependencies for better functionality and performance.

This commit is contained in:
Peter Meier
2026-03-13 10:55:33 +01:00
parent 7754d800f5
commit 606455c59b
42 changed files with 3814 additions and 421 deletions

View File

@@ -11,6 +11,7 @@ use axum::Json;
use serde_json::{json, Value};
use tokio::sync::RwLock;
use crate::referrers::{Referrer, ReferrerIndex};
use crate::schema::types::{SchemaDefinition, VALID_FIELD_TYPES};
use crate::schema::validator;
use crate::schema::SchemaRegistry;
@@ -53,6 +54,10 @@ pub struct AppState {
pub stores: Option<HashMap<String, Arc<dyn ContentStore>>>,
/// Assets dir per environment when environments is set. Key = env name.
pub assets_dirs: Option<HashMap<String, PathBuf>>,
/// Reverse index for "who references this entry?". Updated on create/update/delete. None when environments are used.
pub referrer_index: Option<Arc<RwLock<ReferrerIndex>>>,
/// Path to persist referrer index (e.g. content/_referrers.json).
pub referrer_index_path: Option<PathBuf>,
}
impl AppState {
@@ -726,6 +731,7 @@ pub async fn create_entry(
// Normalize reference arrays: objects with _slug → slug strings (so clients can send resolved refs)
validator::normalize_reference_arrays(&schema, &mut body);
validator::normalize_multi_select(&schema, &mut body);
// Validate against schema (type checks, constraints, strict mode, …)
let errors = validator::validate_content(&schema, &body);
@@ -766,6 +772,27 @@ pub async fn create_entry(
.map_err(ApiError::from)?;
state.cache.invalidate_collection(&env, &collection).await;
// Update referrer index: this entry references (ref_coll, ref_slug)
if let (Some(ref idx), Some(ref path)) = (&state.referrer_index, &state.referrer_index_path) {
let refs = validator::extract_references(&schema, &body);
let referrer = Referrer {
collection: collection.clone(),
slug: slug.clone(),
field: String::new(), // we add per (ref_coll, ref_slug, field) below
locale: locale_ref.map(str::to_string),
};
let mut index = idx.write().await;
for (ref_coll, ref_slug, field) in refs {
let mut r = referrer.clone();
r.field = field;
index.add_referrer(&ref_coll, &ref_slug, r);
}
drop(index);
if let Err(e) = idx.read().await.save(path) {
tracing::warn!("Failed to save referrer index: {}", e);
}
}
// Return created entry (with reference format)
let entry = store
.get(&collection, &slug, locale_ref)
@@ -846,6 +873,7 @@ pub async fn update_entry(
// Normalize reference arrays: objects with _slug → slug strings (so clients can send resolved refs)
validator::normalize_reference_arrays(&schema, &mut body);
validator::normalize_multi_select(&schema, &mut body);
// Load existing content for readonly check
let existing = store
@@ -902,6 +930,28 @@ pub async fn update_entry(
.map_err(ApiError::from)?;
state.cache.invalidate_collection(&env, &collection).await;
// Update referrer index: remove old refs from this entry, add new refs
if let (Some(ref idx), Some(ref path)) = (&state.referrer_index, &state.referrer_index_path) {
let mut index = idx.write().await;
index.remove_all_referrers_from(&collection, &slug);
let refs = validator::extract_references(&schema, &body);
let referrer = Referrer {
collection: collection.clone(),
slug: slug.clone(),
field: String::new(),
locale: locale_ref.map(str::to_string),
};
for (ref_coll, ref_slug, field) in refs {
let mut r = referrer.clone();
r.field = field;
index.add_referrer(&ref_coll, &ref_slug, r);
}
drop(index);
if let Err(e) = idx.read().await.save(path) {
tracing::warn!("Failed to save referrer index: {}", e);
}
}
// Return updated entry (with reference format)
let entry = store
.get(&collection, &slug, locale_ref)
@@ -933,6 +983,22 @@ pub async fn update_entry(
Ok(Json(formatted))
}
// ---------------------------------------------------------------------------
// GET /api/content/:collection/:slug/referrers
// ---------------------------------------------------------------------------
pub async fn get_referrers(
State(state): State<Arc<AppState>>,
Path((collection, slug)): Path<(String, String)>,
) -> Result<Json<Value>, ApiError> {
let referrers = match &state.referrer_index {
Some(idx) => idx.read().await.get_referrers(&collection, &slug),
None => vec![],
};
Ok(Json(
serde_json::to_value(&referrers).unwrap_or_else(|_| json!([])),
))
}
// ---------------------------------------------------------------------------
// GET /api/locales
// ---------------------------------------------------------------------------
@@ -982,6 +1048,17 @@ pub async fn delete_entry(
)));
}
// Update referrer index: remove this entry from every (ref_coll, ref_slug) it referenced; and remove key (collection, slug)
if let (Some(ref idx), Some(ref path)) = (&state.referrer_index, &state.referrer_index_path) {
let mut index = idx.write().await;
index.remove_all_referrers_from(&collection, &slug);
index.remove_referenced_entry(&collection, &slug);
drop(index);
if let Err(e) = idx.read().await.save(path) {
tracing::warn!("Failed to save referrer index: {}", e);
}
}
store
.delete(&collection, &slug, locale_ref)
.await

View File

@@ -235,6 +235,41 @@ pub fn generate_spec(registry: &SchemaRegistry, server_url: &str) -> Value {
}
}),
);
// GET /api/content/:collection/:slug/referrers who references this entry
paths.insert(
format!("/api/content/{}/{{slug}}/referrers", name),
json!({
"get": {
"summary": format!("List referrers of '{}' entry", name),
"description": "Returns all entries that reference this entry (reverse index). Empty when not using referrer index (e.g. with RUSTYCMS_ENVIRONMENTS).",
"operationId": format!("get{}Referrers", pascal),
"tags": [tag],
"parameters": [
{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Entry slug" }
],
"responses": {
"200": {
"description": "List of referrers (collection, slug, field, locale)",
"content": { "application/json": {
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"collection": { "type": "string", "description": "Collection of the referring entry" },
"slug": { "type": "string", "description": "Slug of the referring entry" },
"field": { "type": "string", "description": "Field that holds the reference" },
"locale": { "type": "string", "nullable": true, "description": "Locale of the referring entry if applicable" }
}
}
}
}}
}
}
}
}),
);
}
// ── Asset management ─────────────────────────────────────────────────
@@ -701,6 +736,7 @@ const API_INDEX_HTML: &str = r#"<!DOCTYPE html>
<li><code>POST</code> /api/content/:type Create entry</li>
<li><code>PUT</code> /api/content/:type/:slug Update entry</li>
<li><code>DELETE</code> /api/content/:type/:slug Delete entry</li>
<li><code>GET</code> /api/content/:type/:slug/referrers List entries that reference this entry (reverse index)</li>
<li><code>GET</code> <a href="/api/transform?url=https://httpbin.org/image/png&w=80&h=80">/api/transform</a> Transform image from URL (w, h, ar, fit, format)</li>
<li><code>GET</code> <a href="/health">/health</a> Health check</li>
</ul>

View File

@@ -41,6 +41,10 @@ pub fn create_router(state: Arc<AppState>) -> Router {
"/api/content/:collection",
get(handlers::list_entries).post(handlers::create_entry),
)
.route(
"/api/content/:collection/:slug/referrers",
get(handlers::get_referrers),
)
.route(
"/api/content/:collection/:slug",
get(handlers::get_entry)