Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build:
name: Lint, Test, Build
runs-on: ubuntu-latest

env:
CARGO_TERM_COLOR: always

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Rust (stable)
uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt

- name: Cache cargo registry + build
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-

- name: Cargo fmt (check)
run: cargo fmt --all -- --check

- name: Cargo clippy
run: cargo clippy --workspace --all-targets -- -D warnings

- name: Cargo test
run: cargo test --workspace --all-features --no-fail-fast

- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Cache node_modules (bun)
uses: actions/cache@v4
with:
path: |
node_modules
~/.cache/bun
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-

- name: Install JS dependencies
run: bun install --frozen-lockfile

- name: Build CSS (Tailwind)
run: bun run build

- name: Cargo build (release)
run: cargo build --release

- name: Generate site (SSG)
run: ./target/release/typstify-ssg

- name: Upload site artifact
uses: actions/upload-artifact@v4
with:
name: site
path: |
site/**
style/output.css
23 changes: 11 additions & 12 deletions typstify-ssg/build.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
use std::process::Command;
use std::path::Path;
use std::{path::Path, process::Command};

fn main() {
println!("cargo:rerun-if-changed=../style/input.css");
println!("cargo:rerun-if-changed=../tailwind.config.js");
println!("cargo:rerun-if-changed=../package.json");

// Check if bun is available
let has_bun = Command::new("bun")
.arg("--version")
.output()
.is_ok();

let has_bun = Command::new("bun").arg("--version").output().is_ok();

if !has_bun {
println!("cargo:warning=bun is not available, CSS will not be built automatically");
return;
}

// Check if output.css exists, if not, build it
let output_css = Path::new("../style/output.css");
if !output_css.exists() {
println!("cargo:warning=Building CSS with Tailwind...");

let output = Command::new("bun")
.arg("x")
.arg("tailwindcss")
Expand All @@ -32,13 +28,16 @@ fn main() {
.arg("--minify")
.current_dir("..")
.output();

match output {
Ok(output) if output.status.success() => {
println!("cargo:warning=CSS built successfully");
}
Ok(output) => {
println!("cargo:warning=Failed to build CSS: {}", String::from_utf8_lossy(&output.stderr));
println!(
"cargo:warning=Failed to build CSS: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Err(e) => {
println!("cargo:warning=Failed to run tailwindcss command: {}", e);
Expand Down
53 changes: 32 additions & 21 deletions typstify-ssg/src/feed.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use atom_syndication::{Feed, Entry, Link, Person, Text};
use crate::config::AppConfig;
use crate::content::Content;
use atom_syndication::{Entry, Feed, Link, Person, Text};
use chrono::{DateTime, Utc};

use crate::{config::AppConfig, content::Content};

pub fn create_feed(config: &AppConfig, content: &[Content]) -> Feed {
let mut feed = Feed::default();

// Set feed metadata
feed.set_title(config.site.title.clone());
feed.set_subtitle(Text::plain(config.site.description.clone()));

// Set feed link
let feed_link = Link {
href: format!("{}/{}", config.site.base_url, config.feed.filename),
Expand All @@ -20,10 +20,10 @@ pub fn create_feed(config: &AppConfig, content: &[Content]) -> Feed {
length: None,
};
feed.set_links(vec![feed_link]);

// Set feed ID (usually the website URL)
feed.set_id(config.site.base_url.clone());

// Set updated time to the most recent content
let now = Utc::now();
if let Some(latest_content) = content.first() {
Expand All @@ -42,23 +42,27 @@ pub fn create_feed(config: &AppConfig, content: &[Content]) -> Feed {
} else {
feed.set_updated(now);
}

// Create entries from content
let mut entries = Vec::new();

for content_item in content.iter().take(config.feed.max_items) {
// Skip draft content
if content_item.metadata.is_draft() {
continue;
}

let mut entry = Entry::default();

// Set entry title
entry.set_title(content_item.metadata.get_title());

// Set entry ID and link
let content_url = format!("{}/{}.html", config.site.base_url.trim_end_matches('/'), content_item.slug());
let content_url = format!(
"{}/{}.html",
config.site.base_url.trim_end_matches('/'),
content_item.slug()
);
entry.set_id(content_url.clone());
entry.set_links(vec![Link {
href: content_url,
Expand All @@ -68,13 +72,13 @@ pub fn create_feed(config: &AppConfig, content: &[Content]) -> Feed {
title: None,
length: None,
}]);

// Set entry content
let summary = content_item.metadata.get_description();
if !summary.is_empty() {
entry.set_summary(Some(Text::html(summary)));
}

// Set entry author
if let Some(author) = content_item.metadata.get_author() {
entry.set_authors(vec![Person {
Expand All @@ -90,14 +94,18 @@ pub fn create_feed(config: &AppConfig, content: &[Content]) -> Feed {
uri: None,
}]);
}

// Set published date if available
if let Some(date_str) = content_item.metadata.get_date() {
// Try to parse as RFC3339 first, then as simple date
if let Ok(fixed_date) = DateTime::parse_from_rfc3339(date_str) {
entry.set_published(Some(fixed_date));
} else if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
let fixed_date = naive_date.and_hms_opt(0, 0, 0).unwrap().and_utc().fixed_offset();
let fixed_date = naive_date
.and_hms_opt(0, 0, 0)
.unwrap()
.and_utc()
.fixed_offset();
entry.set_published(Some(fixed_date));
} else {
// Fallback to current time if date parsing fails
Expand All @@ -107,20 +115,23 @@ pub fn create_feed(config: &AppConfig, content: &[Content]) -> Feed {
// Use current time as fallback
entry.set_published(Some(now.fixed_offset()));
}

// Set entry categories/tags
let categories: Vec<_> = content_item.metadata.tags.iter()
let categories: Vec<_> = content_item
.metadata
.tags
.iter()
.map(|tag| atom_syndication::Category {
term: tag.clone(),
scheme: None,
label: Some(tag.clone()),
})
.collect();
entry.set_categories(categories);

entries.push(entry);
}

feed.set_entries(entries);
feed
}
32 changes: 15 additions & 17 deletions typstify-ssg/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,38 +245,36 @@ impl Site {
match (a.metadata.get_date(), b.metadata.get_date()) {
(Some(date_a), Some(date_b)) => {
// Try to parse as RFC3339 first, then as simple date
let parsed_a = chrono::DateTime::parse_from_rfc3339(date_a)
.or_else(|_| {
chrono::NaiveDate::parse_from_str(date_a, "%Y-%m-%d")
.map(|d| d.and_hms_opt(0, 0, 0).unwrap().and_utc().into())
});
let parsed_b = chrono::DateTime::parse_from_rfc3339(date_b)
.or_else(|_| {
chrono::NaiveDate::parse_from_str(date_b, "%Y-%m-%d")
.map(|d| d.and_hms_opt(0, 0, 0).unwrap().and_utc().into())
});

let parsed_a = chrono::DateTime::parse_from_rfc3339(date_a).or_else(|_| {
chrono::NaiveDate::parse_from_str(date_a, "%Y-%m-%d")
.map(|d| d.and_hms_opt(0, 0, 0).unwrap().and_utc().into())
});
let parsed_b = chrono::DateTime::parse_from_rfc3339(date_b).or_else(|_| {
chrono::NaiveDate::parse_from_str(date_b, "%Y-%m-%d")
.map(|d| d.and_hms_opt(0, 0, 0).unwrap().and_utc().into())
});

match (parsed_a, parsed_b) {
(Ok(a), Ok(b)) => b.cmp(&a), // Most recent first
(Ok(_), Err(_)) => std::cmp::Ordering::Less, // Valid date comes first
(Ok(a), Ok(b)) => b.cmp(&a), // Most recent first
(Ok(_), Err(_)) => std::cmp::Ordering::Less, // Valid date comes first
(Err(_), Ok(_)) => std::cmp::Ordering::Greater, // Valid date comes first
(Err(_), Err(_)) => date_b.cmp(date_a), // Fallback to string comparison
}
}
(Some(_), None) => std::cmp::Ordering::Less, // Items with dates come first
(Some(_), None) => std::cmp::Ordering::Less, // Items with dates come first
(None, Some(_)) => std::cmp::Ordering::Greater, // Items with dates come first
(None, None) => std::cmp::Ordering::Equal, // No preference
(None, None) => std::cmp::Ordering::Equal, // No preference
}
});

// Generate feed
let feed = crate::feed::create_feed(&self.config, &sorted_content);

// Write feed to file
let feed_path = self.output_dir.join(&self.config.feed.filename);
let feed_xml = feed.to_string();
std::fs::write(&feed_path, feed_xml)?;

info!("Generated feed: {}", feed_path.display());
Ok(())
}
Expand Down
Loading
Loading