diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index b7944ce..99a32f5 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -109,12 +109,17 @@ jobs:
# Tag + GitHub Release whenever ANY publish happened. Tag scheme
# branches on what actually shipped so a one-package bump can't
# collide with a stale tag from a prior multi-package run:
- # cli shipped → v$CLI_VERSION (CLI is the user-facing handle
- # for `npx openhop@beta`, so its
- # version anchors the combined
- # release name)
- # server only → server/v$SERVER_VERSION
- # web only → web/v$WEB_VERSION
+ # cli shipped → v$CLI_VERSION (CLI is the user-facing
+ # handle for `npx
+ # openhop@beta`, so its
+ # version anchors the
+ # combined release)
+ # server (± web) → server/v$SERVER_VERSION (server is the lowest
+ # layer and a more
+ # meaningful anchor
+ # than web when both
+ # ship together)
+ # web only → web/v$WEB_VERSION
- name: Create tag + GitHub Release
# Gate on actual step outcomes, not the pre-check intent — a failed
# `npm publish` (network, auth, registry rejection) shouldn't still
@@ -138,10 +143,11 @@ jobs:
run: |
if [ "$CLI_PUBLISHED" = "true" ]; then
tag="v$CLI_VERSION"
- elif [ "$WEB_PUBLISHED" = "true" ]; then
- tag="web/v$WEB_VERSION"
- else
+ elif [ "$SERVER_PUBLISHED" = "true" ]; then
+ # Covers server-only AND server+web combined runs.
tag="server/v$SERVER_VERSION"
+ else
+ tag="web/v$WEB_VERSION"
fi
# If THIS tag already exists (re-run of a workflow whose tag
diff --git a/packages/web/src/AppFragment.tsx b/packages/web/src/AppFragment.tsx
index b3e862f..24d0cd8 100644
--- a/packages/web/src/AppFragment.tsx
+++ b/packages/web/src/AppFragment.tsx
@@ -5,7 +5,8 @@ import { FlowCanvas } from './components/FlowCanvas'
import { DataInspectionPanel, BookmarkTab, type DockSide } from './components/DataInspectionPanel'
import { FlowEditorModal } from './components/FlowEditorModal'
import { buildStarterYaml } from './lib/starter-yaml'
-import { buildShareUrl, decodeFragment } from './lib/share-url'
+import { buildShareUrl, decodeFragment, encodeFragment } from './lib/share-url'
+import { EXAMPLE_FLOWS } from './lib/example-flows'
import type { FlowNode, FlowStep, FlowData, Flow } from './types'
interface FlowNavItem {
@@ -328,13 +329,36 @@ export default function AppFragment() {
)}
{!decodedFlow ? (
- Open a flow by visiting a share URL, or click "+ New flow" above to create one.
+
+ Open a flow by visiting a share URL, click "+ New flow" above, or pick a
+ pre-built example:
+
+ {EXAMPLE_FLOWS.map((ex) => (
+
+
+
+ ))}
+
) : displayFlow ? (
diff --git a/packages/web/src/components/DataPixel.tsx b/packages/web/src/components/DataPixel.tsx
index 8d75c4b..9d3715d 100644
--- a/packages/web/src/components/DataPixel.tsx
+++ b/packages/web/src/components/DataPixel.tsx
@@ -3,7 +3,9 @@ import { useViewport } from '@xyflow/react'
import type { FlowStep, FlowData } from '../types'
import { DataTooltip } from './DataTooltip'
-const CARROT_SPRITE = '/sprites/carrot_pixels.svg'
+// Use Vite's BASE_URL so the sprite resolves under the project base on
+// the Pages deploy (`/openhop/sprites/...`) and at the root in dev (`/`).
+const CARROT_SPRITE = `${import.meta.env.BASE_URL}sprites/carrot_pixels.svg`
const DEFAULT_PIXEL_COLOR = '#ff8a4a' // VARIANT_ACCENT[0] — sprite's original orange.
interface DataPixelProps {
diff --git a/packages/web/src/components/nodes/node-sprites.ts b/packages/web/src/components/nodes/node-sprites.ts
index 9b5693c..9835dd1 100644
--- a/packages/web/src/components/nodes/node-sprites.ts
+++ b/packages/web/src/components/nodes/node-sprites.ts
@@ -7,20 +7,27 @@ export interface BuildingProps {
active?: boolean
}
+// Vite's BASE_URL is `/` in dev and `/openhop/` on the GitHub Pages
+// deploy. Without it, root-absolute sprite paths 404 on Pages because
+// the page lives under /openhop/ but `/sprites/foo.svg` resolves to the
+// site root, not the project base. BASE_URL always ends with `/`, so
+// `${BASE}sprites/foo.svg` is the correct concatenation.
+const BASE = import.meta.env.BASE_URL
+
// Sprite per node type. Types not in this map fall back to the service sprite
// at render time (see FlowNode.tsx).
export const NODE_TYPE_SPRITE: Record = {
- actor: '/sprites/user_node.svg',
- endpoint: '/sprites/endpoint_node.svg',
- auth: '/sprites/auth_node.svg',
- database: '/sprites/database_node.svg',
- external: '/sprites/external_node.svg',
- cache: '/sprites/cache_node.svg',
- queue: '/sprites/queue_node.svg',
- service: '/sprites/service_node.svg',
- docker: '/sprites/docker_node.svg',
- k8s: '/sprites/k8s_node.svg',
- scheduler: '/sprites/scheduler_node.svg',
+ actor: `${BASE}sprites/user_node.svg`,
+ endpoint: `${BASE}sprites/endpoint_node.svg`,
+ auth: `${BASE}sprites/auth_node.svg`,
+ database: `${BASE}sprites/database_node.svg`,
+ external: `${BASE}sprites/external_node.svg`,
+ cache: `${BASE}sprites/cache_node.svg`,
+ queue: `${BASE}sprites/queue_node.svg`,
+ service: `${BASE}sprites/service_node.svg`,
+ docker: `${BASE}sprites/docker_node.svg`,
+ k8s: `${BASE}sprites/k8s_node.svg`,
+ scheduler: `${BASE}sprites/scheduler_node.svg`,
}
export const SPRITE_SIZE = 108
diff --git a/packages/web/src/globals.d.ts b/packages/web/src/globals.d.ts
index 9cd1e02..3c1809f 100644
--- a/packages/web/src/globals.d.ts
+++ b/packages/web/src/globals.d.ts
@@ -1,5 +1,12 @@
/** Global ambient declarations for OpenHop's web bundle. */
+// Vite's `?raw` query — load a file as its raw text contents at build
+// time. Used by example-flows.ts to embed YAML examples in the bundle.
+declare module '*.yaml?raw' {
+ const content: string
+ export default content
+}
+
declare global {
interface Window {
/** Test/debug hook: scales animation speed (default 1, e.g. 4 for 4× faster). */
diff --git a/packages/web/src/lib/example-flows.ts b/packages/web/src/lib/example-flows.ts
new file mode 100644
index 0000000..0062e3b
--- /dev/null
+++ b/packages/web/src/lib/example-flows.ts
@@ -0,0 +1,40 @@
+/**
+ * Curated example flows shown on the empty state of the Pages deploy.
+ *
+ * Each entry is the same YAML that lives in `examples/*.yaml` at the
+ * repo root, imported as a raw string via Vite's `?raw` query so the
+ * source-of-truth stays in one place. Vite inlines these into the
+ * bundle — together they're ~6 KB which is comfortable to ship.
+ */
+
+import authFlow from '../../../../examples/auth-flow.yaml?raw'
+import orderFlow from '../../../../examples/order-flow.yaml?raw'
+import simpleCrud from '../../../../examples/simple-crud.yaml?raw'
+
+export interface ExampleFlow {
+ id: string
+ title: string
+ description: string
+ yaml: string
+}
+
+export const EXAMPLE_FLOWS: ExampleFlow[] = [
+ {
+ id: 'simple-crud',
+ title: 'Simple CRUD',
+ description: 'Basic REST API CRUD — the smallest useful flow.',
+ yaml: simpleCrud,
+ },
+ {
+ id: 'auth-flow',
+ title: 'OAuth2 Login',
+ description: 'Browser → app → Google OAuth → DB + cache.',
+ yaml: authFlow,
+ },
+ {
+ id: 'order-flow',
+ title: 'Order Processing',
+ description: 'Multi-service order pipeline with payment, audit, and retry.',
+ yaml: orderFlow,
+ },
+]