From d14eb574b0b0faa0734eee2f8c3bb13ddde2eaa0 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 11 May 2026 21:48:44 -1000 Subject: [PATCH] Make Pro evaluation path obvious --- prototypes/docusaurus/src/pages/examples.tsx | 2 +- .../docusaurus/src/pages/index.module.css | 19 +++- prototypes/docusaurus/src/pages/index.tsx | 23 ++++- .../docusaurus/src/pages/pro.module.css | 20 ++++ prototypes/docusaurus/src/pages/pro.tsx | 35 ++++--- scripts/audit-docs.mjs | 3 - scripts/prepare-docs.mjs | 44 +++++---- scripts/prepare-docs.test.mjs | 96 +++++++++++++++++++ 8 files changed, 204 insertions(+), 38 deletions(-) create mode 100644 scripts/prepare-docs.test.mjs diff --git a/prototypes/docusaurus/src/pages/examples.tsx b/prototypes/docusaurus/src/pages/examples.tsx index f0a3c5f..3baca49 100644 --- a/prototypes/docusaurus/src/pages/examples.tsx +++ b/prototypes/docusaurus/src/pages/examples.tsx @@ -26,7 +26,7 @@ const evaluationPaths = [ eyebrow: 'Upgrade path', title: 'Move from OSS to Pro', description: - 'If your current app needs more SSR throughput or RSC support, compare OSS and Pro before adding the Pro package.', + 'If your current app needs more SSR throughput or RSC support, compare OSS and Pro, then evaluate Pro without a token before production licensing.', href: docsRoutes.ossVsPro, cta: 'Compare OSS and Pro', }, diff --git a/prototypes/docusaurus/src/pages/index.module.css b/prototypes/docusaurus/src/pages/index.module.css index b5dd4a8..b9b2617 100644 --- a/prototypes/docusaurus/src/pages/index.module.css +++ b/prototypes/docusaurus/src/pages/index.module.css @@ -282,7 +282,7 @@ } .upgradeGrid { - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); } .flowGrid { @@ -316,6 +316,23 @@ margin-bottom: 0.7rem; } +.licenseStrip { + display: flex; + flex-wrap: wrap; + gap: 0.35rem 0.5rem; + align-items: baseline; + margin-bottom: 1rem; + padding: 0.85rem 0.95rem; + border: 1px solid rgba(9, 105, 218, 0.3); + border-left: 4px solid var(--ifm-color-primary); + border-radius: 8px; + background: var(--site-surface); +} + +.licenseStrip strong { + color: var(--ifm-color-primary-dark); +} + .inlineCode { display: block; margin: 0 0 1rem; diff --git a/prototypes/docusaurus/src/pages/index.tsx b/prototypes/docusaurus/src/pages/index.tsx index 87cdcd8..a2b3d7c 100644 --- a/prototypes/docusaurus/src/pages/index.tsx +++ b/prototypes/docusaurus/src/pages/index.tsx @@ -26,7 +26,7 @@ const personaPaths = [ { title: 'Already on OSS and need more performance', description: - 'Compare OSS and Pro first, then upgrade only when higher-throughput SSR, RSC, or support is worth it.', + 'Compare OSS and Pro first, then evaluate Pro without a token before buying a production license.', href: docsRoutes.ossVsPro, cta: 'Compare OSS and Pro', }, @@ -57,7 +57,7 @@ const recommendedFlows = [ { title: 'When OSS is no longer enough', summary: - 'Pro is an upgrade tier, not a separate product. Compare first, then add it when the extra SSR throughput or guided support matters.', + 'Pro is an upgrade tier, not a separate product. Evaluate it in development, test, CI/CD, and staging before you need a production license.', command: 'bundle add react_on_rails_pro', href: docsRoutes.proUpgrade, cta: 'Open the upgrade guide', @@ -379,7 +379,7 @@ function HeroSection() {

Already on OSS? Start with the comparison guide, then use the upgrade guide if you - need Pro. + need Pro. No token is required for non-production evaluation.

@@ -444,6 +444,11 @@ function UpgradeSection() {

OSS to Pro

Upgrade when you're ready.

+
+ Friendly license model: evaluate Pro without a token in development, + test, CI/CD, and staging. Production deployments require a paid license. + See pricing and sign up +

1. Compare OSS and Pro

@@ -459,12 +464,22 @@ function UpgradeSection() {

2. Upgrade to Pro

Once the comparison says Pro is worth it, follow the upgrade guide and add the Pro - package. + package. The friendly license model keeps non-production evaluation token-free.

Open the upgrade guide
+
+

3. Get a production license

+

+ Development, test, CI/CD, and staging can run without a token. Production deployments + require a paid license. +

+ + Pro pricing and sign up + +
diff --git a/prototypes/docusaurus/src/pages/pro.module.css b/prototypes/docusaurus/src/pages/pro.module.css index b404f2d..3d1adca 100644 --- a/prototypes/docusaurus/src/pages/pro.module.css +++ b/prototypes/docusaurus/src/pages/pro.module.css @@ -31,6 +31,26 @@ flex-wrap: wrap; } +.licenseHighlight { + max-width: 48rem; + margin-top: 1rem; + padding: 0.85rem 0.95rem; + border: 1px solid rgba(9, 105, 218, 0.3); + border-left: 4px solid var(--ifm-color-primary); + border-radius: 8px; + background: var(--site-surface); +} + +.licenseHighlight strong { + display: block; + margin-bottom: 0.25rem; + color: var(--ifm-color-primary-dark); +} + +.licenseHighlight span { + color: var(--ifm-color-content); +} + .grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); diff --git a/prototypes/docusaurus/src/pages/pro.tsx b/prototypes/docusaurus/src/pages/pro.tsx index 9da9df0..3255081 100644 --- a/prototypes/docusaurus/src/pages/pro.tsx +++ b/prototypes/docusaurus/src/pages/pro.tsx @@ -64,9 +64,16 @@ export default function ProPage(): ReactNode {

React on Rails Pro

Pro extends React on Rails for teams that need higher SSR throughput, RSC-oriented - rendering features, and guided production support. You can evaluate Pro without a - license. + rendering features, and guided production support. The friendly license model lets + you evaluate Pro without a token before you need a production license.

+
+ Friendly license model + + No token is required for development, test, CI/CD, or staging. Production + deployments require a paid license. + +
Review the upgrade guide @@ -78,8 +85,8 @@ export default function ProPage(): ReactNode { - Contact ShakaCode + href="https://pro.reactonrails.com/"> + Pro pricing / sign up
@@ -104,18 +111,23 @@ export default function ProPage(): ReactNode {
-

Friendly evaluation policy

-

Evaluate first, sort licensing second.

-

You can try React on Rails Pro without a license while evaluating.

+

Friendly license model

+

Evaluate without a token.

+

+ Try Pro freely in development, test, CI/CD, and staging. If no license is + configured, Pro keeps running in unlicensed mode and logs license status instead of + blocking your app. +

+

+ Production deployments require a paid license. Visit{' '} + Pro pricing and sign up for current + options. +

If your organization is budget-constrained, email{' '} justin@shakacode.com. We can grant free licenses in qualifying cases.

-

- The goal is to make the upgrade path clear and low-friction, not to force a second - docs silo. -

@@ -144,6 +156,7 @@ export default function ProPage(): ReactNode {

Need pricing, implementation guidance, or a free-license discussion? Visit{' '} + Pro pricing and sign up or{' '} the Pro docs landing page.

diff --git a/scripts/audit-docs.mjs b/scripts/audit-docs.mjs index 4aecc1a..edddb4b 100644 --- a/scripts/audit-docs.mjs +++ b/scripts/audit-docs.mjs @@ -169,9 +169,6 @@ function findSuspiciousLinks(lineInfo) { if (target.includes("www.shakacode.com/react-on-rails-pro/docs/")) { issues.push(`Legacy Pro docs domain link (${target}).`); } - if (target.includes("pro.reactonrails.com")) { - issues.push(`Dead pro subdomain link (${target}).`); - } } return [...new Set(issues)]; diff --git a/scripts/prepare-docs.mjs b/scripts/prepare-docs.mjs index 2e5bd8e..9411639 100644 --- a/scripts/prepare-docs.mjs +++ b/scripts/prepare-docs.mjs @@ -269,6 +269,10 @@ async function archiveLegacyDocs(docsRoot) { return true; } +export function fixProNodeRendererMdx(content) { + return content.replace("Direct render: <50ms", "Direct render: <50ms"); +} + async function fixKnownDocsIssues(docsRoot) { await rewriteDocsByPattern(docsRoot, [ { @@ -358,6 +362,8 @@ async function fixKnownDocsIssues(docsRoot) { content.replace("using React 18's `renderToPipeableStream`", "using React 19's `renderToPipeableStream`") ); + await rewriteDoc(docsRoot, "pro/node-renderer.md", fixProNodeRendererMdx); + await rewriteDoc(docsRoot, "api-reference/view-helpers-api.md", (content) => content.replace("using React 18+ streaming", "using React 19+ streaming") ); @@ -394,14 +400,10 @@ async function fixKnownDocsIssues(docsRoot) { pattern: /https:\/\/www\.shakacode\.com\/react-on-rails-pro\/docs\//g, replacement: "https://reactonrails.com/docs/pro/" }, - { - pattern: /https:\/\/pro\.reactonrails\.com\/?/g, - replacement: "https://reactonrails.com/docs/pro/" - } ]); } -async function rewriteProLinks(proDocsRoot) { +export async function rewriteProLinks(proDocsRoot) { if (!(await exists(proDocsRoot))) { return; } @@ -414,15 +416,14 @@ async function rewriteProLinks(proDocsRoot) { const updated = original .replace(/((?:\.\.\/)+)oss\//g, "$1") .replace(/https:\/\/www\.shakacode\.com\/react-on-rails\/docs\//g, "https://reactonrails.com/docs/") - .replace(/https:\/\/www\.shakacode\.com\/react-on-rails-pro\/docs\//g, "https://reactonrails.com/docs/pro/") - .replace(/https:\/\/pro\.reactonrails\.com\/?/g, "https://reactonrails.com/docs/pro/"); + .replace(/https:\/\/www\.shakacode\.com\/react-on-rails-pro\/docs\//g, "https://reactonrails.com/docs/pro/"); if (updated !== original) { await fs.writeFile(absoluteFile, updated, "utf8"); } }); } -async function rewriteFlattenedOssLinks(docsRoot) { +export async function rewriteFlattenedOssLinks(docsRoot) { await walkFiles(docsRoot, async (absoluteFile, relativeFile) => { if (!relativeFile.endsWith(".md") && !relativeFile.endsWith(".mdx")) { return; @@ -436,15 +437,14 @@ async function rewriteFlattenedOssLinks(docsRoot) { .replace(/\.\.\/\.\.\/images\//g, "../images/") .replace(/\.\.\/\.\.\/\.\.\/assets\//g, "../../assets/") .replace(/https:\/\/www\.shakacode\.com\/react-on-rails\/docs\//g, "https://reactonrails.com/docs/") - .replace(/https:\/\/www\.shakacode\.com\/react-on-rails-pro\/docs\//g, "https://reactonrails.com/docs/pro/") - .replace(/https:\/\/pro\.reactonrails\.com\/?/g, "https://reactonrails.com/docs/pro/"); + .replace(/https:\/\/www\.shakacode\.com\/react-on-rails-pro\/docs\//g, "https://reactonrails.com/docs/pro/"); if (updated !== original) { await fs.writeFile(absoluteFile, updated, "utf8"); } }); } -async function injectProFriendlyNotice(docsRoot) { +export async function injectProFriendlyNotice(docsRoot) { const proIntroPath = path.join(docsRoot, "pro", "react-on-rails-pro.md"); if (!(await exists(proIntroPath))) { return; @@ -464,8 +464,8 @@ async function injectProFriendlyNotice(docsRoot) { } } - if (!updated.includes("Friendly evaluation policy")) { - const notice = `> **Friendly evaluation policy**\n> You can evaluate React on Rails Pro without a license.\n> If your organization is budget-constrained, email [justin@shakacode.com](mailto:justin@shakacode.com). We can provide free licenses in qualifying cases.\n\n`; + if (!/Friendly license model/i.test(updated)) { + const notice = `> **Friendly license model**\n> Try React on Rails Pro freely in development, test, CI/CD, and staging. No token is required to evaluate. If no license is configured, Pro keeps running in unlicensed mode and logs license status instead of blocking your app. Production deployments require a paid license; see [Pro pricing and sign up](https://pro.reactonrails.com/).\n\n`; updated = updated.replace(/^# React on Rails Pro\s*\n+/m, `# React on Rails Pro\n\n${notice}`); } @@ -652,14 +652,20 @@ async function normalizeCodeFences(docsRoot) { } } -function docsHomeMarkdown(sourceMarkdown, { hasArchive }) { +export function docsHomeMarkdown(sourceMarkdown, { hasArchive }) { const archiveBlock = hasArchive ? "- [Historical Reference](./archive/README.md)\n" : ""; + const friendlyLicenseSection = `## Friendly License Model + +- Try React on Rails Pro freely in development, test, CI/CD, and staging. No token is required to evaluate. +- Production deployments require a paid license. See [Pro pricing and sign up](https://pro.reactonrails.com/) for current options. If your organization is budget-constrained, [contact us](mailto:justin@shakacode.com) about free or low-cost licenses. +`; const updated = sourceMarkdown .trim() .replaceAll("(./oss/", "(./") .replace("](https://reactonrails.com/examples)", "](/examples)") .replace(/\n- \[Documentation website\]\(https:\/\/reactonrails\.com\/docs\/\)\s*/g, "\n") + .replace(/## Friendly evaluation policy\n\n[\s\S]*?(?=\n## )/, `${friendlyLicenseSection}\n`) .replace("## Need more help?\n\n", `## Need more help?\n\n${archiveBlock}`); return `---\ncustom_edit_url: null\n---\n\n${updated}\n`; @@ -778,7 +784,9 @@ async function main() { await prepareDocusaurus(); } -main().catch((error) => { - console.error(error); - process.exitCode = 1; -}); +if (process.argv[1] && path.resolve(process.argv[1]) === __filename) { + main().catch((error) => { + console.error(error); + process.exitCode = 1; + }); +} diff --git a/scripts/prepare-docs.test.mjs b/scripts/prepare-docs.test.mjs new file mode 100644 index 0000000..9ecd957 --- /dev/null +++ b/scripts/prepare-docs.test.mjs @@ -0,0 +1,96 @@ +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { + docsHomeMarkdown, + fixProNodeRendererMdx, + injectProFriendlyNotice, + rewriteFlattenedOssLinks, + rewriteProLinks, +} from "./prepare-docs.mjs"; + +async function withTempDir(callback) { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "prepare-docs-test-")); + try { + return await callback(tmpDir); + } finally { + await fs.rm(tmpDir, { recursive: true, force: true }); + } +} + +test("prepare docs keeps external Pro pricing links", async () => { + await withTempDir(async (docsRoot) => { + const ossDocPath = path.join(docsRoot, "getting-started", "oss-vs-pro.md"); + const proDocPath = path.join(docsRoot, "pro", "installation.md"); + await fs.mkdir(path.dirname(ossDocPath), { recursive: true }); + await fs.mkdir(path.dirname(proDocPath), { recursive: true }); + await fs.writeFile( + ossDocPath, + "[Pro pricing and sign up](https://pro.reactonrails.com/)\n", + "utf8" + ); + await fs.writeFile( + proDocPath, + "[Pro pricing and sign up](https://pro.reactonrails.com/)\n", + "utf8" + ); + + await rewriteFlattenedOssLinks(docsRoot); + await rewriteProLinks(path.join(docsRoot, "pro")); + + assert.match(await fs.readFile(ossDocPath, "utf8"), /https:\/\/pro\.reactonrails\.com\//); + assert.match(await fs.readFile(proDocPath, "utf8"), /https:\/\/pro\.reactonrails\.com\//); + }); +}); + +test("prepare docs injects current friendly license model notice", async () => { + await withTempDir(async (docsRoot) => { + const proIntroPath = path.join(docsRoot, "pro", "react-on-rails-pro.md"); + await fs.mkdir(path.dirname(proIntroPath), { recursive: true }); + await fs.writeFile( + proIntroPath, + "# React on Rails Pro\n\nExisting Pro overview.\n", + "utf8" + ); + + await injectProFriendlyNotice(docsRoot); + + const updated = await fs.readFile(proIntroPath, "utf8"); + assert.match(updated, /slug: \/pro/); + assert.match(updated, /Friendly license model/); + assert.match(updated, /development, test, CI\/CD, and staging/); + assert.match(updated, /https:\/\/pro\.reactonrails\.com\//); + assert.doesNotMatch(updated, /Friendly evaluation policy/); + }); +}); + +test("docs homepage uses current friendly license model copy", () => { + const sourceMarkdown = `# React on Rails + +## Friendly evaluation policy + +- You can try React on Rails Pro without a license while evaluating. +- If your organization is budget-constrained, [contact us](mailto:justin@shakacode.com) about free licenses. + +## Need more help? +`; + + const updated = docsHomeMarkdown(sourceMarkdown, { hasArchive: false }); + + assert.match(updated, /## Friendly License Model/); + assert.match(updated, /development, test, CI\/CD, and staging/); + assert.match(updated, /https:\/\/pro\.reactonrails\.com\//); + assert.doesNotMatch(updated, /Friendly evaluation policy/); +}); + +test("Pro node renderer table comparisons are escaped for MDX", () => { + const sourceMarkdown = "| First request on fresh deploy | 410->retry | Direct render: <50ms |\n"; + + assert.equal( + fixProNodeRendererMdx(sourceMarkdown), + "| First request on fresh deploy | 410->retry | Direct render: <50ms |\n" + ); +});