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
130 changes: 129 additions & 1 deletion .eleventy.js
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,7 @@ module.exports = function(eleventyConfig) {

const features = frontmatter.features;
const release = frontmatter.release;
if (!features || !Array.isArray(features) || features.length === 0) return content;
if (!release || !features || !Array.isArray(features) || features.length === 0) return content;

// Build injection map: heading text -> { badges HTML, changelogs HTML }
const injections = [];
Expand Down Expand Up @@ -872,11 +872,139 @@ module.exports = function(eleventyConfig) {
return content;
});

function findFeatureByDocsLink(pageUrl) {
if (!pageUrl) return null;
const normalizedPage = pageUrl.replace(/\/$/, '') + '/';
for (const section of featureCatalog.sections) {
for (const feature of section.features) {
if (!feature.docsLink || feature.subfeature) continue;
let link = feature.docsLink;
// Strip full domain if present
link = link.replace(/^https?:\/\/flowfuse\.com/, '');
// Strip fragment
link = link.replace(/#.*$/, '');
const normalizedLink = link.replace(/\/$/, '') + '/';
if (normalizedPage === normalizedLink) return feature;
}
}
return null;
}

function findSubfeaturesForDocsPage(pageUrl) {
if (!pageUrl) return [];
const normalizedPage = pageUrl.replace(/\/$/, '') + '/';
const results = [];
for (const section of featureCatalog.sections) {
for (const feature of section.features) {
if (!feature.docsLink || !feature.subfeature) continue;
let link = feature.docsLink;
link = link.replace(/^https?:\/\/flowfuse\.com/, '');
const fragment = (link.match(/#(.+)/) || [])[1];
if (!fragment) continue;
const linkPath = link.replace(/#.*/, '').replace(/\/$/, '') + '/';
if (normalizedPage === linkPath) {
results.push({ feature, fragment });
}
}
}
return results;
}

// Inject tier badges into docs 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;

const parentFeature = findFeatureByDocsLink(this.page.url);
const subfeatures = findSubfeaturesForDocsPage(this.page.url);

// Parse frontmatter for features array (same format as changelog posts)
let fmFeatures = [];
const inputPath = this.page.inputPath;
if (inputPath && inputPath.endsWith('.md')) {
try {
const source = fs.readFileSync(inputPath, 'utf8');
const fmMatch = source.match(/^---\n([\s\S]*?)\n---/);
if (fmMatch) {
const fm = yaml.load(fmMatch[1]);
if (fm.features && Array.isArray(fm.features)) {
fmFeatures = fm.features;
}
}
} catch (e) { /* ignore */ }
}

if (!parentFeature && subfeatures.length === 0 && fmFeatures.length === 0) return content;

const ops = [];

// Inject parent feature badges after the first H1
if (parentFeature) {
const h1Regex = /<h1[^>]*>.*?<\/h1>/s;
const h1Match = h1Regex.exec(content);
if (h1Match) {
const badges = renderTierBadges(parentFeature);
if (badges) {
const wrapped = badges.replace('class="ff-tier-badges"', 'class="ff-tier-badges not-prose"');
ops.push({ index: h1Match.index + h1Match[0].length, html: wrapped });
}
}
}

// Scan headings for subfeature and frontmatter-based injections
if (subfeatures.length > 0 || fmFeatures.length > 0) {
const headingRegex = /<h([2-6])\s[^>]*id="([^"]*)"[^>]*>.*?<\/h\1>/gs;
const headingMatches = [];
let hmatch;
while ((hmatch = headingRegex.exec(content)) !== null) {
const textContent = hmatch[0].replace(/<[^>]+>/g, '').trim();
headingMatches.push({ index: hmatch.index, length: hmatch[0].length, id: hmatch[2], text: textContent, level: parseInt(hmatch[1]) });
}

// Frontmatter features take priority — track handled heading IDs
const handledHeadingIds = new Set();
for (const entry of fmFeatures) {
if (!entry.id || !entry.heading) continue;
const feature = findFeatureById(entry.id);
if (!feature) continue;
const heading = headingMatches.find(h => h.text === entry.heading);
if (!heading) continue;
handledHeadingIds.add(heading.id);
const badges = renderTierBadges(feature);
if (badges) {
const wrapped = badges.replace('class="ff-tier-badges"', 'class="ff-tier-badges not-prose"');
ops.push({ index: heading.index + heading.length, html: wrapped });
}
}

// Subfeatures matched by docsLink fragment (skip if frontmatter already handled)
for (const { feature, fragment } of subfeatures) {
if (handledHeadingIds.has(fragment)) continue;
const heading = headingMatches.find(h => h.id === fragment);
if (!heading) continue;
const badges = renderTierBadges(feature);
if (badges) {
const wrapped = badges.replace('class="ff-tier-badges"', 'class="ff-tier-badges not-prose"');
ops.push({ index: heading.index + heading.length, html: wrapped });
}
}
}

ops.sort((a, b) => b.index - a.index);
for (const op of ops) {
content = content.slice(0, op.index) + op.html + content.slice(op.index);
}
return content;
});

// Make helpers available to changelog layout via filters
eleventyConfig.addFilter("featureForChangelog", function(url) {
return findFeatureByChangelog(url);
});

eleventyConfig.addFilter("featureForDocsPage", function(url) {
return findFeatureByDocsLink(url);
});

eleventyConfig.addFilter("tierLabel", function(tierData) {
return deriveTierLabel(tierData);
});
Expand Down
20 changes: 10 additions & 10 deletions src/_data/featureCatalog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ sections:
- id: custom-hostname
label: Custom Hostnames
description: "Access your Node-RED application via your own domain name."
docsLink: null
docsLink: /docs/user/custom-hostnames/
changelog: null
solutions: []
showOnPricing: true
Expand Down Expand Up @@ -343,7 +343,7 @@ sections:
- id: persistent-context
label: Persistent Context
description: "In-memory values defined in a Node-RED flow persist across project restarts and upgrades."
docsLink: null
docsLink: /docs/user/persistent-context/
changelog: null
solutions: [mes, data-integration]
showOnPricing: true
Expand Down Expand Up @@ -385,7 +385,7 @@ sections:
- id: mqtt-broker
label: MQTT Broker
description: "Manage and create MQTT clients to transport data for efficient messaging and communication within your applications."
docsLink: null
docsLink: /docs/user/teambroker/
changelog: null
solutions: [uns, it-ot-middleware, data-integration, mes, scada]
showOnPricing: true
Expand Down Expand Up @@ -480,7 +480,7 @@ sections:
- id: pipelines
label: DevOps Pipelines
description: "Set up different environments for development, testing, and production Node-RED instances to support a full software delivery lifecycle."
docsLink: null
docsLink: /docs/user/devops-pipelines/
changelog: null
solutions: [mes, scada, uns, it-ot-middleware, data-integration]
showOnPricing: true
Expand Down Expand Up @@ -549,7 +549,7 @@ sections:
- id: device-groups
label: Device Group Management
description: "Logically group devices assigned to an application and integrate device groups into your DevOps Pipeline for coordinated fleet updates."
docsLink: null
docsLink: /docs/user/device-groups/
changelog: null
solutions: [mes, scada, edge-connectivity]
showOnPricing: true
Expand Down Expand Up @@ -595,7 +595,7 @@ sections:
- id: ha
label: High Availability
description: "Leverage horizontal scaling for reliable and scalable processing of your data through Node-RED."
docsLink: null
docsLink: /docs/user/high-availability/
changelog: null
solutions: [mes, scada, uns]
showOnPricing: true
Expand Down Expand Up @@ -641,7 +641,7 @@ sections:
- id: tables
label: FlowFuse Tables
description: "Integrated database feature for storing, reading, writing, and querying data within FlowFuse."
docsLink: null
docsLink: /docs/user/ff-tables/
changelog: null
solutions: [mes, scada, data-integration]
showOnPricing: true
Expand All @@ -667,7 +667,7 @@ sections:
- id: audit-log
label: Audit Log
description: "Keep track of everything going on in your Node-RED instances and FlowFuse. Audit Logs provide details on what actions have taken place, when they happened, and who did them."
docsLink: null
docsLink: /docs/user/logs/
changelog: null
solutions: [mes, scada, uns, it-ot-middleware]
showOnPricing: true
Expand Down Expand Up @@ -836,7 +836,7 @@ sections:
- id: sso
label: Single Sign-On (SSO)
description: "Configure FlowFuse to work with your own SSO provider, allowing users to access FlowFuse with a single set of login credentials."
docsLink: null
docsLink: /docs/admin/sso/
changelog: null
solutions: [mes, scada, uns, it-ot-middleware]
showOnPricing: true
Expand Down Expand Up @@ -993,7 +993,7 @@ sections:
- id: team-library
label: Team Library
description: "Set up standard nodes and flows that can be shared with all team members across your organisation."
docsLink: null
docsLink: /docs/user/shared-library/
changelog: null
solutions: [mes, scada, uns]
showOnPricing: true
Expand Down
Loading