diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
index 57078ff..979b6df 100644
--- a/.github/workflows/deploy.yaml
+++ b/.github/workflows/deploy.yaml
@@ -1,56 +1,39 @@
name: Deploy to GitHub Pages
on:
- workflow_dispatch:
+ # Trigger the workflow every time you push to the `main` branch
+ # Using a different branch name? Replace `main` with your branch’s name
push:
- branches:
- - main
- # Review gh actions docs if you want to further define triggers, paths, etc
- # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on
+ branches: [ main ]
+ # Allows you to run this workflow manually from the Actions tab on GitHub.
+ workflow_dispatch:
+
+# Allow this job to clone the repo and create a page deployment
+permissions:
+ contents: read
+ pages: write
+ id-token: write
jobs:
build:
- name: Build Site
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- with:
- fetch-depth: 0
- - uses: actions/setup-node@v4
- with:
- node-version: 20
- cache: npm
-
- - name: Install dependencies
- run: npm install
- - name: Run astro check
- run: npm run astro check
- - name: Run lint
- run: npm run lint
- - name: Test build website
- run: npm run build
-
- - name: Upload Build Artifact
- uses: actions/upload-pages-artifact@v3
- with:
- path: dist
+ - name: Checkout your repository using git
+ uses: actions/checkout@v4
+ - name: Install, build, and upload your site
+ uses: withastro/action@v3
+ # with:
+ # path: . # The root location of your Astro project inside the repository. (optional)
+ # node-version: 20 # The specific version of Node that should be used to build your site. Defaults to 20. (optional)
+ # package-manager: pnpm@latest # The Node package manager that should be used to install dependencies and build your site. Automatically detected based on your lockfile. (optional)
deploy:
- name: Deploy to GitHub Pages
needs: build
-
- # Grant GITHUB_TOKEN the permissions required to make a Pages deployment
- permissions:
- pages: write # to deploy to Pages
- id-token: write # to verify the deployment originates from an appropriate source
-
- # Deploy to the github-pages environment
+ runs-on: ubuntu-latest
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
+ uses: actions/deploy-pages@v4
\ No newline at end of file
diff --git a/README.md b/README.md
index 3f87e3f..8abd83b 100644
--- a/README.md
+++ b/README.md
@@ -63,10 +63,21 @@ EducatesHub is a static site built with [Astro](https://astro.build/) and TypeSc
3. **Open your browser:**
Visit [http://localhost:4321](http://localhost:4321)
-## Customizing Workshops
+## Adding Educates Resources
-- Add or edit YAML files in `src/data/` to update the workshop catalog.
+- Add or edit YAML files in `src/content/` to update the workshop, extensions, kyverno policies or themes catalog.
- Images referenced in YAML should be placed in `public/images/` or mapped in the loader utility.
+- Fetch referenced assets, run `./fetch_assets.sh` to fetch referenced workshops, kyverno policies, themes and extensions.
+
+## Release a new version of the site
+
+- Always work in develop branch
+- Make sure that everything works locally and make astro check:
+ ```
+ npm run astro check
+ ```
+- When things are ready to be released, merge code into main via PR (this will publish the site)
+- Rebase develop on main
## Contributing
diff --git a/astro.config.mjs b/astro.config.mjs
index f17737c..d154610 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -6,4 +6,5 @@ import react from "@astrojs/react";
// https://astro.build/config
export default defineConfig({
integrations: [react()],
+ site: 'https://hub.educates.dev',
});
diff --git a/fetch_assets.sh b/fetch_assets.sh
new file mode 100755
index 0000000..1654155
--- /dev/null
+++ b/fetch_assets.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+set -e
+
+WORKSHOP_DIR="src/content/workshops"
+ASSET_DIR="public/assets/workshops"
+mkdir -p "$ASSET_DIR"
+
+for yaml in "$WORKSHOP_DIR"/*.yaml*; do
+ # Extract slug (first occurrence)
+ slug=$(grep '^slug:' "$yaml" | head -n1 | awk '{print $2}')
+ # Extract install_url (first occurrence)
+ url=$(grep '^install_url:' "$yaml" | head -n1 | awk '{print $2}')
+ if [ -z "$slug" ]; then
+ echo "No slug found in $yaml, skipping."
+ continue
+ fi
+ if [ -n "$url" ]; then
+ outname="$slug.yaml"
+ echo "Downloading $url to $ASSET_DIR/$outname"
+ if curl -sSL --fail "$url" -o "$ASSET_DIR/$outname"; then
+ echo "Downloaded successfully."
+ else
+ echo "Failed to download $url (not found or error)."
+ rm -f "$ASSET_DIR/$outname"
+ fi
+ else
+ echo "No install_url found in $yaml, skipping."
+ fi
+done
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index fd84443..fd1d2f2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,8 +14,12 @@
"@types/react-dom": "^19.1.5",
"astro": "^5.7.13",
"bootstrap": "5.3",
+ "bootstrap-icons": "^1.13.1",
"react": "^19.1.0",
+ "react-bootstrap-icons": "^1.11.6",
"react-dom": "^19.1.0",
+ "react-icons": "^5.5.0",
+ "react-syntax-highlighter": "^15.6.1",
"typescript": "^5.8.3",
"yaml": "^2.8.0"
},
@@ -435,6 +439,15 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.27.3",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.3.tgz",
+ "integrity": "sha512-7EYtGezsdiDMyY80+65EzwiGmcJqpmcZCojSXaRgdrBaGtWTgDZKq69cPIVped6MkIM78cTQ2GOiEYjwOlG4xw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@@ -2801,6 +2814,22 @@
"@popperjs/core": "^2.11.8"
}
},
+ "node_modules/bootstrap-icons": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz",
+ "integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/twbs"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/bootstrap"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/boxen": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz",
@@ -2981,6 +3010,16 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/character-reference-invalid": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
+ "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -3890,6 +3929,19 @@
"reusify": "^1.0.4"
}
},
+ "node_modules/fault": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
+ "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
+ "license": "MIT",
+ "dependencies": {
+ "format": "^0.2.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/fdir": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
@@ -4003,6 +4055,14 @@
"unicode-trie": "^2.0.0"
}
},
+ "node_modules/format": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
+ "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
+ "engines": {
+ "node": ">=0.4.x"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -4288,6 +4348,21 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/highlight.js": {
+ "version": "10.7.3",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
+ "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/highlightjs-vue": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
+ "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
+ "license": "CC0-1.0"
+ },
"node_modules/html-escaper": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
@@ -4366,6 +4441,30 @@
"url": "https://github.com/sponsors/brc-dd"
}
},
+ "node_modules/is-alphabetical": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
+ "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
+ "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^1.0.0",
+ "is-decimal": "^1.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
@@ -4373,6 +4472,16 @@
"license": "MIT",
"optional": true
},
+ "node_modules/is-decimal": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
+ "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-docker": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
@@ -4418,6 +4527,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-hexadecimal": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
+ "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/is-inside-container": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
@@ -4870,6 +4989,32 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lowlight": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
+ "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
+ "license": "MIT",
+ "dependencies": {
+ "fault": "^1.0.0",
+ "highlight.js": "~10.7.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
@@ -5862,6 +6007,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/ofetch": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.4.1.tgz",
@@ -6027,6 +6181,44 @@
"node": ">=6"
}
},
+ "node_modules/parse-entities": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
+ "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^1.0.0",
+ "character-entities-legacy": "^1.0.0",
+ "character-reference-invalid": "^1.0.0",
+ "is-alphanumerical": "^1.0.0",
+ "is-decimal": "^1.0.0",
+ "is-hexadecimal": "^1.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/character-entities": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
+ "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/character-entities-legacy": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
+ "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/parse-latin": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz",
@@ -6215,6 +6407,17 @@
"node": ">=6"
}
},
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
"node_modules/property-information": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
@@ -6270,6 +6473,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-bootstrap-icons": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/react-bootstrap-icons/-/react-bootstrap-icons-1.11.6.tgz",
+ "integrity": "sha512-ycXiyeSyzbS1C4+MlPTYe0riB+UlZ7LV7YZQYqlERV2cxDiKtntI0huHmP/3VVvzPt4tGxqK0K+Y6g7We3U6tQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.7.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.6"
+ }
+ },
"node_modules/react-dom": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
@@ -6282,6 +6497,21 @@
"react": "^19.1.0"
}
},
+ "node_modules/react-icons": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
+ "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
@@ -6291,6 +6521,23 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-syntax-highlighter": {
+ "version": "15.6.1",
+ "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz",
+ "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "highlight.js": "^10.4.1",
+ "highlightjs-vue": "^1.0.0",
+ "lowlight": "^1.17.0",
+ "prismjs": "^1.27.0",
+ "refractor": "^3.6.0"
+ },
+ "peerDependencies": {
+ "react": ">= 0.14.0"
+ }
+ },
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -6304,6 +6551,105 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/refractor": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz",
+ "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==",
+ "license": "MIT",
+ "dependencies": {
+ "hastscript": "^6.0.0",
+ "parse-entities": "^2.0.0",
+ "prismjs": "~1.27.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/@types/hast": {
+ "version": "2.3.10",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz",
+ "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2"
+ }
+ },
+ "node_modules/refractor/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
+ "node_modules/refractor/node_modules/comma-separated-tokens": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
+ "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/hast-util-parse-selector": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
+ "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/refractor/node_modules/hastscript": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz",
+ "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "comma-separated-tokens": "^1.0.0",
+ "hast-util-parse-selector": "^2.0.0",
+ "property-information": "^5.0.0",
+ "space-separated-tokens": "^1.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/refractor/node_modules/prismjs": {
+ "version": "1.27.0",
+ "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz",
+ "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/refractor/node_modules/property-information": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz",
+ "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/refractor/node_modules/space-separated-tokens": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
+ "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz",
@@ -7859,6 +8205,15 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
"node_modules/xxhash-wasm": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz",
diff --git a/package.json b/package.json
index 8a5082d..a08e5fc 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,10 @@
"astro": "^5.7.13",
"bootstrap": "5.3",
"react": "^19.1.0",
+ "react-bootstrap-icons": "^1.11.6",
"react-dom": "^19.1.0",
+ "react-icons": "^5.5.0",
+ "react-syntax-highlighter": "^15.6.1",
"typescript": "^5.8.3",
"yaml": "^2.8.0"
},
diff --git a/public/CNAME b/public/CNAME
new file mode 100644
index 0000000..cad3f81
--- /dev/null
+++ b/public/CNAME
@@ -0,0 +1 @@
+hub.educates.dev
\ No newline at end of file
diff --git a/public/assets/kyverno-policies/educates-default.yaml b/public/assets/kyverno-policies/educates-default.yaml
new file mode 100644
index 0000000..f4ea9e9
--- /dev/null
+++ b/public/assets/kyverno-policies/educates-default.yaml
@@ -0,0 +1,33 @@
+apiVersion: kyverno.io/v1
+kind: ClusterPolicy
+metadata:
+ name: require-ingress-session-name
+spec:
+ validationFailureAction: enforce
+ background: true
+ rules:
+ - name: require-ingress-session-name
+ match:
+ resources:
+ kinds:
+ - Ingress
+ context:
+ - name: session_namespace
+ apiCall:
+ urlPath: "/api/v1/namespaces/{{request.namespace}}"
+ jmesPath: 'metadata.labels."training.educates.dev/session.name" || ''@'''
+ preconditions:
+ all:
+ - key: "{{ request.operation }}"
+ operator: AnyIn
+ value: ["CREATE", "UPDATE"]
+ validate:
+ message: "Ingress host name must embed the workshop session name."
+ foreach:
+ - list: "request.object.spec.rules"
+ deny:
+ conditions:
+ any:
+ - key: "{{ contains(element.host, session_namespace) }}"
+ operator: NotEquals
+ value: true
\ No newline at end of file
diff --git a/public/assets/workshops/containerizing-a-spring-application.yaml b/public/assets/workshops/containerizing-a-spring-application.yaml
new file mode 100644
index 0000000..646ed3b
--- /dev/null
+++ b/public/assets/workshops/containerizing-a-spring-application.yaml
@@ -0,0 +1,36 @@
+apiVersion: training.educates.dev/v1beta1
+kind: Workshop
+metadata:
+ name: lab-containerizing-spring
+spec:
+ description: A workshop describing how to run a Spring Boot application in docker.
+ session:
+ applications:
+ docker:
+ enabled: true
+ editor:
+ enabled: true
+ terminal:
+ enabled: true
+ layout: split
+ ingresses:
+ - name: demo
+ port: 8080
+ namespaces:
+ security:
+ token:
+ enabled: false
+ resources:
+ memory: 1Gi
+ title: Containerizing a Spring Boot application
+ version: "4.3"
+ workshop:
+ files:
+ - image:
+ url: ghcr.io/educates/lab-containerizing-spring-files:4.3
+ includePaths:
+ - /workshop/**
+ - /exercises/**
+ - /README.md
+ newRootPath: workshops/lab-containerizing-spring
+ image: jdk17-environment:*
diff --git a/public/assets/workshops/creating-a-spring-application.yaml b/public/assets/workshops/creating-a-spring-application.yaml
new file mode 100644
index 0000000..0b5af50
--- /dev/null
+++ b/public/assets/workshops/creating-a-spring-application.yaml
@@ -0,0 +1,84 @@
+apiVersion: training.educates.dev/v1beta1
+kind: Workshop
+metadata:
+ name: lab-creating-a-spring-app
+spec:
+ description: A workshop introducing the start.spring.io web site.
+ environment:
+ objects:
+ - apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+ labels:
+ app: initializr
+ name: initializr
+ spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: initializr
+ template:
+ metadata:
+ labels:
+ app: initializr
+ spec:
+ containers:
+ - image: ghcr.io/vmware-tanzu-labs/educates-spring-initializr:2.0
+ imagePullPolicy: Always
+ name: dashboard
+ ports:
+ - containerPort: 8080
+ name: 8080-tcp
+ protocol: TCP
+ - apiVersion: v1
+ kind: Service
+ metadata:
+ labels:
+ app: initializr
+ name: initializr
+ spec:
+ ports:
+ - name: 8080-tcp
+ port: 8080
+ protocol: TCP
+ targetPort: 8080
+ selector:
+ app: initializr
+ type: ClusterIP
+ session:
+ applications:
+ editor:
+ enabled: true
+ terminal:
+ enabled: true
+ layout: split
+ dashboards:
+ - name: Initializr
+ url: $(ingress_protocol)://initializr-$(session_namespace).$(ingress_domain)
+ ingresses:
+ - host: initializr.$(workshop_namespace).svc.cluster.local
+ name: initializr
+ port: 8080
+ protocol: http
+ - name: demo
+ port: 8080
+ namespaces:
+ security:
+ token:
+ enabled: false
+ resources:
+ memory: 1Gi
+ title: Creating a Spring Boot application
+ version: "4.3"
+ workshop:
+ files:
+ - image:
+ url: ghcr.io/educates/labs-creating-a-spring-app-files:4.3
+ includePaths:
+ - /workshop/**
+ - /exercises/**
+ - /README.md
+ - image:
+ url: ghcr.io/educates/lab-spring-boot-on-k8s-maven:latest
+ path: .m2
+ image: jdk17-environment:*
diff --git a/public/assets/workshops/lab-kubernetes-fundamentals.yaml b/public/assets/workshops/lab-kubernetes-fundamentals.yaml
new file mode 100644
index 0000000..d717a97
--- /dev/null
+++ b/public/assets/workshops/lab-kubernetes-fundamentals.yaml
@@ -0,0 +1,33 @@
+apiVersion: training.educates.dev/v1beta1
+kind: Workshop
+metadata:
+ name: lab-k8s-fundamentals
+spec:
+ description: An interactive workshop on Kubernetes fundamentals.
+ session:
+ applications:
+ console:
+ enabled: true
+ editor:
+ enabled: true
+ slides:
+ enabled: true
+ reveal.js:
+ version: 3.x
+ terminal:
+ enabled: true
+ layout: split
+ namespaces:
+ budget: medium
+ security:
+ policy: restricted
+ title: Kubernetes Fundamentals
+ version: "8.3"
+ workshop:
+ files:
+ - image:
+ url: ghcr.io/educates/lab-k8s-fundamentals-files:8.3
+ includePaths:
+ - /workshop/**
+ - /templates/**
+ - /README.md
diff --git a/public/assets/workshops/lab-spring-boot-on-k8s.yaml b/public/assets/workshops/lab-spring-boot-on-k8s.yaml
new file mode 100644
index 0000000..f71132e
--- /dev/null
+++ b/public/assets/workshops/lab-spring-boot-on-k8s.yaml
@@ -0,0 +1,89 @@
+apiVersion: training.educates.dev/v1beta1
+kind: Workshop
+metadata:
+ name: lab-spring-boot-on-k8s
+spec:
+ description: Introduction to Spring Boot on Kubernetes
+ environment:
+ objects:
+ - apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+ labels:
+ app: initializr
+ name: initializr
+ spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: initializr
+ template:
+ metadata:
+ labels:
+ app: initializr
+ spec:
+ containers:
+ - image: ghcr.io/vmware-tanzu-labs/educates-spring-initializr:2.0
+ imagePullPolicy: Always
+ name: dashboard
+ ports:
+ - containerPort: 8080
+ name: 8080-tcp
+ protocol: TCP
+ - apiVersion: v1
+ kind: Service
+ metadata:
+ labels:
+ app: initializr
+ name: initializr
+ spec:
+ ports:
+ - name: 8080-tcp
+ port: 8080
+ protocol: TCP
+ targetPort: 8080
+ selector:
+ app: initializr
+ type: ClusterIP
+ session:
+ applications:
+ console:
+ enabled: true
+ vendor: octant
+ docker:
+ enabled: true
+ editor:
+ enabled: true
+ registry:
+ enabled: true
+ terminal:
+ enabled: true
+ layout: split
+ dashboards:
+ - name: Initializr
+ url: $(ingress_protocol)://initializr-$(session_namespace).$(ingress_domain)
+ ingresses:
+ - host: initializr.$(workshop_namespace).svc.cluster.local
+ name: initializr
+ port: 8080
+ protocol: http
+ namespaces:
+ budget: large
+ security:
+ policy: baseline
+ resources:
+ memory: 2Gi
+ title: Spring Boot on Kubernetes
+ version: "4.3"
+ workshop:
+ files:
+ - image:
+ url: ghcr.io/educates/lab-spring-boot-on-k8s-files:4.3
+ includePaths:
+ - /workshop/**
+ - /exercises/**
+ - /README.md
+ - image:
+ url: ghcr.io/educates/lab-spring-boot-on-k8s-maven:latest
+ path: .m2
+ image: jdk17-environment:*
diff --git a/src/components/WorkshopGrid.jsx b/src/components/EducatesResourceGrid.jsx
similarity index 71%
rename from src/components/WorkshopGrid.jsx
rename to src/components/EducatesResourceGrid.jsx
index 9ad6904..7d84ee0 100644
--- a/src/components/WorkshopGrid.jsx
+++ b/src/components/EducatesResourceGrid.jsx
@@ -1,8 +1,9 @@
import { useState, useMemo } from "react";
+import EducatesResourceTile from './EducatesResourceTile.jsx';
const PAGE_SIZE_OPTIONS = [12, 24, 48];
-export default function WorkshopGrid({ workshops }) {
+export default function EducatesResourceGrid({ resources = [] }) {
const [search, setSearch] = useState("");
const [selectedLabels, setSelectedLabels] = useState([]);
const [page, setPage] = useState(1);
@@ -10,13 +11,13 @@ export default function WorkshopGrid({ workshops }) {
// Get all unique labels
const allLabels = useMemo(
- () => Array.from(new Set(workshops.flatMap((w) => w.labels || []))).sort(),
- [workshops],
+ () => Array.from(new Set(resources.flatMap((w) => w.labels || []))).sort(),
+ [resources],
);
// Filtering logic
const filtered = useMemo(() => {
- return workshops.filter((w) => {
+ return resources.filter((w) => {
const matchesSearch =
w.title.toLowerCase().includes(search.toLowerCase()) ||
w.description.toLowerCase().includes(search.toLowerCase());
@@ -25,7 +26,7 @@ export default function WorkshopGrid({ workshops }) {
selectedLabels.every((l) => (w.labels || []).includes(l));
return matchesSearch && matchesLabels;
});
- }, [workshops, search, selectedLabels]);
+ }, [resources, search, selectedLabels]);
// Pagination logic
const totalPages = Math.ceil(filtered.length / pageSize);
@@ -96,7 +97,7 @@ export default function WorkshopGrid({ workshops }) {
- {paginated.map((workshop) => (
-
diff --git a/src/components/EducatesResourceSelectorIsland.jsx b/src/components/EducatesResourceSelectorIsland.jsx
new file mode 100644
index 0000000..297e261
--- /dev/null
+++ b/src/components/EducatesResourceSelectorIsland.jsx
@@ -0,0 +1,62 @@
+import React, { useState, useEffect } from "react";
+import EducatesResourceGrid from "./EducatesResourceGrid.jsx";
+
+function getTypeFromUrl(defaultType, allResources) {
+ if (typeof window === 'undefined') return defaultType;
+ const params = new URLSearchParams(window.location.search);
+ const type = params.get('type');
+ if (type && Object.keys(allResources).includes(type)) {
+ return type;
+ }
+ return defaultType;
+}
+
+export default function EducatesResourceSelectorIsland({ allResources }) {
+ const defaultType = "Workshop";
+ const [type, setType] = useState(() => getTypeFromUrl(defaultType, allResources));
+ const [items, setItems] = useState(allResources[type] || []);
+
+ useEffect(() => {
+ setItems(allResources[type] || []);
+ // Update the URL query parameter when type changes
+ if (typeof window !== 'undefined') {
+ const params = new URLSearchParams(window.location.search);
+ params.set('type', type);
+ window.history.replaceState({}, '', `${window.location.pathname}?${params.toString()}`);
+ }
+ }, [type, allResources]);
+
+ useEffect(() => {
+ // Listen for popstate (back/forward navigation)
+ const onPopState = () => {
+ setType(getTypeFromUrl(defaultType, allResources));
+ };
+ window.addEventListener('popstate', onPopState);
+ return () => window.removeEventListener('popstate', onPopState);
+ }, [allResources]);
+
+ return (
+ <>
+
+
Educates Resource Type:
+ {Object.keys(allResources).map((key) => (
+
+ setType(key)}
+ />
+
+ {key.replace(/([A-Z])/g, " $1").trim()}
+
+
+ ))}
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/components/EducatesResourceTile.jsx b/src/components/EducatesResourceTile.jsx
new file mode 100644
index 0000000..463b679
--- /dev/null
+++ b/src/components/EducatesResourceTile.jsx
@@ -0,0 +1,31 @@
+import React from "react";
+
+export default function EducatesResourceTile({ educatesResource, class: className = "" }) {
+ return (
+
+
+
+
+
{educatesResource.title}
+
{educatesResource.description}
+
+ {(educatesResource.labels || []).map((label) => (
+
+ {label}
+
+ ))}
+
+
By {educatesResource.author}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/ExtensionPackageDetail.jsx b/src/components/ExtensionPackageDetail.jsx
new file mode 100644
index 0000000..5dfbb60
--- /dev/null
+++ b/src/components/ExtensionPackageDetail.jsx
@@ -0,0 +1,257 @@
+import { useState, useEffect } from 'react';
+import InstallIsland from './InstallIsland.jsx';
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
+import { Clipboard, X } from 'react-bootstrap-icons';
+import { fetchSourceFile, handleDownloadSourceFile, parseOciUrl } from '../utils/sources.ts';
+
+export default function ExtensionPackageDetail({ extensionPackage }) {
+
+
+ const { repo, prefix, version, destination } = parseOciUrl(extensionPackage.oci_image);
+
+ const sampleSourceCopyContent =
+ ` packages:
+ - name: ${extensionPackage.slug}
+ files:
+ - path: .
+ image:
+ url: ${extensionPackage.oci_image}
+ ...`
+
+ const sampleSource =
+ ` apiVersion: training.educates.dev/v1beta1
+ kind: Workshop
+ metadata:
+ name: "your-workshop"
+ spec:
+ ...
+ workshop:
+ files:
+ - image:
+ url: $(image_repository)/your-workshop-files:$(workshop_version)
+ path: .
+ ${sampleSourceCopyContent}`
+
+
+ const sampleSourceWithCacheCopyContent =
+ ` packages:
+ - name: ${extensionPackage.slug}
+ files:
+ - path: .
+ image:
+ url: $(oci_image_cache)/${destination}:${version}`
+
+ const sampleSourceWithCache =
+ ` apiVersion: training.educates.dev/v1beta1
+ kind: Workshop
+ metadata:
+ name: "your-workshop"
+ spec:
+ ...
+ workshop:
+ files:
+ - image:
+ url: $(image_repository)/your-workshop-files:$(workshop_version)
+ path: .
+ ${sampleSourceWithCacheCopyContent}
+ ...`
+
+ const ociImageCacheCopyContent = `environment:
+ images:
+ registries:
+ - content:
+ - destination: ${destination}
+ prefix: ${prefix}
+ stripPrefix: true
+ onDemand: true
+ urls:
+ - https://${repo}`
+
+ const ociImageCache = `...
+ ${ociImageCacheCopyContent}
+ ...`
+
+ useEffect(() => {
+ async function fetchSource() {
+ const source = await fetchSourceFile("extension-packages", extensionPackage.slug);
+ setSourceContent(source);
+ }
+ fetchSource();
+ }, [extensionPackage]);
+
+ return (
+
+
+
+
+
window.history.back()}
+ >
+
+
+
+ {/* Left: Image, Title, Labels, Main Info */}
+
+
+
+
+
{extensionPackage.title}
+
+ {extensionPackage.labels && extensionPackage.labels.map((label) => (
+
+ {label}
+
+ ))}
+
+
+
+
+
DESCRIPTION:
+
{extensionPackage.description}
+
+ {extensionPackage.release_notes && extensionPackage.release_notes.trim() !== '' && (
+
+
RELEASE NOTES:
+
{extensionPackage.release_notes}
+
+ )}
+ {extensionPackage.oci_image && ( // Only show if the extension package has an OCI image
+
+
USING THE EXTENSION PACKAGE:
+
Add to your workshop:
+
+ { navigator.clipboard.writeText(sampleSourceCopyContent) }}
+ >
+ Copy YAML
+
+
+
+ {
+ let style = { display: 'block' };
+ if (lineNumber >= 12 && lineNumber <= 17) {
+ style.backgroundColor = '#dbffdb';
+ }
+ return { style };
+ }}
+ >
+ {sampleSource}
+
+
+
+ Note that typically you would cache the extension images locally to speed up the process. For that you can use
+ Educates built in
OCI image cache
+ functionality, in that case, replace the extension package image reference
+ with the local image reference.
+
+
+ { navigator.clipboard.writeText(sampleSourceWithCacheCopyContent) }}
+ >
+ Copy YAML
+
+
+
+ {
+ let style = { display: 'block' };
+ if (lineNumber >= 12 && lineNumber <= 17) {
+ style.backgroundColor = '#dbffdb';
+ }
+ return { style };
+ }}>
+ {sampleSourceWithCache}
+
+
+
+ This feature, makes the url of the extension on the previous snippet different than if you
+ were to fetch the upstream extension {extensionPackage.oci_image}
+ but the one from the cache $(oci_image_cache)/{destination}:{version}
+
+
+ { navigator.clipboard.writeText(ociImageCacheCopyContent) }}
+ >
+ Copy YAML
+
+
+
+ {
+ let style = { display: 'block' };
+ if (lineNumber >= 2 && lineNumber <= 11) {
+ style.backgroundColor = '#dbffdb';
+ }
+ return { style };
+ }}
+ >
+ {ociImageCache}
+
+
+
+ )}
+
+ {/* Right: Actions and Repo Info */}
+
+ {extensionPackage.repo_url && (
+
+ )}
+ {extensionPackage.oci_image && (
+
+
OCI IMAGE:
+
{extensionPackage.oci_image}
+
+ )}
+ {(extensionPackage.author || extensionPackage.date_created || extensionPackage.version) && (
+
+ {extensionPackage.author &&
Author: {extensionPackage.author}
}
+ {extensionPackage.date_created &&
Date: {new Date(extensionPackage.date_created).toLocaleDateString()}
}
+ {extensionPackage.version &&
Version: {extensionPackage.version}
}
+
+ )}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/FilterSidebar.astro b/src/components/FilterSidebar.astro
deleted file mode 100644
index 1e7c029..0000000
--- a/src/components/FilterSidebar.astro
+++ /dev/null
@@ -1,35 +0,0 @@
----
-const { allLabels } = Astro.props;
----
-
-
-
-
-
Filter by Label
-
Clear All
-
- {
- allLabels.map((label: string) => (
-
-
-
- {label}
-
-
- ))
- }
-
-
-
-
diff --git a/src/components/InstallIsland.jsx b/src/components/InstallIsland.jsx
new file mode 100644
index 0000000..054e786
--- /dev/null
+++ b/src/components/InstallIsland.jsx
@@ -0,0 +1,25 @@
+import { useState } from 'react';
+import InstallModal from './InstallModal.jsx';
+
+export default function InstallIsland({ downloadUrl }) {
+ const [show, setShow] = useState(false);
+ return (
+ <>
+
setShow(true)}>
+ Install
+
+
setShow(false)}
+ downloadUrl={downloadUrl}
+ />
+ {show && (
+ setShow(false)}
+ >
+ )}
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/components/InstallModal.jsx b/src/components/InstallModal.jsx
new file mode 100644
index 0000000..7af675e
--- /dev/null
+++ b/src/components/InstallModal.jsx
@@ -0,0 +1,79 @@
+import { useRef, useState, useEffect } from 'react';
+
+export default function InstallModal({ show, onClose, downloadUrl }) {
+ const [copyStatus, setCopyStatus] = useState('Copy');
+ const installCmdRef = useRef(null);
+ const modalRef = useRef(null);
+
+ useEffect(() => {
+ if (window.bootstrap && modalRef.current) {
+ const modal = new window.bootstrap.Modal(modalRef.current, {
+ backdrop: 'static',
+ });
+ if (show) {
+ modal.show();
+ } else {
+ modal.hide();
+ }
+ return () => {
+ modal.hide();
+ };
+ }
+ }, [show]);
+
+ const handleCopy = () => {
+ if (installCmdRef.current) {
+ const text = installCmdRef.current.innerText;
+ navigator.clipboard.writeText(text).then(() => {
+ setCopyStatus('Copied!');
+ setTimeout(() => setCopyStatus('Copy'), 1500);
+ });
+ }
+ };
+
+ return (
+
+
+
+
+
+ Install Instructions
+
+
+
+
+
+ Install Command
+
+ {copyStatus}
+
+
+
+{`educates deploy-workshop -f ${downloadUrl}`}
+
+ {/*
Replace my-{workshopSlug} with your desired release name.
*/}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/KyvernoPolicyDetail.jsx b/src/components/KyvernoPolicyDetail.jsx
new file mode 100644
index 0000000..8ab889e
--- /dev/null
+++ b/src/components/KyvernoPolicyDetail.jsx
@@ -0,0 +1,119 @@
+import { useState, useEffect } from 'react';
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
+import { Clipboard, X } from 'react-bootstrap-icons';
+import { fetchSourceFile, handleDownloadSourceFile } from '../utils/sources.ts';
+
+export default function KyvernoPolicyDetail({ kyvernoPolicy }) {
+ const [sourceContent, setSourceContent] = useState("");
+
+ useEffect(() => {
+ async function fetchSource() {
+ const source = await fetchSourceFile("kyverno-policies", kyvernoPolicy.slug);
+ setSourceContent(source);
+ }
+ fetchSource();
+ }, [kyvernoPolicy]);
+
+ return (
+
+
+
+
+
window.history.back()}
+ >
+
+
+
+ {/* Left: Image, Title, Labels, Main Info */}
+
+
+
+
+
{kyvernoPolicy.title}
+
+ {kyvernoPolicy.labels && kyvernoPolicy.labels.map((label) => (
+
+ {label}
+
+ ))}
+
+
+
+
+
DESCRIPTION:
+
{kyvernoPolicy.description}
+
+ {kyvernoPolicy.release_notes && kyvernoPolicy.release_notes.trim() !== '' && (
+
+
RELEASE NOTES:
+
{kyvernoPolicy.release_notes}
+
+ )}
+
+
SOURCE FILE
+ {/* Copy button overlay */}
+
{ navigator.clipboard.writeText(sourceContent) }}
+ >
+
+
+
+
+ {sourceContent}
+
+
+
+
+ {/* Right: Actions and Repo Info */}
+
+
+ handleDownloadSourceFile('kyverno-policies', kyvernoPolicy.slug)}
+ type="button"
+ >
+ Download
+
+
+ {kyvernoPolicy.repo_url && (
+
+ )}
+ {(kyvernoPolicy.author || kyvernoPolicy.date_created || kyvernoPolicy.version) && (
+
+ {kyvernoPolicy.author &&
Author: {kyvernoPolicy.author}
}
+ {kyvernoPolicy.date_created &&
Date: {new Date(kyvernoPolicy.date_created).toLocaleDateString()}
}
+ {kyvernoPolicy.version &&
Version: {kyvernoPolicy.version}
}
+
+ )}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/ThemeDetail.jsx b/src/components/ThemeDetail.jsx
new file mode 100644
index 0000000..6d32f6b
--- /dev/null
+++ b/src/components/ThemeDetail.jsx
@@ -0,0 +1,119 @@
+import { useState, useEffect } from 'react';
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
+import { Clipboard, X } from 'react-bootstrap-icons';
+import { fetchSourceFile, handleDownloadSourceFile } from '../utils/sources.ts';
+
+export default function ThemeDetail({ theme }) {
+ const [sourceContent, setSourceContent] = useState("");
+
+ useEffect(() => {
+ async function fetchSource() {
+ const source = await fetchSourceFile("themes", theme.slug);
+ setSourceContent(source);
+ }
+ fetchSource();
+ }, [theme]);
+
+ return (
+
+
+
+
+
window.history.back()}
+ >
+
+
+
+ {/* Left: Image, Title, Labels, Main Info */}
+
+
+
+
+
{theme.title}
+
+ {theme.labels && theme.labels.map((label) => (
+
+ {label}
+
+ ))}
+
+
+
+
+
DESCRIPTION:
+
{theme.description}
+
+ {theme.release_notes && theme.release_notes.trim() !== '' && (
+
+
RELEASE NOTES:
+
{theme.release_notes}
+
+ )}
+
+
SOURCE FILE
+ {/* Copy button overlay */}
+
{ navigator.clipboard.writeText(sourceContent) }}
+ >
+
+
+
+
+ {sourceContent}
+
+
+
+
+ {/* Right: Actions and Repo Info */}
+
+
+ handleDownloadSourceFile('themes', theme.slug)}
+ type="button"
+ >
+ Download
+
+
+ {theme.repo_url && (
+
+ )}
+ {(theme.author || theme.date_created || theme.version) && (
+
+ {theme.author &&
Author: {theme.author}
}
+ {theme.date_created &&
Date: {new Date(theme.date_created).toLocaleDateString()}
}
+ {theme.version &&
Version: {theme.version}
}
+
+ )}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/ViewSourceIsland.jsx b/src/components/ViewSourceIsland.jsx
new file mode 100644
index 0000000..fe20894
--- /dev/null
+++ b/src/components/ViewSourceIsland.jsx
@@ -0,0 +1,14 @@
+import { useState } from 'react';
+import ViewSourceModal from './ViewSourceModal.jsx';
+
+export default function ViewSourceIsland({ downloadUrl, sourceContent }) {
+ const [show, setShow] = useState(false);
+ return (
+ <>
+ setShow(true)}>
+ View Source
+
+ setShow(false)} downloadUrl={downloadUrl} sourceContent={sourceContent} />
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/components/ViewSourceModal.jsx b/src/components/ViewSourceModal.jsx
new file mode 100644
index 0000000..1954525
--- /dev/null
+++ b/src/components/ViewSourceModal.jsx
@@ -0,0 +1,155 @@
+import { useEffect, useRef, useState } from "react";
+import { IoOpen, IoCopy, IoDownload } from "react-icons/io5";
+
+export default function ViewSourceModal({ show, onClose, downloadUrl, sourceContent }) {
+ const modalRef = useRef(null);
+ const [copyStatus, setCopyStatus] = useState("copy");
+ const [fileContent, setFileContent] = useState("");
+ const [releaseContent, setReleaseContent] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [releaseLoading, setReleaseLoading] = useState(false);
+ const [error, setError] = useState("");
+ const [releaseError, setReleaseError] = useState("");
+
+ useEffect(() => {
+ if (window.bootstrap && modalRef.current) {
+ const modal = new window.bootstrap.Modal(modalRef.current, {
+ backdrop: 'static',
+ });
+ if (show) {
+ modal.show();
+ } else {
+ modal.hide();
+ }
+ return () => {
+ modal.hide();
+ };
+ }
+ }, [show]);
+
+ // If sourceContent is provided, use it as the file content
+ const displayContent = sourceContent !== undefined ? sourceContent : fileContent;
+
+ const logError = (error) => {
+ console.error(error);
+ setError(error.message || error.toString());
+ };
+ const logReleaseError = (error) => {
+ console.error(error);
+ setReleaseError(error.message || error.toString());
+ };
+
+ const handleCopy = () => {
+ if (displayContent && navigator.clipboard) {
+ navigator.clipboard.writeText(displayContent);
+ } else if (navigator.clipboard) {
+ navigator.clipboard.writeText(src);
+ }
+ setCopyStatus("copied");
+ setTimeout(() => setCopyStatus("copy"), 1500);
+ };
+
+ // We link to the download url from GitHub
+ const handleDownload = () => {
+ console.log("downloadUrl: ", downloadUrl);
+ const link = document.createElement('a');
+ link.href = downloadUrl;
+ link.download = downloadUrl.split('/').pop() || 'workshop.yaml';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ };
+
+ const handleOpenInNewTab = () => {
+ if (displayContent) {
+ const blob = new Blob([displayContent], { type: 'text/plain' });
+ const url = URL.createObjectURL(blob);
+ window.open(url, '_blank');
+ } else {
+ window.open(src, '_blank');
+ }
+ };
+
+ return (
+
+
+
+
+
+ Workshop Source
+
+
+
+
+
+
+ {copyStatus === "copied" ? (
+ Copied!
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+ {/* If sourceContent is provided, show it directly, else fallback to old logic */}
+ {sourceContent !== undefined ? (
+
{sourceContent}
+ ) : loading ? (
+
+ Loading...
+
+ ) : error ? (
+
{error}
+ ) : (
+
{fileContent}
+ )}
+ {releaseLoading && (
+
+ Loading release asset...
+
+ )}
+ {releaseError && (
+
{releaseError}
+ )}
+
+
+
NOTE: Keep in mind that this is the source code for the workshop and not the distribution workshop file that is available at{' '}
+
{downloadUrl}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/WorkshopDetail.jsx b/src/components/WorkshopDetail.jsx
new file mode 100644
index 0000000..328cfa9
--- /dev/null
+++ b/src/components/WorkshopDetail.jsx
@@ -0,0 +1,136 @@
+import { useState, useEffect } from 'react';
+import InstallIsland from './InstallIsland.jsx';
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
+import { Clipboard, X } from 'react-bootstrap-icons';
+import { fetchSourceFile, handleDownloadSourceFile, getRepoUrl } from '../utils/sources.ts';
+
+export default function WorkshopDetail({ workshop }) {
+ const [sourceContent, setSourceContent] = useState("");
+
+ useEffect(() => {
+ async function fetchSource() {
+ const source = await fetchSourceFile("workshops", workshop.slug);
+ setSourceContent(source);
+ }
+ fetchSource();
+ }, [workshop]);
+
+ const repoUrl = getRepoUrl(workshop.repository);
+
+ return (
+
+
+
+
+
window.history.back()}
+ >
+
+
+
+ {/* Left: Image, Title, Labels, Main Info */}
+
+
+
+
+
{workshop.title}
+
+ {workshop.labels && workshop.labels.map((label) => (
+
+ {label}
+
+ ))}
+
+
+
+
+
WORKSHOP DESCRIPTION:
+
{workshop.description}
+
+ {workshop.prerequisites && workshop.prerequisites.trim() !== '' && (
+
+
WORKSHOP PREREQUISITES:
+
{workshop.prerequisites}
+
+ )}
+ {workshop.release_notes && workshop.release_notes.trim() !== '' && (
+
+
RELEASE NOTES:
+
{workshop.release_notes}
+
+ )}
+
+
SOURCE FILE
+ {/* Copy button overlay */}
+
{ navigator.clipboard.writeText(sourceContent) }}
+ >
+
+
+
+
+ {sourceContent}
+
+
+
+
+ {/* Right: Actions and Repo Info */}
+
+
+
+ handleDownloadSourceFile('workshops', workshop.slug)}
+ type="button"
+ >
+ Download
+
+
+ {workshop.repository && (workshop.repository.org || workshop.repository.name || workshop.repository.ref || workshop.repository.path) && (
+
+
REPOSITORY:
+
+ {workshop.repository.org && org: {workshop.repository.org} }
+ {workshop.repository.name && name: {workshop.repository.name} }
+ {workshop.repository.ref && ref: {workshop.repository.ref} }
+ {workshop.repository.path && path: {workshop.repository.path} }
+
+ {repoUrl && (
+
+ Go to Repo
+
+ )}
+
+ )}
+ {(workshop.author || workshop.date_created) && (
+
+ {workshop.author &&
Author: {workshop.author}
}
+ {workshop.date_created &&
Date: {new Date(workshop.date_created).toLocaleDateString()}
}
+
+ )}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/WorkshopTile.astro b/src/components/WorkshopTile.astro
deleted file mode 100644
index f2507ba..0000000
--- a/src/components/WorkshopTile.astro
+++ /dev/null
@@ -1,38 +0,0 @@
----
-const { workshop, class: className = "" } = Astro.props;
----
-
-
-
-
-
diff --git a/src/content/config.ts b/src/content/config.ts
new file mode 100644
index 0000000..1201aec
--- /dev/null
+++ b/src/content/config.ts
@@ -0,0 +1,86 @@
+import { defineCollection, z } from "astro:content";
+
+// 2. Import loader(s)
+import { glob, file } from 'astro/loaders';
+
+export const workshops = defineCollection({
+ loader: glob({ pattern: "*.yaml", base: "./src/content/workshops" }),
+ schema: z.object({
+ title: z.string(),
+ slug: z.string().optional(),
+ description: z.string(),
+ prerequisites: z.string().optional(),
+ runs_on_docker: z.boolean().optional(),
+ author: z.string(),
+ image: z.string(),
+ date_created: z.coerce.string().optional(),
+ version: z.coerce.string().optional(),
+ release_notes: z.string().optional(),
+ labels: z.array(z.string()).optional(),
+ install_url: z.string().optional(),
+ repository: z
+ .object({
+ org: z.string(),
+ name: z.string(),
+ ref: z.string().optional(),
+ path: z.string().optional(),
+ })
+ .optional(),
+ }),
+});
+
+export const extensionPackages = defineCollection({
+ loader: glob({ pattern: "*.yaml", base: "./src/content/extension-packages" }),
+ schema: z.object({
+ title: z.string(),
+ slug: z.string().optional(),
+ description: z.string(),
+ author: z.string(),
+ image: z.string(),
+ date_created: z.coerce.string().optional(),
+ version: z.coerce.string().optional(),
+ release_notes: z.string().optional(),
+ labels: z.array(z.string()).optional(),
+ repo_url: z.string().optional(),
+ oci_image: z.string().optional(),
+ }),
+});
+
+export const kyvernoPolicies = defineCollection({
+ loader: glob({ pattern: "*.yaml", base: "./src/content/kyverno-policies" }),
+ schema: z.object({
+ title: z.string(),
+ slug: z.string().optional(),
+ description: z.string(),
+ author: z.string(),
+ image: z.string(),
+ date_created: z.coerce.string().optional(),
+ version: z.coerce.string().optional(),
+ release_notes: z.string().optional(),
+ labels: z.array(z.string()).optional(),
+ repo_url: z.string().optional(),
+ }),
+});
+
+export const themes = defineCollection({
+ loader: glob({ pattern: "*.yaml", base: "./src/content/themes" }),
+ schema: z.object({
+ title: z.string(),
+ slug: z.string().optional(),
+ description: z.string(),
+ author: z.string(),
+ image: z.string(),
+ date_created: z.coerce.string().optional(),
+ version: z.coerce.string().optional(),
+ release_notes: z.string().optional(),
+ labels: z.array(z.string()).optional(),
+ repo_url: z.string().optional(),
+ }),
+});
+
+export const collections = {
+ "workshops": workshops,
+ "extension-packages": extensionPackages,
+ "kyverno-policies": kyvernoPolicies,
+ "themes": themes,
+};
\ No newline at end of file
diff --git a/src/content/extension-packages/educates-cli.yaml b/src/content/extension-packages/educates-cli.yaml
new file mode 100644
index 0000000..c8ba97a
--- /dev/null
+++ b/src/content/extension-packages/educates-cli.yaml
@@ -0,0 +1,12 @@
+title: Educates CLI Extension Package
+description: Provides Educates CLI to any workshop
+image: educates
+author: Jorge Morales
+date_created: 2025-05-26
+labels:
+ - educates
+ - cli
+version: 3.3.2
+repo_url: https://github.com/educates/educates-extension-packages
+oci_image: ghcr.io/educates/educates-extension-packages/educates:v3.3.2
+release_notes: Initial release with latest versions of the Educates CLI.
\ No newline at end of file
diff --git a/src/content/extension-packages/spring-academy-vscode-tools.yaml b/src/content/extension-packages/spring-academy-vscode-tools.yaml
new file mode 100644
index 0000000..1923eb2
--- /dev/null
+++ b/src/content/extension-packages/spring-academy-vscode-tools.yaml
@@ -0,0 +1,8 @@
+title: Spring Academy VSCode Extension Package
+description: VSCode extension package for the Spring Academy to add support for Spring Boot and Spring Cloud.
+author: Spring Academy
+image: spring
+labels:
+ - spring-academy
+repo_url: https://github.com/spring-academy/spring-academy-tools
+version: v1.0.1
\ No newline at end of file
diff --git a/src/content/kyverno-policies/educates-default.yaml b/src/content/kyverno-policies/educates-default.yaml
new file mode 100644
index 0000000..94d48a9
--- /dev/null
+++ b/src/content/kyverno-policies/educates-default.yaml
@@ -0,0 +1,11 @@
+title: Educates Default Kyverno Policies
+description: |-
+ Default Kyverno policies for Educates
+ - require-ingress-session-name
+ - require-ingress-session-name
+author: Educates
+image: educates
+labels:
+ - educates
+ - kyverno-policy
+repo_url: https://github.com/educates/educates-training-platform/blob/develop/carvel-packages/installer/bundle/config/ytt/_ytt_lib/packages/educates/_ytt_lib/kyverno-policies/require-ingress-session-name.yaml
\ No newline at end of file
diff --git a/src/content/themes/.gitkeep b/src/content/themes/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/data/examples.yaml b/src/content/workshops/WIP/examples.yaml__
similarity index 50%
rename from src/data/examples.yaml
rename to src/content/workshops/WIP/examples.yaml__
index 79ff65f..33bfabb 100644
--- a/src/data/examples.yaml
+++ b/src/content/workshops/WIP/examples.yaml__
@@ -1,14 +1,3 @@
-title: GitOps with Argo CD
-description: Implement GitOps workflows using Argo CD in Kubernetes.
-image: argo
-author: Carlos Rivera
-date_created: 2024-06-12
-labels:
- - gitops
- - kubernetes
-url: https://github.com/educates/workshops/gitops-argo
-version: 1.0.2
-release_notes: Added new GitOps deployment strategies.
---
title: Grafana Visualization
description: Visualize metrics and logs with Grafana dashboards.
@@ -17,7 +6,9 @@ author: Sofia Garcia
date_created: 2024-06-22
labels:
- monitoring
-url: https://github.com/educates/workshops/grafana-visualization
+install_url: https://github.com/educates/workshops/grafana-visualization
+repo_url: https://github.com/educates/workshops/grafana-visualization
+workshop_definition_url: https://github.com/educates/workshops/grafana-visualization/releases/download/main/workshop.yaml
version: 1.0.1
release_notes: Added new dashboard templates.
---
@@ -29,7 +20,9 @@ date_created: 2024-06-15
labels:
- kubernetes
- beginner
-url: https://github.com/educates/workshops/helm-basics
+install_url: https://github.com/educates/workshops/helm-basics
+repo_url: https://github.com/educates/workshops/helm-basics
+workshop_definition_url: https://github.com/educates/workshops/helm-basics/releases/download/main/workshop.yaml
version: 1.0.0
release_notes: First release.
---
@@ -41,7 +34,9 @@ date_created: 2024-06-18
labels:
- service-mesh
- kubernetes
-url: https://github.com/educates/workshops/istio-service-mesh
+install_url: https://github.com/educates/workshops/istio-service-mesh
+repo_url: https://github.com/educates/workshops/istio-service-mesh
+workshop_definition_url: https://github.com/educates/workshops/istio-service-mesh/releases/download/main/workshop.yaml
version: 1.2.0
release_notes: Added traffic management labs.
---
@@ -53,7 +48,9 @@ date_created: 2024-06-01
labels:
- kubernetes
- beginner
-url: https://github.com/educates/workshops/k8s101
+install_url: https://github.com/educates/workshops/k8s101
+repo_url: https://github.com/educates/workshops/k8s101
+workshop_definition_url: https://github.com/educates/workshops/k8s101/releases/download/main/workshop.yaml
version: 1.0.0
release_notes: Initial release.
---
@@ -65,7 +62,9 @@ date_created: 2024-06-28
labels:
- ingress
- kubernetes
-url: https://github.com/educates/workshops/nginx-ingress
+install_url: https://github.com/educates/workshops/nginx-ingress
+repo_url: https://github.com/educates/workshops/nginx-ingress
+workshop_definition_url: https://github.com/educates/workshops/nginx-ingress/releases/download/main/workshop.yaml
version: 1.0.0
release_notes: Initial NGINX Ingress Controller workshop.
---
@@ -77,7 +76,9 @@ date_created: 2024-06-25
labels:
- database
- kubernetes
-url: https://github.com/educates/workshops/postgresql-k8s
+install_url: https://github.com/educates/workshops/postgresql-k8s
+repo_url: https://github.com/educates/workshops/postgresql-k8s
+workshop_definition_url: https://github.com/educates/workshops/postgresql-k8s/releases/download/main/workshop.yaml
version: 1.0.0
release_notes: Initial PostgreSQL on Kubernetes workshop.
---
@@ -89,7 +90,9 @@ date_created: 2024-06-10
labels:
- monitoring
- kubernetes
-url: https://github.com/educates/workshops/prometheus-monitoring
+install_url: https://github.com/educates/workshops/prometheus-monitoring
+repo_url: https://github.com/educates/workshops/prometheus-monitoring
+workshop_definition_url: https://github.com/educates/workshops/prometheus-monitoring/releases/download/main/workshop.yaml
version: 1.1.0
release_notes: Updated for Prometheus Operator.
---
@@ -101,6 +104,8 @@ date_created: 2024-06-20
labels:
- ci/cd
- kubernetes
-url: https://github.com/educates/workshops/tekton-ci
+install_url: https://github.com/educates/workshops/tekton-ci
+repo_url: https://github.com/educates/workshops/tekton-ci
+workshop_definition_url: https://github.com/educates/workshops/tekton-ci/releases/download/main/workshop.yaml
version: 1.0.0
release_notes: Initial Tekton CI/CD workshop.
diff --git a/src/content/workshops/WIP/labs-authoring-guides.yaml b/src/content/workshops/WIP/labs-authoring-guides.yaml
new file mode 100644
index 0000000..bbde2b2
--- /dev/null
+++ b/src/content/workshops/WIP/labs-authoring-guides.yaml
@@ -0,0 +1,13 @@
+title: Workshop Authoring
+slug: workshop-authoring
+description: Getting started with workshop authoring
+author: Educates Team
+image: educates
+labels:
+ - educates-showcase
+repository:
+ org: educates
+ name: labs-authoring-guides
+ ref: "main"
+ path: workshops.yaml
+install_url: https://github.com/educates/labs-authoring-guides/releases/download/main/workshops.yaml
diff --git a/src/content/workshops/WIP/labs-educates-showcase.yaml__ b/src/content/workshops/WIP/labs-educates-showcase.yaml__
new file mode 100644
index 0000000..937b96b
--- /dev/null
+++ b/src/content/workshops/WIP/labs-educates-showcase.yaml__
@@ -0,0 +1,202 @@
+---
+title: Workshop Session
+slug: workshop-session
+description: Overview of the containerized workshop environment.
+author: Educates Team
+image: educates
+install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-educates-showcase
+workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+---
+title: Session Namespace
+slug: session-namespace
+description: Overview of Kubernetes application deployment.
+author: Educates Team
+image: kubernetes
+install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-educates-showcase
+workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+---
+title: Virtual Cluster
+slug: virtual-cluster
+description: Overview of optional virtual Kubernetes cluster.
+author: Educates Team
+image: kubernetes
+install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-educates-showcase
+workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+ - vcluster
+---
+title: Multiple Clusters
+slug: multiple-clusters
+description: Overview of working with multiple clusters.
+author: Educates Team
+image: kubernetes
+install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-educates-showcase
+workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+---
+title: Virtual Machines
+slug: virtual-machines
+description: Overview of provisioning virtual machines.
+author: Educates Team
+image: educates
+install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-educates-showcase
+workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+ - virtual-machine
+---
+title: Integrated Editor
+slug: integrated-editor
+description: Overview of integrated VS Code editor.
+author: Educates Team
+image: educates
+install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-educates-showcase
+workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+---
+title: Slide Presentations
+slug: slide-presentations
+description: Overview of integrating slide presentations.
+author: Educates Team
+image: educates
+install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-educates-showcase
+workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+---
+title: Docker Runtime
+slug: docker-runtime
+description: Overview of deploying applications using docker.
+author: Educates Team
+image: educates
+install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-educates-showcase
+workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+ - docker
+---
+title: Examiner Scripts
+slug: examiner-scripts
+description: Overview of using self check examinations.
+author: Educates Team
+image: educates
+labels:
+ - educates-showcase
+---
+title: Java Environment
+slug: java-environment
+description: Overview of working on Java applications.
+author: Educates Team
+image: educates
+labels:
+ - educates-showcase
+ - java
+---
+title: Conda Environment
+slug: conda-environment
+description: Overview of working on Python applications.
+author: Educates Team
+image: educates
+labels:
+ - educates-showcase
+ - python
+---
+title: Shared Resources
+slug: shared-resources
+description: Overview of pre-creating shared resources.
+author: Educates Team
+image: educates
+install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-educates-showcase
+workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+---
+title: Session Resources
+slug: session-resources
+description: Overview of creating per session resources.
+author: Educates Team
+image: educates
+install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-educates-showcase
+workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+---
+title: Extension Packages
+slug: extension-packages
+description: Overview of adding additional extension packages.
+author: Educates Team
+image: educates
+install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-educates-showcase
+workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+---
+title: Managed Services
+slug: managed-services
+description: Overview of running additional managed processes.
+author: Educates Team
+image: educates
+install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-educates-showcase
+workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+---
+title: Ingress Proxy
+slug: ingress-proxy
+description: Overview of adding ingresses for local processes.
+author: Educates Team
+image: nginx
+install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-educates-showcase
+workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+---
+title: Git Repositories
+slug: git-repositories
+description: Overview of using local hosted Git repositories.
+author: Educates Team
+image: educates
+labels:
+ - educates-showcase
+---
+title: Installing Educates
+slug: installing-educates
+description: Overview of installing Educates using the CLI.
+author: Educates Team
+image: educates
+install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-educates-showcase
+workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+---
+title: Workshop Authoring
+slug: workshop-authoring
+description: Overview of authoring workshops for Educates.
+author: Educates Team
+image: educates
+install_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-educates-showcase
+workshop_definition_url: https://github.com/educates/labs-educates-showcase/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
diff --git a/src/content/workshops/WIP/labs-installation-guides.yaml__ b/src/content/workshops/WIP/labs-installation-guides.yaml__
new file mode 100644
index 0000000..fc47e07
--- /dev/null
+++ b/src/content/workshops/WIP/labs-installation-guides.yaml__
@@ -0,0 +1,76 @@
+---
+title: Installing Educates
+slug: installing-educates-cli
+description: Installing Educates using the CLI
+author: Educates Team
+image: educates
+install_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-installation-guides
+workshop_definition_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+ - installation-guides
+---
+title: Installing Educates
+slug: installing-educates-carvel
+description: installing Educates using kapp-controller
+author: Educates Team
+image: educates
+install_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-installation-guides
+workshop_definition_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+ - installation-guides
+---
+title: Installing Educates Lookup Service
+slug: installing-educates-lookup-service
+description: Installing Educates Lookup Service
+author: Educates Team
+image: educates
+install_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-installation-guides
+workshop_definition_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+ - installation-guides
+ - lookup-service
+---
+title: Installing Educates standalone Lookup Service
+slug: installing-educates-standalone-lookup-service
+description: Installing Educates standalone Lookup Service
+author: Educates Team
+image: educates
+install_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-installation-guides
+workshop_definition_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+ - installation-guides
+ - lookup-service
+---
+title: Lookup Service Configuration
+slug: lookup-service-configuration
+description: Configuring the Educates lookup service
+author: Educates Team
+image: educates
+install_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-installation-guides
+workshop_definition_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+ - installation-guides
+ - lookup-service
+---
+title: Lookup Service Usage
+slug: lookup-service-usage
+description: Using the Educates lookup service
+author: Educates Team
+image: educates
+install_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml
+repo_url: https://github.com/educates/labs-installation-guides
+workshop_definition_url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml
+labels:
+ - educates-showcase
+ - lookup-service
+author: Educates Team
\ No newline at end of file
diff --git a/src/content/workshops/containerizing-a-spring-application.yaml b/src/content/workshops/containerizing-a-spring-application.yaml
new file mode 100644
index 0000000..e4109c2
--- /dev/null
+++ b/src/content/workshops/containerizing-a-spring-application.yaml
@@ -0,0 +1,32 @@
+title: Containerizing a Spring Application
+slug: containerizing-a-spring-application
+description: How to containerize a Spring Boot application and run it in docker
+# prerequisites: |-
+# In order to install this workshop, you need to have the following prerequisites:
+# - Docker installed
+# - Java installed
+# - Spring Boot installed
+# - Kubernetes installed
+# - Kubernetes CLI installed
+# - Kubernetes Dashboard installed
+#runs_on_docker: true
+author: Educates Team
+date_created: 2025-05-30
+version: "4.2"
+release_notes: |-
+ - Initial release
+ - Added prerequisites
+ - Added release notes
+ - Added source file
+image: spring
+repository:
+ org: educates
+ name: labs-spring-workshops
+ ref: "4.3"
+ path: workshops/lab-containerizing-spring/resources/workshop.yaml
+install_url: https://github.com/educates/labs-spring-workshops/releases/download/4.3/lab-containerizing-spring.yaml
+labels:
+ - educates-showcase
+ - spring
+ - java
+ - docker
diff --git a/src/content/workshops/creating-a-spring-application.yaml b/src/content/workshops/creating-a-spring-application.yaml
new file mode 100644
index 0000000..6fc01e1
--- /dev/null
+++ b/src/content/workshops/creating-a-spring-application.yaml
@@ -0,0 +1,16 @@
+title: Creating a Spring Application
+slug: creating-a-spring-application
+description: Introduces how to use the start.spring.io web site to create a Spring Boot application
+author: Educates Team
+image: spring
+# runs_on_docker: true
+repository:
+ org: educates
+ name: labs-spring-workshops
+ ref: "4.3"
+ path: workshops/lab-creating-a-spring-app/resources/workshop.yaml
+install_url: https://github.com/educates/labs-spring-workshops/releases/download/4.3/lab-creating-a-spring-app.yaml
+labels:
+ - educates-showcase
+ - spring
+ - java
\ No newline at end of file
diff --git a/src/content/workshops/lab-kubernetes-fundamentals.yaml b/src/content/workshops/lab-kubernetes-fundamentals.yaml
new file mode 100644
index 0000000..defef2d
--- /dev/null
+++ b/src/content/workshops/lab-kubernetes-fundamentals.yaml
@@ -0,0 +1,18 @@
+slug: lab-kubernetes-fundamentals
+title: Kubernetes Fundamentals
+description: An interactive workshop on Kubernetes fundamentals
+image: kubernetes
+author: GrahamDumpleton
+#date_created: 2024-07-12
+labels:
+ - kubernetes
+ - fundamentals
+release_notes: |-
+ - Convert it to Hugo
+ - Upgrade sample app to make it work on arm64
+repository:
+ org: educates
+ name: lab-k8s-fundamentals
+ ref: "8.3"
+ path: workshop.yaml
+install_url: https://github.com/educates/lab-k8s-fundamentals/releases/download/8.3/workshop.yaml
diff --git a/src/content/workshops/lab-spring-boot-on-k8s.yaml b/src/content/workshops/lab-spring-boot-on-k8s.yaml
new file mode 100644
index 0000000..df52d9b
--- /dev/null
+++ b/src/content/workshops/lab-spring-boot-on-k8s.yaml
@@ -0,0 +1,16 @@
+title: SpringBoot on Kubernetes
+slug: lab-spring-boot-on-k8s
+description: Steps through creating a Spring Boot application, containerizing it, and deploying it to Kubernetes.
+author: Educates Team
+image: spring
+repository:
+ org: educates
+ name: labs-spring-workshops
+ ref: "4.3"
+ path: workshops/lab-spring-boot-on-k8s/resources/workshop.yaml
+install_url: https://github.com/educates/labs-spring-workshops/releases/download/4.3/lab-spring-boot-on-k8s.yaml
+labels:
+ - educates-showcase
+ - spring
+ - java
+ - kubernetes
\ No newline at end of file
diff --git a/src/data/lab-kubernetes-fundamentals.yaml b/src/data/lab-kubernetes-fundamentals.yaml
deleted file mode 100644
index e8755c6..0000000
--- a/src/data/lab-kubernetes-fundamentals.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-title: Kubernetes Fundamentals
-description: An interactive workshop on Kubernetes fundamentals
-image: kubernetes
-author: GrahamDumpleton
-date_created: 2024-07-12
-labels: [kubernetes, fundamentals]
-url: https://github.com/educates/lab-k8s-fundamentals/releases/download/7.4/workshop.yaml
-version: 7.4
-release_notes: Updating for new repository location
diff --git a/src/data/labs-authoring-guides.yaml b/src/data/labs-authoring-guides.yaml
deleted file mode 100644
index 904298c..0000000
--- a/src/data/labs-authoring-guides.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
----
-title: Workshop Authoring
-slug: workshop-authoring
-description: Getting started with workshop authoring
-author: Educates Team
-image: educates
-url: https://github.com/educates/labs-authoring-guides/releases/download/main/workshops.yaml
-labels:
- - educates-showcase
diff --git a/src/data/labs-educates-showcase.yaml b/src/data/labs-educates-showcase.yaml
deleted file mode 100644
index 561765d..0000000
--- a/src/data/labs-educates-showcase.yaml
+++ /dev/null
@@ -1,157 +0,0 @@
----
-title: Workshop Session
-slug: workshop-session
-description: Overview of the containerized workshop environment.
-author: Educates Team
-image: educates
-labels:
- - educates-showcase
----
-title: Session Namespace
-slug: session-namespace
-description: Overview of Kubernetes application deployment.
-author: Educates Team
-image: kubernetes
-labels:
- - educates-showcase
----
-title: Virtual Cluster
-slug: virtual-cluster
-description: Overview of optional virtual Kubernetes cluster.
-author: Educates Team
-image: kubernetes
-labels:
- - educates-showcase
- - vcluster
----
-title: Multiple Clusters
-slug: multiple-clusters
-description: Overview of working with multiple clusters.
-author: Educates Team
-image: kubernetes
-labels:
- - educates-showcase
----
-title: Virtual Machines
-slug: virtual-machines
-description: Overview of provisioning virtual machines.
-author: Educates Team
-image: educates
-labels:
- - educates-showcase
- - virtual-machine
----
-title: Integrated Editor
-slug: integrated-editor
-description: Overview of integrated VS Code editor.
-author: Educates Team
-image: educates
-labels:
- - educates-showcase
----
-title: Slide Presentations
-slug: slide-presentations
-description: Overview of integrating slide presentations.
-author: Educates Team
-image: educates
-labels:
- - educates-showcase
----
-title: Docker Runtime
-slug: docker-runtime
-description: Overview of deploying applications using docker.
-author: Educates Team
-image: educates
-labels:
- - educates-showcase
- - docker
----
-title: Examiner Scripts
-slug: examiner-scripts
-description: Overview of using self check examinations.
-author: Educates Team
-image: educates
-labels:
- - educates-showcase
----
-title: Java Environment
-slug: java-environment
-description: Overview of working on Java applications.
-author: Educates Team
-image: educates
-labels:
- - educates-showcase
- - java
----
-title: Conda Environment
-slug: conda-environment
-description: Overview of working on Python applications.
-author: Educates Team
-image: educates
-labels:
- - educates-showcase
- - python
----
-title: Shared Resources
-slug: shared-resources
-description: Overview of pre-creating shared resources.
-author: Educates Team
-image: educates
-labels:
- - educates-showcase
----
-title: Session Resources
-slug: session-resources
-description: Overview of creating per session resources.
-author: Educates Team
-image: educates
-labels:
- - educates-showcase
----
-title: Extension Packages
-slug: extension-packages
-description: Overview of adding additional extension packages.
-author: Educates Team
-image: educates
-labels:
- - educates-showcase
----
-title: Managed Services
-slug: managed-services
-description: Overview of running additional managed processes.
-author: Educates Team
-image: educates
-labels:
- - educates-showcase
----
-title: Ingress Proxy
-slug: ingress-proxy
-description: Overview of adding ingresses for local processes.
-author: Educates Team
-image: nginx
-labels:
- - educates-showcase
----
-title: Git Repositories
-slug: git-repositories
-description: Overview of using local hosted Git repositories.
-author: Educates Team
-image: educates
-labels:
- - educates-showcase
----
-title: Installing Educates
-slug: installing-educates
-description: Overview of installing Educates using the CLI.
-author: Educates Team
-image: educates
-labels:
- - educates-showcase
----
-title: Workshop Authoring
-slug: workshop-authoring
-description: Overview of authoring workshops for Educates.
-author: Educates Team
-image: educates
-labels:
- - educates-showcase
diff --git a/src/data/labs-installation-guides.yaml b/src/data/labs-installation-guides.yaml
deleted file mode 100644
index 60313ba..0000000
--- a/src/data/labs-installation-guides.yaml
+++ /dev/null
@@ -1,64 +0,0 @@
----
-title: Installing Educates
-slug: installing-educates-cli
-description: Installing Educates using the CLI
-author: Educates Team
-image: educates
-url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml
-labels:
- - educates-showcase
- - installation-guides
----
-title: Installing Educates
-slug: installing-educates-carvel
-description: installing Educates using kapp-controller
-author: Educates Team
-image: educates
-url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml
-labels:
- - educates-showcase
- - installation-guides
----
-title: Installing Educates Lookup Service
-slug: installing-educates-lookup-service
-description: Installing Educates Lookup Service
-author: Educates Team
-image: educates
-url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml
-labels:
- - educates-showcase
- - installation-guides
- - lookup-service
----
-title: Installing Educates standalone Lookup Service
-slug: installing-educates-standalone-lookup-service
-description: Installing Educates standalone Lookup Service
-author: Educates Team
-image: educates
-url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml
-labels:
- - educates-showcase
- - installation-guides
- - lookup-service
----
-title: Lookup Service Configuration
-slug: lookup-service-configuration
-description: Configuring the Educates lookup service
-author: Educates Team
-image: educates
-url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml
-labels:
- - educates-showcase
- - installation-guides
- - lookup-service
----
-title: Lookup Service Usage
-slug: lookup-service-usage
-description: Using the Educates lookup service
-author: Educates Team
-image: educates
-url: https://github.com/educates/labs-installation-guides/releases/download/main/workshops.yaml
-labels:
- - educates-showcase
- - lookup-service
-author: Educates Team
\ No newline at end of file
diff --git a/src/data/labs-spring-workshops.yaml b/src/data/labs-spring-workshops.yaml
deleted file mode 100644
index 51c0da3..0000000
--- a/src/data/labs-spring-workshops.yaml
+++ /dev/null
@@ -1,35 +0,0 @@
----
-title: Creating a Spring Application
-slug: creating-a-spring-application
-description: Introduces how to use the start.spring.io web site to create a Spring Boot application
-author: Educates Team
-image: spring
-url: https://github.com/educates/labs-spring-workshops/releases/download/4.2/workshops.yaml
-labels:
- - educates-showcase
- - spring
- - java
----
-title: Containerizing a Spring Application
-slug: containerizing-a-spring-application
-description: How to containerize a Spring Boot application and run it in docker
-author: Educates Team
-image: spring
-url: https://github.com/educates/labs-spring-workshops/releases/download/4.2/workshops.yaml
-labels:
- - educates-showcase
- - spring
- - java
- - docker
----
-title: SpringBoot on Kubernetes
-slug: springboot-on-kubernetes
-description: Steps through creating a Spring Boot application, containerizing it, and deploying it to Kubernetes.
-author: Educates Team
-image: spring
-url: https://github.com/educates/labs-spring-workshops/releases/download/4.2/workshops.yaml
-labels:
- - educates-showcase
- - spring
- - java
- - kubernetes
\ No newline at end of file
diff --git a/src/components/Footer.astro b/src/layouts/Footer.astro
similarity index 100%
rename from src/components/Footer.astro
rename to src/layouts/Footer.astro
diff --git a/src/layouts/MainLayout.astro b/src/layouts/MainLayout.astro
index 8151154..1adf6ca 100644
--- a/src/layouts/MainLayout.astro
+++ b/src/layouts/MainLayout.astro
@@ -1,6 +1,6 @@
---
-import Topbar from "../components/Topbar.astro";
-import Footer from "../components/Footer.astro";
+import Topbar from "../layouts/Topbar.astro";
+import Footer from "../layouts/Footer.astro";
const {
title = "EducatesHub - Workshop Catalog",
description = "Browse and discover Educates workshops.",
diff --git a/src/components/Topbar.astro b/src/layouts/Topbar.astro
similarity index 100%
rename from src/components/Topbar.astro
rename to src/layouts/Topbar.astro
diff --git a/src/pages/[slug].astro b/src/pages/[slug].astro
index a7f2a80..225e2a5 100644
--- a/src/pages/[slug].astro
+++ b/src/pages/[slug].astro
@@ -1,152 +1,71 @@
---
-import { loadWorkshops } from "../utils/loadWorkshops";
+import { getCollection } from "astro:content";
+import { imageMap, educatesDefaultImage } from "../utils/imageMap";
import MainLayout from "../layouts/MainLayout.astro";
+import WorkshopDetail from '../components/WorkshopDetail.jsx';
+import ExtensionPackageDetail from '../components/ExtensionPackageDetail.jsx';
+import KyvernoPolicyDetail from '../components/KyvernoPolicyDetail.jsx';
+import ThemeDetail from '../components/ThemeDetail.jsx';
export async function getStaticPaths() {
- const workshops = await loadWorkshops();
- return workshops.map((w) => ({
- params: { slug: w.slug },
- }));
+ const workshops = await getCollection('workshops');
+ const extensionPackages = await getCollection('extension-packages');
+ const kyvernoPolicies = await getCollection('kyverno-policies');
+ const themes = await getCollection('themes');
+ return [
+ ...workshops.map(w => ({ params: { slug: w.id } })),
+ ...extensionPackages.map(e => ({ params: { slug: e.id } })),
+ ...kyvernoPolicies.map(e => ({ params: { slug: e.id } })),
+ ...themes.map(t => ({ params: { slug: t.id } })),
+ ];
}
const { slug } = Astro.params;
-const workshops = await loadWorkshops();
-const workshop = workshops.find((w) => w.slug === slug);
-if (!workshop) {
- throw new Error("Workshop not found");
+
+function mapImage(resource: any) {
+ let image = resource.image;
+ if (typeof image === "string" && imageMap[image]) {
+ image = imageMap[image];
+ }
+ if (typeof image === "string" && !Object.values(imageMap).includes(image)) {
+ image = educatesDefaultImage;
+ }
+ return { ...resource, image };
+}
+
+const collections: { name: any, type: string }[] = [
+ { name: "workshops", type: "workshop" },
+ { name: "extension-packages", type: "extension-package" },
+ { name: "kyverno-policies", type: "kyverno-policy" },
+ { name: "themes", type: "theme" },
+];
+
+let resource = null;
+for (const { name, type } of collections) {
+ const entries = (await getCollection(name)) as any[];
+ const found = entries.find((e: any) => e.id === slug);
+ if (found) {
+ resource = mapImage({ ...found.data, type, slug: found.id });
+ break;
+ }
+}
+
+if (!resource) {
+ throw new Error("Resource not found");
}
---
-
-
-
-
-
-
-
-
-
-
-
- {workshop.title}
-
-
- {
- workshop.labels &&
- workshop.labels.map((label: string) => (
-
- {label}
-
- ))
- }
-
-
- {workshop.description}
-
-
- By {workshop.author} • Created {workshop.date_created}
-
-
- Version: {workshop.version}
-
-
-
Release Notes
-
- {workshop.release_notes}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Helm Install Command
- Copy
-
-
-educates deploy-workshop -f {workshop.url}
-
-
-
-
-
-
-
-
+ {resource.type === 'workshop' ? (
+
+ ) : resource.type === 'extension-package' ? (
+
+ ) : resource.type === 'kyverno-policy' ? (
+
+ ) : resource.type === 'theme' ? (
+
+ ) : null}
diff --git a/src/pages/about.astro b/src/pages/about.astro
index 659ad33..b78d7a2 100644
--- a/src/pages/about.astro
+++ b/src/pages/about.astro
@@ -9,9 +9,9 @@ import MainLayout from "../layouts/MainLayout.astro";
About EducatesHub
EducatesHub is a community-driven catalog of Educates Workshops Educates Workshops, Extension Packages, Themes and Kyverno Policies, making it easy to discover, search, and install hands-on Kubernetes
- training content.
+ training content and other Educates resources.
What is Educates?
@@ -29,9 +29,9 @@ import MainLayout from "../layouts/MainLayout.astro";
What can you do with EducatesHub?
Here, you will be able to:
- Browse curated workshops and training scenarios
+ Browse curated workshops, extension packages, themes, and kyverno policies
Filter by topic, labels, and search keywords
- View install instructions and workshop details
+ View install instructions and resource details
Learn More
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 010c915..8eae789 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -1,15 +1,38 @@
---
-import { loadWorkshops } from "../utils/loadWorkshops";
+import { getCollection } from "astro:content";
+import { imageMap, educatesDefaultImage } from "../utils/imageMap";
import MainLayout from "../layouts/MainLayout.astro";
-import WorkshopGrid from "../components/WorkshopGrid.jsx";
-const workshops = await loadWorkshops();
+import EducatesResourceSelectorIsland from "../components/EducatesResourceSelectorIsland.jsx";
+
+function mapImage(resource: any) {
+ let image = resource.image;
+ if (typeof image === "string" && imageMap[image]) {
+ image = imageMap[image];
+ }
+ if (typeof image === "string" && !Object.values(imageMap).includes(image)) {
+ image = educatesDefaultImage;
+ }
+ return { ...resource, image };
+}
+
+const workshops = (await getCollection('workshops')).map(w => mapImage({ ...w.data, slug: w.id, type: "workshop" }));
+const extensionPackages = (await getCollection('extension-packages')).map(e => mapImage({ ...e.data, slug: e.id, type: "extension-package" }));
+const kyvernoPolicies = (await getCollection('kyverno-policies')).map(e => mapImage({ ...e.data, slug: e.id, type: "kyverno-policy" }));
+const themes = (await getCollection('themes')).map(t => mapImage({ ...t.data, slug: t.id, type: "theme" }));
+
+const allResources = {
+ Workshop: workshops,
+ ExtensionPackage: extensionPackages,
+ // KyvernoPolicy: kyvernoPolicies,
+ // Theme: themes,
+};
---
-
+
-
+
\ No newline at end of file
diff --git a/src/utils/imageMap.ts b/src/utils/imageMap.ts
new file mode 100644
index 0000000..788f49d
--- /dev/null
+++ b/src/utils/imageMap.ts
@@ -0,0 +1,18 @@
+export const imageMap: Record = {
+ kubernetes: "/images/kubernetes.svg",
+ prometheus: "/images/prometheus.png",
+ grafana: "/images/grafana.svg",
+ argo: "/images/argo-cd.png",
+ helm: "/images/helm.png",
+ istio: "/images/istio.png",
+ tekton: "/images/tekton.png",
+ postgresql: "/images/postgresql.png",
+ nginx: "/images/nginx.png",
+ educates: "/images/educates.svg",
+ spring: "/images/spring.png",
+ java: "/images/java.png",
+ docker: "/images/docker.png",
+ // Add more mappings as needed
+};
+
+export const educatesDefaultImage = "/images/educates.svg";
\ No newline at end of file
diff --git a/src/utils/loadWorkshops.ts b/src/utils/loadWorkshops.ts
deleted file mode 100644
index 64af89a..0000000
--- a/src/utils/loadWorkshops.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import yaml from "yaml";
-
-const imageMap: Record = {
- kubernetes: "/images/k8s101.png",
- prometheus: "/images/prometheus.png",
- grafana: "/images/grafana.svg",
- argo: "/images/argo-cd.png",
- helm: "/images/helm.png",
- istio: "/images/istio.png",
- tekton: "/images/tekton.png",
- postgresql: "/images/postgresql.png",
- nginx: "/images/nginx.png",
- educates: "/images/educates.svg",
- spring: "/images/spring.png",
- java: "/images/java.png",
- docker: "/images/docker.png",
- // Add more mappings as needed
-};
-
-const educatesDefaultImage = "/images/educates.svg";
-
-export async function loadWorkshops() {
- // Get all available images in public/images
- const imageFiles = import.meta.glob("/public/images/*", {
- query: "?url",
- import: "default",
- eager: true,
- });
- const availableImages = new Set(
- Object.keys(imageFiles).map((path) => path.replace("/public", "")),
- );
-
- const files = import.meta.glob("../data/*.yaml", {
- query: "?raw",
- import: "default",
- });
- const entriesNested = await Promise.all(
- Object.entries(files).map(async ([path, loader]) => {
- const raw = await loader();
- if (typeof raw !== "string") {
- throw new Error("Invalid YAML file");
- }
- const docs = yaml.parseAllDocuments(raw).map((doc) => doc.toJSON());
- // Derive slug from filename, but allow override in doc
- return docs.map((data, i) => {
- let slug =
- data.slug ||
- (docs.length === 1
- ? path.split("/").pop()?.replace(".yaml", "") || ""
- : `${path.split("/").pop()?.replace(".yaml", "")}-${i + 1}`);
- let image = data.image;
- if (typeof image === "string" && imageMap[image]) {
- image = imageMap[image];
- }
- if (typeof image === "string" && !availableImages.has(image)) {
- image = educatesDefaultImage;
- }
- return { ...data, image, slug };
- });
- }),
- );
- return entriesNested.flat();
-}
-
-// Later, this can be replaced with a server-side loader or API endpoint that reads YAML files from /public/workshops
diff --git a/src/utils/sources.ts b/src/utils/sources.ts
new file mode 100644
index 0000000..584b6a7
--- /dev/null
+++ b/src/utils/sources.ts
@@ -0,0 +1,62 @@
+export async function fetchSourceFile(type: string, slug: string): Promise {
+ if (type && slug) {
+ try {
+ const res = await fetch(`/assets/${type}/${slug}.yaml`);
+ if (res.ok) {
+ const text = await res.text();
+ return text;
+ } else {
+ return "Source file not found.";
+ }
+ } catch (e) {
+ return "Error loading source file.";
+ }
+ } else {
+ return "";
+ }
+}
+
+export function handleDownloadSourceFile(type: string, slug: string) {
+ const assetPath = `/assets/${type}/${slug}.yaml`;
+ const link = document.createElement('a');
+ link.href = assetPath;
+ link.download = `${slug}.yaml`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+}
+
+export function getRepoUrl(repository: { org?: string, name?: string }) {
+ if (repository && repository.org && repository.name) {
+ return `https://github.com/${repository.org}/${repository.name}`;
+ }
+ return null;
+}
+
+export function parseOciUrl(url: string | undefined | null): { repo: string, prefix: string, version: string, destination: string } {
+ if (!url) {
+ return { repo: '', prefix: '', version: '', destination: '' };
+ }
+ // Split off the version (after the last colon)
+ const [main, version] = url.split(/:(?=[^:]*$)/);
+ // The repo is the first segment before the first '/'
+ const firstSlash = main.indexOf('/');
+ const repo = main.substring(0, firstSlash);
+ // The prefix is everything after the first slash
+ const prefix = main.substring(firstSlash);
+ // The destination is the last segment before the colon
+ const segments = main.split('/');
+ const destination = segments[segments.length - 1];
+ return { repo, prefix, version, destination };
+}
+
+/*
+Example usage:
+
+const url = "ghcr.io/educates/educates-extension-packages/educates:v3.3.2";
+const { repo, prefix, version, destination } = parseOciUrl(url);
+console.log(repo); // "ghcr.io"
+console.log(prefix); // "/educates/educates-extension-packages/educates"
+console.log(version); // "v3.3.2"
+console.log(destination); // "educates"
+*/
\ No newline at end of file