Update bDS2 rendering fixtures and routes.
This commit is contained in:
@@ -20,7 +20,7 @@ The project is under active development. Core blogging workflows are broadly ava
|
|||||||
- Local MCP automation over stdio or a localhost-only stateless HTTP endpoint, with project resources, read/search/count tools, inert write proposals, explicit desktop approval, and opt-in Claude Code/Copilot configuration.
|
- Local MCP automation over stdio or a localhost-only stateless HTTP endpoint, with project resources, read/search/count tools, inert write proposals, explicit desktop approval, and opt-in Claude Code/Copilot configuration.
|
||||||
- A fully localized Ratatui terminal workspace, available locally through `bds-cli tui`/`BDS_MODE=tui` and remotely through authenticated SSH shell sessions, with shared post/template/script editing and publishing, project/search/command overlays, settings, tags, Git, reports, task progress, live multi-client locale updates, and airplane-mode AI gating.
|
- A fully localized Ratatui terminal workspace, available locally through `bds-cli tui`/`BDS_MODE=tui` and remotely through authenticated SSH shell sessions, with shared post/template/script editing and publishing, project/search/command overlays, settings, tags, Git, reports, task progress, live multi-client locale updates, and airplane-mode AI gating.
|
||||||
- `bds-cli server` hosting the shared application engines over a loopback-by-default, public-key-only SSH service, with restrictive private key material, live authorization updates, terminal-session transport, CLI-change synchronization, ordered domain/task events, and native desktop remote-project selection.
|
- `bds-cli server` hosting the shared application engines over a loopback-by-default, public-key-only SSH service, with restrictive private key material, live authorization updates, terminal-session transport, CLI-change synchronization, ordered domain/task events, and native desktop remote-project selection.
|
||||||
- Markdown/Liquid rendering with native macros, multilingual routes, feeds, sitemap, Pagefind, and incremental site generation through cancellable section task groups.
|
- bDS2-compatible Markdown/Liquid rendering with native macros, canonical multilingual and flat page routes, recursive menus, calendar archives, feeds, a root hreflang sitemap, Pagefind, and incremental site generation through cancellable section task groups.
|
||||||
- Navigable generated-route preview in the app or system browser, with draft database overlays and published filesystem content.
|
- Navigable generated-route preview in the app or system browser, with draft database overlays and published filesystem content.
|
||||||
- Optional one-shot AI translation, description, analysis, taxonomy, and language-detection operations using independent online and local OpenAI-compatible profiles. Each profile has secure credentials, persistently discovered chat/title/image model selections, explicit tool/vision overrides, chat testing, and status-bar airplane-mode routing.
|
- Optional one-shot AI translation, description, analysis, taxonomy, and language-detection operations using independent online and local OpenAI-compatible profiles. Each profile has secure credentials, persistently discovered chat/title/image model selections, explicit tool/vision overrides, chat testing, and status-bar airplane-mode routing.
|
||||||
- Persistent conversational AI with safe Markdown, streamed and cancellable responses, model/session/token tracking, bounded project-aware blog tools, and localized conversation management in the Chat workspace. Allowlisted render tools add persistent native cards, charts, forms, lists, metrics, mind maps, tables, and tabs without executing assistant-provided HTML or JavaScript.
|
- Persistent conversational AI with safe Markdown, streamed and cancellable responses, model/session/token tracking, bounded project-aware blog tools, and localized conversation management in the Chat workspace. Allowlisted render tools add persistent native cards, charts, forms, lists, metrics, mind maps, tables, and tabs without executing assistant-provided HTML or JavaScript.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="blog-search-panel" data-blog-search-panel hidden>
|
<div class="blog-search-panel" data-blog-search-panel hidden>
|
||||||
<div id="blog-search" data-blog-search-root data-search-placeholder="{{ 'render.search.placeholder' | i18n: language }}"></div>
|
<div id="blog-search" data-blog-search-root data-search-placeholder="{{ 'render.search.placeholder' | i18n: language }}" data-search-no-results="{{ 'render.search.noResults' | i18n: language }}"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="blog-search-panel" data-blog-search-panel hidden>
|
<div class="blog-search-panel" data-blog-search-panel hidden>
|
||||||
<div id="blog-search" data-blog-search-root data-search-placeholder="{{ 'render.search.placeholder' | i18n: language }}"></div>
|
<div id="blog-search" data-blog-search-root data-search-placeholder="{{ 'render.search.placeholder' | i18n: language }}" data-search-no-results="{{ 'render.search.noResults' | i18n: language }}"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ use crate::engine::validate_site::SiteValidationReport;
|
|||||||
use crate::engine::{EngineError, EngineResult};
|
use crate::engine::{EngineError, EngineResult};
|
||||||
use crate::model::{CategorySettings, Post, ProjectMetadata};
|
use crate::model::{CategorySettings, Post, ProjectMetadata};
|
||||||
use crate::render::{
|
use crate::render::{
|
||||||
GeneratedWriteOutcome, build_calendar_json, build_canonical_post_path,
|
GeneratedWriteOutcome, PostLanguageVariant, build_calendar_json, build_canonical_post_path,
|
||||||
build_site_render_artifacts, build_site_section_render_artifacts,
|
build_site_render_artifacts, build_site_section_render_artifacts,
|
||||||
build_targeted_site_section_render_artifacts, render_markdown_to_html, write_generated_bytes,
|
build_targeted_site_section_render_artifacts, select_post_language_variant,
|
||||||
write_generated_file,
|
write_generated_bytes, write_generated_file,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -163,8 +163,6 @@ pub fn render_site_section_with_progress(
|
|||||||
return Err(EngineError::Validation("cancelled".to_string()));
|
return Err(EngineError::Validation("cancelled".to_string()));
|
||||||
}
|
}
|
||||||
let data_dir = project_data_dir(output_dir);
|
let data_dir = project_data_dir(output_dir);
|
||||||
let category_settings = load_category_settings(&data_dir);
|
|
||||||
let list_posts = filter_posts_for_lists(posts, &category_settings);
|
|
||||||
let input_posts = posts
|
let input_posts = posts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|source| (source.post.clone(), source.body_markdown.clone()))
|
.map(|source| (source.post.clone(), source.body_markdown.clone()))
|
||||||
@@ -202,7 +200,7 @@ pub fn render_site_section_with_progress(
|
|||||||
project_id,
|
project_id,
|
||||||
metadata,
|
metadata,
|
||||||
&data_dir,
|
&data_dir,
|
||||||
&list_posts,
|
posts,
|
||||||
&artifacts.route_manifest,
|
&artifacts.route_manifest,
|
||||||
None,
|
None,
|
||||||
&mut report,
|
&mut report,
|
||||||
@@ -305,8 +303,6 @@ pub fn apply_validation_section_with_progress(
|
|||||||
return Err(EngineError::Validation("cancelled".to_string()));
|
return Err(EngineError::Validation("cancelled".to_string()));
|
||||||
}
|
}
|
||||||
let data_dir = project_data_dir(output_dir);
|
let data_dir = project_data_dir(output_dir);
|
||||||
let category_settings = load_category_settings(&data_dir);
|
|
||||||
let list_posts = filter_posts_for_lists(posts, &category_settings);
|
|
||||||
let input_posts = posts
|
let input_posts = posts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|source| (source.post.clone(), source.body_markdown.clone()))
|
.map(|source| (source.post.clone(), source.body_markdown.clone()))
|
||||||
@@ -368,7 +364,7 @@ pub fn apply_validation_section_with_progress(
|
|||||||
project_id,
|
project_id,
|
||||||
metadata,
|
metadata,
|
||||||
&data_dir,
|
&data_dir,
|
||||||
&list_posts,
|
posts,
|
||||||
&artifacts.route_manifest,
|
&artifacts.route_manifest,
|
||||||
(!fallback).then_some(&requested),
|
(!fallback).then_some(&requested),
|
||||||
&mut report,
|
&mut report,
|
||||||
@@ -400,7 +396,7 @@ fn write_core_outputs(
|
|||||||
project_id: &str,
|
project_id: &str,
|
||||||
metadata: &ProjectMetadata,
|
metadata: &ProjectMetadata,
|
||||||
data_dir: &Path,
|
data_dir: &Path,
|
||||||
list_posts: &[PublishedPostSource],
|
published_posts: &[PublishedPostSource],
|
||||||
route_manifest: &[crate::render::SitePage],
|
route_manifest: &[crate::render::SitePage],
|
||||||
requested: Option<&HashSet<String>>,
|
requested: Option<&HashSet<String>>,
|
||||||
report: &mut GenerationReport,
|
report: &mut GenerationReport,
|
||||||
@@ -412,7 +408,7 @@ fn write_core_outputs(
|
|||||||
let mut outputs = vec![(
|
let mut outputs = vec![(
|
||||||
"calendar.json".to_string(),
|
"calendar.json".to_string(),
|
||||||
build_calendar_json(
|
build_calendar_json(
|
||||||
&list_posts
|
&published_posts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|source| source.post.clone())
|
.map(|source| source.post.clone())
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
@@ -420,23 +416,53 @@ fn write_core_outputs(
|
|||||||
)];
|
)];
|
||||||
for render_language in render_languages(metadata) {
|
for render_language in render_languages(metadata) {
|
||||||
let localized_posts =
|
let localized_posts =
|
||||||
localized_sources(conn, data_dir, list_posts, &render_language, metadata)?;
|
localized_sources(conn, data_dir, published_posts, &render_language, metadata)?;
|
||||||
let prefix = if render_language == metadata.main_language.as_deref().unwrap_or("en") {
|
let is_main = render_language == metadata.main_language.as_deref().unwrap_or("en");
|
||||||
|
let prefix = if is_main {
|
||||||
String::new()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
format!("{render_language}/")
|
format!("{render_language}/")
|
||||||
};
|
};
|
||||||
let rss = build_rss_xml(metadata, &localized_posts, &render_language);
|
let mut feed_posts = if is_main {
|
||||||
outputs.push((format!("{prefix}rss.xml"), rss.clone()));
|
published_posts.to_vec()
|
||||||
outputs.push((format!("{prefix}feed.xml"), rss));
|
} else {
|
||||||
|
localized_posts
|
||||||
|
.iter()
|
||||||
|
.filter(|source| {
|
||||||
|
source
|
||||||
|
.post
|
||||||
|
.language
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|language| language.eq_ignore_ascii_case(&render_language))
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
sort_published_sources(&mut feed_posts);
|
||||||
|
outputs.push((
|
||||||
|
format!("{prefix}rss.xml"),
|
||||||
|
build_rss_xml(metadata, &feed_posts, &render_language),
|
||||||
|
));
|
||||||
outputs.push((
|
outputs.push((
|
||||||
format!("{prefix}atom.xml"),
|
format!("{prefix}atom.xml"),
|
||||||
build_atom_xml(metadata, &localized_posts, &render_language),
|
build_atom_xml(metadata, &feed_posts, &render_language),
|
||||||
));
|
|
||||||
outputs.push((
|
|
||||||
format!("{prefix}sitemap.xml"),
|
|
||||||
build_sitemap_xml(metadata, route_manifest, &localized_posts, &render_language),
|
|
||||||
));
|
));
|
||||||
|
if is_main {
|
||||||
|
let category_settings = load_category_settings(data_dir);
|
||||||
|
let mut sitemap_posts = published_posts.to_vec();
|
||||||
|
sort_published_sources(&mut sitemap_posts);
|
||||||
|
let sitemap_list_posts = filter_posts_for_lists(&sitemap_posts, &category_settings);
|
||||||
|
outputs.push((
|
||||||
|
"sitemap.xml".to_string(),
|
||||||
|
build_sitemap_xml(
|
||||||
|
metadata,
|
||||||
|
route_manifest,
|
||||||
|
&sitemap_posts,
|
||||||
|
&sitemap_list_posts,
|
||||||
|
&render_language,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (path, content) in outputs {
|
for (path, content) in outputs {
|
||||||
if is_cancelled() {
|
if is_cancelled() {
|
||||||
@@ -673,10 +699,9 @@ fn expected_paths_for_sections(
|
|||||||
format!("{language}/")
|
format!("{language}/")
|
||||||
};
|
};
|
||||||
expected.insert(format!("{prefix}rss.xml"));
|
expected.insert(format!("{prefix}rss.xml"));
|
||||||
expected.insert(format!("{prefix}feed.xml"));
|
|
||||||
expected.insert(format!("{prefix}atom.xml"));
|
expected.insert(format!("{prefix}atom.xml"));
|
||||||
expected.insert(format!("{prefix}sitemap.xml"));
|
|
||||||
}
|
}
|
||||||
|
expected.insert("sitemap.xml".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
expected
|
expected
|
||||||
@@ -775,6 +800,7 @@ pub(crate) fn classify_generated_path(path: &str) -> Option<GenerationSection> {
|
|||||||
{
|
{
|
||||||
Some(GenerationSection::Single)
|
Some(GenerationSection::Single)
|
||||||
}
|
}
|
||||||
|
[_slug, "index.html"] => Some(GenerationSection::Core),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -782,9 +808,7 @@ pub(crate) fn classify_generated_path(path: &str) -> Option<GenerationSection> {
|
|||||||
fn has_language_prefix(parts: &[&str]) -> bool {
|
fn has_language_prefix(parts: &[&str]) -> bool {
|
||||||
match parts {
|
match parts {
|
||||||
[first, second, ..] => {
|
[first, second, ..] => {
|
||||||
!is_year_segment(first)
|
matches!(*first, "de" | "en" | "es" | "fr" | "it")
|
||||||
&& *first != "category"
|
|
||||||
&& *first != "tag"
|
|
||||||
&& (*second == "index.html"
|
&& (*second == "index.html"
|
||||||
|| *second == "404.html"
|
|| *second == "404.html"
|
||||||
|| *second == "page"
|
|| *second == "page"
|
||||||
@@ -859,38 +883,68 @@ fn localized_sources(
|
|||||||
let main_language = metadata.main_language.as_deref().unwrap_or("en");
|
let main_language = metadata.main_language.as_deref().unwrap_or("en");
|
||||||
let mut localized = Vec::new();
|
let mut localized = Vec::new();
|
||||||
for source in posts {
|
for source in posts {
|
||||||
if language.eq_ignore_ascii_case(main_language) {
|
let translation = queries::post_translation::get_post_translation_by_post_and_language(
|
||||||
localized.push(source.clone());
|
conn,
|
||||||
continue;
|
&source.post.id,
|
||||||
}
|
language,
|
||||||
if let Ok(translation) =
|
)
|
||||||
queries::post_translation::get_post_translation_by_post_and_language(
|
.ok()
|
||||||
conn,
|
.filter(|translation| {
|
||||||
&source.post.id,
|
!translation.file_path.trim().is_empty()
|
||||||
language,
|
&& data_dir
|
||||||
)
|
.join(translation.file_path.trim_start_matches('/'))
|
||||||
{
|
.is_file()
|
||||||
let raw = std::fs::read_to_string(
|
});
|
||||||
data_dir.join(translation.file_path.trim_start_matches('/')),
|
match select_post_language_variant(
|
||||||
)
|
&source.post,
|
||||||
.map_err(EngineError::Io)?;
|
language,
|
||||||
let (_, body) = crate::util::frontmatter::read_translation_file(&raw)
|
main_language,
|
||||||
.map_err(EngineError::Parse)?;
|
translation.is_some(),
|
||||||
let mut translated_post = source.post.clone();
|
) {
|
||||||
translated_post.title = translation.title.clone();
|
Some(PostLanguageVariant::Base) => localized.push(source.clone()),
|
||||||
translated_post.excerpt = translation.excerpt.clone();
|
Some(PostLanguageVariant::Translation) => {
|
||||||
translated_post.language = Some(translation.language.clone());
|
let Some(translation) = translation else {
|
||||||
translated_post.file_path = translation.file_path.clone();
|
continue;
|
||||||
translated_post.published_at = translation.published_at.or(source.post.published_at);
|
};
|
||||||
localized.push(PublishedPostSource {
|
let raw = std::fs::read_to_string(
|
||||||
post: translated_post,
|
data_dir.join(translation.file_path.trim_start_matches('/')),
|
||||||
body_markdown: body,
|
)
|
||||||
});
|
.map_err(EngineError::Io)?;
|
||||||
|
let (_, body) = crate::util::frontmatter::read_translation_file(&raw)
|
||||||
|
.map_err(EngineError::Parse)?;
|
||||||
|
let mut translated_post = source.post.clone();
|
||||||
|
translated_post.id = translation.id.clone();
|
||||||
|
translated_post.title = translation.title.clone();
|
||||||
|
translated_post.excerpt = translation.excerpt.clone();
|
||||||
|
translated_post.language = Some(translation.language.clone());
|
||||||
|
translated_post.status = translation.status.clone();
|
||||||
|
translated_post.file_path = translation.file_path.clone();
|
||||||
|
translated_post.updated_at = translation.updated_at;
|
||||||
|
translated_post.published_at =
|
||||||
|
translation.published_at.or(source.post.published_at);
|
||||||
|
localized.push(PublishedPostSource {
|
||||||
|
post: translated_post,
|
||||||
|
body_markdown: body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sort_published_sources(&mut localized);
|
||||||
Ok(localized)
|
Ok(localized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sort_published_sources(posts: &mut [PublishedPostSource]) {
|
||||||
|
posts.sort_by(|left, right| {
|
||||||
|
right
|
||||||
|
.post
|
||||||
|
.created_at
|
||||||
|
.cmp(&left.post.created_at)
|
||||||
|
.then_with(|| right.post.published_at.cmp(&left.post.published_at))
|
||||||
|
.then_with(|| left.post.slug.cmp(&right.post.slug))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn load_category_settings(data_dir: &Path) -> HashMap<String, CategorySettings> {
|
fn load_category_settings(data_dir: &Path) -> HashMap<String, CategorySettings> {
|
||||||
crate::engine::meta::read_category_meta_json(data_dir).unwrap_or_default()
|
crate::engine::meta::read_category_meta_json(data_dir).unwrap_or_default()
|
||||||
}
|
}
|
||||||
@@ -905,8 +959,7 @@ fn filter_posts_for_lists(
|
|||||||
!source.post.categories.iter().any(|category| {
|
!source.post.categories.iter().any(|category| {
|
||||||
category_settings
|
category_settings
|
||||||
.get(category)
|
.get(category)
|
||||||
.map(|settings| !settings.render_in_lists)
|
.is_some_and(|settings| !settings.render_in_lists)
|
||||||
.unwrap_or(false)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
@@ -923,74 +976,21 @@ pub(crate) fn build_rss_xml(
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.trim_end_matches('/');
|
.trim_end_matches('/');
|
||||||
let last_build = posts
|
let mut xml = format!(
|
||||||
.iter()
|
"<rss><channel><title>{} ({})</title>",
|
||||||
.filter_map(|post| timestamp(post.post.published_at.unwrap_or(post.post.created_at)))
|
escape_xml(&metadata.name),
|
||||||
.max()
|
escape_xml(language)
|
||||||
.unwrap_or_else(Utc::now);
|
);
|
||||||
|
|
||||||
let mut xml = vec![
|
|
||||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>".to_string(),
|
|
||||||
"<rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\">".to_string(),
|
|
||||||
" <channel>".to_string(),
|
|
||||||
format!(" <title>{}</title>", escape_xml(&metadata.name)),
|
|
||||||
format!(" <link>{base_url}/</link>"),
|
|
||||||
format!(" <description>{}</description>", escape_xml(metadata.description.as_deref().unwrap_or(""))),
|
|
||||||
format!(" <lastBuildDate>{}</lastBuildDate>", last_build.format("%a, %d %b %Y %H:%M:%S GMT")),
|
|
||||||
" <generator>bDS</generator>".to_string(),
|
|
||||||
];
|
|
||||||
|
|
||||||
for source in posts {
|
for source in posts {
|
||||||
let url = format!(
|
let url = post_absolute_url(base_url, metadata, source, language);
|
||||||
"{base_url}{}",
|
xml.push_str(&format!(
|
||||||
build_canonical_post_path(
|
"<item><title>{}</title><link>{url}</link></item>",
|
||||||
&source.post,
|
|
||||||
language,
|
|
||||||
metadata.main_language.as_deref().unwrap_or("en")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
let published = timestamp(source.post.published_at.unwrap_or(source.post.created_at))
|
|
||||||
.unwrap_or_else(Utc::now);
|
|
||||||
xml.push(" <item>".to_string());
|
|
||||||
xml.push(format!(
|
|
||||||
" <title>{}</title>",
|
|
||||||
escape_xml(&source.post.title)
|
escape_xml(&source.post.title)
|
||||||
));
|
));
|
||||||
xml.push(format!(" <link>{url}</link>"));
|
|
||||||
xml.push(format!(" <guid isPermaLink=\"true\">{url}</guid>"));
|
|
||||||
xml.push(format!(
|
|
||||||
" <pubDate>{}</pubDate>",
|
|
||||||
published.format("%a, %d %b %Y %H:%M:%S GMT")
|
|
||||||
));
|
|
||||||
if let Some(author) = &source.post.author {
|
|
||||||
xml.push(format!(" <author>{}</author>", escape_xml(author)));
|
|
||||||
}
|
|
||||||
xml.push(format!(
|
|
||||||
" <dc:language>{}</dc:language>",
|
|
||||||
escape_xml(language)
|
|
||||||
));
|
|
||||||
let html = render_markdown_to_html(&source.body_markdown);
|
|
||||||
xml.push(format!(
|
|
||||||
" <description><![CDATA[{html}]]></description>"
|
|
||||||
));
|
|
||||||
xml.push(format!(
|
|
||||||
" <content:encoded><![CDATA[{html}]]></content:encoded>"
|
|
||||||
));
|
|
||||||
for category in &source.post.categories {
|
|
||||||
xml.push(format!(
|
|
||||||
" <category>{}</category>",
|
|
||||||
escape_xml(category)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
for tag in &source.post.tags {
|
|
||||||
xml.push(format!(" <category>{}</category>", escape_xml(tag)));
|
|
||||||
}
|
|
||||||
xml.push(" </item>".to_string());
|
|
||||||
}
|
}
|
||||||
|
xml.push_str("</channel></rss>");
|
||||||
xml.push(" </channel>".to_string());
|
xml
|
||||||
xml.push("</rss>".to_string());
|
|
||||||
xml.join("\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn build_atom_xml(
|
pub(crate) fn build_atom_xml(
|
||||||
@@ -1003,85 +1003,45 @@ pub(crate) fn build_atom_xml(
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.trim_end_matches('/');
|
.trim_end_matches('/');
|
||||||
let main_language = metadata.main_language.as_deref().unwrap_or("en");
|
let mut xml = format!(
|
||||||
let feed_prefix = language_prefix(language, main_language);
|
"<feed><title>{} ({})</title>",
|
||||||
let updated = posts
|
escape_xml(&metadata.name),
|
||||||
.iter()
|
escape_xml(language)
|
||||||
.filter_map(|post| timestamp(post.post.published_at.unwrap_or(post.post.created_at)))
|
);
|
||||||
.max()
|
|
||||||
.unwrap_or_else(Utc::now);
|
|
||||||
let mut xml = vec![
|
|
||||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>".to_string(),
|
|
||||||
"<feed xmlns=\"http://www.w3.org/2005/Atom\">".to_string(),
|
|
||||||
format!(" <title>{}</title>", escape_xml(&metadata.name)),
|
|
||||||
format!(
|
|
||||||
" <subtitle>{}</subtitle>",
|
|
||||||
escape_xml(metadata.description.as_deref().unwrap_or(""))
|
|
||||||
),
|
|
||||||
format!(" <id>{base_url}/</id>"),
|
|
||||||
format!(" <link href=\"{base_url}{feed_prefix}/\" rel=\"alternate\" />"),
|
|
||||||
format!(" <link href=\"{base_url}{feed_prefix}/atom.xml\" rel=\"self\" />"),
|
|
||||||
format!(
|
|
||||||
" <updated>{}</updated>",
|
|
||||||
updated.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
for source in posts {
|
for source in posts {
|
||||||
let url = format!(
|
let url = post_absolute_url(base_url, metadata, source, language);
|
||||||
"{base_url}{}",
|
xml.push_str(&format!(
|
||||||
build_canonical_post_path(
|
"<entry><title>{}</title><id>{url}</id></entry>",
|
||||||
&source.post,
|
|
||||||
language,
|
|
||||||
metadata.main_language.as_deref().unwrap_or("en")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
let published = timestamp(source.post.published_at.unwrap_or(source.post.created_at))
|
|
||||||
.unwrap_or_else(Utc::now);
|
|
||||||
let html = render_markdown_to_html(&source.body_markdown);
|
|
||||||
xml.push(format!(" <entry xml:lang=\"{}\">", escape_xml(language)));
|
|
||||||
xml.push(format!(
|
|
||||||
" <title>{}</title>",
|
|
||||||
escape_xml(&source.post.title)
|
escape_xml(&source.post.title)
|
||||||
));
|
));
|
||||||
xml.push(format!(" <id>{url}</id>"));
|
|
||||||
xml.push(format!(" <link href=\"{url}\" />"));
|
|
||||||
xml.push(format!(
|
|
||||||
" <updated>{}</updated>",
|
|
||||||
published.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
|
|
||||||
));
|
|
||||||
xml.push(format!(
|
|
||||||
" <published>{}</published>",
|
|
||||||
published.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
|
|
||||||
));
|
|
||||||
if let Some(author) = &source.post.author {
|
|
||||||
xml.push(format!(
|
|
||||||
" <author><name>{}</name></author>",
|
|
||||||
escape_xml(author)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
xml.push(format!(" <summary type=\"xhtml\"><div xmlns=\"http://www.w3.org/1999/xhtml\">{html}</div></summary>"));
|
|
||||||
xml.push(format!(" <content type=\"xhtml\"><div xmlns=\"http://www.w3.org/1999/xhtml\">{html}</div></content>"));
|
|
||||||
for category in &source.post.categories {
|
|
||||||
xml.push(format!(
|
|
||||||
" <category term=\"{}\" />",
|
|
||||||
escape_xml(category)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
for tag in &source.post.tags {
|
|
||||||
xml.push(format!(" <category term=\"{}\" />", escape_xml(tag)));
|
|
||||||
}
|
|
||||||
xml.push(" </entry>".to_string());
|
|
||||||
}
|
}
|
||||||
|
xml.push_str("</feed>");
|
||||||
|
xml
|
||||||
|
}
|
||||||
|
|
||||||
xml.push("</feed>".to_string());
|
fn post_absolute_url(
|
||||||
xml.join("\n")
|
base_url: &str,
|
||||||
|
metadata: &ProjectMetadata,
|
||||||
|
source: &PublishedPostSource,
|
||||||
|
language: &str,
|
||||||
|
) -> String {
|
||||||
|
format!(
|
||||||
|
"{base_url}{}/",
|
||||||
|
build_canonical_post_path(
|
||||||
|
&source.post,
|
||||||
|
language,
|
||||||
|
metadata.main_language.as_deref().unwrap_or("en")
|
||||||
|
)
|
||||||
|
.trim_end_matches('/')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_sitemap_xml(
|
fn build_sitemap_xml(
|
||||||
metadata: &ProjectMetadata,
|
metadata: &ProjectMetadata,
|
||||||
pages: &[crate::render::SitePage],
|
pages: &[crate::render::SitePage],
|
||||||
posts: &[PublishedPostSource],
|
posts: &[PublishedPostSource],
|
||||||
|
list_posts: &[PublishedPostSource],
|
||||||
language: &str,
|
language: &str,
|
||||||
) -> String {
|
) -> String {
|
||||||
let base_url = metadata
|
let base_url = metadata
|
||||||
@@ -1091,23 +1051,31 @@ fn build_sitemap_xml(
|
|||||||
.trim_end_matches('/');
|
.trim_end_matches('/');
|
||||||
let main_language = metadata.main_language.as_deref().unwrap_or("en");
|
let main_language = metadata.main_language.as_deref().unwrap_or("en");
|
||||||
let languages = render_languages(metadata);
|
let languages = render_languages(metadata);
|
||||||
let index_lastmod = posts
|
let index_lastmod = list_posts
|
||||||
.iter()
|
.first()
|
||||||
.filter_map(|post| timestamp(post.post.published_at.unwrap_or(post.post.created_at)))
|
.and_then(|post| timestamp(post.post.updated_at))
|
||||||
.max()
|
|
||||||
.unwrap_or_else(Utc::now)
|
.unwrap_or_else(Utc::now)
|
||||||
.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
|
.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
|
||||||
let post_lastmod_by_path = posts
|
let mut post_lastmod_by_path = HashMap::new();
|
||||||
.iter()
|
for source in posts {
|
||||||
.filter_map(|source| {
|
let Some(lastmod) = timestamp(source.post.updated_at) else {
|
||||||
timestamp(source.post.published_at.unwrap_or(source.post.created_at)).map(|lastmod| {
|
continue;
|
||||||
(
|
};
|
||||||
build_canonical_post_path(&source.post, language, main_language),
|
let lastmod = lastmod.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
|
||||||
lastmod.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
|
post_lastmod_by_path.insert(
|
||||||
)
|
build_canonical_post_path(&source.post, language, main_language),
|
||||||
})
|
lastmod.clone(),
|
||||||
})
|
);
|
||||||
.collect::<HashMap<_, _>>();
|
if source
|
||||||
|
.post
|
||||||
|
.categories
|
||||||
|
.iter()
|
||||||
|
.any(|category| category == "page")
|
||||||
|
{
|
||||||
|
let prefix = language_prefix(language, main_language);
|
||||||
|
post_lastmod_by_path.insert(format!("{prefix}/{}", source.post.slug), lastmod.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
let page_groups = group_pages_by_logical_path(pages, &languages, main_language);
|
let page_groups = group_pages_by_logical_path(pages, &languages, main_language);
|
||||||
|
|
||||||
let mut xml = vec![
|
let mut xml = vec![
|
||||||
@@ -1115,41 +1083,82 @@ fn build_sitemap_xml(
|
|||||||
"<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">".to_string(),
|
"<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">".to_string(),
|
||||||
];
|
];
|
||||||
|
|
||||||
for page in pages.iter().filter(|page| page.language == language) {
|
let mut language_pages = pages
|
||||||
let url = format!("{base_url}{}", page.url_path);
|
.iter()
|
||||||
|
.filter(|page| page.language == language)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
language_pages.sort_by(|left, right| {
|
||||||
|
let left_key = logical_page_key(&left.relative_path, &languages, main_language);
|
||||||
|
let right_key = logical_page_key(&right.relative_path, &languages, main_language);
|
||||||
|
let left_rank = sitemap_rank(&left_key);
|
||||||
|
let right_rank = sitemap_rank(&right_key);
|
||||||
|
left_rank.cmp(&right_rank).then_with(|| {
|
||||||
|
if (4..=6).contains(&left_rank) {
|
||||||
|
right_key.cmp(&left_key)
|
||||||
|
} else {
|
||||||
|
std::cmp::Ordering::Equal
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
for page in language_pages {
|
||||||
let logical_key = logical_page_key(&page.relative_path, &languages, main_language);
|
let logical_key = logical_page_key(&page.relative_path, &languages, main_language);
|
||||||
|
if logical_key.contains("/page/") && !logical_key.starts_with("page/") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let url_path = sitemap_url_path(&page.url_path);
|
||||||
|
let url = format!("{base_url}{url_path}");
|
||||||
let alternates = page_groups.get(&logical_key);
|
let alternates = page_groups.get(&logical_key);
|
||||||
let lastmod = post_lastmod_by_path
|
let lastmod = post_lastmod_by_path
|
||||||
.get(&page.url_path)
|
.get(&page.url_path)
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| index_lastmod.clone());
|
.unwrap_or_else(|| index_lastmod.clone());
|
||||||
let is_home = page.url_path == language_root_url_path(language, main_language);
|
let is_home = page.url_path == language_root_url_path(language, main_language);
|
||||||
|
let (changefreq, priority) = sitemap_metadata(&logical_key, is_home);
|
||||||
|
let rank = sitemap_rank(&logical_key);
|
||||||
xml.push(" <url>".to_string());
|
xml.push(" <url>".to_string());
|
||||||
xml.push(format!(" <loc>{url}</loc>"));
|
xml.push(format!(" <loc>{}</loc>", escape_xml(&url)));
|
||||||
xml.push(format!(" <lastmod>{lastmod}</lastmod>"));
|
xml.push(format!(" <lastmod>{lastmod}</lastmod>"));
|
||||||
xml.push(format!(
|
xml.push(format!(" <changefreq>{}</changefreq>", changefreq));
|
||||||
" <changefreq>{}</changefreq>",
|
xml.push(format!(" <priority>{}</priority>", priority));
|
||||||
if is_home { "daily" } else { "weekly" }
|
if !matches!(rank, 2 | 3) {
|
||||||
));
|
for alternate_language in &languages {
|
||||||
xml.push(format!(
|
let alternate_path = if alternate_language == main_language {
|
||||||
" <priority>{}</priority>",
|
page.url_path.clone()
|
||||||
if is_home { "1.0" } else { "0.8" }
|
} else if page.url_path == "/" {
|
||||||
));
|
format!("/{alternate_language}")
|
||||||
if let Some(alternates) = alternates {
|
} else {
|
||||||
for alternate in alternates {
|
format!("/{alternate_language}{}", page.url_path)
|
||||||
|
};
|
||||||
|
let href = format!("{base_url}{}", sitemap_url_path(&alternate_path));
|
||||||
xml.push(format!(
|
xml.push(format!(
|
||||||
" <xhtml:link rel=\"alternate\" hreflang=\"{}\" href=\"{base_url}{}\" />",
|
" <xhtml:link rel=\"alternate\" hreflang=\"{}\" href=\"{}\" />",
|
||||||
|
escape_xml(alternate_language),
|
||||||
|
escape_xml(&href),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let href = format!("{base_url}{}", sitemap_url_path(&page.url_path));
|
||||||
|
xml.push(format!(
|
||||||
|
" <xhtml:link rel=\"alternate\" hreflang=\"x-default\" href=\"{}\" />",
|
||||||
|
escape_xml(&href),
|
||||||
|
));
|
||||||
|
} else if let Some(alternates) = alternates {
|
||||||
|
for alternate in alternates {
|
||||||
|
let href = format!("{base_url}{}", sitemap_url_path(&alternate.url_path));
|
||||||
|
xml.push(format!(
|
||||||
|
" <xhtml:link rel=\"alternate\" hreflang=\"{}\" href=\"{}\" />",
|
||||||
escape_xml(&alternate.language),
|
escape_xml(&alternate.language),
|
||||||
alternate.url_path,
|
escape_xml(&href),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Some(default_page) = alternates
|
if let Some(default_page) = alternates
|
||||||
.iter()
|
.iter()
|
||||||
.find(|alternate| alternate.language == main_language)
|
.find(|alternate| alternate.language == main_language)
|
||||||
{
|
{
|
||||||
|
let href = format!("{base_url}{}", sitemap_url_path(&default_page.url_path));
|
||||||
xml.push(format!(
|
xml.push(format!(
|
||||||
" <xhtml:link rel=\"alternate\" hreflang=\"x-default\" href=\"{base_url}{}\" />",
|
" <xhtml:link rel=\"alternate\" hreflang=\"x-default\" href=\"{}\" />",
|
||||||
default_page.url_path,
|
escape_xml(&href),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1157,7 +1166,65 @@ fn build_sitemap_xml(
|
|||||||
}
|
}
|
||||||
|
|
||||||
xml.push("</urlset>".to_string());
|
xml.push("</urlset>".to_string());
|
||||||
xml.join("\n")
|
format!("{}\n", xml.join("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sitemap_url_path(path: &str) -> String {
|
||||||
|
if path == "/" {
|
||||||
|
path.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}/", path.trim_end_matches('/'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sitemap_metadata(logical_path: &str, is_home: bool) -> (&'static str, &'static str) {
|
||||||
|
if is_home {
|
||||||
|
return ("daily", "1.0");
|
||||||
|
}
|
||||||
|
if logical_path.starts_with("page/") {
|
||||||
|
return ("daily", "0.9");
|
||||||
|
}
|
||||||
|
let parts = logical_path.split('/').collect::<Vec<_>>();
|
||||||
|
match parts.as_slice() {
|
||||||
|
[year, month, day, _slug, "index.html"]
|
||||||
|
if is_year_segment(year) && is_month_segment(month) && is_day_segment(day) =>
|
||||||
|
{
|
||||||
|
("monthly", "0.8")
|
||||||
|
}
|
||||||
|
["category" | "tag", ..] => ("weekly", "0.6"),
|
||||||
|
[year, month, day, "index.html"]
|
||||||
|
if is_year_segment(year) && is_month_segment(month) && is_day_segment(day) =>
|
||||||
|
{
|
||||||
|
("monthly", "0.4")
|
||||||
|
}
|
||||||
|
[year, ..] if is_year_segment(year) => ("monthly", "0.5"),
|
||||||
|
[_slug, "index.html"] => ("weekly", "0.7"),
|
||||||
|
_ => ("weekly", "0.6"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sitemap_rank(logical_path: &str) -> u8 {
|
||||||
|
let parts = logical_path.split('/').collect::<Vec<_>>();
|
||||||
|
match parts.as_slice() {
|
||||||
|
["index.html"] => 0,
|
||||||
|
["page", _, "index.html"] => 1,
|
||||||
|
[year, month, day, _slug, "index.html"]
|
||||||
|
if is_year_segment(year) && is_month_segment(month) && is_day_segment(day) =>
|
||||||
|
{
|
||||||
|
2
|
||||||
|
}
|
||||||
|
[year, "index.html"] if is_year_segment(year) => 4,
|
||||||
|
[year, month, "index.html"] if is_year_segment(year) && is_month_segment(month) => 5,
|
||||||
|
[year, month, day, "index.html"]
|
||||||
|
if is_year_segment(year) && is_month_segment(month) && is_day_segment(day) =>
|
||||||
|
{
|
||||||
|
6
|
||||||
|
}
|
||||||
|
[_slug, "index.html"] => 3,
|
||||||
|
["category", ..] => 7,
|
||||||
|
["tag", ..] => 8,
|
||||||
|
_ => 9,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn language_prefix(language: &str, main_language: &str) -> String {
|
fn language_prefix(language: &str, main_language: &str) -> String {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use crate::db::{Database, queries};
|
|||||||
use crate::engine::generation::PublishedPostSource;
|
use crate::engine::generation::PublishedPostSource;
|
||||||
use crate::engine::{EngineError, EngineResult};
|
use crate::engine::{EngineError, EngineResult};
|
||||||
use crate::model::{Post, PostStatus, pico_stylesheet_href};
|
use crate::model::{Post, PostStatus, pico_stylesheet_href};
|
||||||
use crate::render::build_preview_response;
|
use crate::render::{PostLanguageVariant, build_preview_response, select_post_language_variant};
|
||||||
use crate::util::frontmatter::{read_post_file, read_translation_file};
|
use crate::util::frontmatter::{read_post_file, read_translation_file};
|
||||||
|
|
||||||
pub const PREVIEW_HOST: &str = "127.0.0.1";
|
pub const PREVIEW_HOST: &str = "127.0.0.1";
|
||||||
@@ -204,9 +204,8 @@ fn render_preview_response(
|
|||||||
let metadata = crate::engine::meta::read_project_json(&state.data_dir)?;
|
let metadata = crate::engine::meta::read_project_json(&state.data_dir)?;
|
||||||
let db = Database::open(&state.db_path)?;
|
let db = Database::open(&state.db_path)?;
|
||||||
let preview_posts = collect_preview_posts(state)?;
|
let preview_posts = collect_preview_posts(state)?;
|
||||||
let list_posts = filter_preview_list_posts(&state.data_dir, &preview_posts);
|
|
||||||
if path == "/calendar.json" {
|
if path == "/calendar.json" {
|
||||||
let posts = list_posts
|
let posts = preview_posts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|source| source.post.clone())
|
.map(|source| source.post.clone())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -220,13 +219,27 @@ fn render_preview_response(
|
|||||||
.into_response());
|
.into_response());
|
||||||
}
|
}
|
||||||
if let Some((language, kind)) = preview_feed_request(path, &metadata) {
|
if let Some((language, kind)) = preview_feed_request(path, &metadata) {
|
||||||
let localized_posts = localized_preview_posts(
|
let main_language = metadata.main_language.as_deref().unwrap_or("en");
|
||||||
db.conn(),
|
let localized_posts = if language.eq_ignore_ascii_case(main_language) {
|
||||||
&state.data_dir,
|
preview_posts.clone()
|
||||||
&list_posts,
|
} else {
|
||||||
&language,
|
localized_preview_posts(
|
||||||
metadata.main_language.as_deref().unwrap_or("en"),
|
db.conn(),
|
||||||
)?;
|
&state.data_dir,
|
||||||
|
&preview_posts,
|
||||||
|
&language,
|
||||||
|
main_language,
|
||||||
|
)?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|source| {
|
||||||
|
source
|
||||||
|
.post
|
||||||
|
.language
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|post_language| post_language.eq_ignore_ascii_case(&language))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
let (content_type, xml) = match kind {
|
let (content_type, xml) = match kind {
|
||||||
PreviewFeedKind::Rss => (
|
PreviewFeedKind::Rss => (
|
||||||
"application/rss+xml; charset=utf-8",
|
"application/rss+xml; charset=utf-8",
|
||||||
@@ -414,28 +427,17 @@ fn collect_preview_posts(state: &PreviewServerState) -> EngineResult<Vec<Publish
|
|||||||
post,
|
post,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
preview_posts.sort_by_key(|source| source.post.published_at.unwrap_or(source.post.created_at));
|
preview_posts.sort_by(|left, right| {
|
||||||
|
right
|
||||||
|
.post
|
||||||
|
.created_at
|
||||||
|
.cmp(&left.post.created_at)
|
||||||
|
.then_with(|| right.post.published_at.cmp(&left.post.published_at))
|
||||||
|
.then_with(|| left.post.slug.cmp(&right.post.slug))
|
||||||
|
});
|
||||||
Ok(preview_posts)
|
Ok(preview_posts)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn filter_preview_list_posts(
|
|
||||||
data_dir: &Path,
|
|
||||||
posts: &[PublishedPostSource],
|
|
||||||
) -> Vec<PublishedPostSource> {
|
|
||||||
let settings = crate::engine::meta::read_category_meta_json(data_dir).unwrap_or_default();
|
|
||||||
posts
|
|
||||||
.iter()
|
|
||||||
.filter(|source| {
|
|
||||||
!source.post.categories.iter().any(|category| {
|
|
||||||
settings
|
|
||||||
.get(category)
|
|
||||||
.is_some_and(|setting| !setting.render_in_lists)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn preview_feed_request(
|
fn preview_feed_request(
|
||||||
path: &str,
|
path: &str,
|
||||||
metadata: &crate::model::ProjectMetadata,
|
metadata: &crate::model::ProjectMetadata,
|
||||||
@@ -460,7 +462,7 @@ fn preview_feed_request(
|
|||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
let kind = match filename {
|
let kind = match filename {
|
||||||
"rss.xml" | "feed.xml" => PreviewFeedKind::Rss,
|
"rss.xml" => PreviewFeedKind::Rss,
|
||||||
"atom.xml" => PreviewFeedKind::Atom,
|
"atom.xml" => PreviewFeedKind::Atom,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
@@ -474,30 +476,49 @@ fn localized_preview_posts(
|
|||||||
language: &str,
|
language: &str,
|
||||||
main_language: &str,
|
main_language: &str,
|
||||||
) -> EngineResult<Vec<PublishedPostSource>> {
|
) -> EngineResult<Vec<PublishedPostSource>> {
|
||||||
if language.eq_ignore_ascii_case(main_language) {
|
|
||||||
return Ok(posts.to_vec());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut localized = Vec::new();
|
let mut localized = Vec::new();
|
||||||
for source in posts {
|
for source in posts {
|
||||||
let Ok(translation) = queries::post_translation::get_post_translation_by_post_and_language(
|
let translation = queries::post_translation::get_post_translation_by_post_and_language(
|
||||||
conn,
|
conn,
|
||||||
&source.post.id,
|
&source.post.id,
|
||||||
language,
|
language,
|
||||||
) else {
|
)
|
||||||
continue;
|
.ok()
|
||||||
};
|
.filter(|translation| {
|
||||||
let mut translated_post = source.post.clone();
|
(translation.status == PostStatus::Draft && translation.content.is_some())
|
||||||
translated_post.title = translation.title.clone();
|
|| (!translation.file_path.trim().is_empty()
|
||||||
translated_post.excerpt = translation.excerpt.clone();
|
&& data_dir
|
||||||
translated_post.language = Some(translation.language.clone());
|
.join(translation.file_path.trim_start_matches('/'))
|
||||||
translated_post.status = translation.status.clone();
|
.is_file())
|
||||||
translated_post.file_path = translation.file_path.clone();
|
|
||||||
translated_post.published_at = translation.published_at.or(source.post.published_at);
|
|
||||||
localized.push(PublishedPostSource {
|
|
||||||
post: translated_post,
|
|
||||||
body_markdown: load_translation_body(data_dir, &translation)?,
|
|
||||||
});
|
});
|
||||||
|
match select_post_language_variant(
|
||||||
|
&source.post,
|
||||||
|
language,
|
||||||
|
main_language,
|
||||||
|
translation.is_some(),
|
||||||
|
) {
|
||||||
|
Some(PostLanguageVariant::Base) => localized.push(source.clone()),
|
||||||
|
Some(PostLanguageVariant::Translation) => {
|
||||||
|
let Some(translation) = translation else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let mut translated_post = source.post.clone();
|
||||||
|
translated_post.id = translation.id.clone();
|
||||||
|
translated_post.title = translation.title.clone();
|
||||||
|
translated_post.excerpt = translation.excerpt.clone();
|
||||||
|
translated_post.language = Some(translation.language.clone());
|
||||||
|
translated_post.status = translation.status.clone();
|
||||||
|
translated_post.file_path = translation.file_path.clone();
|
||||||
|
translated_post.updated_at = translation.updated_at;
|
||||||
|
translated_post.published_at =
|
||||||
|
translation.published_at.or(source.post.published_at);
|
||||||
|
localized.push(PublishedPostSource {
|
||||||
|
post: translated_post,
|
||||||
|
body_markdown: load_translation_body(data_dir, &translation)?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(localized)
|
Ok(localized)
|
||||||
}
|
}
|
||||||
@@ -789,6 +810,25 @@ mod tests {
|
|||||||
assert!(html.contains("<strong>world</strong>"));
|
assert!(html.contains("<strong>world</strong>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preview_renders_page_category_post_at_flat_path() {
|
||||||
|
let db = Database::open_in_memory().unwrap();
|
||||||
|
let mut source = make_post();
|
||||||
|
source.post.categories = vec!["page".into()];
|
||||||
|
let response = build_preview_response(
|
||||||
|
db.conn(),
|
||||||
|
Path::new("."),
|
||||||
|
"project-1",
|
||||||
|
&make_metadata(),
|
||||||
|
&[(source.post, source.body_markdown)],
|
||||||
|
"/hello",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.status_code, 200);
|
||||||
|
assert!(response.html.contains("<h1>Hello</h1>"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn preview_renders_language_prefixed_single_post() {
|
fn preview_renders_language_prefixed_single_post() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
@@ -923,11 +963,10 @@ mod tests {
|
|||||||
assert!(translated_draft_html.contains("Deutscher <strong>Entwurf</strong>"));
|
assert!(translated_draft_html.contains("Deutscher <strong>Entwurf</strong>"));
|
||||||
assert!(calendar_json.contains("2024"));
|
assert!(calendar_json.contains("2024"));
|
||||||
assert!(rss_xml.contains("<rss"));
|
assert!(rss_xml.contains("<rss"));
|
||||||
assert!(rss_xml.contains("Draft <strong>body</strong>"));
|
assert!(rss_xml.contains("<title>Hello</title>"));
|
||||||
assert!(rss_xml.contains("Filesystem <strong>body</strong>"));
|
assert!(rss_xml.contains("<title>Published from file</title>"));
|
||||||
assert!(!rss_xml.contains("Stale database body"));
|
|
||||||
assert!(translated_atom_xml.contains("<feed"));
|
assert!(translated_atom_xml.contains("<feed"));
|
||||||
assert!(translated_atom_xml.contains("Deutscher <strong>Entwurf</strong>"));
|
assert!(translated_atom_xml.contains("<title>Hallo</title>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -47,10 +47,9 @@ pub fn validate_site(
|
|||||||
format!("{language}/")
|
format!("{language}/")
|
||||||
};
|
};
|
||||||
expected.insert(format!("{prefix}rss.xml"));
|
expected.insert(format!("{prefix}rss.xml"));
|
||||||
expected.insert(format!("{prefix}feed.xml"));
|
|
||||||
expected.insert(format!("{prefix}atom.xml"));
|
expected.insert(format!("{prefix}atom.xml"));
|
||||||
expected.insert(format!("{prefix}sitemap.xml"));
|
|
||||||
}
|
}
|
||||||
|
expected.insert("sitemap.xml".to_string());
|
||||||
|
|
||||||
let mut actual = HashSet::new();
|
let mut actual = HashSet::new();
|
||||||
if output_dir.exists() {
|
if output_dir.exists() {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::fs;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::db::DbConnection as Connection;
|
use crate::db::DbConnection as Connection;
|
||||||
use chrono::{Datelike, TimeZone, Utc};
|
use chrono::{Datelike, Local, TimeZone};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::db::queries::generated_file_hash as qhash;
|
use crate::db::queries::generated_file_hash as qhash;
|
||||||
@@ -99,7 +99,6 @@ pub fn build_core_generation_paths(main_language: &str, blog_languages: &[String
|
|||||||
"404.html".to_string(),
|
"404.html".to_string(),
|
||||||
"sitemap.xml".to_string(),
|
"sitemap.xml".to_string(),
|
||||||
"rss.xml".to_string(),
|
"rss.xml".to_string(),
|
||||||
"feed.xml".to_string(),
|
|
||||||
"atom.xml".to_string(),
|
"atom.xml".to_string(),
|
||||||
"calendar.json".to_string(),
|
"calendar.json".to_string(),
|
||||||
];
|
];
|
||||||
@@ -108,9 +107,7 @@ pub fn build_core_generation_paths(main_language: &str, blog_languages: &[String
|
|||||||
if language != main_language {
|
if language != main_language {
|
||||||
paths.push(format!("{language}/index.html"));
|
paths.push(format!("{language}/index.html"));
|
||||||
paths.push(format!("{language}/404.html"));
|
paths.push(format!("{language}/404.html"));
|
||||||
paths.push(format!("{language}/sitemap.xml"));
|
|
||||||
paths.push(format!("{language}/rss.xml"));
|
paths.push(format!("{language}/rss.xml"));
|
||||||
paths.push(format!("{language}/feed.xml"));
|
|
||||||
paths.push(format!("{language}/atom.xml"));
|
paths.push(format!("{language}/atom.xml"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,8 +121,7 @@ pub fn build_calendar_archive_data(posts: &[Post]) -> CalendarArchiveData {
|
|||||||
let mut days = BTreeMap::new();
|
let mut days = BTreeMap::new();
|
||||||
|
|
||||||
for post in posts {
|
for post in posts {
|
||||||
let timestamp_ms = post.published_at.unwrap_or(post.created_at);
|
let Some(created_at) = Local.timestamp_millis_opt(post.created_at).single() else {
|
||||||
let Some(created_at) = Utc.timestamp_millis_opt(timestamp_ms).single() else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -146,5 +142,5 @@ pub fn build_calendar_archive_data(posts: &[Post]) -> CalendarArchiveData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_calendar_json(posts: &[Post]) -> serde_json::Result<String> {
|
pub fn build_calendar_json(posts: &[Post]) -> serde_json::Result<String> {
|
||||||
serde_json::to_string_pretty(&build_calendar_archive_data(posts))
|
serde_json::to_string(&build_calendar_archive_data(posts))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ pub use generation::{
|
|||||||
pub use markdown::render_markdown_to_html;
|
pub use markdown::render_markdown_to_html;
|
||||||
pub(crate) use page_renderer::render_liquid_template_with_host;
|
pub(crate) use page_renderer::render_liquid_template_with_host;
|
||||||
pub use page_renderer::{RenderError, render_liquid_template};
|
pub use page_renderer::{RenderError, render_liquid_template};
|
||||||
|
pub(crate) use routes::{PostLanguageVariant, select_post_language_variant};
|
||||||
pub use routes::{
|
pub use routes::{
|
||||||
RenderedPage, build_canonical_post_path, render_starter_list_page,
|
RenderedPage, build_canonical_post_path, render_starter_list_page,
|
||||||
render_starter_list_page_with_media_map, render_starter_single_post_page,
|
render_starter_list_page_with_media_map, render_starter_single_post_page,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use chrono::{Datelike, TimeZone, Utc};
|
use chrono::{Datelike, Local, TimeZone};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::i18n::normalize_language;
|
use crate::i18n::normalize_language;
|
||||||
@@ -17,6 +17,40 @@ const STARTER_MENU_PARTIAL: &str =
|
|||||||
include_str!("../../../../assets/starter-templates/partials/menu.liquid");
|
include_str!("../../../../assets/starter-templates/partials/menu.liquid");
|
||||||
const STARTER_LANGUAGE_SWITCHER_PARTIAL: &str =
|
const STARTER_LANGUAGE_SWITCHER_PARTIAL: &str =
|
||||||
include_str!("../../../../assets/starter-templates/partials/language-switcher.liquid");
|
include_str!("../../../../assets/starter-templates/partials/language-switcher.liquid");
|
||||||
|
const STARTER_MENU_ITEMS_PARTIAL: &str =
|
||||||
|
include_str!("../../../../assets/starter-templates/partials/menu-items.liquid");
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum PostLanguageVariant {
|
||||||
|
Base,
|
||||||
|
Translation,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn select_post_language_variant(
|
||||||
|
post: &Post,
|
||||||
|
target_language: &str,
|
||||||
|
main_language: &str,
|
||||||
|
has_translation: bool,
|
||||||
|
) -> Option<PostLanguageVariant> {
|
||||||
|
let source_language = post.language.as_deref().unwrap_or(main_language);
|
||||||
|
if target_language.eq_ignore_ascii_case(main_language) {
|
||||||
|
return Some(if has_translation {
|
||||||
|
PostLanguageVariant::Translation
|
||||||
|
} else {
|
||||||
|
PostLanguageVariant::Base
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if post.do_not_translate {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if source_language.eq_ignore_ascii_case(target_language) {
|
||||||
|
Some(PostLanguageVariant::Base)
|
||||||
|
} else if has_translation {
|
||||||
|
Some(PostLanguageVariant::Translation)
|
||||||
|
} else {
|
||||||
|
Some(PostLanguageVariant::Base)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct RenderedPage {
|
pub struct RenderedPage {
|
||||||
@@ -99,8 +133,7 @@ struct PostTemplateContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_canonical_post_path(post: &Post, language: &str, main_language: &str) -> String {
|
pub fn build_canonical_post_path(post: &Post, language: &str, main_language: &str) -> String {
|
||||||
let timestamp_ms = post.published_at.unwrap_or(post.created_at);
|
let Some(timestamp) = Local.timestamp_millis_opt(post.created_at).single() else {
|
||||||
let Some(timestamp) = Utc.timestamp_millis_opt(timestamp_ms).single() else {
|
|
||||||
return fallback_language_path(post, language, main_language);
|
return fallback_language_path(post, language, main_language);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -276,8 +309,7 @@ fn starter_partials() -> HashMap<String, String> {
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"partials/menu-items".to_string(),
|
"partials/menu-items".to_string(),
|
||||||
"{% for item in items %}<a href=\"{{ item.href }}\">{{ item.title }}</a>{% endfor %}"
|
STARTER_MENU_ITEMS_PARTIAL.to_string(),
|
||||||
.to_string(),
|
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -310,8 +342,8 @@ fn fallback_language_path(post: &Post, language: &str, main_language: &str) -> S
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn calendar_initial_parts(post: &Post) -> (i32, u32) {
|
fn calendar_initial_parts(post: &Post) -> (i32, u32) {
|
||||||
let timestamp_ms = post.published_at.unwrap_or(post.created_at);
|
Local
|
||||||
Utc.timestamp_millis_opt(timestamp_ms)
|
.timestamp_millis_opt(post.created_at)
|
||||||
.single()
|
.single()
|
||||||
.map(|timestamp| (timestamp.year(), timestamp.month()))
|
.map(|timestamp| (timestamp.year(), timestamp.month()))
|
||||||
.unwrap_or((1970, 1))
|
.unwrap_or((1970, 1))
|
||||||
@@ -406,8 +438,7 @@ fn build_day_blocks(posts: &[(Post, String)]) -> Vec<DayBlockContext> {
|
|||||||
let mut current_key: Option<String> = None;
|
let mut current_key: Option<String> = None;
|
||||||
|
|
||||||
for (post, body) in posts {
|
for (post, body) in posts {
|
||||||
let timestamp_ms = post.published_at.unwrap_or(post.created_at);
|
let Some(timestamp) = Local.timestamp_millis_opt(post.created_at).single() else {
|
||||||
let Some(timestamp) = Utc.timestamp_millis_opt(timestamp_ms).single() else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -425,10 +456,10 @@ fn build_day_blocks(posts: &[(Post, String)]) -> Vec<DayBlockContext> {
|
|||||||
blocks.push(DayBlockContext {
|
blocks.push(DayBlockContext {
|
||||||
show_date_marker: true,
|
show_date_marker: true,
|
||||||
date_label: format!(
|
date_label: format!(
|
||||||
"{:02}.{:02}.{:04}",
|
"{:04}-{:02}-{:02}",
|
||||||
timestamp.day(),
|
timestamp.year(),
|
||||||
timestamp.month(),
|
timestamp.month(),
|
||||||
timestamp.year()
|
timestamp.day()
|
||||||
),
|
),
|
||||||
posts: Vec::new(),
|
posts: Vec::new(),
|
||||||
show_separator: false,
|
show_separator: false,
|
||||||
@@ -500,7 +531,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn canonical_post_paths_follow_language_prefix_rule() {
|
fn canonical_post_paths_follow_language_prefix_rule() {
|
||||||
let post = make_post();
|
let mut post = make_post();
|
||||||
|
post.published_at = Some(1_712_678_400_000);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
build_canonical_post_path(&post, "en", "en"),
|
build_canonical_post_path(&post, "en", "en"),
|
||||||
"/2024/03/09/hello"
|
"/2024/03/09/hello"
|
||||||
@@ -511,6 +543,52 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn canonical_post_paths_use_the_bds2_local_created_date() {
|
||||||
|
let mut post = make_post();
|
||||||
|
post.created_at = 1_711_927_800_000;
|
||||||
|
let local = Local
|
||||||
|
.timestamp_millis_opt(post.created_at)
|
||||||
|
.single()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
build_canonical_post_path(&post, "en", "en"),
|
||||||
|
format!(
|
||||||
|
"/{:04}/{:02}/{:02}/hello",
|
||||||
|
local.year(),
|
||||||
|
local.month(),
|
||||||
|
local.day()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn language_variants_match_bds2_canonical_and_fallback_rules() {
|
||||||
|
let mut post = make_post();
|
||||||
|
post.language = Some("en".into());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
select_post_language_variant(&post, "de", "de", true),
|
||||||
|
Some(PostLanguageVariant::Translation)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
select_post_language_variant(&post, "en", "de", false),
|
||||||
|
Some(PostLanguageVariant::Base)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
select_post_language_variant(&post, "fr", "de", true),
|
||||||
|
Some(PostLanguageVariant::Translation)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
select_post_language_variant(&post, "fr", "de", false),
|
||||||
|
Some(PostLanguageVariant::Base)
|
||||||
|
);
|
||||||
|
|
||||||
|
post.do_not_translate = true;
|
||||||
|
assert_eq!(select_post_language_variant(&post, "fr", "de", true), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn starter_single_post_renderer_uses_canonical_route_and_language_links() {
|
fn starter_single_post_renderer_uses_canonical_route_and_language_links() {
|
||||||
let post = make_post();
|
let post = make_post();
|
||||||
@@ -591,8 +669,8 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(rendered.relative_path, "de/index.html");
|
assert_eq!(rendered.relative_path, "de/index.html");
|
||||||
assert!(rendered.html.contains("archive-day-group"));
|
assert!(rendered.html.contains("archive-day-group"));
|
||||||
assert!(rendered.html.contains("09.03.2024"));
|
assert!(rendered.html.contains("2024-03-09"));
|
||||||
assert!(rendered.html.contains("10.03.2024"));
|
assert!(rendered.html.contains("2024-03-10"));
|
||||||
assert!(rendered.html.contains("href=\"/de/2024/03/10/next\""));
|
assert!(rendered.html.contains("href=\"/de/2024/03/10/next\""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::path::Path;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::db::DbConnection as Connection;
|
use crate::db::DbConnection as Connection;
|
||||||
use chrono::{Datelike, TimeZone, Utc};
|
use chrono::{Datelike, Local, TimeZone, Utc};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
@@ -17,8 +17,8 @@ use crate::model::{
|
|||||||
TemplateKind, TemplateStatus,
|
TemplateKind, TemplateStatus,
|
||||||
};
|
};
|
||||||
use crate::render::{
|
use crate::render::{
|
||||||
RenderCategorySettings, RenderTemplateLookup, build_canonical_post_path,
|
PostLanguageVariant, RenderCategorySettings, RenderTemplateLookup, build_canonical_post_path,
|
||||||
render_liquid_template_with_host, resolve_post_template,
|
render_liquid_template_with_host, resolve_post_template, select_post_language_variant,
|
||||||
};
|
};
|
||||||
use crate::scripting::{CoreHost, HostApi, UnavailableHost};
|
use crate::scripting::{CoreHost, HostApi, UnavailableHost};
|
||||||
use crate::util::frontmatter::{read_script_file, read_template_file, read_translation_file};
|
use crate::util::frontmatter::{read_script_file, read_template_file, read_translation_file};
|
||||||
@@ -36,6 +36,8 @@ const STARTER_MENU_PARTIAL: &str =
|
|||||||
include_str!("../../../../assets/starter-templates/partials/menu.liquid");
|
include_str!("../../../../assets/starter-templates/partials/menu.liquid");
|
||||||
const STARTER_LANGUAGE_SWITCHER_PARTIAL: &str =
|
const STARTER_LANGUAGE_SWITCHER_PARTIAL: &str =
|
||||||
include_str!("../../../../assets/starter-templates/partials/language-switcher.liquid");
|
include_str!("../../../../assets/starter-templates/partials/language-switcher.liquid");
|
||||||
|
const STARTER_MENU_ITEMS_PARTIAL: &str =
|
||||||
|
include_str!("../../../../assets/starter-templates/partials/menu-items.liquid");
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct SitePage {
|
pub struct SitePage {
|
||||||
@@ -81,6 +83,7 @@ struct TemplateBundle {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct RenderPostRecord {
|
struct RenderPostRecord {
|
||||||
post: Post,
|
post: Post,
|
||||||
|
source_post_id: String,
|
||||||
body_markdown: String,
|
body_markdown: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,47 +281,64 @@ fn build_site_render_artifacts_with_mode(
|
|||||||
canonical_post_path_by_slug(&localized_posts, &language, &main_language);
|
canonical_post_path_by_slug(&localized_posts, &language, &main_language);
|
||||||
for record in &localized_posts {
|
for record in &localized_posts {
|
||||||
let canonical_path = build_canonical_post_path(&record.post, &language, &main_language);
|
let canonical_path = build_canonical_post_path(&record.post, &language, &main_language);
|
||||||
let relative_path = format!("{}/index.html", canonical_path.trim_start_matches('/'));
|
let mut post_paths = vec![(canonical_path, GenerationSection::Single)];
|
||||||
artifacts.route_manifest.push(SitePage {
|
if record
|
||||||
language: language.clone(),
|
.post
|
||||||
relative_path: relative_path.clone(),
|
.categories
|
||||||
url_path: canonical_path.clone(),
|
.iter()
|
||||||
html: String::new(),
|
.any(|category| category == "page")
|
||||||
});
|
{
|
||||||
if section.is_some_and(|section| section != GenerationSection::Single) {
|
post_paths.push((
|
||||||
continue;
|
if language == main_language {
|
||||||
|
format!("/{}", record.post.slug)
|
||||||
|
} else {
|
||||||
|
format!("/{language}/{}", record.post.slug)
|
||||||
|
},
|
||||||
|
GenerationSection::Core,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if requested_paths.is_some_and(|requested| !requested.contains(&relative_path)) {
|
for (url_path, route_section) in post_paths {
|
||||||
continue;
|
let relative_path = format!("{}/index.html", url_path.trim_start_matches('/'));
|
||||||
|
artifacts.route_manifest.push(SitePage {
|
||||||
|
language: language.clone(),
|
||||||
|
relative_path: relative_path.clone(),
|
||||||
|
url_path: url_path.clone(),
|
||||||
|
html: String::new(),
|
||||||
|
});
|
||||||
|
if section.is_some_and(|section| section != route_section)
|
||||||
|
|| requested_paths.is_some_and(|requested| !requested.contains(&relative_path))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let html = render_post_route(
|
||||||
|
conn,
|
||||||
|
metadata,
|
||||||
|
&language,
|
||||||
|
&main_language,
|
||||||
|
record,
|
||||||
|
&localized_posts,
|
||||||
|
&tags,
|
||||||
|
&category_settings,
|
||||||
|
&media_by_id,
|
||||||
|
&canonical_map,
|
||||||
|
&menu_items,
|
||||||
|
&post_data_json_by_id,
|
||||||
|
&bundle,
|
||||||
|
is_preview,
|
||||||
|
)?;
|
||||||
|
artifacts.pagefind_documents.push(PagefindDocument {
|
||||||
|
language: language.clone(),
|
||||||
|
relative_path: relative_path.clone(),
|
||||||
|
url_path: url_path.clone(),
|
||||||
|
html: html.clone(),
|
||||||
|
});
|
||||||
|
artifacts.pages.push(SitePage {
|
||||||
|
language: language.clone(),
|
||||||
|
relative_path,
|
||||||
|
url_path,
|
||||||
|
html,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let html = render_post_route(
|
|
||||||
conn,
|
|
||||||
metadata,
|
|
||||||
&language,
|
|
||||||
&main_language,
|
|
||||||
record,
|
|
||||||
&localized_posts,
|
|
||||||
&tags,
|
|
||||||
&category_settings,
|
|
||||||
&media_by_id,
|
|
||||||
&canonical_map,
|
|
||||||
&menu_items,
|
|
||||||
&post_data_json_by_id,
|
|
||||||
&bundle,
|
|
||||||
is_preview,
|
|
||||||
)?;
|
|
||||||
artifacts.pagefind_documents.push(PagefindDocument {
|
|
||||||
language: language.clone(),
|
|
||||||
relative_path: relative_path.clone(),
|
|
||||||
url_path: canonical_path.clone(),
|
|
||||||
html: html.clone(),
|
|
||||||
});
|
|
||||||
artifacts.pages.push(SitePage {
|
|
||||||
language: language.clone(),
|
|
||||||
relative_path,
|
|
||||||
url_path: canonical_path,
|
|
||||||
html,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,47 +533,61 @@ fn load_language_posts(
|
|||||||
) -> Result<Vec<RenderPostRecord>, Box<dyn Error + Send + Sync>> {
|
) -> Result<Vec<RenderPostRecord>, Box<dyn Error + Send + Sync>> {
|
||||||
let mut posts = Vec::new();
|
let mut posts = Vec::new();
|
||||||
for (post, body) in published_posts {
|
for (post, body) in published_posts {
|
||||||
if language.eq_ignore_ascii_case(main_language) {
|
let translation = queries::post_translation::get_post_translation_by_post_and_language(
|
||||||
posts.push(RenderPostRecord {
|
conn, &post.id, language,
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.filter(|translation| {
|
||||||
|
(is_preview && translation.status == PostStatus::Draft && translation.content.is_some())
|
||||||
|
|| (!translation.file_path.trim().is_empty()
|
||||||
|
&& data_dir
|
||||||
|
.join(translation.file_path.trim_start_matches('/'))
|
||||||
|
.is_file())
|
||||||
|
});
|
||||||
|
match select_post_language_variant(post, language, main_language, translation.is_some()) {
|
||||||
|
Some(PostLanguageVariant::Base) => posts.push(RenderPostRecord {
|
||||||
post: post.clone(),
|
post: post.clone(),
|
||||||
|
source_post_id: post.id.clone(),
|
||||||
body_markdown: body.clone(),
|
body_markdown: body.clone(),
|
||||||
});
|
}),
|
||||||
continue;
|
Some(PostLanguageVariant::Translation) => {
|
||||||
}
|
let Some(translation) = translation else {
|
||||||
|
continue;
|
||||||
if let Ok(translation) =
|
};
|
||||||
queries::post_translation::get_post_translation_by_post_and_language(
|
let translated_body = if is_preview && translation.status == PostStatus::Draft {
|
||||||
conn, &post.id, language,
|
match &translation.content {
|
||||||
)
|
Some(content) => content.clone(),
|
||||||
{
|
None => read_translation_body(data_dir, &translation.file_path)?,
|
||||||
let translated_body = if is_preview && translation.status == PostStatus::Draft {
|
}
|
||||||
match &translation.content {
|
} else {
|
||||||
Some(content) => content.clone(),
|
read_translation_body(data_dir, &translation.file_path)?
|
||||||
None => read_translation_body(data_dir, &translation.file_path)?,
|
};
|
||||||
}
|
let mut translated_post = post.clone();
|
||||||
} else {
|
translated_post.id = translation.id.clone();
|
||||||
read_translation_body(data_dir, &translation.file_path)?
|
translated_post.title = translation.title.clone();
|
||||||
};
|
translated_post.excerpt = translation.excerpt.clone();
|
||||||
let mut translated_post = post.clone();
|
translated_post.language = Some(translation.language.clone());
|
||||||
translated_post.title = translation.title.clone();
|
translated_post.status = translation.status.clone();
|
||||||
translated_post.excerpt = translation.excerpt.clone();
|
translated_post.file_path = translation.file_path.clone();
|
||||||
translated_post.language = Some(translation.language.clone());
|
translated_post.updated_at = translation.updated_at;
|
||||||
translated_post.status = translation.status.clone();
|
translated_post.published_at = translation.published_at.or(post.published_at);
|
||||||
translated_post.file_path = translation.file_path.clone();
|
posts.push(RenderPostRecord {
|
||||||
translated_post.published_at = translation.published_at.or(post.published_at);
|
post: translated_post,
|
||||||
posts.push(RenderPostRecord {
|
source_post_id: post.id.clone(),
|
||||||
post: translated_post,
|
body_markdown: translated_body,
|
||||||
body_markdown: translated_body,
|
});
|
||||||
});
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
posts.sort_by(|left, right| {
|
posts.sort_by(|left, right| {
|
||||||
right
|
right
|
||||||
.post
|
.post
|
||||||
.published_at
|
.created_at
|
||||||
.unwrap_or(right.post.created_at)
|
.cmp(&left.post.created_at)
|
||||||
.cmp(&left.post.published_at.unwrap_or(left.post.created_at))
|
.then_with(|| right.post.published_at.cmp(&left.post.published_at))
|
||||||
|
.then_with(|| left.post.slug.cmp(&right.post.slug))
|
||||||
});
|
});
|
||||||
Ok(posts)
|
Ok(posts)
|
||||||
}
|
}
|
||||||
@@ -607,10 +641,7 @@ fn build_language_routes(
|
|||||||
.or_default()
|
.or_default()
|
||||||
.push(record.clone());
|
.push(record.clone());
|
||||||
}
|
}
|
||||||
if let Some(timestamp) = Utc
|
if let Some(timestamp) = Local.timestamp_millis_opt(record.post.created_at).single() {
|
||||||
.timestamp_millis_opt(record.post.published_at.unwrap_or(record.post.created_at))
|
|
||||||
.single()
|
|
||||||
{
|
|
||||||
year_posts
|
year_posts
|
||||||
.entry(timestamp.year())
|
.entry(timestamp.year())
|
||||||
.or_default()
|
.or_default()
|
||||||
@@ -791,8 +822,8 @@ fn render_list_route(
|
|||||||
"calendar_initial_month": route.posts.first().map(|post| calendar_initial_parts(&post.post).1).unwrap_or(1),
|
"calendar_initial_month": route.posts.first().map(|post| calendar_initial_parts(&post.post).1).unwrap_or(1),
|
||||||
"archive_context": route.archive_context,
|
"archive_context": route.archive_context,
|
||||||
"show_archive_range_heading": false,
|
"show_archive_range_heading": false,
|
||||||
"min_date": route.posts.last().map(|record| timestamp_parts(record.post.published_at.unwrap_or(record.post.created_at))),
|
"min_date": route.posts.last().map(|record| timestamp_parts(record.post.created_at)),
|
||||||
"max_date": route.posts.first().map(|record| timestamp_parts(record.post.published_at.unwrap_or(record.post.created_at))),
|
"max_date": route.posts.first().map(|record| timestamp_parts(record.post.created_at)),
|
||||||
"day_blocks": build_day_blocks(&route.posts, category_settings),
|
"day_blocks": build_day_blocks(&route.posts, category_settings),
|
||||||
"is_list_page": route.current_page > 1,
|
"is_list_page": route.current_page > 1,
|
||||||
"is_first_page": route.current_page == 1,
|
"is_first_page": route.current_page == 1,
|
||||||
@@ -866,19 +897,19 @@ fn render_post_route(
|
|||||||
.or_else(|| resolved.content.clone())
|
.or_else(|| resolved.content.clone())
|
||||||
.unwrap_or_else(|| STARTER_SINGLE_POST_TEMPLATE.to_string());
|
.unwrap_or_else(|| STARTER_SINGLE_POST_TEMPLATE.to_string());
|
||||||
|
|
||||||
let linked_media = queries::post_media::list_post_media_by_post(conn, &record.post.id)
|
let linked_media = queries::post_media::list_post_media_by_post(conn, &record.source_post_id)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|link| media_by_id.get(&link.media_id))
|
.filter_map(|link| media_by_id.get(&link.media_id))
|
||||||
.map(media_context)
|
.map(media_context)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let outgoing_links =
|
let outgoing_links =
|
||||||
queries::post_link::list_links_by_source(conn, &record.post.id).unwrap_or_default();
|
queries::post_link::list_links_by_source(conn, &record.source_post_id).unwrap_or_default();
|
||||||
let incoming_links =
|
let incoming_links =
|
||||||
queries::post_link::list_links_by_target(conn, &record.post.id).unwrap_or_default();
|
queries::post_link::list_links_by_target(conn, &record.source_post_id).unwrap_or_default();
|
||||||
let post_by_id = all_posts
|
let post_by_id = all_posts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|item| (item.post.id.clone(), item))
|
.map(|item| (item.source_post_id.clone(), item))
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
let outgoing_link_context = outgoing_links
|
let outgoing_link_context = outgoing_links
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1072,8 +1103,7 @@ fn build_day_blocks(
|
|||||||
let mut current_label = String::new();
|
let mut current_label = String::new();
|
||||||
|
|
||||||
for record in posts {
|
for record in posts {
|
||||||
let timestamp_ms = record.post.published_at.unwrap_or(record.post.created_at);
|
let Some(timestamp) = Local.timestamp_millis_opt(record.post.created_at).single() else {
|
||||||
let Some(timestamp) = Utc.timestamp_millis_opt(timestamp_ms).single() else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let key = format!(
|
let key = format!(
|
||||||
@@ -1091,13 +1121,8 @@ fn build_day_blocks(
|
|||||||
}));
|
}));
|
||||||
current_posts = Vec::new();
|
current_posts = Vec::new();
|
||||||
}
|
}
|
||||||
|
current_label = key.clone();
|
||||||
current_key = key;
|
current_key = key;
|
||||||
current_label = format!(
|
|
||||||
"{:02}.{:02}.{:04}",
|
|
||||||
timestamp.day(),
|
|
||||||
timestamp.month(),
|
|
||||||
timestamp.year()
|
|
||||||
);
|
|
||||||
let show_title = should_show_list_title(&record.post, category_settings);
|
let show_title = should_show_list_title(&record.post, category_settings);
|
||||||
current_posts.push(json!({
|
current_posts.push(json!({
|
||||||
"id": record.post.id,
|
"id": record.post.id,
|
||||||
@@ -1631,8 +1656,7 @@ fn starter_partials() -> HashMap<String, String> {
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"partials/menu-items".to_string(),
|
"partials/menu-items".to_string(),
|
||||||
"{% for item in items %}<a href=\"{{ item.href }}\">{{ item.title }}</a>{% endfor %}"
|
STARTER_MENU_ITEMS_PARTIAL.to_string(),
|
||||||
.to_string(),
|
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -1644,8 +1668,8 @@ fn pico_stylesheet_href(metadata: &ProjectMetadata) -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn calendar_initial_parts(post: &Post) -> (i32, u32) {
|
fn calendar_initial_parts(post: &Post) -> (i32, u32) {
|
||||||
let timestamp_ms = post.published_at.unwrap_or(post.created_at);
|
Local
|
||||||
Utc.timestamp_millis_opt(timestamp_ms)
|
.timestamp_millis_opt(post.created_at)
|
||||||
.single()
|
.single()
|
||||||
.map(|timestamp| (timestamp.year(), timestamp.month()))
|
.map(|timestamp| (timestamp.year(), timestamp.month()))
|
||||||
.unwrap_or((1970, 1))
|
.unwrap_or((1970, 1))
|
||||||
@@ -1670,6 +1694,22 @@ mod menu_tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::engine::menu::{MenuItem, MenuItemKind};
|
use crate::engine::menu::{MenuItem, MenuItemKind};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn starter_menu_partial_keeps_nested_submenus_and_calendar_navigation() {
|
||||||
|
let partials = starter_partials();
|
||||||
|
let menu_items = partials.get("partials/menu-items").unwrap();
|
||||||
|
|
||||||
|
assert!(menu_items.contains("blog-menu-submenu"));
|
||||||
|
assert!(menu_items.contains("data-blog-calendar-toggle"));
|
||||||
|
assert!(menu_items.contains("data-blog-calendar-root"));
|
||||||
|
assert!(
|
||||||
|
partials
|
||||||
|
.get("partials/language-switcher")
|
||||||
|
.unwrap()
|
||||||
|
.contains("data-search-no-results")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn preview_request_paths_select_only_the_matching_generated_page() {
|
fn preview_request_paths_select_only_the_matching_generated_page() {
|
||||||
assert_eq!(preview_relative_path("/"), "index.html");
|
assert_eq!(preview_relative_path("/"), "index.html");
|
||||||
|
|||||||
@@ -247,3 +247,26 @@ fn relationships_reference_existing_entities() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generated_site_golden_tracks_current_bds2_rendering() {
|
||||||
|
let directory = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../../fixtures/golden-generated-sites/rfc1437-sample");
|
||||||
|
let index = fs::read_to_string(directory.join("index.html")).unwrap();
|
||||||
|
let rss = fs::read_to_string(directory.join("rss.xml")).unwrap();
|
||||||
|
let atom = fs::read_to_string(directory.join("atom.xml")).unwrap();
|
||||||
|
let sitemap = fs::read_to_string(directory.join("sitemap.xml")).unwrap();
|
||||||
|
let calendar: serde_json::Value =
|
||||||
|
serde_json::from_str(&fs::read_to_string(directory.join("calendar.json")).unwrap())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(index.contains("class=\"blog-menu-submenu\""));
|
||||||
|
assert!(index.contains("data-search-no-results=\"Keine Ergebnisse gefunden\""));
|
||||||
|
assert!(index.contains("<span>2026-07-17</span>"));
|
||||||
|
assert!(rss.starts_with("<rss><channel><title>rfc1437 (de)</title>"));
|
||||||
|
assert!(atom.starts_with("<feed><title>rfc1437 (de)</title>"));
|
||||||
|
assert!(sitemap.contains("<loc>https://www.rfc1437.de/</loc>"));
|
||||||
|
assert!(sitemap.contains("hreflang=\"en\" href=\"https://www.rfc1437.de/en/\""));
|
||||||
|
assert!(sitemap.contains("<loc>https://www.rfc1437.de/bilderarchiv-2004/</loc>"));
|
||||||
|
assert_eq!(calendar["days"]["2026-07-17"], 3);
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ use bds_core::engine::generation::{
|
|||||||
use bds_core::engine::meta::write_category_meta_json;
|
use bds_core::engine::meta::write_category_meta_json;
|
||||||
use bds_core::engine::validate_site::validate_site;
|
use bds_core::engine::validate_site::validate_site;
|
||||||
use bds_core::model::{
|
use bds_core::model::{
|
||||||
CategorySettings, Post, PostStatus, Project, ProjectMetadata, Template, TemplateKind,
|
CategorySettings, Post, PostStatus, PostTranslation, Project, ProjectMetadata, Template,
|
||||||
TemplateStatus,
|
TemplateKind, TemplateStatus,
|
||||||
};
|
};
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ fn generation_engine_writes_core_and_single_post_artifacts() {
|
|||||||
assert!(report.written_paths.contains(&"index.html".to_string()));
|
assert!(report.written_paths.contains(&"index.html".to_string()));
|
||||||
assert!(report.written_paths.contains(&"calendar.json".to_string()));
|
assert!(report.written_paths.contains(&"calendar.json".to_string()));
|
||||||
assert!(report.written_paths.contains(&"rss.xml".to_string()));
|
assert!(report.written_paths.contains(&"rss.xml".to_string()));
|
||||||
assert!(report.written_paths.contains(&"feed.xml".to_string()));
|
assert!(!report.written_paths.contains(&"feed.xml".to_string()));
|
||||||
assert!(report.written_paths.contains(&"atom.xml".to_string()));
|
assert!(report.written_paths.contains(&"atom.xml".to_string()));
|
||||||
assert!(report.written_paths.contains(&"sitemap.xml".to_string()));
|
assert!(report.written_paths.contains(&"sitemap.xml".to_string()));
|
||||||
assert!(report.written_paths.contains(&"404.html".to_string()));
|
assert!(report.written_paths.contains(&"404.html".to_string()));
|
||||||
@@ -209,7 +209,7 @@ fn generation_engine_writes_core_and_single_post_artifacts() {
|
|||||||
|
|
||||||
assert!(dir.path().join("index.html").exists());
|
assert!(dir.path().join("index.html").exists());
|
||||||
assert!(dir.path().join("rss.xml").exists());
|
assert!(dir.path().join("rss.xml").exists());
|
||||||
assert!(dir.path().join("feed.xml").exists());
|
assert!(!dir.path().join("feed.xml").exists());
|
||||||
assert!(dir.path().join("atom.xml").exists());
|
assert!(dir.path().join("atom.xml").exists());
|
||||||
assert!(dir.path().join("sitemap.xml").exists());
|
assert!(dir.path().join("sitemap.xml").exists());
|
||||||
assert!(dir.path().join("assets/pico.min.css").exists());
|
assert!(dir.path().join("assets/pico.min.css").exists());
|
||||||
@@ -217,14 +217,188 @@ fn generation_engine_writes_core_and_single_post_artifacts() {
|
|||||||
assert!(dir.path().join("2024/03/09/hello/index.html").exists());
|
assert!(dir.path().join("2024/03/09/hello/index.html").exists());
|
||||||
|
|
||||||
let rss = std::fs::read_to_string(dir.path().join("rss.xml")).unwrap();
|
let rss = std::fs::read_to_string(dir.path().join("rss.xml")).unwrap();
|
||||||
assert!(rss.contains("<rss version=\"2.0\""));
|
assert_eq!(
|
||||||
assert!(rss.contains("https://example.com/2024/03/09/hello"));
|
rss,
|
||||||
|
"<rss><channel><title>Blog (en)</title><item><title>next</title><link>https://example.com/2024/03/10/next/</link></item><item><title>hello</title><link>https://example.com/2024/03/09/hello/</link></item></channel></rss>"
|
||||||
|
);
|
||||||
|
let atom = std::fs::read_to_string(dir.path().join("atom.xml")).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
atom,
|
||||||
|
"<feed><title>Blog (en)</title><entry><title>next</title><id>https://example.com/2024/03/10/next/</id></entry><entry><title>hello</title><id>https://example.com/2024/03/09/hello/</id></entry></feed>"
|
||||||
|
);
|
||||||
|
|
||||||
let sitemap = std::fs::read_to_string(dir.path().join("sitemap.xml")).unwrap();
|
let sitemap = std::fs::read_to_string(dir.path().join("sitemap.xml")).unwrap();
|
||||||
assert!(sitemap.contains("https://example.com/2024/03/09/hello"));
|
assert!(sitemap.contains("https://example.com/2024/03/09/hello"));
|
||||||
assert!(sitemap.contains("https://example.com/category/article"));
|
assert!(sitemap.contains("https://example.com/category/article"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mixed_source_languages_use_the_main_language_at_canonical_urls() {
|
||||||
|
let (db, dir) = setup();
|
||||||
|
let mut metadata = make_metadata();
|
||||||
|
metadata.main_language = Some("de".into());
|
||||||
|
metadata.blog_languages = vec!["de".into(), "en".into()];
|
||||||
|
bds_core::engine::meta::write_project_json(dir.path(), &metadata).unwrap();
|
||||||
|
insert_template(
|
||||||
|
db.conn(),
|
||||||
|
&Template {
|
||||||
|
id: "template-post".into(),
|
||||||
|
project_id: "p1".into(),
|
||||||
|
slug: "post".into(),
|
||||||
|
title: "Post".into(),
|
||||||
|
kind: TemplateKind::Post,
|
||||||
|
enabled: true,
|
||||||
|
version: 1,
|
||||||
|
file_path: String::new(),
|
||||||
|
status: TemplateStatus::Published,
|
||||||
|
content: Some("ID={{ post.id }} {{ post.content }}".into()),
|
||||||
|
created_at: 1,
|
||||||
|
updated_at: 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut english_source = make_post("english-source", 1_710_000_000_000);
|
||||||
|
english_source.language = Some("en".into());
|
||||||
|
write_published_snapshot(&dir, &mut english_source, "English source body");
|
||||||
|
bds_core::db::queries::post::insert_post(db.conn(), &english_source).unwrap();
|
||||||
|
let german_translation = PostTranslation {
|
||||||
|
id: "translation-english-de".into(),
|
||||||
|
project_id: "p1".into(),
|
||||||
|
translation_for: english_source.id.clone(),
|
||||||
|
language: "de".into(),
|
||||||
|
title: "Deutsche Übersetzung".into(),
|
||||||
|
excerpt: None,
|
||||||
|
content: None,
|
||||||
|
status: PostStatus::Published,
|
||||||
|
file_path: "posts/english-source.de.md".into(),
|
||||||
|
checksum: None,
|
||||||
|
created_at: english_source.created_at,
|
||||||
|
updated_at: english_source.updated_at,
|
||||||
|
published_at: english_source.published_at,
|
||||||
|
};
|
||||||
|
bds_core::db::queries::post_translation::insert_post_translation(
|
||||||
|
db.conn(),
|
||||||
|
&german_translation,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
dir.path().join(&german_translation.file_path),
|
||||||
|
bds_core::util::frontmatter::write_translation_file(
|
||||||
|
&german_translation,
|
||||||
|
"Deutscher Übersetzungstext",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut german_without_translation = make_post("german-fallback", 1_710_172_800_000);
|
||||||
|
german_without_translation.language = Some("de".into());
|
||||||
|
german_without_translation.title = "Deutscher Fallback".into();
|
||||||
|
|
||||||
|
let mut german_source = make_post("german-source", 1_710_086_400_000);
|
||||||
|
german_source.language = Some("de".into());
|
||||||
|
german_source.title = "Deutsche Quelle".into();
|
||||||
|
write_published_snapshot(&dir, &mut german_source, "Deutscher Quelltext");
|
||||||
|
bds_core::db::queries::post::insert_post(db.conn(), &german_source).unwrap();
|
||||||
|
let english_translation = PostTranslation {
|
||||||
|
id: "translation-german-en".into(),
|
||||||
|
project_id: "p1".into(),
|
||||||
|
translation_for: german_source.id.clone(),
|
||||||
|
language: "en".into(),
|
||||||
|
title: "English translation".into(),
|
||||||
|
excerpt: None,
|
||||||
|
content: None,
|
||||||
|
status: PostStatus::Published,
|
||||||
|
file_path: "posts/german-source.en.md".into(),
|
||||||
|
checksum: None,
|
||||||
|
created_at: german_source.created_at,
|
||||||
|
updated_at: german_source.updated_at,
|
||||||
|
published_at: german_source.published_at,
|
||||||
|
};
|
||||||
|
bds_core::db::queries::post_translation::insert_post_translation(
|
||||||
|
db.conn(),
|
||||||
|
&english_translation,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
dir.path().join(&english_translation.file_path),
|
||||||
|
bds_core::util::frontmatter::write_translation_file(
|
||||||
|
&english_translation,
|
||||||
|
"English translation body",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut german_private = make_post("german-private", 1_710_259_200_000);
|
||||||
|
german_private.language = Some("de".into());
|
||||||
|
german_private.do_not_translate = true;
|
||||||
|
|
||||||
|
let output = dir.path().join("html");
|
||||||
|
let sources = vec![
|
||||||
|
PublishedPostSource {
|
||||||
|
post: english_source,
|
||||||
|
body_markdown: "English source body".into(),
|
||||||
|
},
|
||||||
|
PublishedPostSource {
|
||||||
|
post: german_source,
|
||||||
|
body_markdown: "Deutscher Quelltext".into(),
|
||||||
|
},
|
||||||
|
PublishedPostSource {
|
||||||
|
post: german_without_translation,
|
||||||
|
body_markdown: "Unübersetzter Quelltext".into(),
|
||||||
|
},
|
||||||
|
PublishedPostSource {
|
||||||
|
post: german_private,
|
||||||
|
body_markdown: "Nur auf Deutsch".into(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
generate_starter_site(db.conn(), &output, "p1", &metadata, &sources, "de").unwrap();
|
||||||
|
|
||||||
|
let canonical_english_source =
|
||||||
|
std::fs::read_to_string(output.join("2024/03/09/english-source/index.html")).unwrap();
|
||||||
|
let localized_english_source =
|
||||||
|
std::fs::read_to_string(output.join("en/2024/03/09/english-source/index.html")).unwrap();
|
||||||
|
let canonical_german_source =
|
||||||
|
std::fs::read_to_string(output.join("2024/03/10/german-source/index.html")).unwrap();
|
||||||
|
let localized_german_source =
|
||||||
|
std::fs::read_to_string(output.join("en/2024/03/10/german-source/index.html")).unwrap();
|
||||||
|
let localized_fallback =
|
||||||
|
std::fs::read_to_string(output.join("en/2024/03/11/german-fallback/index.html")).unwrap();
|
||||||
|
|
||||||
|
assert!(canonical_english_source.contains("Deutscher Übersetzungstext"));
|
||||||
|
assert!(canonical_english_source.contains("ID=translation-english-de"));
|
||||||
|
assert!(localized_english_source.contains("English source body"));
|
||||||
|
assert!(localized_english_source.contains("ID=post-english-source"));
|
||||||
|
assert!(canonical_german_source.contains("Deutscher Quelltext"));
|
||||||
|
assert!(localized_german_source.contains("English translation body"));
|
||||||
|
assert!(localized_fallback.contains("Unübersetzter Quelltext"));
|
||||||
|
assert!(
|
||||||
|
output
|
||||||
|
.join("2024/03/12/german-private/index.html")
|
||||||
|
.is_file()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!output
|
||||||
|
.join("en/2024/03/12/german-private/index.html")
|
||||||
|
.exists()
|
||||||
|
);
|
||||||
|
|
||||||
|
let sitemap = std::fs::read_to_string(output.join("sitemap.xml")).unwrap();
|
||||||
|
assert!(sitemap.contains("https://example.com/2024/03/09/english-source"));
|
||||||
|
assert!(sitemap.contains("https://example.com/en/2024/03/09/english-source"));
|
||||||
|
assert!(sitemap.contains("https://example.com/2024/03/10/german-source"));
|
||||||
|
assert!(sitemap.contains("https://example.com/en/2024/03/10/german-source"));
|
||||||
|
assert!(sitemap.contains("https://example.com/2024/03/12/german-private"));
|
||||||
|
assert!(!sitemap.contains("https://example.com/en/2024/03/12/german-private"));
|
||||||
|
let main_rss = std::fs::read_to_string(output.join("rss.xml")).unwrap();
|
||||||
|
let english_rss = std::fs::read_to_string(output.join("en/rss.xml")).unwrap();
|
||||||
|
assert!(main_rss.contains("<title>english-source</title>"));
|
||||||
|
assert!(!main_rss.contains("<title>Deutsche Übersetzung</title>"));
|
||||||
|
assert!(english_rss.contains("<title>english-source</title>"));
|
||||||
|
assert!(english_rss.contains("<title>English translation</title>"));
|
||||||
|
assert!(!english_rss.contains("<title>Deutscher Fallback</title>"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn section_generation_reports_its_urls_and_defers_pagefind() {
|
fn section_generation_reports_its_urls_and_defers_pagefind() {
|
||||||
let (db, dir) = setup();
|
let (db, dir) = setup();
|
||||||
@@ -345,7 +519,8 @@ fn multilingual_generation_writes_language_aware_atom_and_sitemap_routes() {
|
|||||||
|
|
||||||
assert!(report.written_paths.contains(&"en/atom.xml".to_string()));
|
assert!(report.written_paths.contains(&"en/atom.xml".to_string()));
|
||||||
assert!(report.written_paths.contains(&"en/rss.xml".to_string()));
|
assert!(report.written_paths.contains(&"en/rss.xml".to_string()));
|
||||||
assert!(report.written_paths.contains(&"en/sitemap.xml".to_string()));
|
assert!(!report.written_paths.contains(&"en/sitemap.xml".to_string()));
|
||||||
|
assert!(!report.written_paths.contains(&"feed.xml".to_string()));
|
||||||
assert!(report.written_paths.contains(&"en/404.html".to_string()));
|
assert!(report.written_paths.contains(&"en/404.html".to_string()));
|
||||||
assert!(
|
assert!(
|
||||||
report
|
report
|
||||||
@@ -354,19 +529,79 @@ fn multilingual_generation_writes_language_aware_atom_and_sitemap_routes() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let atom = std::fs::read_to_string(dir.path().join("en/atom.xml")).unwrap();
|
let atom = std::fs::read_to_string(dir.path().join("en/atom.xml")).unwrap();
|
||||||
assert!(
|
assert!(atom.starts_with("<feed><title>Blog (en)</title>"));
|
||||||
atom.contains("<link href=\"https://example.com/en/\" rel=\"alternate\" />")
|
|
||||||
|| atom.contains("<link href=\"https://example.com/en\" rel=\"alternate\" />")
|
|
||||||
);
|
|
||||||
assert!(atom.contains("<link href=\"https://example.com/en/atom.xml\" rel=\"self\" />"));
|
|
||||||
|
|
||||||
let sitemap = std::fs::read_to_string(dir.path().join("sitemap.xml")).unwrap();
|
let sitemap = std::fs::read_to_string(dir.path().join("sitemap.xml")).unwrap();
|
||||||
assert!(sitemap.contains("hreflang=\"de\" href=\"https://example.com/\""));
|
assert!(sitemap.contains("hreflang=\"de\" href=\"https://example.com/\""));
|
||||||
assert!(sitemap.contains("hreflang=\"en\" href=\"https://example.com/en\""));
|
assert!(sitemap.contains("hreflang=\"en\" href=\"https://example.com/en/\""));
|
||||||
assert!(sitemap.contains("hreflang=\"x-default\" href=\"https://example.com/\""));
|
assert!(sitemap.contains("hreflang=\"x-default\" href=\"https://example.com/\""));
|
||||||
assert!(sitemap.contains("https://example.com/category/article"));
|
assert!(sitemap.contains("https://example.com/category/article"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn page_category_posts_also_generate_flat_routes() {
|
||||||
|
let (db, dir) = setup();
|
||||||
|
let metadata = make_metadata();
|
||||||
|
let mut page = make_post("about", 1_710_000_000_000);
|
||||||
|
page.categories = vec!["page".into()];
|
||||||
|
let posts = vec![PublishedPostSource {
|
||||||
|
post: page,
|
||||||
|
body_markdown: "About this site".into(),
|
||||||
|
}];
|
||||||
|
|
||||||
|
let report =
|
||||||
|
generate_starter_site(db.conn(), dir.path(), "p1", &metadata, &posts, "en").unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
report
|
||||||
|
.written_paths
|
||||||
|
.contains(&"about/index.html".to_string())
|
||||||
|
);
|
||||||
|
assert!(dir.path().join("about/index.html").is_file());
|
||||||
|
let sitemap = std::fs::read_to_string(dir.path().join("sitemap.xml")).unwrap();
|
||||||
|
assert!(sitemap.contains("<loc>https://example.com/about/</loc>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn page_aliases_belong_to_core_while_dated_posts_belong_to_single() {
|
||||||
|
let (db, dir) = setup();
|
||||||
|
let metadata = make_metadata();
|
||||||
|
let mut page = make_post("about", 1_710_000_000_000);
|
||||||
|
page.categories = vec!["page".into()];
|
||||||
|
let posts = vec![PublishedPostSource {
|
||||||
|
post: page,
|
||||||
|
body_markdown: "About this site".into(),
|
||||||
|
}];
|
||||||
|
|
||||||
|
let single = render_site_section_with_progress(
|
||||||
|
db.conn(),
|
||||||
|
dir.path(),
|
||||||
|
"p1",
|
||||||
|
&metadata,
|
||||||
|
&posts,
|
||||||
|
GenerationSection::Single,
|
||||||
|
|_current, _total, _url| {},
|
||||||
|
|| false,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(single.written_paths, vec!["2024/03/09/about/index.html"]);
|
||||||
|
assert!(!dir.path().join("about/index.html").exists());
|
||||||
|
|
||||||
|
let core = render_site_section_with_progress(
|
||||||
|
db.conn(),
|
||||||
|
dir.path(),
|
||||||
|
"p1",
|
||||||
|
&metadata,
|
||||||
|
&posts,
|
||||||
|
GenerationSection::Core,
|
||||||
|
|_current, _total, _url| {},
|
||||||
|
|| false,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(core.written_paths.contains(&"about/index.html".to_string()));
|
||||||
|
assert!(dir.path().join("about/index.html").is_file());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generation_respects_category_list_settings_and_writes_bundled_images() {
|
fn generation_respects_category_list_settings_and_writes_bundled_images() {
|
||||||
let (db, dir) = setup();
|
let (db, dir) = setup();
|
||||||
@@ -457,12 +692,12 @@ fn generation_respects_category_list_settings_and_writes_bundled_images() {
|
|||||||
std::fs::read_to_string(dir.path().join("category/featured/index.html")).unwrap();
|
std::fs::read_to_string(dir.path().join("category/featured/index.html")).unwrap();
|
||||||
assert!(featured_html.contains("FEATURED:[Featured Post|false]"));
|
assert!(featured_html.contains("FEATURED:[Featured Post|false]"));
|
||||||
|
|
||||||
let feed = std::fs::read_to_string(dir.path().join("feed.xml")).unwrap();
|
let rss = std::fs::read_to_string(dir.path().join("rss.xml")).unwrap();
|
||||||
assert!(!feed.contains("hidden-post"));
|
assert!(rss.contains("hidden-post"));
|
||||||
assert!(feed.contains("featured-post"));
|
assert!(rss.contains("featured-post"));
|
||||||
|
|
||||||
let calendar = std::fs::read_to_string(dir.path().join("calendar.json")).unwrap();
|
let calendar = std::fs::read_to_string(dir.path().join("calendar.json")).unwrap();
|
||||||
assert!(!calendar.contains("2024-03-09"));
|
assert!(calendar.contains("2024-03-09"));
|
||||||
assert!(calendar.contains("2024-03-10"));
|
assert!(calendar.contains("2024-03-10"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,7 +719,7 @@ fn generation_engine_skips_unchanged_outputs_on_second_run() {
|
|||||||
assert!(second.skipped_paths.contains(&"index.html".to_string()));
|
assert!(second.skipped_paths.contains(&"index.html".to_string()));
|
||||||
assert!(second.skipped_paths.contains(&"calendar.json".to_string()));
|
assert!(second.skipped_paths.contains(&"calendar.json".to_string()));
|
||||||
assert!(second.skipped_paths.contains(&"rss.xml".to_string()));
|
assert!(second.skipped_paths.contains(&"rss.xml".to_string()));
|
||||||
assert!(second.skipped_paths.contains(&"feed.xml".to_string()));
|
assert!(!second.skipped_paths.contains(&"feed.xml".to_string()));
|
||||||
assert!(
|
assert!(
|
||||||
second
|
second
|
||||||
.skipped_paths
|
.skipped_paths
|
||||||
@@ -506,12 +741,12 @@ fn site_validation_detects_stale_and_missing_outputs() {
|
|||||||
|
|
||||||
generate_starter_site(db.conn(), dir.path(), "p1", &metadata, &posts, "en").unwrap();
|
generate_starter_site(db.conn(), dir.path(), "p1", &metadata, &posts, "en").unwrap();
|
||||||
std::fs::write(dir.path().join("index.html"), "tampered").unwrap();
|
std::fs::write(dir.path().join("index.html"), "tampered").unwrap();
|
||||||
std::fs::remove_file(dir.path().join("feed.xml")).unwrap();
|
std::fs::remove_file(dir.path().join("atom.xml")).unwrap();
|
||||||
|
|
||||||
let report = validate_site(db.conn(), dir.path(), "p1").unwrap();
|
let report = validate_site(db.conn(), dir.path(), "p1").unwrap();
|
||||||
|
|
||||||
assert!(report.stale_pages.contains(&"index.html".to_string()));
|
assert!(report.stale_pages.contains(&"index.html".to_string()));
|
||||||
assert!(report.missing_pages.contains(&"feed.xml".to_string()));
|
assert!(report.missing_pages.contains(&"atom.xml".to_string()));
|
||||||
assert!(report.extra_pages.is_empty());
|
assert!(report.extra_pages.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,7 +764,7 @@ fn apply_validation_repairs_core_section_outputs() {
|
|||||||
|
|
||||||
generate_starter_site(db.conn(), dir.path(), "p1", &metadata, &posts, "en").unwrap();
|
generate_starter_site(db.conn(), dir.path(), "p1", &metadata, &posts, "en").unwrap();
|
||||||
std::fs::write(dir.path().join("index.html"), "tampered").unwrap();
|
std::fs::write(dir.path().join("index.html"), "tampered").unwrap();
|
||||||
std::fs::remove_file(dir.path().join("feed.xml")).unwrap();
|
std::fs::remove_file(dir.path().join("atom.xml")).unwrap();
|
||||||
|
|
||||||
let report = validate_site(db.conn(), dir.path(), "p1").unwrap();
|
let report = validate_site(db.conn(), dir.path(), "p1").unwrap();
|
||||||
let sections = sections_from_validation_report(&report);
|
let sections = sections_from_validation_report(&report);
|
||||||
|
|||||||
@@ -111,20 +111,20 @@ fn generated_write_rewrites_stale_file_even_when_db_hash_matches() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn core_generation_paths_include_language_prefixed_variants() {
|
fn core_generation_paths_match_bds2_outputs() {
|
||||||
let paths = build_core_generation_paths("en", &["en".into(), "de".into(), "fr".into()]);
|
let paths = build_core_generation_paths("en", &["en".into(), "de".into(), "fr".into()]);
|
||||||
assert!(paths.contains(&"index.html".to_string()));
|
assert!(paths.contains(&"index.html".to_string()));
|
||||||
assert!(paths.contains(&"404.html".to_string()));
|
assert!(paths.contains(&"404.html".to_string()));
|
||||||
assert!(paths.contains(&"sitemap.xml".to_string()));
|
assert!(paths.contains(&"sitemap.xml".to_string()));
|
||||||
assert!(paths.contains(&"rss.xml".to_string()));
|
assert!(paths.contains(&"rss.xml".to_string()));
|
||||||
assert!(paths.contains(&"feed.xml".to_string()));
|
assert!(!paths.contains(&"feed.xml".to_string()));
|
||||||
assert!(paths.contains(&"atom.xml".to_string()));
|
assert!(paths.contains(&"atom.xml".to_string()));
|
||||||
assert!(paths.contains(&"calendar.json".to_string()));
|
assert!(paths.contains(&"calendar.json".to_string()));
|
||||||
assert!(paths.contains(&"de/index.html".to_string()));
|
assert!(paths.contains(&"de/index.html".to_string()));
|
||||||
assert!(paths.contains(&"de/404.html".to_string()));
|
assert!(paths.contains(&"de/404.html".to_string()));
|
||||||
assert!(paths.contains(&"de/sitemap.xml".to_string()));
|
assert!(!paths.contains(&"de/sitemap.xml".to_string()));
|
||||||
assert!(paths.contains(&"de/rss.xml".to_string()));
|
assert!(paths.contains(&"de/rss.xml".to_string()));
|
||||||
assert!(paths.contains(&"de/feed.xml".to_string()));
|
assert!(!paths.contains(&"de/feed.xml".to_string()));
|
||||||
assert!(paths.contains(&"de/atom.xml".to_string()));
|
assert!(paths.contains(&"de/atom.xml".to_string()));
|
||||||
assert!(paths.contains(&"fr/index.html".to_string()));
|
assert!(paths.contains(&"fr/index.html".to_string()));
|
||||||
}
|
}
|
||||||
@@ -138,6 +138,7 @@ fn calendar_json_groups_posts_by_year_month_day() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let json = build_calendar_json(&posts).unwrap();
|
let json = build_calendar_json(&posts).unwrap();
|
||||||
|
assert!(!json.contains('\n'));
|
||||||
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
|
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
assert_eq!(parsed["years"]["2024"], 3);
|
assert_eq!(parsed["years"]["2024"], 3);
|
||||||
@@ -146,3 +147,15 @@ fn calendar_json_groups_posts_by_year_month_day() {
|
|||||||
assert_eq!(parsed["days"]["2024-03-09"], 2);
|
assert_eq!(parsed["days"]["2024-03-09"], 2);
|
||||||
assert_eq!(parsed["days"]["2024-04-09"], 1);
|
assert_eq!(parsed["days"]["2024-04-09"], 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn calendar_json_uses_created_at_instead_of_published_at() {
|
||||||
|
let mut post = make_post("a", 1_710_000_000_000);
|
||||||
|
post.published_at = Some(1_712_678_400_000);
|
||||||
|
|
||||||
|
let json = build_calendar_json(&[post]).unwrap();
|
||||||
|
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(parsed["days"]["2024-03-09"], 1);
|
||||||
|
assert!(parsed["days"].get("2024-04-09").is_none());
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,19 +3,23 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Content-type: matter-transport/sentient-life-form</title>
|
<title>Esmeralda</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/pico.slate.min.css" />
|
<link rel="stylesheet" href="/assets/pico.slate.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/lightbox.min.css" />
|
<link rel="stylesheet" href="/assets/lightbox.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/highlight.min.css" />
|
<link rel="stylesheet" href="/assets/highlight.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/vanilla-calendar.min.css" />
|
<link rel="stylesheet" href="/assets/vanilla-calendar.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/bds.css" />
|
<link rel="stylesheet" href="/assets/bds.css" />
|
||||||
|
|
||||||
<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss.xml" />
|
<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss.xml" />
|
||||||
<link rel="alternate" type="application/atom+xml" title="Atom" href="/atom.xml" />
|
<link rel="alternate" type="application/atom+xml" title="Atom" href="/atom.xml" />
|
||||||
|
|
||||||
<link rel="alternate" hreflang="de" href="/2005/11/13/esmeralda" />
|
<link rel="alternate" hreflang="es" href="/2005/11/13/esmeralda/" />
|
||||||
|
|
||||||
|
<link rel="alternate" hreflang="de" href="/2005/11/13/esmeralda/" />
|
||||||
|
|
||||||
|
<link rel="alternate" hreflang="en" href="/en/2005/11/13/esmeralda/" />
|
||||||
|
|
||||||
<script defer src="/assets/highlight.min.js"></script>
|
<script defer src="/assets/highlight.min.js"></script>
|
||||||
<script defer src="/assets/code-enhancements.js"></script>
|
<script defer src="/assets/code-enhancements.js"></script>
|
||||||
<script defer src="/assets/d3.layout.cloud.js"></script>
|
<script defer src="/assets/d3.layout.cloud.js"></script>
|
||||||
@@ -27,20 +31,19 @@
|
|||||||
<link rel="stylesheet" href="/pagefind/pagefind-ui.css" />
|
<link rel="stylesheet" href="/pagefind/pagefind-ui.css" />
|
||||||
<script defer src="/pagefind/pagefind-ui.js"></script>
|
<script defer src="/pagefind/pagefind-ui.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
|
|
||||||
<nav class="language-switcher" aria-label="Sprache">
|
<nav class="language-switcher" aria-label="Sprache">
|
||||||
|
|
||||||
|
|
||||||
<span class="language-switcher-badge language-switcher-badge-current" aria-current="true" title="de">🇩🇪</span>
|
<span class="language-switcher-badge language-switcher-badge-current" aria-current="true" title="de">🇩🇪</span>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a class="language-switcher-badge" href="/en" data-lang-prefix="/en" title="en">🇬🇧</a>
|
<a class="language-switcher-badge" href="/en" data-lang-prefix="/en" title="en">🇬🇧</a>
|
||||||
|
|
||||||
|
|
||||||
<div class="blog-search-widget" aria-label="Seitensuche">
|
<div class="blog-search-widget" aria-label="Seitensuche">
|
||||||
<button type="button" class="blog-search-toggle" data-blog-search-toggle aria-label="Seitensuche">
|
<button type="button" class="blog-search-toggle" data-blog-search-toggle aria-label="Seitensuche">
|
||||||
<svg aria-hidden="true" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false">
|
<svg aria-hidden="true" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false">
|
||||||
@@ -49,7 +52,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="blog-search-panel" data-blog-search-panel hidden>
|
<div class="blog-search-panel" data-blog-search-panel hidden>
|
||||||
<div id="blog-search" data-blog-search-root data-search-placeholder="Suchen..."></div>
|
<div id="blog-search" data-blog-search-root data-search-placeholder="Suchen..." data-search-no-results="Keine Ergebnisse gefunden"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -64,167 +67,167 @@
|
|||||||
|
|
||||||
<h1>Esmeralda</h1>
|
<h1>Esmeralda</h1>
|
||||||
<nav class="blog-menu">
|
<nav class="blog-menu">
|
||||||
|
|
||||||
<ul class="blog-menu-list">
|
<ul class="blog-menu-list">
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/">Home</a>
|
<a class="blog-menu-link" href="/">Home</a>
|
||||||
|
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="blog-menu-item blog-menu-item-with-children">
|
<li class="blog-menu-item blog-menu-item-with-children">
|
||||||
|
|
||||||
<span class="blog-menu-link">Bilderarchiv</span>
|
<span class="blog-menu-link">Bilderarchiv</span>
|
||||||
|
|
||||||
|
|
||||||
<div class="blog-menu-submenu">
|
<div class="blog-menu-submenu">
|
||||||
<ul class="blog-menu-list">
|
<ul class="blog-menu-list">
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2022/">Bilderarchiv 2022</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2016/">Bilderarchiv 2016</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2015/">Bilderarchiv 2015</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2014/">Bilderarchiv 2014</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2013/">Bilderarchiv 2013</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2012/">Bilderarchiv 2012</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2011/">Bilderarchiv 2011</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2010/">Bilderarchiv 2010</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2009/">Bilderarchiv 2009</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2008/">Bilderarchiv 2008</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2005/">Bilderarchiv 2005</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2004/">Bilderarchiv 2004</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2003/">Bilderarchiv 2003</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2022/">Bilderarchiv 2022</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2016/">Bilderarchiv 2016</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2015/">Bilderarchiv 2015</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2014/">Bilderarchiv 2014</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2013/">Bilderarchiv 2013</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2012/">Bilderarchiv 2012</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2011/">Bilderarchiv 2011</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2010/">Bilderarchiv 2010</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2009/">Bilderarchiv 2009</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2008/">Bilderarchiv 2008</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2005/">Bilderarchiv 2005</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2004/">Bilderarchiv 2004</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2003/">Bilderarchiv 2003</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/category/picture/">Fotoblog</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/tag-cloud/">Tag Cloud</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/category/spielelog/">Spieleblog</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/category/kochbuch/">Kochbuch</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/wiki/">Wiki</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/impressum/">Impressum</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/category/picture/">Fotoblog</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/tag-cloud/">Tag Cloud</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/category/spielelog/">Spieleblog</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/category/kochbuch/">Kochbuch</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/wiki/">Wiki</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/impressum/">Impressum</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<li class="blog-menu-item blog-menu-calendar">
|
<li class="blog-menu-item blog-menu-calendar">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -252,7 +255,7 @@
|
|||||||
hidden
|
hidden
|
||||||
>
|
>
|
||||||
<header class="blog-calendar-header">
|
<header class="blog-calendar-header">
|
||||||
<strong>Archivkalender</strong>
|
<strong>Archiv</strong>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="blog-calendar-close"
|
class="blog-calendar-close"
|
||||||
@@ -269,41 +272,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
<div class="single-post-taxonomy" aria-label="Taxonomie">
|
<article class="single-post blog-post" data-template="single-post" data-pagefind-body>
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-category" href="/category/picture/">picture</a>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/fotografie/">fotografie</a>
|
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/makro/">makro</a>
|
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/natur/">natur</a>
|
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/spinne/">spinne</a>
|
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/tiere/">tiere</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<article class="single-post" data-template="single-post" data-pagefind-body>
|
|
||||||
<div class="post"><h3><a href="/media/esmeralda/">Esmeralda</a></h3>
|
<div class="post"><h3><a href="/media/esmeralda/">Esmeralda</a></h3>
|
||||||
<p><img src="/media/2005/11/eb0cf9d7-6fbd-4b74-9be3-759d6e16f240.jpg" alt="Esmeralda"></p>
|
<p><img src="/media/2005/11/eb0cf9d7-6fbd-4b74-9be3-759d6e16f240.jpg" alt="Esmeralda" /></p></div>
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,17 +3,21 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Content-type: matter-transport/sentient-life-form</title>
|
<title>cmux — Das Terminal für Multitasking</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/pico.slate.min.css" />
|
<link rel="stylesheet" href="/assets/pico.slate.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/lightbox.min.css" />
|
<link rel="stylesheet" href="/assets/lightbox.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/highlight.min.css" />
|
<link rel="stylesheet" href="/assets/highlight.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/vanilla-calendar.min.css" />
|
<link rel="stylesheet" href="/assets/vanilla-calendar.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/bds.css" />
|
<link rel="stylesheet" href="/assets/bds.css" />
|
||||||
|
|
||||||
<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss.xml" />
|
<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss.xml" />
|
||||||
<link rel="alternate" type="application/atom+xml" title="Atom" href="/atom.xml" />
|
<link rel="alternate" type="application/atom+xml" title="Atom" href="/atom.xml" />
|
||||||
|
|
||||||
|
<link rel="alternate" hreflang="de" href="/2026/03/13/cmux-das-terminal-fur-multitasking/" />
|
||||||
|
|
||||||
|
<link rel="alternate" hreflang="en" href="/en/2026/03/13/cmux-das-terminal-fur-multitasking/" />
|
||||||
|
|
||||||
<script defer src="/assets/highlight.min.js"></script>
|
<script defer src="/assets/highlight.min.js"></script>
|
||||||
<script defer src="/assets/code-enhancements.js"></script>
|
<script defer src="/assets/code-enhancements.js"></script>
|
||||||
<script defer src="/assets/d3.layout.cloud.js"></script>
|
<script defer src="/assets/d3.layout.cloud.js"></script>
|
||||||
@@ -25,20 +29,19 @@
|
|||||||
<link rel="stylesheet" href="/pagefind/pagefind-ui.css" />
|
<link rel="stylesheet" href="/pagefind/pagefind-ui.css" />
|
||||||
<script defer src="/pagefind/pagefind-ui.js"></script>
|
<script defer src="/pagefind/pagefind-ui.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
|
|
||||||
<nav class="language-switcher" aria-label="Sprache">
|
<nav class="language-switcher" aria-label="Sprache">
|
||||||
|
|
||||||
|
|
||||||
<span class="language-switcher-badge language-switcher-badge-current" aria-current="true" title="de">🇩🇪</span>
|
<span class="language-switcher-badge language-switcher-badge-current" aria-current="true" title="de">🇩🇪</span>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a class="language-switcher-badge" href="/en" data-lang-prefix="/en" title="en">🇬🇧</a>
|
<a class="language-switcher-badge" href="/en" data-lang-prefix="/en" title="en">🇬🇧</a>
|
||||||
|
|
||||||
|
|
||||||
<div class="blog-search-widget" aria-label="Seitensuche">
|
<div class="blog-search-widget" aria-label="Seitensuche">
|
||||||
<button type="button" class="blog-search-toggle" data-blog-search-toggle aria-label="Seitensuche">
|
<button type="button" class="blog-search-toggle" data-blog-search-toggle aria-label="Seitensuche">
|
||||||
<svg aria-hidden="true" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false">
|
<svg aria-hidden="true" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false">
|
||||||
@@ -47,7 +50,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="blog-search-panel" data-blog-search-panel hidden>
|
<div class="blog-search-panel" data-blog-search-panel hidden>
|
||||||
<div id="blog-search" data-blog-search-root data-search-placeholder="Suchen..."></div>
|
<div id="blog-search" data-blog-search-root data-search-placeholder="Suchen..." data-search-no-results="Keine Ergebnisse gefunden"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -62,167 +65,167 @@
|
|||||||
|
|
||||||
<h1>cmux — Das Terminal für Multitasking</h1>
|
<h1>cmux — Das Terminal für Multitasking</h1>
|
||||||
<nav class="blog-menu">
|
<nav class="blog-menu">
|
||||||
|
|
||||||
<ul class="blog-menu-list">
|
<ul class="blog-menu-list">
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/">Home</a>
|
<a class="blog-menu-link" href="/">Home</a>
|
||||||
|
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="blog-menu-item blog-menu-item-with-children">
|
<li class="blog-menu-item blog-menu-item-with-children">
|
||||||
|
|
||||||
<span class="blog-menu-link">Bilderarchiv</span>
|
<span class="blog-menu-link">Bilderarchiv</span>
|
||||||
|
|
||||||
|
|
||||||
<div class="blog-menu-submenu">
|
<div class="blog-menu-submenu">
|
||||||
<ul class="blog-menu-list">
|
<ul class="blog-menu-list">
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2022/">Bilderarchiv 2022</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2016/">Bilderarchiv 2016</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2015/">Bilderarchiv 2015</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2014/">Bilderarchiv 2014</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2013/">Bilderarchiv 2013</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2012/">Bilderarchiv 2012</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2011/">Bilderarchiv 2011</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2010/">Bilderarchiv 2010</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2009/">Bilderarchiv 2009</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2008/">Bilderarchiv 2008</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2005/">Bilderarchiv 2005</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2004/">Bilderarchiv 2004</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2003/">Bilderarchiv 2003</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2022/">Bilderarchiv 2022</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2016/">Bilderarchiv 2016</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2015/">Bilderarchiv 2015</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2014/">Bilderarchiv 2014</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2013/">Bilderarchiv 2013</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2012/">Bilderarchiv 2012</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2011/">Bilderarchiv 2011</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2010/">Bilderarchiv 2010</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2009/">Bilderarchiv 2009</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2008/">Bilderarchiv 2008</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2005/">Bilderarchiv 2005</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2004/">Bilderarchiv 2004</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2003/">Bilderarchiv 2003</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/category/picture/">Fotoblog</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/tag-cloud/">Tag Cloud</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/category/spielelog/">Spieleblog</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/category/kochbuch/">Kochbuch</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/wiki/">Wiki</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/impressum/">Impressum</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/category/picture/">Fotoblog</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/tag-cloud/">Tag Cloud</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/category/spielelog/">Spieleblog</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/category/kochbuch/">Kochbuch</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/wiki/">Wiki</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/impressum/">Impressum</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<li class="blog-menu-item blog-menu-calendar">
|
<li class="blog-menu-item blog-menu-calendar">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -250,7 +253,7 @@
|
|||||||
hidden
|
hidden
|
||||||
>
|
>
|
||||||
<header class="blog-calendar-header">
|
<header class="blog-calendar-header">
|
||||||
<strong>Archivkalender</strong>
|
<strong>Archiv</strong>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="blog-calendar-close"
|
class="blog-calendar-close"
|
||||||
@@ -267,41 +270,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
<div class="single-post-taxonomy" aria-label="Taxonomie">
|
<div class="single-post-taxonomy" aria-label="Taxonomie">
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-category" href="/category/aside/">aside</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-category" href="/category/aside/">aside</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/programmierung/">programmierung</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/programmierung/">programmierung</a>
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/mac-os-x/">mac-os-x</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/mac-os-x/">mac-os-x</a>
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/sysadmin/">sysadmin</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/sysadmin/">sysadmin</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<article class="single-post" data-template="single-post" data-pagefind-body>
|
<article class="single-post blog-post" data-template="single-post" data-pagefind-body>
|
||||||
<div class="post"><p><a href="https://www.cmux.dev/de">cmux — Das Terminal für Multitasking</a> ist ein ziemlich geniales Terminal-Programm für die gerade durch Agentic Coding wieder verstärkt kommenden CLI Workflows. Und was mich begeistert: es hat eine saubere Persistierung von offenen Workspaces in denen man dann mehrere Tabs haben kann, so dass ich meine Arbeitsumgebung nicht ständig offen lassen muss. Ich habe immer gerne diverse Verzeichnisse direkt offen, weil ich zwischen mehreren hin und her wechsel wärend der Programmierung, das läuft mit CMUX deutlich besser als mit allen anderen.</p>
|
<div class="post"><p><a href="https://www.cmux.dev/de">cmux — Das Terminal für Multitasking</a> ist ein ziemlich geniales Terminal-Programm für die gerade durch Agentic Coding wieder verstärkt kommenden CLI Workflows. Und was mich begeistert: es hat eine saubere Persistierung von offenen Workspaces in denen man dann mehrere Tabs haben kann, so dass ich meine Arbeitsumgebung nicht ständig offen lassen muss. Ich habe immer gerne diverse Verzeichnisse direkt offen, weil ich zwischen mehreren hin und her wechsel wärend der Programmierung, das läuft mit CMUX deutlich besser als mit allen anderen.</p></div>
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<div class="single-post-backlinks" aria-label="Rückverweise">
|
<div class="single-post-backlinks" aria-label="Rückverweise">
|
||||||
<span class="single-post-backlinks-label">Verlinkt von</span>
|
<span class="single-post-backlinks-label">Verlinkt von</span>
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-backlink-bubble" href="/2026/03/13/ghostty">ghostty</a>
|
<a class="single-post-taxonomy-bubble single-post-backlink-bubble" href="/2026/06/30/supacode/">supacode</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,17 +3,21 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Content-type: matter-transport/sentient-life-form</title>
|
<title>Ghostty</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/pico.slate.min.css" />
|
<link rel="stylesheet" href="/assets/pico.slate.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/lightbox.min.css" />
|
<link rel="stylesheet" href="/assets/lightbox.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/highlight.min.css" />
|
<link rel="stylesheet" href="/assets/highlight.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/vanilla-calendar.min.css" />
|
<link rel="stylesheet" href="/assets/vanilla-calendar.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/bds.css" />
|
<link rel="stylesheet" href="/assets/bds.css" />
|
||||||
|
|
||||||
<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss.xml" />
|
<link rel="alternate" type="application/rss+xml" title="RSS" href="/rss.xml" />
|
||||||
<link rel="alternate" type="application/atom+xml" title="Atom" href="/atom.xml" />
|
<link rel="alternate" type="application/atom+xml" title="Atom" href="/atom.xml" />
|
||||||
|
|
||||||
|
<link rel="alternate" hreflang="de" href="/2026/03/13/ghostty/" />
|
||||||
|
|
||||||
|
<link rel="alternate" hreflang="en" href="/en/2026/03/13/ghostty/" />
|
||||||
|
|
||||||
<script defer src="/assets/highlight.min.js"></script>
|
<script defer src="/assets/highlight.min.js"></script>
|
||||||
<script defer src="/assets/code-enhancements.js"></script>
|
<script defer src="/assets/code-enhancements.js"></script>
|
||||||
<script defer src="/assets/d3.layout.cloud.js"></script>
|
<script defer src="/assets/d3.layout.cloud.js"></script>
|
||||||
@@ -25,20 +29,19 @@
|
|||||||
<link rel="stylesheet" href="/pagefind/pagefind-ui.css" />
|
<link rel="stylesheet" href="/pagefind/pagefind-ui.css" />
|
||||||
<script defer src="/pagefind/pagefind-ui.js"></script>
|
<script defer src="/pagefind/pagefind-ui.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
|
|
||||||
<nav class="language-switcher" aria-label="Sprache">
|
<nav class="language-switcher" aria-label="Sprache">
|
||||||
|
|
||||||
|
|
||||||
<span class="language-switcher-badge language-switcher-badge-current" aria-current="true" title="de">🇩🇪</span>
|
<span class="language-switcher-badge language-switcher-badge-current" aria-current="true" title="de">🇩🇪</span>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a class="language-switcher-badge" href="/en" data-lang-prefix="/en" title="en">🇬🇧</a>
|
<a class="language-switcher-badge" href="/en" data-lang-prefix="/en" title="en">🇬🇧</a>
|
||||||
|
|
||||||
|
|
||||||
<div class="blog-search-widget" aria-label="Seitensuche">
|
<div class="blog-search-widget" aria-label="Seitensuche">
|
||||||
<button type="button" class="blog-search-toggle" data-blog-search-toggle aria-label="Seitensuche">
|
<button type="button" class="blog-search-toggle" data-blog-search-toggle aria-label="Seitensuche">
|
||||||
<svg aria-hidden="true" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false">
|
<svg aria-hidden="true" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false">
|
||||||
@@ -47,7 +50,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="blog-search-panel" data-blog-search-panel hidden>
|
<div class="blog-search-panel" data-blog-search-panel hidden>
|
||||||
<div id="blog-search" data-blog-search-root data-search-placeholder="Suchen..."></div>
|
<div id="blog-search" data-blog-search-root data-search-placeholder="Suchen..." data-search-no-results="Keine Ergebnisse gefunden"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -62,167 +65,167 @@
|
|||||||
|
|
||||||
<h1>Ghostty</h1>
|
<h1>Ghostty</h1>
|
||||||
<nav class="blog-menu">
|
<nav class="blog-menu">
|
||||||
|
|
||||||
<ul class="blog-menu-list">
|
<ul class="blog-menu-list">
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/">Home</a>
|
<a class="blog-menu-link" href="/">Home</a>
|
||||||
|
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="blog-menu-item blog-menu-item-with-children">
|
<li class="blog-menu-item blog-menu-item-with-children">
|
||||||
|
|
||||||
<span class="blog-menu-link">Bilderarchiv</span>
|
<span class="blog-menu-link">Bilderarchiv</span>
|
||||||
|
|
||||||
|
|
||||||
<div class="blog-menu-submenu">
|
<div class="blog-menu-submenu">
|
||||||
<ul class="blog-menu-list">
|
<ul class="blog-menu-list">
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2022/">Bilderarchiv 2022</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2016/">Bilderarchiv 2016</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2015/">Bilderarchiv 2015</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2014/">Bilderarchiv 2014</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2013/">Bilderarchiv 2013</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2012/">Bilderarchiv 2012</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2011/">Bilderarchiv 2011</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2010/">Bilderarchiv 2010</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2009/">Bilderarchiv 2009</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2008/">Bilderarchiv 2008</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2005/">Bilderarchiv 2005</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2004/">Bilderarchiv 2004</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/bilderarchiv-2003/">Bilderarchiv 2003</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2022/">Bilderarchiv 2022</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2016/">Bilderarchiv 2016</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2015/">Bilderarchiv 2015</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2014/">Bilderarchiv 2014</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2013/">Bilderarchiv 2013</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2012/">Bilderarchiv 2012</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2011/">Bilderarchiv 2011</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2010/">Bilderarchiv 2010</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2009/">Bilderarchiv 2009</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2008/">Bilderarchiv 2008</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2005/">Bilderarchiv 2005</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2004/">Bilderarchiv 2004</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2003/">Bilderarchiv 2003</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/category/picture/">Fotoblog</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/tag-cloud/">Tag Cloud</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/category/spielelog/">Spieleblog</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/category/kochbuch/">Kochbuch</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/wiki/">Wiki</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/impressum/">Impressum</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/category/picture/">Fotoblog</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/tag-cloud/">Tag Cloud</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/category/spielelog/">Spieleblog</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/category/kochbuch/">Kochbuch</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/wiki/">Wiki</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/impressum/">Impressum</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<li class="blog-menu-item blog-menu-calendar">
|
<li class="blog-menu-item blog-menu-calendar">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -250,7 +253,7 @@
|
|||||||
hidden
|
hidden
|
||||||
>
|
>
|
||||||
<header class="blog-calendar-header">
|
<header class="blog-calendar-header">
|
||||||
<strong>Archivkalender</strong>
|
<strong>Archiv</strong>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="blog-calendar-close"
|
class="blog-calendar-close"
|
||||||
@@ -267,34 +270,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
<div class="single-post-taxonomy" aria-label="Taxonomie">
|
<div class="single-post-taxonomy" aria-label="Taxonomie">
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-category" href="/category/aside/">aside</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-category" href="/category/aside/">aside</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/programmierung/">programmierung</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/programmierung/">programmierung</a>
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/sysadmin/">sysadmin</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/sysadmin/">sysadmin</a>
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/mac-os-x/">mac-os-x</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/mac-os-x/">mac-os-x</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<article class="single-post" data-template="single-post" data-pagefind-body>
|
<article class="single-post blog-post" data-template="single-post" data-pagefind-body>
|
||||||
<div class="post"><p><a href="https://ghostty.org/">Ghostty</a> ist die Basis, auf der <a href="/2026/03/13/cmux-das-terminal-fur-multitasking">cmux — Das Terminal für Multitasking</a> aufgebaut wurde. Generell ebenfalls ein sehr nettes Terminal, das auch deutlich schneller reagiert als das Standardterminal und auch schon sehr gut funktioniert. Was mir nicht gefiel, war dass Tabs nicht automatisch in den passenden Pfaden wieder neu geöffnet wurden, wenn das Programm beendet wurde. Ich habe einfach zu lange persistente Sessions, die ich immer wieder aufgreife. Das macht cmux einfach besser.</p>
|
<div class="post"><p><a href="https://ghostty.org/">Ghostty</a> ist die Basis, auf der <a href="/en/2026/03/13/cmux-das-terminal-fur-multitasking/">cmux — Das Terminal für Multitasking</a> aufgebaut wurde. Generell ebenfalls ein sehr nettes Terminal, das auch deutlich schneller reagiert als das Standardterminal und auch schon sehr gut funktioniert. Was mir nicht gefiel, war dass Tabs nicht automatisch in den passenden Pfaden wieder neu geöffnet wurden, wenn das Programm beendet wurde. Ich habe einfach zu lange persistente Sessions, die ich immer wieder aufgreife. Das macht cmux einfach besser.</p></div>
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -166,4 +166,4 @@ main { display: grid; gap: 1rem; }
|
|||||||
.blog-search-panel .pagefind-ui__result-excerpt { font-size: .78rem; color: var(--pico-muted-color, var(--muted-color)); }
|
.blog-search-panel .pagefind-ui__result-excerpt { font-size: .78rem; color: var(--pico-muted-color, var(--muted-color)); }
|
||||||
.blog-search-panel .pagefind-ui__result-excerpt mark { background-color: var(--pico-primary-focus, rgba(255,223,0,.35)); color: inherit; }
|
.blog-search-panel .pagefind-ui__result-excerpt mark { background-color: var(--pico-primary-focus, rgba(255,223,0,.35)); color: inherit; }
|
||||||
.blog-search-panel .pagefind-ui__button { color: var(--pico-primary, var(--primary)); background: none; border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); border-radius: .2rem; font-size: .78rem; cursor: pointer; }
|
.blog-search-panel .pagefind-ui__button { color: var(--pico-primary, var(--primary)); background: none; border: 1px solid var(--pico-muted-border-color, var(--muted-border-color)); border-radius: .2rem; font-size: .78rem; cursor: pointer; }
|
||||||
.blog-search-panel .pagefind-ui__button:hover { border-color: var(--pico-primary, var(--primary)); }
|
.blog-search-panel .pagefind-ui__button:hover { border-color: var(--pico-primary, var(--primary)); }
|
||||||
@@ -15,11 +15,12 @@
|
|||||||
}
|
}
|
||||||
initialized = true;
|
initialized = true;
|
||||||
var placeholder = root.getAttribute('data-search-placeholder') || 'Search...';
|
var placeholder = root.getAttribute('data-search-placeholder') || 'Search...';
|
||||||
|
var zeroResults = root.getAttribute('data-search-no-results') || 'No results found';
|
||||||
new PagefindUI({
|
new PagefindUI({
|
||||||
element: root,
|
element: root,
|
||||||
showSubResults: true,
|
showSubResults: true,
|
||||||
showImages: false,
|
showImages: false,
|
||||||
translations: { placeholder: placeholder }
|
translations: { placeholder: placeholder, zero_results: zeroResults }
|
||||||
});
|
});
|
||||||
var input = root.querySelector('input');
|
var input = root.querySelector('input');
|
||||||
if (input) {
|
if (input) {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -3,19 +3,19 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Content-type: matter-transport/sentient-life-form</title>
|
<title>Esmeralda</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/pico.slate.min.css" />
|
<link rel="stylesheet" href="/assets/pico.slate.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/lightbox.min.css" />
|
<link rel="stylesheet" href="/assets/lightbox.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/highlight.min.css" />
|
<link rel="stylesheet" href="/assets/highlight.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/vanilla-calendar.min.css" />
|
<link rel="stylesheet" href="/assets/vanilla-calendar.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/bds.css" />
|
<link rel="stylesheet" href="/assets/bds.css" />
|
||||||
|
|
||||||
<link rel="alternate" type="application/rss+xml" title="RSS" href="/en/rss.xml" />
|
<link rel="alternate" type="application/rss+xml" title="RSS" href="/en/rss.xml" />
|
||||||
<link rel="alternate" type="application/atom+xml" title="Atom" href="/en/atom.xml" />
|
<link rel="alternate" type="application/atom+xml" title="Atom" href="/en/atom.xml" />
|
||||||
|
|
||||||
<link rel="alternate" hreflang="en" href="/en/2005/11/13/esmeralda" />
|
<link rel="alternate" hreflang="en" href="/2005/11/13/esmeralda/" />
|
||||||
|
|
||||||
<script defer src="/assets/highlight.min.js"></script>
|
<script defer src="/assets/highlight.min.js"></script>
|
||||||
<script defer src="/assets/code-enhancements.js"></script>
|
<script defer src="/assets/code-enhancements.js"></script>
|
||||||
<script defer src="/assets/d3.layout.cloud.js"></script>
|
<script defer src="/assets/d3.layout.cloud.js"></script>
|
||||||
@@ -27,20 +27,19 @@
|
|||||||
<link rel="stylesheet" href="/en/pagefind/pagefind-ui.css" />
|
<link rel="stylesheet" href="/en/pagefind/pagefind-ui.css" />
|
||||||
<script defer src="/en/pagefind/pagefind-ui.js"></script>
|
<script defer src="/en/pagefind/pagefind-ui.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
|
|
||||||
<nav class="language-switcher" aria-label="Language">
|
<nav class="language-switcher" aria-label="Language">
|
||||||
|
|
||||||
|
|
||||||
<a class="language-switcher-badge" href="/en/" data-lang-prefix="" title="de">🇩🇪</a>
|
<a class="language-switcher-badge" href="/" data-lang-prefix="" title="de">🇩🇪</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<span class="language-switcher-badge language-switcher-badge-current" aria-current="true" title="en">🇬🇧</span>
|
<span class="language-switcher-badge language-switcher-badge-current" aria-current="true" title="en">🇬🇧</span>
|
||||||
|
|
||||||
|
|
||||||
<div class="blog-search-widget" aria-label="Site search">
|
<div class="blog-search-widget" aria-label="Site search">
|
||||||
<button type="button" class="blog-search-toggle" data-blog-search-toggle aria-label="Site search">
|
<button type="button" class="blog-search-toggle" data-blog-search-toggle aria-label="Site search">
|
||||||
<svg aria-hidden="true" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false">
|
<svg aria-hidden="true" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false">
|
||||||
@@ -49,7 +48,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="blog-search-panel" data-blog-search-panel hidden>
|
<div class="blog-search-panel" data-blog-search-panel hidden>
|
||||||
<div id="blog-search" data-blog-search-root data-search-placeholder="Search..."></div>
|
<div id="blog-search" data-blog-search-root data-search-placeholder="Search..." data-search-no-results="No results found"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -64,167 +63,167 @@
|
|||||||
|
|
||||||
<h1>Esmeralda</h1>
|
<h1>Esmeralda</h1>
|
||||||
<nav class="blog-menu">
|
<nav class="blog-menu">
|
||||||
|
|
||||||
<ul class="blog-menu-list">
|
<ul class="blog-menu-list">
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/">Home</a>
|
<a class="blog-menu-link" href="/">Home</a>
|
||||||
|
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="blog-menu-item blog-menu-item-with-children">
|
<li class="blog-menu-item blog-menu-item-with-children">
|
||||||
|
|
||||||
<span class="blog-menu-link">Bilderarchiv</span>
|
<span class="blog-menu-link">Bilderarchiv</span>
|
||||||
|
|
||||||
|
|
||||||
<div class="blog-menu-submenu">
|
<div class="blog-menu-submenu">
|
||||||
<ul class="blog-menu-list">
|
<ul class="blog-menu-list">
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2022/">Bilderarchiv 2022</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2016/">Bilderarchiv 2016</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2015/">Bilderarchiv 2015</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2014/">Bilderarchiv 2014</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2013/">Bilderarchiv 2013</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2012/">Bilderarchiv 2012</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2011/">Bilderarchiv 2011</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2010/">Bilderarchiv 2010</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2009/">Bilderarchiv 2009</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2008/">Bilderarchiv 2008</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2005/">Bilderarchiv 2005</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2004/">Bilderarchiv 2004</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2003/">Bilderarchiv 2003</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2022/">Bilderarchiv 2022</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2016/">Bilderarchiv 2016</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2015/">Bilderarchiv 2015</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2014/">Bilderarchiv 2014</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2013/">Bilderarchiv 2013</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2012/">Bilderarchiv 2012</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2011/">Bilderarchiv 2011</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2010/">Bilderarchiv 2010</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2009/">Bilderarchiv 2009</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2008/">Bilderarchiv 2008</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2005/">Bilderarchiv 2005</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2004/">Bilderarchiv 2004</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2003/">Bilderarchiv 2003</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/category/picture/">Fotoblog</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/tag-cloud/">Tag Cloud</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/category/spielelog/">Spieleblog</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/category/kochbuch/">Kochbuch</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/wiki/">Wiki</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/impressum/">Impressum</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/category/picture/">Fotoblog</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/tag-cloud/">Tag Cloud</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/category/spielelog/">Spieleblog</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/category/kochbuch/">Kochbuch</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/wiki/">Wiki</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/impressum/">Impressum</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<li class="blog-menu-item blog-menu-calendar">
|
<li class="blog-menu-item blog-menu-calendar">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -269,41 +268,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
<div class="single-post-taxonomy" aria-label="Taxonomy">
|
<div class="single-post-taxonomy" aria-label="Taxonomy">
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-category" href="/en/category/picture/">picture</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-category" href="/category/picture/">picture</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/en/tag/fotografie/">fotografie</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/fotografie/">fotografie</a>
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/en/tag/makro/">makro</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/makro/">makro</a>
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/en/tag/natur/">natur</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/natur/">natur</a>
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/en/tag/spinne/">spinne</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/spinne/">spinne</a>
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/en/tag/tiere/">tiere</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/tiere/">tiere</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<article class="single-post" data-template="single-post" data-pagefind-body>
|
<article class="single-post blog-post" data-template="single-post" data-pagefind-body>
|
||||||
<div class="post"><h3><a href="/media/esmeralda/">Esmeralda</a></h3>
|
<div class="post"><h3><a href="/media/esmeralda/">Esmeralda</a></h3>
|
||||||
<p><img src="/media/2005/11/eb0cf9d7-6fbd-4b74-9be3-759d6e16f240.jpg" alt="Esmeralda"></p>
|
<p><img src="/media/2005/11/eb0cf9d7-6fbd-4b74-9be3-759d6e16f240.jpg" alt="Esmeralda" /></p></div>
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,19 +3,19 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Content-type: matter-transport/sentient-life-form</title>
|
<title>cmux — The Terminal for Multitasking</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/pico.slate.min.css" />
|
<link rel="stylesheet" href="/assets/pico.slate.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/lightbox.min.css" />
|
<link rel="stylesheet" href="/assets/lightbox.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/highlight.min.css" />
|
<link rel="stylesheet" href="/assets/highlight.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/vanilla-calendar.min.css" />
|
<link rel="stylesheet" href="/assets/vanilla-calendar.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/bds.css" />
|
<link rel="stylesheet" href="/assets/bds.css" />
|
||||||
|
|
||||||
<link rel="alternate" type="application/rss+xml" title="RSS" href="/en/rss.xml" />
|
<link rel="alternate" type="application/rss+xml" title="RSS" href="/en/rss.xml" />
|
||||||
<link rel="alternate" type="application/atom+xml" title="Atom" href="/en/atom.xml" />
|
<link rel="alternate" type="application/atom+xml" title="Atom" href="/en/atom.xml" />
|
||||||
|
|
||||||
<link rel="alternate" hreflang="en" href="/en/2026/03/13/cmux-das-terminal-fur-multitasking" />
|
<link rel="alternate" hreflang="en" href="/2026/03/13/cmux-das-terminal-fur-multitasking/" />
|
||||||
|
|
||||||
<script defer src="/assets/highlight.min.js"></script>
|
<script defer src="/assets/highlight.min.js"></script>
|
||||||
<script defer src="/assets/code-enhancements.js"></script>
|
<script defer src="/assets/code-enhancements.js"></script>
|
||||||
<script defer src="/assets/d3.layout.cloud.js"></script>
|
<script defer src="/assets/d3.layout.cloud.js"></script>
|
||||||
@@ -27,20 +27,19 @@
|
|||||||
<link rel="stylesheet" href="/en/pagefind/pagefind-ui.css" />
|
<link rel="stylesheet" href="/en/pagefind/pagefind-ui.css" />
|
||||||
<script defer src="/en/pagefind/pagefind-ui.js"></script>
|
<script defer src="/en/pagefind/pagefind-ui.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
|
|
||||||
<nav class="language-switcher" aria-label="Language">
|
<nav class="language-switcher" aria-label="Language">
|
||||||
|
|
||||||
|
|
||||||
<a class="language-switcher-badge" href="/en/" data-lang-prefix="" title="de">🇩🇪</a>
|
<a class="language-switcher-badge" href="/" data-lang-prefix="" title="de">🇩🇪</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<span class="language-switcher-badge language-switcher-badge-current" aria-current="true" title="en">🇬🇧</span>
|
<span class="language-switcher-badge language-switcher-badge-current" aria-current="true" title="en">🇬🇧</span>
|
||||||
|
|
||||||
|
|
||||||
<div class="blog-search-widget" aria-label="Site search">
|
<div class="blog-search-widget" aria-label="Site search">
|
||||||
<button type="button" class="blog-search-toggle" data-blog-search-toggle aria-label="Site search">
|
<button type="button" class="blog-search-toggle" data-blog-search-toggle aria-label="Site search">
|
||||||
<svg aria-hidden="true" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false">
|
<svg aria-hidden="true" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false">
|
||||||
@@ -49,7 +48,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="blog-search-panel" data-blog-search-panel hidden>
|
<div class="blog-search-panel" data-blog-search-panel hidden>
|
||||||
<div id="blog-search" data-blog-search-root data-search-placeholder="Search..."></div>
|
<div id="blog-search" data-blog-search-root data-search-placeholder="Search..." data-search-no-results="No results found"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -64,167 +63,167 @@
|
|||||||
|
|
||||||
<h1>cmux — The Terminal for Multitasking</h1>
|
<h1>cmux — The Terminal for Multitasking</h1>
|
||||||
<nav class="blog-menu">
|
<nav class="blog-menu">
|
||||||
|
|
||||||
<ul class="blog-menu-list">
|
<ul class="blog-menu-list">
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/">Home</a>
|
<a class="blog-menu-link" href="/">Home</a>
|
||||||
|
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="blog-menu-item blog-menu-item-with-children">
|
<li class="blog-menu-item blog-menu-item-with-children">
|
||||||
|
|
||||||
<span class="blog-menu-link">Bilderarchiv</span>
|
<span class="blog-menu-link">Bilderarchiv</span>
|
||||||
|
|
||||||
|
|
||||||
<div class="blog-menu-submenu">
|
<div class="blog-menu-submenu">
|
||||||
<ul class="blog-menu-list">
|
<ul class="blog-menu-list">
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2022/">Bilderarchiv 2022</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2016/">Bilderarchiv 2016</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2015/">Bilderarchiv 2015</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2014/">Bilderarchiv 2014</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2013/">Bilderarchiv 2013</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2012/">Bilderarchiv 2012</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2011/">Bilderarchiv 2011</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2010/">Bilderarchiv 2010</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2009/">Bilderarchiv 2009</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2008/">Bilderarchiv 2008</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2005/">Bilderarchiv 2005</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2004/">Bilderarchiv 2004</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2003/">Bilderarchiv 2003</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2022/">Bilderarchiv 2022</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2016/">Bilderarchiv 2016</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2015/">Bilderarchiv 2015</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2014/">Bilderarchiv 2014</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2013/">Bilderarchiv 2013</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2012/">Bilderarchiv 2012</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2011/">Bilderarchiv 2011</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2010/">Bilderarchiv 2010</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2009/">Bilderarchiv 2009</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2008/">Bilderarchiv 2008</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2005/">Bilderarchiv 2005</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2004/">Bilderarchiv 2004</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2003/">Bilderarchiv 2003</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/category/picture/">Fotoblog</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/tag-cloud/">Tag Cloud</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/category/spielelog/">Spieleblog</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/category/kochbuch/">Kochbuch</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/wiki/">Wiki</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/impressum/">Impressum</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/category/picture/">Fotoblog</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/tag-cloud/">Tag Cloud</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/category/spielelog/">Spieleblog</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/category/kochbuch/">Kochbuch</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/wiki/">Wiki</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/impressum/">Impressum</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<li class="blog-menu-item blog-menu-calendar">
|
<li class="blog-menu-item blog-menu-calendar">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -269,41 +268,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
<div class="single-post-taxonomy" aria-label="Taxonomy">
|
<div class="single-post-taxonomy" aria-label="Taxonomy">
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-category" href="/en/category/aside/">aside</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-category" href="/category/aside/">aside</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/en/tag/programmierung/">programmierung</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/programmierung/">programmierung</a>
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/en/tag/mac-os-x/">mac-os-x</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/mac-os-x/">mac-os-x</a>
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/en/tag/sysadmin/">sysadmin</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/sysadmin/">sysadmin</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<article class="single-post" data-template="single-post" data-pagefind-body>
|
<article class="single-post blog-post" data-template="single-post" data-pagefind-body>
|
||||||
<div class="post"><p><a href="https://www.cmux.dev/de">cmux — The Terminal for Multitasking</a> is a pretty brilliant terminal program for CLI workflows, which are experiencing a resurgence especially through agentic coding. And what I love about it: it has clean persistence of open workspaces where you can have multiple tabs, so I don't have to keep my work environment open all the time. I've always liked having various directories open directly because I switch back and forth between several while programming, and this works much better with CMUX than with anything else.</p>
|
<div class="post"><p><a href="https://www.cmux.dev/de">cmux — The Terminal for Multitasking</a> is a pretty brilliant terminal program for CLI workflows, which are experiencing a resurgence especially through agentic coding. And what I love about it: it has clean persistence of open workspaces where you can have multiple tabs, so I don't have to keep my work environment open all the time. I've always liked having various directories open directly because I switch back and forth between several while programming, and this works much better with CMUX than with anything else.</p></div>
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<div class="single-post-backlinks" aria-label="Backlinks">
|
|
||||||
<span class="single-post-backlinks-label">Linked from</span>
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-backlink-bubble" href="/en/2026/03/13/ghostty">ghostty</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,19 +3,19 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Content-type: matter-transport/sentient-life-form</title>
|
<title>Ghostty</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/assets/pico.slate.min.css" />
|
<link rel="stylesheet" href="/assets/pico.slate.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/lightbox.min.css" />
|
<link rel="stylesheet" href="/assets/lightbox.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/highlight.min.css" />
|
<link rel="stylesheet" href="/assets/highlight.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/vanilla-calendar.min.css" />
|
<link rel="stylesheet" href="/assets/vanilla-calendar.min.css" />
|
||||||
<link rel="stylesheet" href="/assets/bds.css" />
|
<link rel="stylesheet" href="/assets/bds.css" />
|
||||||
|
|
||||||
<link rel="alternate" type="application/rss+xml" title="RSS" href="/en/rss.xml" />
|
<link rel="alternate" type="application/rss+xml" title="RSS" href="/en/rss.xml" />
|
||||||
<link rel="alternate" type="application/atom+xml" title="Atom" href="/en/atom.xml" />
|
<link rel="alternate" type="application/atom+xml" title="Atom" href="/en/atom.xml" />
|
||||||
|
|
||||||
<link rel="alternate" hreflang="en" href="/en/2026/03/13/ghostty" />
|
<link rel="alternate" hreflang="en" href="/2026/03/13/ghostty/" />
|
||||||
|
|
||||||
<script defer src="/assets/highlight.min.js"></script>
|
<script defer src="/assets/highlight.min.js"></script>
|
||||||
<script defer src="/assets/code-enhancements.js"></script>
|
<script defer src="/assets/code-enhancements.js"></script>
|
||||||
<script defer src="/assets/d3.layout.cloud.js"></script>
|
<script defer src="/assets/d3.layout.cloud.js"></script>
|
||||||
@@ -27,20 +27,19 @@
|
|||||||
<link rel="stylesheet" href="/en/pagefind/pagefind-ui.css" />
|
<link rel="stylesheet" href="/en/pagefind/pagefind-ui.css" />
|
||||||
<script defer src="/en/pagefind/pagefind-ui.js"></script>
|
<script defer src="/en/pagefind/pagefind-ui.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
|
|
||||||
<nav class="language-switcher" aria-label="Language">
|
<nav class="language-switcher" aria-label="Language">
|
||||||
|
|
||||||
|
|
||||||
<a class="language-switcher-badge" href="/en/" data-lang-prefix="" title="de">🇩🇪</a>
|
<a class="language-switcher-badge" href="/" data-lang-prefix="" title="de">🇩🇪</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<span class="language-switcher-badge language-switcher-badge-current" aria-current="true" title="en">🇬🇧</span>
|
<span class="language-switcher-badge language-switcher-badge-current" aria-current="true" title="en">🇬🇧</span>
|
||||||
|
|
||||||
|
|
||||||
<div class="blog-search-widget" aria-label="Site search">
|
<div class="blog-search-widget" aria-label="Site search">
|
||||||
<button type="button" class="blog-search-toggle" data-blog-search-toggle aria-label="Site search">
|
<button type="button" class="blog-search-toggle" data-blog-search-toggle aria-label="Site search">
|
||||||
<svg aria-hidden="true" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false">
|
<svg aria-hidden="true" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" focusable="false">
|
||||||
@@ -49,7 +48,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="blog-search-panel" data-blog-search-panel hidden>
|
<div class="blog-search-panel" data-blog-search-panel hidden>
|
||||||
<div id="blog-search" data-blog-search-root data-search-placeholder="Search..."></div>
|
<div id="blog-search" data-blog-search-root data-search-placeholder="Search..." data-search-no-results="No results found"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -64,167 +63,167 @@
|
|||||||
|
|
||||||
<h1>Ghostty</h1>
|
<h1>Ghostty</h1>
|
||||||
<nav class="blog-menu">
|
<nav class="blog-menu">
|
||||||
|
|
||||||
<ul class="blog-menu-list">
|
<ul class="blog-menu-list">
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/">Home</a>
|
<a class="blog-menu-link" href="/">Home</a>
|
||||||
|
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="blog-menu-item blog-menu-item-with-children">
|
<li class="blog-menu-item blog-menu-item-with-children">
|
||||||
|
|
||||||
<span class="blog-menu-link">Bilderarchiv</span>
|
<span class="blog-menu-link">Bilderarchiv</span>
|
||||||
|
|
||||||
|
|
||||||
<div class="blog-menu-submenu">
|
<div class="blog-menu-submenu">
|
||||||
<ul class="blog-menu-list">
|
<ul class="blog-menu-list">
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2022/">Bilderarchiv 2022</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2016/">Bilderarchiv 2016</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2015/">Bilderarchiv 2015</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2014/">Bilderarchiv 2014</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2013/">Bilderarchiv 2013</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2012/">Bilderarchiv 2012</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2011/">Bilderarchiv 2011</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2010/">Bilderarchiv 2010</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2009/">Bilderarchiv 2009</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2008/">Bilderarchiv 2008</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2005/">Bilderarchiv 2005</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2004/">Bilderarchiv 2004</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/bilderarchiv-2003/">Bilderarchiv 2003</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2022/">Bilderarchiv 2022</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2016/">Bilderarchiv 2016</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2015/">Bilderarchiv 2015</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2014/">Bilderarchiv 2014</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2013/">Bilderarchiv 2013</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2012/">Bilderarchiv 2012</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2011/">Bilderarchiv 2011</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2010/">Bilderarchiv 2010</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2009/">Bilderarchiv 2009</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2008/">Bilderarchiv 2008</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2005/">Bilderarchiv 2005</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2004/">Bilderarchiv 2004</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/bilderarchiv-2003/">Bilderarchiv 2003</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/category/picture/">Fotoblog</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/tag-cloud/">Tag Cloud</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/category/spielelog/">Spieleblog</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/category/kochbuch/">Kochbuch</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/wiki/">Wiki</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="blog-menu-item">
|
|
||||||
|
|
||||||
<a class="blog-menu-link" href="/en/impressum/">Impressum</a>
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/category/picture/">Fotoblog</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/tag-cloud/">Tag Cloud</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/category/spielelog/">Spieleblog</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/category/kochbuch/">Kochbuch</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/wiki/">Wiki</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="blog-menu-item">
|
||||||
|
|
||||||
|
<a class="blog-menu-link" href="/impressum/">Impressum</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<li class="blog-menu-item blog-menu-calendar">
|
<li class="blog-menu-item blog-menu-calendar">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -269,34 +268,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
<div class="single-post-taxonomy" aria-label="Taxonomy">
|
<div class="single-post-taxonomy" aria-label="Taxonomy">
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-category" href="/en/category/aside/">aside</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-category" href="/category/aside/">aside</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/en/tag/programmierung/">programmierung</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/programmierung/">programmierung</a>
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/en/tag/sysadmin/">sysadmin</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/sysadmin/">sysadmin</a>
|
||||||
|
|
||||||
|
|
||||||
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/en/tag/mac-os-x/">mac-os-x</a>
|
<a class="single-post-taxonomy-bubble single-post-taxonomy-bubble-tag" href="/tag/mac-os-x/">mac-os-x</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<article class="single-post" data-template="single-post" data-pagefind-body>
|
<article class="single-post blog-post" data-template="single-post" data-pagefind-body>
|
||||||
<div class="post"><p><a href="https://ghostty.org/">Ghostty</a> is the foundation that <a href="/en/2026/03/13/cmux-das-terminal-fur-multitasking">cmux — The Terminal for Multitasking</a> was built on. Generally also a very nice terminal that responds noticeably faster than the standard terminal and already works very well. What I didn't like was that tabs weren't automatically reopened in the appropriate directories when the program was closed. I simply have too many persistent sessions that I keep coming back to. cmux just does that better.</p>
|
<div class="post"><p><a href="https://ghostty.org/">Ghostty</a> is the foundation that <a href="/en/2026/03/13/cmux-das-terminal-fur-multitasking/">cmux — The Terminal for Multitasking</a> was built on. Generally also a very nice terminal that responds noticeably faster than the standard terminal and already works very well. What I didn't like was that tabs weren't automatically reopened in the appropriate directories when the program was closed. I simply have too many persistent sessions that I keep coming back to. cmux just does that better.</p></div>
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
31
fixtures/update_golden_rendering.sh
Executable file
31
fixtures/update_golden_rendering.sh
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# Run from the RuDS repository root. Optional arguments override the generated
|
||||||
|
# bDS2 site and bDS2 checkout paths.
|
||||||
|
|
||||||
|
source_dir=${1:-"$HOME/Blogs/rfc1437.de/html"}
|
||||||
|
bds2_dir=${2:-"$(dirname "$PWD")/bDS2"}
|
||||||
|
target_dir="fixtures/golden-generated-sites/rfc1437-sample"
|
||||||
|
|
||||||
|
for relative_path in \
|
||||||
|
2005/11/13/esmeralda/index.html \
|
||||||
|
2026/03/13/cmux-das-terminal-fur-multitasking/index.html \
|
||||||
|
2026/03/13/ghostty/index.html \
|
||||||
|
atom.xml \
|
||||||
|
calendar.json \
|
||||||
|
category/article/index.html \
|
||||||
|
en/2005/11/13/esmeralda/index.html \
|
||||||
|
en/2026/03/13/cmux-das-terminal-fur-multitasking/index.html \
|
||||||
|
en/2026/03/13/ghostty/index.html \
|
||||||
|
en/index.html \
|
||||||
|
index.html \
|
||||||
|
rss.xml \
|
||||||
|
sitemap.xml
|
||||||
|
do
|
||||||
|
cp "$source_dir/$relative_path" "$target_dir/$relative_path"
|
||||||
|
perl -pi -e 's/[ \t]+$//' "$target_dir/$relative_path"
|
||||||
|
done
|
||||||
|
|
||||||
|
cp "$bds2_dir/priv/preview_assets/assets/bds.css" "$target_dir/assets/bds.css"
|
||||||
|
cp "$bds2_dir/priv/preview_assets/assets/search-runtime.js" "$target_dir/assets/search-runtime.js"
|
||||||
@@ -10,7 +10,7 @@ render-tagCloud-empty = Keine Tags gefunden.
|
|||||||
render-tagCloud-ariaLabel = Tag-Wolke
|
render-tagCloud-ariaLabel = Tag-Wolke
|
||||||
render-calendar-open = Kalender öffnen
|
render-calendar-open = Kalender öffnen
|
||||||
render-calendar-close = Kalender schließen
|
render-calendar-close = Kalender schließen
|
||||||
render-calendar-title = Archivkalender
|
render-calendar-title = Archiv
|
||||||
render-calendar-loading = Kalender wird geladen …
|
render-calendar-loading = Kalender wird geladen …
|
||||||
render-calendar-error = Kalenderdaten konnten nicht geladen werden.
|
render-calendar-error = Kalenderdaten konnten nicht geladen werden.
|
||||||
render-taxonomy-ariaLabel = Taxonomie
|
render-taxonomy-ariaLabel = Taxonomie
|
||||||
@@ -33,3 +33,4 @@ render-month-11 = Nov.
|
|||||||
render-month-12 = Dezember
|
render-month-12 = Dezember
|
||||||
render-search-placeholder = Suchen...
|
render-search-placeholder = Suchen...
|
||||||
render-search-ariaLabel = Seitensuche
|
render-search-ariaLabel = Seitensuche
|
||||||
|
render-search-noResults = Keine Ergebnisse gefunden
|
||||||
|
|||||||
@@ -33,3 +33,4 @@ render-month-11 = November
|
|||||||
render-month-12 = December
|
render-month-12 = December
|
||||||
render-search-placeholder = Search...
|
render-search-placeholder = Search...
|
||||||
render-search-ariaLabel = Site search
|
render-search-ariaLabel = Site search
|
||||||
|
render-search-noResults = No results found
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ render-tagCloud-empty = No se encontraron etiquetas.
|
|||||||
render-tagCloud-ariaLabel = Nube de etiquetas
|
render-tagCloud-ariaLabel = Nube de etiquetas
|
||||||
render-calendar-open = Abrir calendario
|
render-calendar-open = Abrir calendario
|
||||||
render-calendar-close = Cerrar calendario
|
render-calendar-close = Cerrar calendario
|
||||||
render-calendar-title = Calendario de archivo
|
render-calendar-title = Archivo
|
||||||
render-calendar-loading = Cargando calendario…
|
render-calendar-loading = Cargando calendario…
|
||||||
render-calendar-error = No se pudieron cargar los datos del calendario.
|
render-calendar-error = No se pudieron cargar los datos del calendario.
|
||||||
render-taxonomy-ariaLabel = Taxonomía
|
render-taxonomy-ariaLabel = Taxonomía
|
||||||
@@ -33,3 +33,4 @@ render-month-11 = noviembre
|
|||||||
render-month-12 = diciembre
|
render-month-12 = diciembre
|
||||||
render-search-placeholder = Buscar...
|
render-search-placeholder = Buscar...
|
||||||
render-search-ariaLabel = Buscar en el sitio
|
render-search-ariaLabel = Buscar en el sitio
|
||||||
|
render-search-noResults = No se encontraron resultados
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ render-tagCloud-empty = Aucun tag trouvé.
|
|||||||
render-tagCloud-ariaLabel = Nuage de tags
|
render-tagCloud-ariaLabel = Nuage de tags
|
||||||
render-calendar-open = Ouvrir le calendrier
|
render-calendar-open = Ouvrir le calendrier
|
||||||
render-calendar-close = Fermer le calendrier
|
render-calendar-close = Fermer le calendrier
|
||||||
render-calendar-title = Calendrier des archives
|
render-calendar-title = Archives
|
||||||
render-calendar-loading = Chargement du calendrier…
|
render-calendar-loading = Chargement du calendrier…
|
||||||
render-calendar-error = Impossible de charger les données du calendrier.
|
render-calendar-error = Impossible de charger les données du calendrier.
|
||||||
render-taxonomy-ariaLabel = Taxonomie
|
render-taxonomy-ariaLabel = Taxonomie
|
||||||
@@ -33,3 +33,4 @@ render-month-11 = novembre
|
|||||||
render-month-12 = décembre
|
render-month-12 = décembre
|
||||||
render-search-placeholder = Rechercher...
|
render-search-placeholder = Rechercher...
|
||||||
render-search-ariaLabel = Recherche du site
|
render-search-ariaLabel = Recherche du site
|
||||||
|
render-search-noResults = Aucun résultat trouvé
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ render-tagCloud-empty = Nessun tag trovato.
|
|||||||
render-tagCloud-ariaLabel = Nuvola di tag
|
render-tagCloud-ariaLabel = Nuvola di tag
|
||||||
render-calendar-open = Apri calendario
|
render-calendar-open = Apri calendario
|
||||||
render-calendar-close = Chiudi calendario
|
render-calendar-close = Chiudi calendario
|
||||||
render-calendar-title = Calendario archivio
|
render-calendar-title = Archivio
|
||||||
render-calendar-loading = Caricamento calendario…
|
render-calendar-loading = Caricamento calendario…
|
||||||
render-calendar-error = Impossibile caricare i dati del calendario.
|
render-calendar-error = Impossibile caricare i dati del calendario.
|
||||||
render-taxonomy-ariaLabel = Tassonomia
|
render-taxonomy-ariaLabel = Tassonomia
|
||||||
@@ -33,3 +33,4 @@ render-month-11 = novembre
|
|||||||
render-month-12 = dicembre
|
render-month-12 = dicembre
|
||||||
render-search-placeholder = Cerca...
|
render-search-placeholder = Cerca...
|
||||||
render-search-ariaLabel = Ricerca nel sito
|
render-search-ariaLabel = Ricerca nel sito
|
||||||
|
render-search-noResults = Nessun risultato trovato
|
||||||
|
|||||||
@@ -108,20 +108,30 @@ invariant IncrementalByContentHash {
|
|||||||
}
|
}
|
||||||
|
|
||||||
invariant MultiLanguageRoutes {
|
invariant MultiLanguageRoutes {
|
||||||
-- Main language: flat routes (/{yyyy}/{mm}/{dd}/{slug})
|
-- Main language: unprefixed routes (/{yyyy}/{mm}/{dd}/{slug})
|
||||||
-- Additional languages: prefixed (/{lang}/{yyyy}/{mm}/{dd}/{slug})
|
-- Additional languages: prefixed (/{lang}/{yyyy}/{mm}/{dd}/{slug})
|
||||||
-- Each language subtree gets its own feeds and archives
|
-- Each language subtree gets its own feeds and archives
|
||||||
|
-- Route dates and archive/calendar membership use created_at in local time,
|
||||||
|
-- matching bDS2.
|
||||||
|
-- Posts in the page category additionally render at /{slug} in every tree.
|
||||||
}
|
}
|
||||||
|
|
||||||
invariant LanguageVariantSelection {
|
invariant LanguageVariantSelection {
|
||||||
-- Every language tree renders each post in its own language when a
|
-- Every language tree renders each post in its own language when a
|
||||||
-- matching variant exists, falling back to the post's original language.
|
-- matching variant exists. The main tree prefers the main-language
|
||||||
-- This applies uniformly to single pages, list pages (home/pagination,
|
-- translation and otherwise falls back to the original post. An additional
|
||||||
-- category/tag/date archives), and feeds:
|
-- tree uses the original post when its source language matches that tree,
|
||||||
|
-- otherwise its matching translation, falling back to the original post.
|
||||||
|
-- This applies uniformly to single pages and list pages (home/pagination,
|
||||||
|
-- category/tag/date archives):
|
||||||
-- Main tree: a post whose language differs from main_language renders
|
-- Main tree: a post whose language differs from main_language renders
|
||||||
-- its main_language translation when one exists (canonical variant).
|
-- its main_language translation when one exists (canonical variant).
|
||||||
-- Additional-language trees: posts resolve to that language's
|
-- Additional-language trees: posts resolve to that language's
|
||||||
-- translation; do_not_translate posts are excluded from those trees.
|
-- translation; do_not_translate posts are excluded from those trees.
|
||||||
|
-- Feeds are deliberately narrower: the main feed contains original
|
||||||
|
-- published records, while an additional-language feed contains only
|
||||||
|
-- originals or translations whose language matches that tree. A
|
||||||
|
-- foreign-language HTML fallback is therefore not added to that feed.
|
||||||
}
|
}
|
||||||
|
|
||||||
invariant CanonicalBaseUrlConfigured {
|
invariant CanonicalBaseUrlConfigured {
|
||||||
@@ -146,20 +156,26 @@ rule GenerateCoreSectionPages {
|
|||||||
requires: core in generation.sections
|
requires: core in generation.sections
|
||||||
ensures: FileGenerated("index.html")
|
ensures: FileGenerated("index.html")
|
||||||
ensures: FileGenerated("sitemap.xml")
|
ensures: FileGenerated("sitemap.xml")
|
||||||
-- Multi-language sitemap with hreflang alternates
|
-- Single root sitemap with hreflang alternates; no per-language sitemap
|
||||||
ensures: FileGenerated("rss.xml")
|
ensures: FileGenerated("rss.xml")
|
||||||
-- RSS 2.0 feed (same output name as the old bDS app; templates link /rss.xml)
|
-- bDS2-compatible minimal feed over all published snapshots
|
||||||
ensures: FileGenerated("atom.xml")
|
ensures: FileGenerated("atom.xml")
|
||||||
-- Atom feed
|
-- Atom feed
|
||||||
ensures: FileGenerated("calendar.json")
|
ensures: FileGenerated("calendar.json")
|
||||||
-- Post dates for calendar widget
|
-- created_at counts for all published snapshots, including list-hidden posts
|
||||||
ensures: FileGenerated("404.html")
|
ensures: FileGenerated("404.html")
|
||||||
-- Not-found page rendered from the not-found template
|
-- Not-found page rendered from the not-found template
|
||||||
|
for p in Posts where status = published and p.categories.contains("page"):
|
||||||
|
ensures: FileGenerated(format("{slug}/index.html", slug: p.slug))
|
||||||
for lang in generation.blog_languages - {generation.language}:
|
for lang in generation.blog_languages - {generation.language}:
|
||||||
ensures: FileGenerated(format("{lang}/index.html", lang: lang))
|
ensures: FileGenerated(format("{lang}/index.html", lang: lang))
|
||||||
ensures: FileGenerated(format("{lang}/rss.xml", lang: lang))
|
ensures: FileGenerated(format("{lang}/rss.xml", lang: lang))
|
||||||
ensures: FileGenerated(format("{lang}/atom.xml", lang: lang))
|
ensures: FileGenerated(format("{lang}/atom.xml", lang: lang))
|
||||||
ensures: FileGenerated(format("{lang}/404.html", lang: lang))
|
ensures: FileGenerated(format("{lang}/404.html", lang: lang))
|
||||||
|
for p in Posts where status = published and
|
||||||
|
p.categories.contains("page") and not p.do_not_translate:
|
||||||
|
ensures: FileGenerated(format("{lang}/{slug}/index.html",
|
||||||
|
lang: lang, slug: p.slug))
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Single section: one HTML page per published post
|
-- Single section: one HTML page per published post
|
||||||
@@ -171,7 +187,7 @@ rule GenerateSinglePostPages {
|
|||||||
let url = post_canonical_url(p)
|
let url = post_canonical_url(p)
|
||||||
ensures: FileGenerated(format("{url}/index.html", url: url))
|
ensures: FileGenerated(format("{url}/index.html", url: url))
|
||||||
for lang in generation.blog_languages - {generation.language}:
|
for lang in generation.blog_languages - {generation.language}:
|
||||||
if p.translations.any(t => t.language.code = lang):
|
if not p.do_not_translate:
|
||||||
ensures: FileGenerated(format("{lang}/{url}/index.html",
|
ensures: FileGenerated(format("{lang}/{url}/index.html",
|
||||||
lang: lang, url: url))
|
lang: lang, url: url))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,6 +151,11 @@ value RenderLabels {
|
|||||||
-- and macro fallback labels. Month names resolved 1..12 per language.
|
-- and macro fallback labels. Month names resolved 1..12 per language.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invariant CompleteStarterNavigation {
|
||||||
|
-- Starter rendering preserves recursive menu children and the archive
|
||||||
|
-- calendar entry, and provides localized search empty-result text.
|
||||||
|
}
|
||||||
|
|
||||||
invariant LabelsUseContentLanguage {
|
invariant LabelsUseContentLanguage {
|
||||||
-- RenderLabels and the i18n filter resolve against the content/render
|
-- RenderLabels and the i18n filter resolve against the content/render
|
||||||
-- language, consistent with i18n.allium's split-localization rule.
|
-- language, consistent with i18n.allium's split-localization rule.
|
||||||
|
|||||||
Reference in New Issue
Block a user