Skip to content

Commit ce752e5

Browse files
authored
Merge pull request #15 from Workday/make-examples-downloadable
Make examples downloadable
2 parents 42a8785 + 71b641f commit ce752e5

13 files changed

Lines changed: 286 additions & 34 deletions

File tree

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ node_modules/
44
# Site build output
55
site/dist/
66
site/.astro/
7+
site/public/downloads/
78

89
# Environment files (never commit credentials)
910
.env

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Thanks for helping build the open home for Workday Build examples. Adding an exa
3535

3636
Each creates `examples/your-example-name/` with a prefilled `example.json` and README skeleton. (You can also copy `examples/_template/` by hand.)
3737

38-
3. **Drop your artifact in.** Whatever it is: Extend app source (exported with Local Disk Sync, the WDCLI, or the ZIP download), orchestration definitions, an agent skill as markdown, diagrams. The folder must be self-contained.
38+
3. **Drop your artifact in.** Whatever it is: Extend app source (exported with Local Disk Sync, the WDCLI, or the ZIP download), orchestration definitions, an agent skill as markdown, diagrams. The folder must be self-contained. Once merged, the gallery serves the folder as a zip download automatically; nothing extra to do.
3939
4. **Fill in the two files.** `example.json` needs a title, a description, and a type; everything else is optional. The README needs four short sections: What it is, What's inside, How to use it, and Before you deploy (everything a reader must change for their own tenant, such as app reference ids, base URLs, WIDs, or dates).
4040
5. **Validate.** From the repository root:
4141

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ hub.config.json Repo URLs and the approved type, component, and product
5555

5656
## Use an example
5757

58+
Every example can be downloaded on its own from the [gallery](https://workday.github.io/WorkdayDeveloperProgram/): each card and example page has a **Download zip** button, so there is no need to clone the whole repository. The zip includes a `SOURCE.md` that records the commit it was built from and how to send changes back. To work with git instead:
59+
5860
```bash
5961
git clone https://github.com/Workday/WorkdayDeveloperProgram
6062
cd WorkdayDeveloperProgram

‎site/README.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Gallery site
2+
3+
The optional gallery for the Workday Examples Hub, built with Astro and Tailwind and deployed to GitHub Pages by `.github/workflows/deploy-gallery.yml`.
4+
5+
```bash
6+
npm install
7+
npm run dev # local preview
8+
npm run build # static output in dist/
9+
```
10+
11+
## How it reads the examples
12+
13+
`src/lib/examples.js` globs every `catalog/*/example.json` and `examples/*/example.json` at build time. There is no manifest to regenerate; a folder shows up as soon as it exists.
14+
15+
## Per-example downloads
16+
17+
`scripts/build-zips.mjs` runs before `dev` and `build` (the `predev` and `prebuild` scripts) and zips every example folder into `public/downloads/<section>/<id>.zip`. Astro copies `public/` into `dist/`, so the same URLs work locally and on Pages.
18+
19+
- Each zip wraps the folder in `<id>/` with `appManifest.json` at its root, includes `README.md`, and leaves out `example.json` (hub metadata only).
20+
- Each zip also gets a generated `SOURCE.md` with the repository, folder, commit, a sparse checkout command, and how to open a pull request. The text lives in `src/lib/gitTrace.js`, shared with the example page.
21+
- `public/downloads/` is generated output and is gitignored. Do not commit it.

‎site/package-lock.json‎

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎site/package.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"description": "Optional gallery site for the Workday Use Case Hub. Built with Astro and Tailwind.",
66
"type": "module",
77
"scripts": {
8+
"predev": "node scripts/build-zips.mjs",
89
"dev": "astro dev",
10+
"prebuild": "node scripts/build-zips.mjs",
911
"build": "astro build",
1012
"preview": "astro preview"
1113
},
@@ -14,5 +16,8 @@
1416
"@tailwindcss/vite": "^4.1.0",
1517
"astro": "^5.0.0",
1618
"tailwindcss": "^4.1.0"
19+
},
20+
"devDependencies": {
21+
"fflate": "^0.8.3"
1722
}
1823
}

‎site/scripts/build-zips.mjs‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Zips every example folder into public/downloads/<section>/<id>.zip so the
2+
// gallery can offer per-example downloads without anyone cloning the repo.
3+
// Runs before `astro dev` and `astro build` (see package.json). Output is
4+
// gitignored. Each zip gets a SOURCE.md that points back to the repository
5+
// and the commit it was built from.
6+
7+
import { execSync } from "node:child_process";
8+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
9+
import { join, relative, sep } from "node:path";
10+
import { fileURLToPath } from "node:url";
11+
import { zipSync } from "fflate";
12+
import { sourceMarkdown } from "../src/lib/gitTrace.js";
13+
14+
const siteDir = fileURLToPath(new URL("..", import.meta.url));
15+
const repoRoot = join(siteDir, "..");
16+
const outRoot = join(siteDir, "public", "downloads");
17+
const hub = JSON.parse(readFileSync(join(repoRoot, "hub.config.json"), "utf8"));
18+
19+
// Hub metadata and OS noise stay out of the zip; everything else ships.
20+
const SKIP = new Set([".DS_Store", ".gitkeep", "example.json", "node_modules", ".git"]);
21+
// Fixed timestamp so building the same commit twice yields identical bytes.
22+
const MTIME = new Date("2000-01-01T00:00:00Z");
23+
24+
const sha = process.env.GITHUB_SHA || git("rev-parse HEAD") || "unknown";
25+
// Commit date rather than wall clock, so the same commit always builds the same bytes.
26+
const date = (git("log -1 --format=%cI") || new Date().toISOString()).slice(0, 10);
27+
28+
function git(args) {
29+
try {
30+
return execSync(`git ${args}`, { cwd: repoRoot, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
31+
} catch {
32+
return "";
33+
}
34+
}
35+
36+
function walk(dir) {
37+
const out = [];
38+
for (const name of readdirSync(dir).sort()) {
39+
if (SKIP.has(name)) continue;
40+
const abs = join(dir, name);
41+
if (statSync(abs).isDirectory()) out.push(...walk(abs));
42+
else out.push(abs);
43+
}
44+
return out;
45+
}
46+
47+
// Same folder rules as src/lib/examples.js: skip _template, need example.json.
48+
function exampleDirs(section) {
49+
const base = join(repoRoot, section);
50+
if (!existsSync(base)) return [];
51+
return readdirSync(base)
52+
.sort()
53+
.filter((id) => !id.startsWith("_") && !id.startsWith("."))
54+
.filter((id) => statSync(join(base, id)).isDirectory())
55+
.filter((id) => existsSync(join(base, id, "example.json")))
56+
.map((id) => ({ id, path: `${section}/${id}`, abs: join(base, id), section }));
57+
}
58+
59+
rmSync(outRoot, { recursive: true, force: true });
60+
61+
let count = 0;
62+
for (const section of ["catalog", "examples"]) {
63+
for (const example of exampleDirs(section)) {
64+
const files = {};
65+
for (const abs of walk(example.abs)) {
66+
const rel = relative(example.abs, abs).split(sep).join("/");
67+
files[`${example.id}/${rel}`] = [readFileSync(abs), { mtime: MTIME }];
68+
}
69+
files[`${example.id}/SOURCE.md`] = [Buffer.from(sourceMarkdown(example, hub, { sha, date })), { mtime: MTIME }];
70+
mkdirSync(join(outRoot, section), { recursive: true });
71+
writeFileSync(join(outRoot, section, `${example.id}.zip`), zipSync(files, { level: 6 }));
72+
count++;
73+
}
74+
}
75+
76+
console.log(`build-zips: wrote ${count} zip(s) to ${relative(repoRoot, outRoot)} (commit ${sha.slice(0, 7)})`);

‎site/src/components/Card.astro‎

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
import TypeIcon from "./TypeIcon.astro";
3-
import { withBase, sourceLabel } from "../lib/examples.js";
3+
import { withBase, sourceLabel, downloadUrl, downloadName } from "../lib/examples.js";
44
55
const { example } = Astro.props;
66
const detailUrl = withBase(`${example.path}/`);
@@ -10,9 +10,10 @@ const searchText = [example.title, example.description, example.id, example.type
1010
.join(" ")
1111
.toLowerCase();
1212
---
13-
<a
14-
href={detailUrl}
15-
class="example-card group flex flex-col gap-3 rounded-xl border border-neutral-200 bg-white p-5 transition-colors hover:border-neutral-400 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:border-neutral-500"
13+
{/* The title link is stretched over the whole card (after:inset-0), so the
14+
card stays one click target while the download link sits above it. */}
15+
<div
16+
class="example-card group relative flex flex-col gap-3 rounded-xl border border-neutral-200 bg-white p-5 transition-colors hover:border-neutral-400 focus-within:border-neutral-400 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:border-neutral-500 dark:focus-within:border-neutral-500"
1617
data-section={example.section}
1718
data-type={example.type}
1819
data-components={example.components.join("|")}
@@ -22,27 +23,43 @@ const searchText = [example.title, example.description, example.id, example.type
2223
>
2324
<div class="flex items-center justify-between">
2425
<TypeIcon type={example.type} />
25-
{badge === "Workday" ? (
26-
<span class="inline-flex items-center gap-1.5 rounded-full bg-wd-blue/10 px-2.5 py-1 text-xs font-semibold text-wd-blue">
27-
<svg viewBox="0 0 24 24" width="13" height="13" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
28-
<circle cx="12" cy="12" r="9"></circle>
29-
<path d="m9 12 2 2 4-4"></path>
26+
<div class="flex items-center gap-2">
27+
{badge === "Workday" ? (
28+
<span class="inline-flex items-center gap-1.5 rounded-full bg-wd-blue/10 px-2.5 py-1 text-xs font-semibold text-wd-blue">
29+
<svg viewBox="0 0 24 24" width="13" height="13" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
30+
<circle cx="12" cy="12" r="9"></circle>
31+
<path d="m9 12 2 2 4-4"></path>
32+
</svg>
33+
Workday
34+
</span>
35+
) : (
36+
<span class="inline-flex items-center gap-1.5 rounded-full border border-neutral-200 px-2.5 py-1 text-xs font-medium text-neutral-500 dark:border-neutral-800 dark:text-neutral-400">
37+
<svg viewBox="0 0 24 24" width="13" height="13" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
38+
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
39+
<circle cx="9" cy="7" r="4"></circle>
40+
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
41+
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
42+
</svg>
43+
Community
44+
</span>
45+
)}
46+
<a
47+
href={downloadUrl(example)}
48+
download={downloadName(example)}
49+
title="Download zip"
50+
aria-label={`Download ${example.title} as a zip`}
51+
class="relative z-10 flex h-7 w-7 items-center justify-center rounded-md text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-900 dark:hover:bg-neutral-900 dark:hover:text-neutral-100"
52+
>
53+
<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
54+
<path d="M12 3v12m0 0 4-4m-4 4-4-4"></path>
55+
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"></path>
3056
</svg>
31-
Workday
32-
</span>
33-
) : (
34-
<span class="inline-flex items-center gap-1.5 rounded-full border border-neutral-200 px-2.5 py-1 text-xs font-medium text-neutral-500 dark:border-neutral-800 dark:text-neutral-400">
35-
<svg viewBox="0 0 24 24" width="13" height="13" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
36-
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
37-
<circle cx="9" cy="7" r="4"></circle>
38-
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
39-
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
40-
</svg>
41-
Community
42-
</span>
43-
)}
57+
</a>
58+
</div>
4459
</div>
45-
<h2 class="text-base font-semibold tracking-tight">{example.title}</h2>
60+
<h2 class="text-base font-semibold tracking-tight">
61+
<a href={detailUrl} class="after:absolute after:inset-0 after:content-[''] focus:outline-none">{example.title}</a>
62+
</h2>
4663
<p class="flex-1 text-sm leading-relaxed text-neutral-500 dark:text-neutral-400">{example.description}</p>
4764
<div class="flex flex-wrap gap-1.5">
4865
{tags.map((tag) => (
@@ -51,4 +68,4 @@ const searchText = [example.title, example.description, example.id, example.type
5168
</span>
5269
))}
5370
</div>
54-
</a>
71+
</div>

‎site/src/components/EntryPage.astro‎

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import "../styles/global.css";
33
import Header from "./Header.astro";
44
import Footer from "./Footer.astro";
5-
import { config, withBase, sourceLabel } from "../lib/examples.js";
5+
import { config, withBase, sourceLabel, downloadUrl, downloadName, sparseCheckout } from "../lib/examples.js";
66
77
const { example } = Astro.props;
88
@@ -25,7 +25,7 @@ const badge = sourceLabel(example);
2525
<head>
2626
<meta charset="utf-8" />
2727
<meta name="viewport" content="width=device-width, initial-scale=1" />
28-
<title>{example.title} | Workday Examples Hub</title>
28+
<title>{example.title} | Workday Developer Program</title>
2929
<meta name="description" content={example.description} />
3030
<link rel="icon" type="image/png" href={withBase("favicon.png")} />
3131
<link rel="preconnect" href="https://fonts.googleapis.com" />
@@ -89,7 +89,7 @@ const badge = sourceLabel(example);
8989
</div>
9090
</div>
9191
</div>
92-
<div class="flex shrink-0 gap-2.5">
92+
<div class="flex shrink-0 flex-wrap gap-2.5">
9393
{example.tutorial && (
9494
<a
9595
href={example.tutorial}
@@ -104,13 +104,49 @@ const badge = sourceLabel(example);
104104
href={codeUrl}
105105
target="_blank"
106106
rel="noopener"
107-
class="rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-neutral-700 dark:bg-white dark:text-black dark:hover:bg-neutral-200"
107+
class="rounded-md border border-neutral-200 px-4 py-2 text-sm font-medium text-neutral-700 transition-colors hover:bg-neutral-100 dark:border-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-900"
108108
>
109109
View code
110110
</a>
111+
<a
112+
href={downloadUrl(example)}
113+
download={downloadName(example)}
114+
class="inline-flex items-center gap-2 rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-neutral-700 dark:bg-white dark:text-black dark:hover:bg-neutral-200"
115+
>
116+
<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
117+
<path d="M12 3v12m0 0 4-4m-4 4-4-4"></path>
118+
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"></path>
119+
</svg>
120+
Download zip
121+
</a>
111122
</div>
112123
</div>
113124

125+
<details class="mt-6 rounded-lg border border-neutral-200 dark:border-neutral-800">
126+
<summary class="cursor-pointer select-none px-4 py-3 text-sm font-medium text-neutral-700 dark:text-neutral-300">
127+
Get it with git instead
128+
</summary>
129+
<div class="border-t border-neutral-200 px-4 py-4 dark:border-neutral-800">
130+
<p class="text-sm text-neutral-500 dark:text-neutral-400">
131+
Check out only this folder and keep the git history, so you can open a pull request from the same checkout.
132+
See the <a href={contributeUrl} target="_blank" rel="noopener" class="text-wd-blue hover:underline">contribution guide</a>.
133+
</p>
134+
<div class="relative mt-3">
135+
<pre class="overflow-x-auto rounded-md bg-neutral-100 p-4 pr-24 text-[13px] leading-relaxed text-neutral-800 dark:bg-neutral-900 dark:text-neutral-200"><code id="sparse-checkout">{sparseCheckout(example)}</code></pre>
136+
<button
137+
type="button"
138+
id="copy-sparse-checkout"
139+
class="absolute right-2 top-2 rounded-md border border-neutral-200 bg-white px-2.5 py-1 text-xs font-medium text-neutral-700 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-950 dark:text-neutral-300 dark:hover:bg-neutral-800"
140+
>
141+
Copy
142+
</button>
143+
</div>
144+
<p class="mt-3 text-xs text-neutral-500 dark:text-neutral-400">
145+
The zip carries the same trail: its <code>SOURCE.md</code> records the commit it was built from and these commands.
146+
</p>
147+
</div>
148+
</details>
149+
114150
<div class="mt-10 flex flex-col gap-12 lg:flex-row">
115151
<article class="prose prose-neutral min-w-0 max-w-3xl flex-1 dark:prose-invert prose-headings:scroll-mt-24 prose-headings:font-semibold prose-headings:tracking-tight">
116152
{Content ? <Content /> : <p>This example has no README yet.</p>}
@@ -140,5 +176,27 @@ const badge = sourceLabel(example);
140176
</main>
141177

142178
<Footer contributeUrl={contributeUrl} />
179+
180+
<script>
181+
const copyButton = document.getElementById("copy-sparse-checkout");
182+
const snippet = document.getElementById("sparse-checkout");
183+
copyButton?.addEventListener("click", async () => {
184+
try {
185+
await navigator.clipboard.writeText(snippet?.textContent ?? "");
186+
copyButton.textContent = "Copied";
187+
} catch {
188+
// Clipboard blocked: select the snippet so a manual copy works.
189+
if (snippet) {
190+
const range = document.createRange();
191+
range.selectNodeContents(snippet);
192+
const selection = window.getSelection();
193+
selection?.removeAllRanges();
194+
selection?.addRange(range);
195+
}
196+
copyButton.textContent = "Selected";
197+
}
198+
setTimeout(() => (copyButton.textContent = "Copy"), 1500);
199+
});
200+
</script>
143201
</body>
144202
</html>

‎site/src/components/Header.astro‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const { repoUrl } = Astro.props;
77
<div class="mx-auto flex h-16 max-w-7xl items-center justify-between px-6">
88
<a href={withBase("")} class="flex items-center gap-2.5 text-[15px] font-semibold tracking-tight">
99
<img src={withBase("favicon.png")} alt="" width="24" height="24" class="h-6 w-6 rounded-full" />
10-
Examples and usecases
10+
Workday Developer Program
1111
</a>
1212
<a
1313
href={repoUrl}

0 commit comments

Comments
 (0)