Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 35 additions & 16 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The site is migrating from Eleventy (11ty) to Nuxt 3. Nuxt is the primary framew
| Section | Status |
|---------|--------|
| `/handbook/**` | **Migrated** — served by Nuxt (`nuxt/content/handbook/`) |
| `/docs/**` | **Migrated** — served by Nuxt; source cloned from `flowfuse/flowfuse` at build time |
| All other routes | Still on 11ty, proxied through Nuxt in dev |

### Production build order
Expand All @@ -25,20 +26,21 @@ The site is migrating from Eleventy (11ty) to Nuxt 3. Nuxt is the primary framew
clean:nuxt → build:js:nuxt → prod:postcss-nuxt → prod:eleventy-nuxt → prod:nuxt
```

11ty outputs to `nuxt/public/` so Nuxt can serve 11ty-generated assets. `nuxt/public/` is gitignored (fully build-generated).
The `docs-source` Nuxt module runs automatically during `prod:nuxt` and sparse-clones `docs/` from `flowfuse/flowfuse` (public repo, no token needed). 11ty outputs to `nuxt/public/` so Nuxt can serve 11ty-generated assets. `nuxt/public/` is gitignored (fully build-generated).

## Dev commands

```bash
npm start # all watchers in parallel (11ty + nuxt + postcss + docs + blueprints)
npm start # all watchers in parallel (11ty + nuxt + postcss + blueprints)
npm run dev # eleventy + postcss + nuxt only
npm run dev:eleventy # 11ty only, port 8080 (legacy; most work doesn't need this)
npm run dev:nuxt # Nuxt only, port 3000 — use this for handbook and migrated pages
npm run docs # sync docs from external source once
npm run dev:nuxt # Nuxt only, port 3000 — use this for handbook, docs, and migrated pages
npm run build # production build
```

> When working on the handbook or other migrated sections, `npm run dev:nuxt` is sufficient. `npm start` is only needed when also touching 11ty-served pages.
> When working on the handbook, docs, or other migrated sections, `npm run dev:nuxt` is sufficient. `npm start` is only needed when also touching 11ty-served pages.
>
> **Local docs development:** set `FLOWFUSE_DOCS_LOCAL=/path/to/flowfuse` to point the docs module at a local checkout instead of cloning from GitHub. If the env var is not set and `nuxt/content/docs/` already exists, that cached copy is used. If neither is true, the module clones fresh from GitHub (public, no token needed).

## Directory layout

Expand All @@ -51,14 +53,27 @@ src/
├── blog/ # Blog posts → /blog/YYYY/MM/slug/
├── changelog/ # Changelog entries → /changelog/YYYY/MM/slug/
├── customer-stories/ # Case studies → /customer-stories/slug/
├── docs/ # Product docs → /docs/section/slug/ (synced from external)
├── handbook/ # Employee handbook → /handbook/section/slug/
├── css/ # Tailwind + custom CSS
├── images/ # Static images
└── public/ # Pass-through static files
scripts/ # Build-time scripts (copy_docs.js, copy_blueprints.js, etc.)
nuxt/
├── content/
│ ├── handbook/ # Handbook pages (edit here)
│ └── docs/ # Product docs (build-generated, gitignored — do not edit)
├── modules/
│ └── docs-source.ts # Clones docs from flowfuse/flowfuse at build time
├── composables/
│ ├── useHandbookNav.ts
│ └── useDocsNav.ts
├── components/
│ ├── HandbookLeftNav.vue
│ └── DocsLeftNav.vue
└── pages/
├── handbook/[...slug].vue
└── docs/[...slug].vue
scripts/ # Build-time scripts (copy_blueprints.js, etc.)
lib/ # Shared helpers used by .eleventy.js and scripts
.eleventy.js # Main Eleventy config (1100+ lines)
.eleventy.js # Main Eleventy config
```

---
Expand Down Expand Up @@ -149,25 +164,29 @@ Collection config: `nuxt/content.config.ts` (defines the `handbook` collection)

### Product docs

**Source:** `src/docs/{section}/{slug}.md` — **do not edit directly**; synced via `node scripts/copy_docs.js` from the external `flowfuse/flowfuse` monorepo.
**Source:** `flowfuse/flowfuse` repo, `docs/` directory — **do not edit in this repo**; cloned automatically at build time by `nuxt/modules/docs-source.ts`.
**URL:** `/docs/{section}/{slug}/`
**Layout:** `layouts/documentation.njk`
**Rendered by:** Nuxt — `nuxt/pages/docs/[...slug].vue` + `DocsLeftNav` component
**Local content:** `nuxt/content/docs/` (gitignored, build-generated)
**Local assets:** `nuxt/public/docs/` (images, etc.)

```yaml
---
navTitle: "Page title for sidebar"
navGroup: "Section heading"
navGroup: "Section heading" # set on section index pages only
navOrder: 3
meta:
description: "Page description"
# optional redirect:
# optional redirect (section index pages):
redirect:
to: https://example.com
to: /docs/section/first-page
layout: redirect
---
```

Collection config: `src/docs/docs.json`
**Nav groups** (in order): FlowFuse User Manuals · Device Agent · FlowFuse Cloud · FlowFuse Self-Hosted · Support · Contributing
**Nav composable:** `nuxt/composables/useDocsNav.ts`
**Collection config:** `nuxt/content.config.ts` (defines the `docs` collection)

---

Expand Down Expand Up @@ -227,7 +246,7 @@ Collection config: `src/customer-stories/customer-stories.json`
| `layouts/base.njk` | HTML shell |
| `layouts/post.njk` | Blog posts |
| `layouts/post-changelog.njk` | Changelog entries |
| `layouts/documentation.njk` | Docs + handbook (with sidebar nav) |
| `layouts/documentation.njk` | Node-RED learning resources (with sidebar nav) |
| `layouts/story.njk` | Customer stories |
| `layouts/nohero.njk` | General pages without hero |

Expand Down
137 changes: 5 additions & 132 deletions .eleventy.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ module.exports = function(eleventyConfig) {


eleventyConfig.addDataExtension("yaml", contents => yaml.load(contents)); // Add support for YAML data files
eleventyConfig.setUseGitIgnore(false); // Otherwise docs are ignored
eleventyConfig.setWatchThrottleWaitTime(500); // in milliseconds
eleventyConfig.setFrontMatterParsingOptions({
excerpt: true,
Expand Down Expand Up @@ -926,10 +925,10 @@ module.exports = function(eleventyConfig) {
return results;
}

// Inject tier badges into docs pages: parent feature after H1, subfeatures after their headings
// Inject tier badges into node-red pages: parent feature after H1, subfeatures after their headings
eleventyConfig.addTransform("docsFeatureBadges", function(content) {
if (!this.page.outputPath || !this.page.outputPath.endsWith(".html")) return content;
if (!this.page.url || !/^(\/docs\/|\/node-red\/)/.test(this.page.url)) return content;
if (!this.page.url || !/^\/node-red\//.test(this.page.url)) return content;

const parentFeature = findFeatureByDocsLink(this.page.url);
const subfeatures = findSubfeaturesForDocsPage(this.page.url);
Expand Down Expand Up @@ -1112,135 +1111,9 @@ module.exports = function(eleventyConfig) {
return await imageHandler(imageSrc, imageDescription, title, [imageSize], null, currentWorkingFilePath, eleventyConfig, async=true, SKIP_IMAGES, priority);
});

// Create a collection for sidebar navigation
eleventyConfig.addCollection('nav', function(collection) {
let nav = {}

createNav('docs')

function createNav(tag) {
const groupOrder = {
docs: [
'FlowFuse User Manuals',
'Device Agent',
'FlowFuse Cloud',
'FlowFuse Self-Hosted',
'Support',
'Contributing'
]
}

collection.getFilteredByTag(tag).filter((page) => {
return !page.url.includes('README')
}).sort((a, b) => {
// sort by depth, so we catch all the correct index.md routes
const hierarchyA = a.url.split('/').filter(n => n)
const hierarchyB = b.url.split('/').filter(n => n)
return hierarchyA.length - hierarchyB.length
}).forEach((page) => {
let url = page.url

// work out ToC Hierarchy
// split the folder URI/URL, as this defines our TOC Hierarchy
const hierarchy = url.split('/').filter(n => n)
// recursively parse the folder hierarchy and created our collection object
// pass nav = {} as the first accumulator - build up hierarchy map of TOC
hierarchy.reduce((accumulator, currentValue, i) => {
// create a nested object detailing the full docs hierarchy
if (!accumulator[currentValue]) {
accumulator[currentValue] = {
'name': currentValue,
'url': page.data.redirect?.to || page.data.redirect || page.url,
'order': page.data.navOrder || Number.MAX_SAFE_INTEGER,
'children': {}
}
if (page.data.navTitle) {
accumulator[currentValue].name = page.data.navTitle
}
// TODO: navGroup will be used in the rendering of the ToC at a later stage
if (page.data.navGroup) {
accumulator[currentValue].group = page.data.navGroup
}
}
return accumulator[currentValue].children
}, nav)
})

// recursive functions to format our nav map to arrays
function childrenToArray (children) {
return Object.values(children)
}
function nestedChildrenToArray (value) {
for (const [key, entry] of Object.entries(value)) {
if (entry.children && Object.keys(entry.children).length > 0) {
// ensure our grandchildren are all converted to arrays before
// we convert the higher level object to an array
nestedChildrenToArray(entry.children)
// now we have converted all grandchildren,
// we can convert our children to an array
entry.children = childrenToArray(entry.children)
} else {
delete entry.children
}
}

}
// convert our objects to arrays so we can render in nunjucks
nestedChildrenToArray(nav)

// add functionality to group to-level items for better navigation.
let groups = {
'Other': {
name: 'Other',
order: Number.MAX_SAFE_INTEGER, // always render last
children: []
}
}

if (nav[tag]) {
for (child of nav[tag].children) {
if (child.group) {
const group = child.group
if (!groups[group]) {
groups[group] = {
name: group,
order: groupOrder[tag] && groupOrder[tag].includes(group) ? groupOrder[tag].indexOf(group) : Number.MAX_SAFE_INTEGER,
children: []
}
}
groups[group].children.push(child)
} else {
// capture & flag top-level docs that haven't had a group assigned
groups['Other'].children.push(child)
}
}

function sortChildren (a, b) {
// sort children by 'order', then alphabetical
return (a.order - b.order) || a.name.localeCompare(b.name)
}

function sortTree (node) {
if (!node || !node.children || !Array.isArray(node.children)) {
return
}

node.children.sort(sortChildren)
node.children.forEach(sortTree)
}

nav[tag].groups = Object.values(groups).sort(sortChildren)

nav[tag].groups.forEach((group) => {
if (group.children) {
sortTree(group)
}
})
}
}

return nav;
});
// Placeholder: docs nav collection removed (docs served by Nuxt)
// Docs nav moved to Nuxt; this empty collection keeps left-nav.njk from erroring
eleventyConfig.addCollection('nav', function() { return {} });

eleventyConfig.addCollection("aiBlog", function(collectionApi) {
return collectionApi.getFilteredByTag("ai").filter(item => {
Expand Down
15 changes: 0 additions & 15 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@ jobs:
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
path: 'website'
- name: Check out FlowFuse/flowfuse repository (to access the docs)
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: 'FlowFuse/flowfuse'
ref: main
path: 'flowfuse'
- name: Generate a token
id: generate_token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
Expand All @@ -43,17 +37,8 @@ jobs:
node-version: 24
cache: 'npm'
cache-dependency-path: './website/package-lock.json'
- run: npm run docs
working-directory: 'website'
- run: npm run blueprints
working-directory: 'website'
- name: Commit Latest Docs
run: |
cd ./website
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add src/docs/* -A -f
git commit -a -m "Bot: update docs"
- name: Commit Latest Blueprints
run: |
cd ./website
Expand Down
10 changes: 0 additions & 10 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,12 @@ jobs:
owner: ${{ github.repository_owner }}
repositories: |
website
flowfuse
blueprint-library
- name: Check out website repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
path: 'website'
token: ${{ steps.generate_token.outputs.token }}
- name: Check out FlowFuse/flowfuse repository (to access the docs)
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: 'FlowFuse/flowfuse'
ref: main
path: 'flowfuse'
token: ${{ steps.generate_token.outputs.token }}
- name: Check out FlowFuse/blueprint-library repository (to access the blueprints)
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
Expand All @@ -41,8 +33,6 @@ jobs:
node-version: 24
cache: 'npm'
cache-dependency-path: './website/package-lock.json'
- run: npm run docs
working-directory: 'website'
- run: npm run blueprints
working-directory: 'website'
- name: Install Dependencies
Expand Down
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
_site
node_modules
src/handbook/media
src/docs/*
!src/docs/docs.json
nuxt/content/docs/
src/blueprints/*
!src/blueprints/*.njk

Expand Down
Loading
Loading