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
12 changes: 11 additions & 1 deletion scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,14 @@ If you're deploying on Vercel, you can get your production database URL from the

### How It Works

The script scans the content directories for articles with `isPaid: true` in their metadata, which identifies them as paid products. It then creates purchase records in the database with a phony Stripe payment ID, effectively granting free access to the specified product. When the user logs in with their email, they'll have access to the product as if they had purchased it.
The script scans the content directories for articles with `isPaid: true` in their metadata, which identifies them as paid products. It then creates purchase records in the database with a phony Stripe payment ID, effectively granting free access to the specified product. When the user logs in with their email, they'll have access to the product as if they had purchased it.
## Comparison Page Generator

The `create-ai-assisted-dev-tools-comparison-pages.js` script generates MDX pages comparing developer tools pairwise. Each page now uses `ArticleLayout` for consistent styling with the rest of the site. The underlying prose generator has been expanded to include company history, usage statistics, market position and recent innovation details for each tool. Run it manually with:

```bash
node scripts/create-ai-assisted-dev-tools-comparison-pages.js
```

Run with `--debug` to log extra details.

14 changes: 14 additions & 0 deletions scripts/__tests__/comparison-pages.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { generateCombinations, slugify } = require('../create-ai-assisted-dev-tools-comparison-pages.js');

describe('comparison page utils', () => {
test('slugify converts to lowercase and replaces spaces with hyphens', () => {
expect(slugify('Hello World')).toBe('hello-world');
});

test('generateCombinations creates pairwise combinations', () => {
const input = [{ name: 'A' }, { name: 'B' }, { name: 'C' }];
const combos = generateCombinations(input);
const slugs = combos.map(([a, b]) => `${a.name}-${b.name}`);
expect(slugs).toEqual(['A-B', 'A-C', 'B-C']);
});
});
2 changes: 1 addition & 1 deletion scripts/create-ai-assisted-dev-tools-comparison-pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function Page() {
const tool2 = ${JSON.stringify(tool2)}
const proseParagraphs = ${JSON.stringify(proseParagraphs, null, 2)}

return <ComparisonPageLayout tool1={tool1} tool2={tool2} proseParagraphs={proseParagraphs} />
return <ComparisonPageLayout metadata={metadata} tool1={tool1} tool2={tool2} proseParagraphs={proseParagraphs} />
}
`;
};
Expand Down
11 changes: 4 additions & 7 deletions src/components/ComparisonPageLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@ import React from 'react';
import ToolComparisonIntro from './ToolComparisonIntro';
import { BarCharts, BusinessInfo, DetailedComparison } from './AIToolComparison';
import NewsletterWrapper from './NewsletterWrapper';
import { SimpleLayout } from '@/components/SimpleLayout';
import { ArticleLayout } from '@/components/ArticleLayout';

const ComparisonPageLayout = ({ tool1, tool2, proseParagraphs }) => {
const ComparisonPageLayout = ({ tool1, tool2, proseParagraphs, metadata }) => {
return (
<SimpleLayout
title={`${tool1.name} vs ${tool2.name}`}
intro="A detailed comparison of AI-assisted developer tools."
>
<ArticleLayout metadata={metadata}>
<h2 className="text-2xl font-bold mt-8 mb-4">Growth compared</h2>
<p>Let&apos;s start by breaking down the key metrics of each tool. You can hover over the chart for more information.</p>
<BarCharts selectedTools={[tool1, tool2]} />
Expand All @@ -34,7 +31,7 @@ const ComparisonPageLayout = ({ tool1, tool2, proseParagraphs }) => {
title="If you made it this far, you can do anything."
body="Get honest reviews of AI-assisted development from someone who codes every day."
/>
</SimpleLayout>
</ArticleLayout>
);
};

Expand Down
71 changes: 63 additions & 8 deletions src/templates/comparison-tool-prose.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ const sentenceVariations = {
"The main features of {tool1} are {uniqueFeatures1}. {tool2}'s primary offerings are {uniqueFeatures2}.",
"{tool1} emphasizes {uniqueFeatures1} in its feature set. {tool2} highlights {uniqueFeatures2} as its core functionalities."
],
businessInfo: [
"{tool1} was founded in {foundingYear1} and has raised {funding1}. {tool2} started in {foundingYear2} with {funding2}.",
"{tool1} dates back to {foundingYear1} with funding of {funding1}, whereas {tool2} began in {foundingYear2} with {funding2}."
],
usageStats: [
"{tool1} counts about {users1} users and {stars1} GitHub stars. {tool2} reports {users2} users and {stars2} stars.",
"{tool1} boasts {users1} users with {stars1} stars on GitHub, while {tool2} has {users2} users and {stars2} stars."
],
marketPosition: [
"{tool1} holds roughly {share1} market share and competes with {competitors1}. {tool2} claims {share2} share competing with {competitors2}.",
"In terms of market share, {tool1} sits at {share1} against competitors {competitors1}. {tool2} occupies {share2} with rivals {competitors2}."
],
innovation: [
"Recent updates for {tool1}: {updates1}. Next up: {roadmap1}. {tool2} recently {updates2} and plans {roadmap2}.",
"{tool1} recently {updates1}; upcoming plans include {roadmap1}. {tool2} has {updates2} and aims for {roadmap2}."
],
sharedFeature: "Both {tool1} and {tool2} offer {sharedFeature} as a key feature."
};

Expand Down Expand Up @@ -97,14 +113,53 @@ module.exports = {
});

// Generate sentence for exclusive features
if (exclusiveFeatures1.length > 0 || exclusiveFeatures2.length > 0) {
const uniqueFeaturesParagraph = getRandomSentence('uniqueFeatures')
.replace(/{tool1}/g, tool1.name || 'Tool 1')
.replace(/{tool2}/g, tool2.name || 'Tool 2')
.replace(/{uniqueFeatures1}/g, exclusiveFeatures1.join(', '))
.replace(/{uniqueFeatures2}/g, exclusiveFeatures2.join(', '));
proseParagraphs.push(uniqueFeaturesParagraph);
}
if (exclusiveFeatures1.length > 0 || exclusiveFeatures2.length > 0) {
const uniqueFeaturesParagraph = getRandomSentence('uniqueFeatures')
.replace(/{tool1}/g, tool1.name || 'Tool 1')
.replace(/{tool2}/g, tool2.name || 'Tool 2')
.replace(/{uniqueFeatures1}/g, exclusiveFeatures1.join(', '))
.replace(/{uniqueFeatures2}/g, exclusiveFeatures2.join(', '));
proseParagraphs.push(uniqueFeaturesParagraph);
}

const formatNumber = (n) =>
n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');

const businessParagraph = getRandomSentence('businessInfo')
.replace(/{tool1}/g, tool1.name || 'Tool 1')
.replace(/{tool2}/g, tool2.name || 'Tool 2')
.replace(/{foundingYear1}/g, tool1.business_info?.founding_year || 'unknown')
.replace(/{foundingYear2}/g, tool2.business_info?.founding_year || 'unknown')
.replace(/{funding1}/g, tool1.business_info?.funding || 'undisclosed funding')
.replace(/{funding2}/g, tool2.business_info?.funding || 'undisclosed funding');
proseParagraphs.push(businessParagraph);

const usageParagraph = getRandomSentence('usageStats')
.replace(/{tool1}/g, tool1.name || 'Tool 1')
.replace(/{tool2}/g, tool2.name || 'Tool 2')
.replace(/{users1}/g, formatNumber(tool1.usage_stats?.number_of_users || 0))
.replace(/{users2}/g, formatNumber(tool2.usage_stats?.number_of_users || 0))
.replace(/{stars1}/g, formatNumber(tool1.usage_stats?.github_stars || 0))
.replace(/{stars2}/g, formatNumber(tool2.usage_stats?.github_stars || 0));
proseParagraphs.push(usageParagraph);

const marketParagraph = getRandomSentence('marketPosition')
.replace(/{tool1}/g, tool1.name || 'Tool 1')
.replace(/{tool2}/g, tool2.name || 'Tool 2')
.replace(/{share1}/g, tool1.market_position?.market_share || 'a portion')
.replace(/{share2}/g, tool2.market_position?.market_share || 'a portion')
.replace(/{competitors1}/g, (tool1.market_position?.competitors || []).join(', ') || 'others')
.replace(/{competitors2}/g, (tool2.market_position?.competitors || []).join(', ') || 'others');
proseParagraphs.push(marketParagraph);

const innovationParagraph = getRandomSentence('innovation')
.replace(/{tool1}/g, tool1.name || 'Tool 1')
.replace(/{tool2}/g, tool2.name || 'Tool 2')
.replace(/{updates1}/g, tool1.innovation?.recent_updates || 'recent updates')
.replace(/{updates2}/g, tool2.innovation?.recent_updates || 'recent updates')
.replace(/{roadmap1}/g, tool1.innovation?.future_roadmap || 'future plans')
.replace(/{roadmap2}/g, tool2.innovation?.future_roadmap || 'future plans');
proseParagraphs.push(innovationParagraph);

// Add newlines between paragraphs
return proseParagraphs.flatMap(paragraph => [paragraph, "\n\n"]).slice(0, -1);
Expand Down
Loading