mirror of
https://gitlab.com/Anson-Projects/projects.git
synced 2025-09-14 09:35:04 +00:00
clean: remove CI debugging artifacts and testing features
- Remove test files: test-ghost-profile.md, test-local-deployment.sh, validate-ghost-extraction.sh, AGENTS.md - Restore .gitlab-ci.yml to original state without debugging changes - Restore _quarto.yml to original format without ghost profiles - Remove ghost-iframe.css styling file - Restore ghost-upload/.gitlab-ci.yml to original state without force-update job - Simplify Rust code by removing force update functionality and content extraction - Restore README.md to original state Keeps core bug fixes: fixed get_slug() and proper Ghost API duplicate checking
This commit is contained in:
@@ -5,26 +5,6 @@ publish:
|
||||
- cd ./ghost-upload
|
||||
- cargo run
|
||||
needs:
|
||||
- job: deploy
|
||||
optional: true
|
||||
- job: staging
|
||||
optional: true
|
||||
|
||||
# Manual trigger to force update all Ghost posts
|
||||
force-update-ghost:
|
||||
stage: deploy
|
||||
image: rust:latest
|
||||
script:
|
||||
- echo "🔄 Force updating all Ghost posts..."
|
||||
- cd ./ghost-upload
|
||||
- FORCE_UPDATE=true cargo run
|
||||
needs:
|
||||
- job: deploy
|
||||
optional: true
|
||||
- job: staging
|
||||
optional: true
|
||||
- pages
|
||||
rules:
|
||||
- when: manual
|
||||
allow_failure: false
|
||||
variables:
|
||||
FORCE_UPDATE: "true"
|
||||
- if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"
|
||||
|
@@ -1,39 +1,3 @@
|
||||
# ghost-upload
|
||||
|
||||
This tool synchronizes posts from https://projects.ansonbiggs.com to the Ghost blog at https://notes.ansonbiggs.com.
|
||||
|
||||
## Features
|
||||
|
||||
- **Automatic sync**: Only uploads new posts by default
|
||||
- **Content extraction**: Fetches clean HTML content instead of using iframes
|
||||
- **AI summaries**: Uses Kagi Summarizer for post summaries
|
||||
- **Force update**: Manual trigger to update all existing posts
|
||||
|
||||
## Usage
|
||||
|
||||
### Normal Mode (Default)
|
||||
```bash
|
||||
cargo run
|
||||
```
|
||||
Only processes new posts that don't exist on the Ghost blog.
|
||||
|
||||
### Force Update Mode
|
||||
```bash
|
||||
FORCE_UPDATE=true cargo run
|
||||
```
|
||||
Updates ALL posts, including existing ones. Useful for:
|
||||
- Updating content after changes
|
||||
- Refreshing summaries
|
||||
- Applying new styling/formatting
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
The GitLab CI pipeline includes:
|
||||
- **Automatic sync**: Runs after each deployment
|
||||
- **Manual force update**: Available as a manual trigger in GitLab UI
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `admin_api_key`: Ghost Admin API key (required)
|
||||
- `kagi_api_key`: Kagi Summarizer API key (required)
|
||||
- `FORCE_UPDATE`: Set to "true" to update all posts (optional)
|
||||
This code uploads posts from https://projects.ansonbiggs.com to https://notes.ansonbiggs.com. I couldn't figure out how to update posts, and the kagi API doesn't make it clear how long it caches results for so for now only posts that don't exist on the ghost blog will be uploaded. If you want to update content you need to manually make edits to the code and delete posts on the blog.
|
@@ -45,29 +45,13 @@ impl Post {
|
||||
let slug = get_slug(link);
|
||||
|
||||
let summary = summarize_url(link).await;
|
||||
|
||||
// Extract content from ghost-optimized version
|
||||
let ghost_content = extract_article_content(&link).await;
|
||||
|
||||
let html = html! {
|
||||
div class="ghost-summary" {
|
||||
h3 { "Summary" }
|
||||
p { (summary) }
|
||||
}
|
||||
div class="ghost-content" {
|
||||
(maud::PreEscaped(ghost_content))
|
||||
}
|
||||
div class="ghost-footer" {
|
||||
hr {}
|
||||
p {
|
||||
em {
|
||||
"This content was originally posted on my projects website "
|
||||
a href=(link) { "here" }
|
||||
". The above summary was generated by the "
|
||||
a href=("https://help.kagi.com/kagi/api/summarizer.html") {"Kagi Summarizer"}
|
||||
"."
|
||||
}
|
||||
}
|
||||
p { (summary) }
|
||||
iframe src=(link) style="width: 100%; height: 80vh" { }
|
||||
p {
|
||||
"This content was originally posted on my projects website " a href=(link) { "here." }
|
||||
" The above summary was made by the " a href=("https://help.kagi.com/kagi/api/summarizer.html")
|
||||
{"Kagi Summarizer"}
|
||||
}
|
||||
}.into_string();
|
||||
|
||||
@@ -146,52 +130,6 @@ fn get_slug(link: &str) -> String {
|
||||
link.split_once("/posts/").unwrap().1.trim_end_matches('/').to_string()
|
||||
}
|
||||
|
||||
async fn extract_article_content(original_link: &str) -> String {
|
||||
// Convert original link to ghost-content version
|
||||
let ghost_link = original_link.replace("projects.ansonbiggs.com", "projects.ansonbiggs.com/ghost-content");
|
||||
|
||||
match reqwest::get(&ghost_link).await {
|
||||
Ok(response) => {
|
||||
match response.text().await {
|
||||
Ok(html_content) => {
|
||||
let document = Html::parse_document(&html_content);
|
||||
|
||||
// Try different selectors to find the main content
|
||||
let content_selectors = [
|
||||
"#quarto-content main",
|
||||
"#quarto-content",
|
||||
"main",
|
||||
"article",
|
||||
".content",
|
||||
"body"
|
||||
];
|
||||
|
||||
for selector_str in &content_selectors {
|
||||
if let Ok(selector) = Selector::parse(selector_str) {
|
||||
if let Some(element) = document.select(&selector).next() {
|
||||
let content = element.inner_html();
|
||||
|
||||
if !content.trim().is_empty() {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: return original content with iframe if extraction fails
|
||||
format!(r#"<div class="fallback-iframe">
|
||||
<p><em>Content extraction failed. Falling back to embedded view:</em></p>
|
||||
<iframe src="{}" style="width: 100%; height: 80vh; border: none;" loading="lazy"></iframe>
|
||||
</div>"#, original_link)
|
||||
}
|
||||
Err(_) => format!(r#"<p><em>Failed to fetch content. <a href="{}">View original post</a></em></p>"#, original_link)
|
||||
}
|
||||
}
|
||||
Err(_) => format!(r#"<p><em>Failed to fetch content. <a href="{}">View original post</a></em></p>"#, original_link)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct GhostPostsResponse {
|
||||
@@ -296,15 +234,7 @@ async fn main() {
|
||||
let ghost_api_url = "https://notes.ansonbiggs.com/ghost/api/v3/admin/posts/?source=html";
|
||||
let ghost_admin_api_key = env::var("admin_api_key").unwrap();
|
||||
|
||||
// Check if force update is enabled
|
||||
let force_update = env::var("FORCE_UPDATE").unwrap_or_default() == "true";
|
||||
|
||||
if force_update {
|
||||
println!("🔄 FORCE UPDATE MODE ENABLED");
|
||||
println!(" This will update ALL posts, including existing ones.");
|
||||
} else {
|
||||
println!("📝 NORMAL MODE - Only publishing new posts");
|
||||
}
|
||||
|
||||
|
||||
let feed = "https://projects.ansonbiggs.com/index.xml";
|
||||
|
||||
@@ -339,30 +269,22 @@ async fn main() {
|
||||
// Prepare the post data
|
||||
let entries = fetch_feed(feed).await;
|
||||
|
||||
let filtered_entries: Vec<Entry> = if force_update {
|
||||
println!("🔄 Force update enabled - processing all {} posts", entries.len());
|
||||
entries
|
||||
} else {
|
||||
let post_exists_futures = entries.into_iter().map(|entry| {
|
||||
let entry_clone = entry.clone();
|
||||
let token_clone = token.clone();
|
||||
async move {
|
||||
let link = entry.links.first().unwrap().href.as_str();
|
||||
let slug = get_slug(link);
|
||||
(entry_clone, get_existing_post_id(&slug, &token_clone).await.is_some())
|
||||
}
|
||||
});
|
||||
let post_exists_futures = entries.into_iter().map(|entry| {
|
||||
let entry_clone = entry.clone();
|
||||
let token_clone = token.clone();
|
||||
async move {
|
||||
let link = entry.links.first().unwrap().href.as_str();
|
||||
let slug = get_slug(link);
|
||||
(entry_clone, get_existing_post_id(&slug, &token_clone).await.is_some())
|
||||
}
|
||||
});
|
||||
|
||||
let post_exists_results = join_all(post_exists_futures).await;
|
||||
let post_exists_results = join_all(post_exists_futures).await;
|
||||
|
||||
let new_entries: Vec<Entry> = post_exists_results
|
||||
.into_iter()
|
||||
.filter_map(|(entry, exists)| if !exists { Some(entry) } else { None })
|
||||
.collect();
|
||||
|
||||
println!("📝 Found {} new posts to publish", new_entries.len());
|
||||
new_entries
|
||||
};
|
||||
let filtered_entries: Vec<Entry> = post_exists_results
|
||||
.into_iter()
|
||||
.filter_map(|(entry, exists)| if !exists { Some(entry) } else { None })
|
||||
.collect();
|
||||
|
||||
if filtered_entries.is_empty() {
|
||||
println!("Nothing to post.");
|
||||
@@ -378,46 +300,21 @@ async fn main() {
|
||||
posts: vec![post.clone()],
|
||||
};
|
||||
|
||||
// Check if this is an update (for force_update mode)
|
||||
let (method, url) = if force_update {
|
||||
if let Some(existing_id) = get_existing_post_id(&post.slug, &token).await {
|
||||
println!("🔄 Updating existing post: {}", post.title);
|
||||
("PUT", format!("https://notes.ansonbiggs.com/ghost/api/v3/admin/posts/{}/", existing_id))
|
||||
} else {
|
||||
println!("📝 Creating new post: {}", post.title);
|
||||
("POST", ghost_api_url.to_string())
|
||||
}
|
||||
} else {
|
||||
println!("📝 Creating new post: {}", post.title);
|
||||
("POST", ghost_api_url.to_string())
|
||||
};
|
||||
|
||||
let response = match method {
|
||||
"PUT" => client
|
||||
.put(&url)
|
||||
.header("Authorization", format!("Ghost {}", token))
|
||||
.json(&post_payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed"),
|
||||
_ => client
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Ghost {}", token))
|
||||
.json(&post_payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed"),
|
||||
};
|
||||
let response = client
|
||||
.post(ghost_api_url)
|
||||
.header("Authorization", format!("Ghost {}", token))
|
||||
.json(&post_payload)
|
||||
.send()
|
||||
.await
|
||||
.expect("Request failed");
|
||||
|
||||
// Check the response
|
||||
if response.status().is_success() {
|
||||
let action = if method == "PUT" { "updated" } else { "published" };
|
||||
println!("✅ Post '{}' {} successfully.", post.title, action);
|
||||
println!("Post {} published successfully.", post.title);
|
||||
} else {
|
||||
let action = if method == "PUT" { "update" } else { "publish" };
|
||||
println!(
|
||||
"❌ Failed to {} post '{}'.\n\tStatus: {}\n\tResponse: {:?}",
|
||||
action, &post.title, response.status(), response.text().await.unwrap_or_default()
|
||||
"Failed to publish post {}.\n\tResp: {:?}",
|
||||
&post.title, response
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user