diff --git a/.github/actions/deploy-subdir/action.yml b/.github/actions/deploy-subdir/action.yml
new file mode 100644
index 0000000..aa10ba3
--- /dev/null
+++ b/.github/actions/deploy-subdir/action.yml
@@ -0,0 +1,117 @@
+name: Assemble Pages subdirectory
+description: >-
+ Overlays a freshly built app onto the assembled-site storage branch, adds the
+ SPA fallback (.nojekyll + 404.html), uploads the merged site as a Pages
+ artifact, and persists the merged tree back to the storage branch. This lets
+ each micro app deploy independently while GitHub Pages still serves a single
+ site (see README "Hosting").
+
+inputs:
+ source-dir:
+ description: Freshly built directory to publish (e.g. dist/apps/portfolio/browser).
+ required: true
+ target-subdir:
+ description: Subdirectory within the site. Empty for the site root (the shell).
+ required: false
+ default: ''
+ storage-branch:
+ description: Branch that stores the full assembled site between deploys.
+ required: false
+ default: pages-content
+ preserve:
+ description: >-
+ Space-separated top-level entries to keep when publishing the site root,
+ so independent micro-app subdirectories survive a shell redeploy.
+ required: false
+ default: 'portfolio storybook'
+ github-token:
+ description: Token used to read and update the storage branch.
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - name: Assemble merged site ๐งฉ
+ shell: bash
+ env:
+ SOURCE_DIR: ${{ inputs.source-dir }}
+ TARGET_SUBDIR: ${{ inputs.target-subdir }}
+ STORAGE_BRANCH: ${{ inputs.storage-branch }}
+ PRESERVE: ${{ inputs.preserve }}
+ GH_TOKEN: ${{ inputs.github-token }}
+ run: |
+ set -euo pipefail
+
+ if [ ! -d "$SOURCE_DIR" ]; then
+ echo "::error::source-dir '$SOURCE_DIR' does not exist" >&2
+ exit 1
+ fi
+
+ REPO_URL="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
+
+ rm -rf _site _storage
+ mkdir -p _site
+
+ # Pull the current full site from the storage branch (if it exists).
+ if git ls-remote --exit-code --heads "$REPO_URL" "$STORAGE_BRANCH" >/dev/null 2>&1; then
+ git clone --quiet --depth 1 --branch "$STORAGE_BRANCH" "$REPO_URL" _storage
+ ( cd _storage && tar -cf - --exclude=.git . ) | ( cd _site && tar -xf - )
+ rm -rf _storage
+ fi
+
+ # Overlay the freshly built app into its slot.
+ if [ -z "$TARGET_SUBDIR" ] || [ "$TARGET_SUBDIR" = "." ]; then
+ # Site root (shell): replace root entries but keep sibling micro apps.
+ keep=".git .nojekyll $PRESERVE"
+ shopt -s dotglob nullglob
+ for entry in _site/*; do
+ name="$(basename "$entry")"
+ case " $keep " in
+ *" $name "*) continue ;;
+ esac
+ rm -rf "$entry"
+ done
+ shopt -u dotglob nullglob
+ cp -R "$SOURCE_DIR"/. _site/
+ else
+ rm -rf "_site/${TARGET_SUBDIR}"
+ mkdir -p "_site/${TARGET_SUBDIR}"
+ cp -R "$SOURCE_DIR"/. "_site/${TARGET_SUBDIR}/"
+ fi
+
+ # SPA fallback + disable Jekyll processing.
+ touch _site/.nojekyll
+ if [ -f _site/index.html ]; then
+ cp _site/index.html _site/404.html
+ fi
+
+ echo "Assembled site tree:"
+ find _site -maxdepth 2 -mindepth 1 | sort
+
+ - name: Upload Pages artifact ๐ฆ
+ uses: actions/upload-pages-artifact@v5
+ with:
+ path: _site
+
+ - name: Persist assembled site to storage branch ๐พ
+ shell: bash
+ env:
+ TARGET_SUBDIR: ${{ inputs.target-subdir }}
+ STORAGE_BRANCH: ${{ inputs.storage-branch }}
+ GH_TOKEN: ${{ inputs.github-token }}
+ run: |
+ set -euo pipefail
+ REPO_URL="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
+ SLOT="${TARGET_SUBDIR:-/ (shell)}"
+
+ cd _site
+ rm -rf .git
+ git init -q
+ git add -A
+ git -c user.name="github-actions[bot]" \
+ -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \
+ commit -q -m "deploy: update ${SLOT} (${GITHUB_SHA})" || {
+ echo "Nothing to persist."; exit 0;
+ }
+ git branch -M "$STORAGE_BRANCH"
+ git push -f "$REPO_URL" "$STORAGE_BRANCH"
diff --git a/.github/workflows/deploy-portfolio.yml b/.github/workflows/deploy-portfolio.yml
new file mode 100644
index 0000000..2f40785
--- /dev/null
+++ b/.github/workflows/deploy-portfolio.yml
@@ -0,0 +1,63 @@
+name: Deploy portfolio
+
+# The portfolio Module Federation remote is served (and exposes its
+# remoteEntry.json) at:
+# http://codestar.nl/nx-reference/portfolio/
+# Only apps/portfolio (and shared libs) changes trigger this workflow, so the
+# remote deploys independently of the shell.
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'apps/portfolio/**'
+ - 'libs/**'
+ - 'package.json'
+ - 'package-lock.json'
+ - 'nx.json'
+ - 'tsconfig.base.json'
+ - '.github/workflows/deploy-portfolio.yml'
+ - '.github/actions/deploy-subdir/**'
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ pages: write
+ id-token: write
+
+# Serialize with the other Pages deploys so storage-branch writes never race.
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Checkout ๐๏ธ
+ uses: actions/checkout@v7
+
+ - name: Use Node.js 24 ๐ข
+ uses: actions/setup-node@v6
+ with:
+ node-version: '24'
+ cache: 'npm'
+
+ - name: Install dependencies ๐ง
+ run: npm ci
+
+ - name: Build portfolio remote ๐๏ธ
+ run: npx nx build portfolio --configuration=production
+
+ - name: Assemble /portfolio ๐งฉ
+ uses: ./.github/actions/deploy-subdir
+ with:
+ source-dir: dist/apps/portfolio/browser
+ target-subdir: portfolio
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Deploy to GitHub Pages ๐
+ id: deployment
+ uses: actions/deploy-pages@v5
diff --git a/.github/workflows/deploy-shell.yml b/.github/workflows/deploy-shell.yml
new file mode 100644
index 0000000..177dea3
--- /dev/null
+++ b/.github/workflows/deploy-shell.yml
@@ -0,0 +1,71 @@
+name: Deploy shell
+
+# The shell (Module Federation host) is served at the site root:
+# http://codestar.nl/nx-reference/
+# Only apps/demo (and shared libs) changes trigger this workflow โ a portfolio
+# remote change never rebuilds the shell, demonstrating module-federation
+# loose coupling.
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'apps/demo/src/**'
+ - 'apps/demo/public/**'
+ - 'apps/demo/federation.config.js'
+ - 'apps/demo/federation.manifest.prod.json'
+ - 'apps/demo/project.json'
+ - 'apps/demo/tsconfig*.json'
+ - 'libs/**'
+ - 'package.json'
+ - 'package-lock.json'
+ - 'nx.json'
+ - 'tsconfig.base.json'
+ - '.github/workflows/deploy-shell.yml'
+ - '.github/actions/deploy-subdir/**'
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ pages: write
+ id-token: write
+
+# Serialize with the other Pages deploys so storage-branch writes never race.
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Checkout ๐๏ธ
+ uses: actions/checkout@v7
+
+ - name: Use Node.js 24 ๐ข
+ uses: actions/setup-node@v6
+ with:
+ node-version: '24'
+ cache: 'npm'
+
+ - name: Install dependencies ๐ง
+ run: npm ci
+
+ - name: Build shell ๐๏ธ
+ run: npx nx build demo --configuration=production
+
+ - name: Apply production federation manifest ๐
+ run: cp apps/demo/federation.manifest.prod.json dist/apps/demo/browser/federation.manifest.json
+
+ - name: Assemble site root ๐งฉ
+ uses: ./.github/actions/deploy-subdir
+ with:
+ source-dir: dist/apps/demo/browser
+ target-subdir: ''
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Deploy to GitHub Pages ๐
+ id: deployment
+ uses: actions/deploy-pages@v5
diff --git a/.github/workflows/deploy-storybook.yml b/.github/workflows/deploy-storybook.yml
new file mode 100644
index 0000000..ec3c1c0
--- /dev/null
+++ b/.github/workflows/deploy-storybook.yml
@@ -0,0 +1,65 @@
+name: Deploy storybook
+
+# The aggregated Storybook catalog is served at:
+# http://codestar.nl/nx-reference/storybook/
+# Triggered by Storybook config, story files, or shared library changes.
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'apps/demo/.storybook/**'
+ - 'libs/**'
+ - '**/*.stories.ts'
+ - '**/*.stories.tsx'
+ - '**/*.mdx'
+ - 'tsconfig.doc.json'
+ - 'package.json'
+ - 'package-lock.json'
+ - '.github/workflows/deploy-storybook.yml'
+ - '.github/actions/deploy-subdir/**'
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ pages: write
+ id-token: write
+
+# Serialize with the other Pages deploys so storage-branch writes never race.
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Checkout ๐๏ธ
+ uses: actions/checkout@v7
+
+ - name: Use Node.js 24 ๐ข
+ uses: actions/setup-node@v6
+ with:
+ node-version: '24'
+ cache: 'npm'
+
+ - name: Install dependencies ๐ง
+ run: npm ci
+
+ - name: Build Storybook ๐
+ run: |
+ npm run docs:json
+ npx nx run demo:build-storybook
+
+ - name: Assemble /storybook ๐งฉ
+ uses: ./.github/actions/deploy-subdir
+ with:
+ source-dir: dist/storybook/demo
+ target-subdir: storybook
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Deploy to GitHub Pages ๐
+ id: deployment
+ uses: actions/deploy-pages@v5
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 1724578..310ab67 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -16,11 +16,11 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v7
with:
fetch-depth: 0
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'
@@ -37,11 +37,11 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v7
with:
fetch-depth: 0
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'
@@ -58,11 +58,11 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v7
with:
fetch-depth: 0
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'
diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml
deleted file mode 100644
index 2d80b6c..0000000
--- a/.github/workflows/prod.yml
+++ /dev/null
@@ -1,73 +0,0 @@
-name: Build Storybook and deploy to Github Pages
-
-on:
- push:
- branches:
- - main
-
-permissions:
- contents: read
- pages: write
- id-token: write
-
-jobs:
- build:
- runs-on: ubuntu-latest
-
- strategy:
- matrix:
- node-version: [24.x]
- steps:
- - name: Checkout ๐๏ธ
- uses: actions/checkout@v4
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4
- with:
- node-version: ${{ matrix.node-version }}
- - name: Install and Build components ๐ง
- run: |
- npm ci
- # TODO not needed to build after install? npm run build
- # TODO - name: Validate โ๏ธ
- # run: npm run validate
- # env:
- # CI: false # true -> fails on warning
- - name: Build Storybook ๐
- run: |
- npm run docs:json
- npx nx run demo:build-storybook
- - name: Upload Pages artifact ๐ฆ
- uses: actions/upload-pages-artifact@v3
- with:
- path: dist/storybook/demo
-
- deploy:
- needs: build
- runs-on: ubuntu-latest
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
- steps:
- - name: Deploy ๐
- id: deployment
- uses: actions/deploy-pages@v4
- # - name: Build Portfolio Micro App
- # run: npx nx run portfolio:build
- # - name: Deploy Portfolio Micro App ๐
- # uses: JamesIves/github-pages-deploy-action@4.1.5
- # with:
- # token: ${{ secrets.ACCESS_TOKEN }} # Access token from the Prod repo, set in Settings > Secrets
- # repository-name: code-star/nx-reference-portfolio
- # branch: gh-pages # The branch the action should deploy to.
- # folder: dist/apps/portfolio # The folder the action should deploy.
- # clean: true # Automatically remove deleted files from the deploy branch
- # - name: Build Shell Micro App
- # run: npx nx run demo:build
- # - name: Deploy Shell Micro App ๐
- # uses: JamesIves/github-pages-deploy-action@4.1.5
- # with:
- # token: ${{ secrets.ACCESS_TOKEN }} # Access token from the Prod repo, set in Settings > Secrets
- # repository-name: code-star/nx-reference-shell
- # branch: gh-pages # The branch the action should deploy to.
- # folder: dist/apps/demo # The folder the action should deploy.
- # clean: true # Automatically remove deleted files from the deploy branch
diff --git a/README.md b/README.md
index 4e794fb..d4e64a8 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ A reference monorepo demonstrating **Module Federation in Nx with Angular**, plu
Made by [Codestar](https://code-star.github.io).
-For a component tour see the [Storybook Intro](https://code-star.github.io/nx-reference/?path=/docs/introduction--docs).
+For a component tour see the [Storybook Intro](https://code-star.github.io/nx-reference/storybook/?path=/docs/introduction--docs).
> **Rebuilt on the latest toolchain** โ Nx 23 ยท Angular 21 (standalone + signals + zoneless) ยท
> Storybook 10 ยท Native Federation. The full rationale and audit trail live under [`docs/`](./docs).
@@ -90,6 +90,58 @@ npx nx build-storybook demo # build the Storybook catalog
The latest verification results are recorded in [`docs/reports/test-report.md`](./docs/reports/test-report.md).
+## Hosting
+
+Everything is served from a **single GitHub Pages site** for this repo
+(`code-star.github.io/nx-reference/`) using
+subdirectory deployment:
+
+| URL | Serves |
+| ------------------------------------------- | ----------------------------------------------------------------- |
+| `http://codestar.nl/nx-reference/` | Shell โ Module Federation host (`apps/demo`) |
+| `http://codestar.nl/nx-reference/portfolio` | Portfolio remote micro app + `remoteEntry.json` (`apps/portfolio`) |
+| `http://codestar.nl/nx-reference/storybook` | Storybook component catalog |
+
+The `server` Express app is not deployed to Pages.
+
+### How it works
+
+- Each app builds with its own base href (`/nx-reference/` for the shell,
+ `/nx-reference/portfolio/` for the remote โ set on the production build
+ configuration in each `project.json`) and is published to its own subdirectory.
+- At runtime the shell reads `federation.manifest.json`. The production manifest
+ ([`apps/demo/federation.manifest.prod.json`](./apps/demo/federation.manifest.prod.json))
+ points the `portfolio` remote at `/nx-reference/portfolio/remoteEntry.json`;
+ the committed dev manifest keeps pointing at `http://localhost:4201` so local
+ development is unchanged.
+- A root `404.html` (copy of the shell `index.html`) plus a `.nojekyll` marker
+ provide SPA deep-link fallback on GitHub Pages.
+
+### Independent deployments
+
+Three path-filtered workflows deploy each piece **independently**, all through the
+`actions/deploy-pages` action:
+
+| Workflow | Triggered by | Deploys |
+| ------------------------------------------ | ------------------------------------- | ------------- |
+| `.github/workflows/deploy-shell.yml` | `apps/demo/**`, shared libs | site root `/` |
+| `.github/workflows/deploy-portfolio.yml` | `apps/portfolio/**`, shared libs | `/portfolio` |
+| `.github/workflows/deploy-storybook.yml` | `.storybook`, story files, shared libs | `/storybook` |
+
+A change confined to `apps/portfolio` rebuilds and redeploys **only** the portfolio
+remote โ the shell is never rebuilt, and vice-versa. That is the module-federation
+payoff: host and remotes are decoupled at deploy time.
+
+Because `deploy-pages` publishes one artifact that replaces the whole site, the
+shared [`.github/actions/deploy-subdir`](./.github/actions/deploy-subdir) composite
+action keeps the full assembled site on a `pages-content` storage branch and
+overlays only the changed subdirectory before each deploy. All Pages deploys share a
+`pages` concurrency group so they serialise safely.
+
+> **Setup:** set the repo's **Settings โ Pages โ Source** to **GitHub Actions**, then
+> trigger each workflow once via **Run workflow** (`workflow_dispatch`) to seed all
+> three subdirectories on the first deploy.
+
## Documentation & audit trail
- [Architecture overview](./docs/architecture/overview.md) and
diff --git a/apps/demo/.storybook/main.ts b/apps/demo/.storybook/main.ts
index 3c431c5..2ff3c24 100644
--- a/apps/demo/.storybook/main.ts
+++ b/apps/demo/.storybook/main.ts
@@ -1,4 +1,5 @@
import type { StorybookConfig } from '@storybook/angular';
+import { join } from 'node:path';
/**
* Single deployable catalog: aggregates the demo app, the @star/ui component
@@ -15,6 +16,17 @@ const config: StorybookConfig = {
name: '@storybook/angular',
options: {},
},
+ // Mirror the `root-package-json` tsconfig path alias for the webpack builder
+ // so MDX/story imports of the root package.json resolve during the catalog build.
+ webpackFinal: async (webpackConfig) => {
+ webpackConfig.resolve ??= {};
+ webpackConfig.resolve.alias = {
+ ...(webpackConfig.resolve.alias ?? {}),
+ 'root-package-json': join(process.cwd(), 'package.json'),
+ };
+ return webpackConfig;
+ },
};
export default config;
+
diff --git a/apps/demo/federation.manifest.prod.json b/apps/demo/federation.manifest.prod.json
new file mode 100644
index 0000000..2c3de27
--- /dev/null
+++ b/apps/demo/federation.manifest.prod.json
@@ -0,0 +1,3 @@
+{
+ "portfolio": "/nx-reference/portfolio/remoteEntry.json"
+}
diff --git a/apps/demo/project.json b/apps/demo/project.json
index bc1f342..12851f3 100644
--- a/apps/demo/project.json
+++ b/apps/demo/project.json
@@ -11,7 +11,8 @@
"options": {},
"configurations": {
"production": {
- "target": "demo:esbuild:production"
+ "target": "demo:esbuild:production",
+ "baseHref": "/nx-reference/"
},
"development": {
"target": "demo:esbuild:development",
diff --git a/apps/demo/src/app/intro.mdx b/apps/demo/src/app/intro.mdx
index c1f3f40..e00ef5b 100644
--- a/apps/demo/src/app/intro.mdx
+++ b/apps/demo/src/app/intro.mdx
@@ -1,9 +1,13 @@
import { Meta } from '@storybook/addon-docs/blocks';
+import { version } from 'root-package-json';
-
+
# Nx Reference
+Version {version}
+
+
A reference example monorepo with [Nx](https://nx.dev) + [Storybook](https://storybook.js.org) + [Atomic Design](https://bradfrost.com/blog/post/atomic-web-design/) in [Angular](https://angular.dev).
Made by [Codestar](https://code-star.github.io) powered by [Sopra Steria](https://www.soprasteria.nl).
diff --git a/apps/portfolio/project.json b/apps/portfolio/project.json
index 5298742..7e92510 100644
--- a/apps/portfolio/project.json
+++ b/apps/portfolio/project.json
@@ -12,7 +12,8 @@
"options": {},
"configurations": {
"production": {
- "target": "portfolio:esbuild:production"
+ "target": "portfolio:esbuild:production",
+ "baseHref": "/nx-reference/portfolio/"
},
"development": {
"target": "portfolio:esbuild:development",
diff --git a/docs/architecture/adr/0007-github-pages-subdirectory-hosting.md b/docs/architecture/adr/0007-github-pages-subdirectory-hosting.md
new file mode 100644
index 0000000..7994a80
--- /dev/null
+++ b/docs/architecture/adr/0007-github-pages-subdirectory-hosting.md
@@ -0,0 +1,75 @@
+# ADR-0007: Single GitHub Pages site with subdirectory hosting and independent per-app deploys
+
+> **date:** 2026-07-03\
+> **status:** accepted
+
+## context
+
+The Module Federation front-ends were previously hosted from **separate GitHub repositories**, one
+per front-end, and the Storybook catalog was deployed at the **Pages root** of this repo (the legacy
+`prod.yml` workflow). `hosting.md` frames the choice: GitHub Pages serves only **one** site per repo,
+so multiple origins are not possible from a single repo (Option 1), but everything can live under one
+Pages site via **subdirectory deployment** (Option 2).
+
+We want to consolidate the demo into this single monorepo and still host the shell, the remote, and
+the Storybook catalog โ while keeping the module-federation selling point visible: the host and its
+remotes are **decoupled at deploy time**.
+
+## decision
+
+Adopt **Option 2 โ one GitHub Pages site with subdirectory deployment** for `code-star/nx-reference`
+(custom domain `codestar.nl`, project base path `/nx-reference/`):
+
+| URL | Serves |
+| ------------------------------ | ---------------------------------------------------------- |
+| `/nx-reference/` | Shell โ Native Federation host (`apps/demo`) |
+| `/nx-reference/portfolio/` | Portfolio remote + `remoteEntry.json` (`apps/portfolio`) |
+| `/nx-reference/storybook/` | Storybook component catalog |
+
+- **Per-app base href** is set on the **production** build configuration in each `project.json`
+ (`/nx-reference/` for the shell, `/nx-reference/portfolio/` for the remote). Development
+ configurations keep base href `/`, so local dev is unchanged.
+- **Production federation manifest** (`apps/demo/federation.manifest.prod.json`) points the
+ `portfolio` remote at `/nx-reference/portfolio/remoteEntry.json`; the committed dev manifest keeps
+ pointing at `http://localhost:4201`. The deploy job swaps the prod manifest into the built shell.
+- **`actions/deploy-pages` for every deployment.** Because it publishes one artifact that replaces
+ the whole site, the full assembled tree is kept on a `pages-content` **storage branch**; a shared
+ composite action (`.github/actions/deploy-subdir`) overlays only the changed subdirectory, adds the
+ SPA fallback (`404.html` + `.nojekyll`), uploads the merged site, and persists it back.
+- **Independent, path-filtered workflows** โ `deploy-shell.yml`, `deploy-portfolio.yml`,
+ `deploy-storybook.yml` โ so a change confined to `apps/portfolio` rebuilds and deploys **only** the
+ remote, and a change to `apps/demo` deploys **only** the shell. Shared libraries trigger all three.
+ All deploys share a `pages` **concurrency group** so storage-branch writes serialise.
+
+## alternatives considered
+
+- **Option 1 โ multiple `gh-pages`-style branches (one per module):** rejected. A repo has exactly one
+ Pages source, so multiple branches cannot yield multiple origins (`hosting.md`).
+- **Status quo โ one repo per front-end:** rejected. More repos, tokens, and CI to maintain; obscures
+ the monorepo/loose-coupling story the demo exists to tell.
+- **Single "rebuild-and-deploy-everything" workflow:** rejected. Simpler, but every remote change would
+ rebuild and redeploy the shell, hiding the module-federation decoupling advantage.
+- **Pages "Deploy from a branch" source instead of `deploy-pages`:** rejected. The Actions artifact
+ flow (`upload-pages-artifact` + `deploy-pages`) is the current first-class path and was the
+ requested mechanism.
+
+## rationale
+
+- Subdirectory hosting is the only way to serve shell + remote + catalog from one repo's Pages site.
+- Native Federation resolves remote chunks relative to `remoteEntry.json`, so a correct base href and
+ a manifest pointing at `/nx-reference/portfolio/remoteEntry.json` are sufficient for the host to
+ lazy-load the remote in production.
+- Path-filtered per-app workflows make the loose coupling **observable in CI**: independent build and
+ deploy per micro app.
+
+## consequences
+
+- **Easier:** one repo and CI surface; independent per-app deploys; standard `deploy-pages` flow;
+ local dev untouched (dev manifest + base href `/`).
+- **Harder / to note:**
+ - `deploy-pages` replaces the whole site, so the `pages-content` storage branch is required to
+ preserve unchanged subdirectories between independent deploys.
+ - **First run** must seed all three subdirectories โ trigger each workflow once
+ (`workflow_dispatch`) and set **Pages โ Source** to **GitHub Actions**.
+ - A shared-library change intentionally triggers all three deploys (real coupling through `@star/*`).
+ - The custom domain `codestar.nl` is managed at the org Pages level; no per-repo `CNAME` is added.
diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md
index 22ab45e..6cc4230 100644
--- a/docs/architecture/overview.md
+++ b/docs/architecture/overview.md
@@ -46,8 +46,9 @@ flowchart LR
DataAccess["@star/shared/data-access"] --> Types
Services["@star/shared/services"] --> Types
Server --> Btc["@star/btc"]
- subgraph Deployed
- SB[Storybook 10 catalog] --> UI
+ subgraph Deployed["Deployed โ one GitHub Pages site (/nx-reference/)"]
+ Shell["/ shell (demo)"] -. Native Federation .-> Remote["/portfolio remote"]
+ SB["/storybook catalog"] --> UI
SB --> Stories[shared + app stories]
end
```
@@ -108,8 +109,10 @@ The Phase-0 open questions below were resolved during implementation:
- **Build/DX:** `nx serve demo` + `nx serve portfolio` run the MF demo; `nx run demo:build-storybook`
produces the deployable catalog; `nx run-many -t lint,test,build` is green.
- **Reliability:** rate fetch failures degrade gracefully (logged alert, no crash) โ preserved.
-- **Deployability:** Storybook static output hostable on GitHub Pages with correct base-href; prod
- remote URL configurable to the Pages URL.
+- **Deployability:** one GitHub Pages site (`/nx-reference/`) hosts the shell (root), the
+ `portfolio` remote (`/portfolio`, also serving `remoteEntry.json`), and the Storybook catalog
+ (`/storybook`) via subdirectory deployment with per-app base-href. Each app builds and deploys
+ independently through `actions/deploy-pages` (see ADR-0007).
- **Security posture:** demo-grade; server has no secrets; cors open (unchanged, demo scope).
## key decisions
@@ -120,6 +123,7 @@ The Phase-0 open questions below were resolved during implementation:
- [ADR-0004](./adr/0004-storybook-catalog.md) โ Storybook 10 as the deployed component catalog.
- [ADR-0005](./adr/0005-testing-strategy.md) โ Testing strategy & coverage parity.
- [ADR-0006](./adr/0006-standalone-signals.md) โ Standalone components + signal APIs.
+- [ADR-0007](./adr/0007-github-pages-subdirectory-hosting.md) โ Single GitHub Pages site, subdirectory hosting, independent per-app deploys.
## open questions
diff --git a/hosting.md b/hosting.md
new file mode 100644
index 0000000..9ab880d
--- /dev/null
+++ b/hosting.md
@@ -0,0 +1,78 @@
+I now use multiple github repos to host multiple front-ends for module federation, for a demo project to demonstrate module federation. I use github pages. Can I somehow integrate the repos into a single repo and still host multiple modules on github pages?
+
+---
+
+Yes, you can consolidate into a single repo and still host multiple sites on GitHub Pages. There are two solid approaches:
+
+**Option 1: Multiple `gh-pages`-style branches (one per module)**
+
+GitHub Pages can serve from any branch. You can have:
+- `gh-pages-shell` โ serves the shell app
+- `gh-pages-remote1` โ serves remote1
+- `gh-pages-remote2` โ serves remote2
+
+Each gets its own GitHub Pages URL like `https://.github.io//` ... but actually this doesn't work cleanly โ a repo only has **one** Pages site, served from one source. So this approach alone won't give you multiple origins.
+
+**Option 2: Subdirectory deployment (recommended)**
+
+Host everything under one Pages site using subdirectories:
+
+```
+https://.github.io//shell/
+https://.github.io//remote1/
+https://.github.io//remote2/
+```
+
+Your repo structure:
+
+```
+/
+โโโ packages/
+โ โโโ shell/
+โ โโโ remote1/
+โ โโโ remote2/
+โโโ .github/workflows/deploy.yml
+```
+
+In each app's webpack/vite config, set `publicPath` / `base` to match:
+
+```js
+// vite.config.js for remote1
+base: '/your-repo-name/remote1/'
+```
+
+And in the shell's module federation config, point remotes at the deployed paths:
+
+```js
+remotes: {
+ remote1: 'remote1@https://.github.io//remote1/remoteEntry.js',
+ remote2: 'remote2@https://.github.io//remote2/remoteEntry.js',
+}
+```
+
+Your deploy workflow builds each package and puts the output in the right subdirectory:
+
+```yaml
+- name: Build all
+ run: |
+ npm run build --workspace=packages/shell
+ npm run build --workspace=packages/remote1
+ npm run build --workspace=packages/remote2
+
+- name: Arrange dist
+ run: |
+ mkdir -p dist/shell dist/remote1 dist/remote2
+ cp -r packages/shell/dist/* dist/shell/
+ cp -r packages/remote1/dist/* dist/remote1/
+ cp -r packages/remote2/dist/* dist/remote2/
+
+- name: Deploy
+ uses: peaceiris/actions-gh-pages@v4
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./dist
+```
+
+**Bonus: you can still develop independently** using an npm/pnpm workspaces monorepo โ each package has its own `package.json`, and the root orchestrates builds. This is probably the cleanest fit for a demo project since it keeps module boundaries clear while unifying the repo and CI.
+
+The main thing to watch is that all `remoteEntry.js` URLs and `publicPath` values match the subdirectory structure โ module federation is sensitive to those base URLs being exactly right.
diff --git a/package.json b/package.json
index 45bb5dc..e646f57 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "nx-reference",
- "version": "2.0.2",
+ "version": "2.2.0",
"license": "MIT",
"scripts": {
"docs:json": "compodoc -p ./tsconfig.doc.json -e json -d ."