Add Gitea Actions deploy workflow and server configuration
Some checks failed
Deploy to Server / deploy (push) Failing after 1m49s

- Add basePath /admin to Next.js config for path-based routing
- Add .gitea/workflows/deploy.yml for CI/CD via Gitea Actions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-03-15 13:52:41 +01:00
parent 11d46049d1
commit ecd257fb8f
16 changed files with 466 additions and 39 deletions

View File

@@ -570,9 +570,7 @@ pub async fn list_entries(
Some(&*registry),
)
.await;
if resolve.is_some() {
expand_asset_urls(item, &state.base_url);
}
expand_asset_urls(item, &state.base_url);
}
let response_value = serde_json::to_value(&result).unwrap();
@@ -676,9 +674,7 @@ pub async fn get_entry(
Some(&*registry),
)
.await;
if resolve.is_some() {
expand_asset_urls(&mut formatted, &state.base_url);
}
expand_asset_urls(&mut formatted, &state.base_url);
// Only cache published entries so unauthenticated requests never see cached drafts.
if !entry_is_draft(&formatted) {
@@ -844,7 +840,7 @@ pub async fn create_entry(
.await
.map_err(ApiError::from)?
.unwrap();
let formatted = format_references(
let mut formatted = format_references(
entry,
&schema,
store.as_ref(),
@@ -853,6 +849,7 @@ pub async fn create_entry(
None,
)
.await;
expand_asset_urls(&mut formatted, &state.base_url);
webhooks::fire(
state.http_client.clone(),
@@ -1003,7 +1000,7 @@ pub async fn update_entry(
.await
.map_err(ApiError::from)?
.unwrap();
let formatted = format_references(
let mut formatted = format_references(
entry,
&schema,
store.as_ref(),
@@ -1012,6 +1009,7 @@ pub async fn update_entry(
None,
)
.await;
expand_asset_urls(&mut formatted, &state.base_url);
webhooks::fire(
state.http_client.clone(),

View File

@@ -9,11 +9,19 @@ use crate::schema::SchemaRegistry;
use crate::store::ContentStore;
/// Recursively expand relative `/api/assets/` paths to absolute URLs.
/// Idempotent: strings already starting with a scheme (http/https) are skipped.
/// Whole-string values and occurrences inside strings (e.g. markdown) are expanded.
/// Idempotent: already-expanded base URL is preserved via placeholder during replace.
pub fn expand_asset_urls(value: &mut Value, base_url: &str) {
let base = base_url.trim_end_matches('/');
let full_prefix = format!("{}/api/assets/", base);
match value {
Value::String(s) if s.starts_with("/api/assets/") => {
*s = format!("{}{}", base_url.trim_end_matches('/'), s);
Value::String(s) if s.contains("/api/assets/") => {
// Avoid double-expand: temporarily replace already-expanded URLs
const PLACEHOLDER: &str = "\x00__ASSET_BASE__\x00";
*s = s
.replace(&full_prefix, PLACEHOLDER)
.replace("/api/assets/", &full_prefix)
.replace(PLACEHOLDER, &full_prefix);
}
Value::Array(arr) => arr.iter_mut().for_each(|v| expand_asset_urls(v, base_url)),
Value::Object(map) => map.values_mut().for_each(|v| expand_asset_urls(v, base_url)),
@@ -23,11 +31,12 @@ pub fn expand_asset_urls(value: &mut Value, base_url: &str) {
/// Reverse of expand_asset_urls: strip the base_url prefix from absolute asset URLs
/// before persisting to disk, so stored paths are always relative.
/// Whole-string values and occurrences inside strings (e.g. markdown) are collapsed.
pub fn collapse_asset_urls(value: &mut Value, base_url: &str) {
let prefix = format!("{}/api/assets/", base_url.trim_end_matches('/'));
match value {
Value::String(s) if s.starts_with(&prefix) => {
*s = format!("/api/assets/{}", &s[prefix.len()..]);
Value::String(s) if s.contains(&prefix) => {
*s = s.replace(&prefix, "/api/assets/");
}
Value::Array(arr) => arr.iter_mut().for_each(|v| collapse_asset_urls(v, base_url)),
Value::Object(map) => map.values_mut().for_each(|v| collapse_asset_urls(v, base_url)),

View File

@@ -2,6 +2,7 @@ use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde_json::Value;
use tokio::fs;
use tokio::io::AsyncWriteExt;
@@ -34,6 +35,32 @@ impl FileStore {
.join(format!("{}.json5", slug))
}
/// Attach _created and _updated (ISO8601) from file metadata when available.
async fn attach_file_timestamps(&self, path: &Path, value: &mut Value) {
let meta = match fs::metadata(path).await {
Ok(m) => m,
Err(_) => return,
};
let modified = match meta.modified() {
Ok(t) => t,
Err(_) => return,
};
let modified_dt: DateTime<Utc> = modified.into();
let updated = modified_dt.to_rfc3339();
let created = meta
.created()
.ok()
.map(|t| {
let dt: DateTime<Utc> = t.into();
dt.to_rfc3339()
})
.unwrap_or_else(|| updated.clone());
if let Some(obj) = value.as_object_mut() {
obj.insert("_created".to_string(), Value::String(created));
obj.insert("_updated".to_string(), Value::String(updated));
}
}
/// Path for optional external content file: same dir as entry, stem.content.md.
/// Used so markdown (and other long text) can live in a .md file instead of JSON.
fn content_file_path(&self, entry_path: &Path) -> PathBuf {
@@ -178,6 +205,7 @@ impl FileStore {
match self.read_file(&path).await {
Ok(mut value) => {
let _ = self.resolve_content_file(&path, &mut value).await;
self.attach_file_timestamps(&path, &mut value).await;
if let Some(obj) = value.as_object_mut() {
obj.insert("_slug".to_string(), Value::String(slug.clone()));
}
@@ -201,6 +229,7 @@ impl FileStore {
let mut value = self.read_file(&path).await?;
self.resolve_content_file(&path, &mut value).await?;
self.attach_file_timestamps(&path, &mut value).await;
if let Some(obj) = value.as_object_mut() {
obj.insert("_slug".to_string(), Value::String(slug.to_string()));
}