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
42 changes: 42 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Pages

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: pages
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.97.1
- uses: actions/configure-pages@v5
- name: Export static site
run: cargo run --locked -p coldfront-static -- dist
env:
CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }}
- uses: actions/upload-pages-artifact@v4
with:
path: dist

deploy:
needs: build
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/target
/dist
/.tools
.DS_Store
.env
Expand Down
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"apps/coldfront",
"crates/coldfront-ui",
"tools/coldfront-coatcheck",
"tools/coldfront-static",
]

[workspace.package]
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,26 @@ Topcoat rebuilds and reloads each server when Rust or component files change.
./scripts/check
```

## Export for GitHub Pages

Cold Front can be built as a static site without running the Topcoat server:

```sh
./scripts/export-pages
python3 -m http.server 8000 --directory dist
```

Open <http://127.0.0.1:8000> to inspect the same production components that
will be published. The export writes relative asset links, so it works from a
GitHub Pages project path such as `https://superposition.github.io/coldfront/`.
Generated files stay in the ignored `dist/` directory.

The [`Pages` workflow](.github/workflows/pages.yml) exports and deploys the site
after changes reach `main`. In the repository's **Settings → Pages** screen,
select **GitHub Actions** as the source once. Authentication is optional; add a
repository Actions variable named `CLERK_PUBLISHABLE_KEY` only when the Clerk
application accepts the GitHub Pages domain.

The initial component glossary is in
[`docs/COMPONENTS.md`](docs/COMPONENTS.md), and the proposed product/design
sequence is in [`docs/PLAN.md`](docs/PLAN.md). The Storybook-to-Coatcheck
Expand Down
2 changes: 1 addition & 1 deletion crates/coldfront-ui/src/organisms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{
pub async fn command_header() -> Result {
view! {
<header class="cf-command-header">
<a href="/" class="cf-command-header__brand">wordmark()</a>
<a href="./" class="cf-command-header__brand">wordmark()</a>
<nav class="cf-command-header__nav" aria-label="Primary navigation">
<a href="#signal">"Signal"</a>
<a href="#briefing">"Briefing"</a>
Expand Down
5 changes: 5 additions & 0 deletions scripts/export-pages
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env sh
set -eu

output_dir="${1:-dist}"
cargo run --locked -p coldfront-static -- "$output_dir"
15 changes: 15 additions & 0 deletions tools/coldfront-static/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "coldfront-static"
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
publish = false

[dependencies]
coldfront-ui.workspace = true
tokio.workspace = true
topcoat.workspace = true

[lints]
workspace = true
14 changes: 14 additions & 0 deletions tools/coldfront-static/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Cold Front static exporter

This package renders the canonical `coldfront-ui` landing page into portable
HTML, CSS, and JavaScript for GitHub Pages. It does not depend on Coatcheck and
does not copy the component markup into a second UI implementation.

From the repository root:

```sh
cargo run --locked -p coldfront-static -- dist
```

The optional final argument selects the output directory. The exporter creates
`index.html`, `404.html`, `.nojekyll`, and the `assets/` directory.
95 changes: 95 additions & 0 deletions tools/coldfront-static/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use std::{
env,
error::Error,
fs, io,
path::{Path, PathBuf},
};

use coldfront_ui::organisms::landing_shell;
use topcoat::{context::Cx, view::view};

const STYLES: &str = include_str!("../../../crates/coldfront-ui/src/coldfront.css");
const AUTH_SCRIPT: &str = include_str!("../../../crates/coldfront-ui/src/auth.js");

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let output_dir = env::args_os()
.nth(1)
.map_or_else(|| PathBuf::from("dist"), PathBuf::from);
let clerk_publishable_key = env::var("CLERK_PUBLISHABLE_KEY").unwrap_or_default();

export(&output_dir, &clerk_publishable_key).await?;
println!("Exported Cold Front to {}", output_dir.display());

Ok(())
}

async fn export(output_dir: &Path, clerk_publishable_key: &str) -> Result<(), Box<dyn Error>> {
let assets_dir = output_dir.join("assets");
fs::create_dir_all(&assets_dir)?;

let html = render_page(clerk_publishable_key)
.await
.map_err(|error| io::Error::other(error.to_string()))?;
fs::write(output_dir.join("index.html"), &html)?;
fs::write(output_dir.join("404.html"), html)?;
fs::write(output_dir.join(".nojekyll"), "")?;
fs::write(assets_dir.join("coldfront.css"), STYLES)?;
fs::write(assets_dir.join("auth.js"), AUTH_SCRIPT)?;

Ok(())
}

async fn render_page(clerk_publishable_key: &str) -> topcoat::Result<String> {
let cx = &Cx::default();
let page = view! { cx =>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta
name="description"
content="Cold Front — a survival story about a man, his dog, and the last warm trail north."
>
<title>"Cold Front — The storm has a memory"</title>
<meta
name="clerk-publishable-key"
content=(clerk_publishable_key)
>
<link rel="stylesheet" href="assets/coldfront.css">
</head>
<body class="cf-site">
<div
class="cf-auth-dock"
data-clerk-auth=""
data-state="loading"
aria-live="polite"
>
<button type="button" disabled="">"Loading access…"</button>
</div>
landing_shell()
<script type="module" src="assets/auth.js"></script>
</body>
</html>
}?;

Ok(page.render(cx))
}

#[cfg(test)]
mod tests {
use super::render_page;

#[tokio::test]
async fn renders_a_pages_safe_document() {
let html = render_page("").await.unwrap();

assert!(html.starts_with("<!DOCTYPE html>"));
assert!(html.contains(r#"href="assets/coldfront.css""#));
assert!(html.contains(r#"src="assets/auth.js""#));
assert!(html.contains(r#"href="./""#));
assert!(html.contains("The world went white."));
assert!(!html.contains("_topcoat"));
}
}
Loading