diff --git a/lib/v2/alternativeto/maintainer.js b/lib/v2/alternativeto/maintainer.js
new file mode 100644
index 000000000..7d0330583
--- /dev/null
+++ b/lib/v2/alternativeto/maintainer.js
@@ -0,0 +1,4 @@
+module.exports = {
+ '/software/:name/:routeParams?': ['JimenezLi'],
+ '/platform/:name/:routeParams?': ['JimenezLi'],
+};
diff --git a/lib/v2/alternativeto/platform.js b/lib/v2/alternativeto/platform.js
new file mode 100644
index 000000000..fef2e5525
--- /dev/null
+++ b/lib/v2/alternativeto/platform.js
@@ -0,0 +1,32 @@
+const { baseURL, puppeteerGet } = require('./utils');
+const cheerio = require('cheerio');
+
+module.exports = async (ctx) => {
+ const name = ctx.params.name;
+ const query = new URLSearchParams(ctx.params.routeParams);
+ const link = `https://alternativeto.net/platform/${name}/?${query.toString()}`;
+
+ // use Puppeteer due to the obstacle by cloudflare challenge
+ const html = await puppeteerGet(link, ctx.cache);
+ const $ = cheerio.load(html);
+
+ ctx.state.data = {
+ title: $('.Heading_h1___Cf5Y').text().trim(),
+ description: $('.intro-text').text().trim(),
+ link,
+ item: $('.AppListItem_appInfo__h9cWP')
+ .toArray()
+ .map((element) => {
+ const item = $(element);
+ const title = item.find('.Heading_h2___LwQD').text().trim();
+ const link = `${baseURL}${item.find('.Heading_h2___LwQD a').attr('href')}`;
+ const description = item.find('.AppListItem_description__wtODK').text().trim();
+
+ return {
+ title,
+ link,
+ description,
+ };
+ }),
+ };
+};
diff --git a/lib/v2/alternativeto/radar.js b/lib/v2/alternativeto/radar.js
new file mode 100644
index 000000000..e33e25eaf
--- /dev/null
+++ b/lib/v2/alternativeto/radar.js
@@ -0,0 +1,19 @@
+module.exports = {
+ 'alternativeto.net': {
+ _name: 'AlternativeTo',
+ www: [
+ {
+ title: 'Software Alternatives',
+ docs: 'https://docs.rsshub.app/routes/programming#alternativeto-software-alternatives',
+ source: '/software/:name',
+ target: '/software/:name',
+ },
+ {
+ title: 'Platform Software',
+ docs: 'https://docs.rsshub.app/routes/programming#alternativeto-platform-software',
+ source: '/platform/:name',
+ target: '/platform/:name',
+ },
+ ],
+ },
+};
diff --git a/lib/v2/alternativeto/router.js b/lib/v2/alternativeto/router.js
new file mode 100644
index 000000000..985663ff9
--- /dev/null
+++ b/lib/v2/alternativeto/router.js
@@ -0,0 +1,4 @@
+module.exports = (router) => {
+ router.get('/software/:name/:routeParams?', require('./software'));
+ router.get('/platform/:name/:routeParams?', require('./platform'));
+};
diff --git a/lib/v2/alternativeto/software.js b/lib/v2/alternativeto/software.js
new file mode 100644
index 000000000..a54d54de7
--- /dev/null
+++ b/lib/v2/alternativeto/software.js
@@ -0,0 +1,32 @@
+const { baseURL, puppeteerGet } = require('./utils');
+const cheerio = require('cheerio');
+
+module.exports = async (ctx) => {
+ const name = ctx.params.name;
+ const query = new URLSearchParams(ctx.params.routeParams);
+ const link = `https://alternativeto.net/software/${name}/?${query.toString()}`;
+
+ // use Puppeteer due to the obstacle by cloudflare challenge
+ const html = await puppeteerGet(link, ctx.cache);
+ const $ = cheerio.load(html);
+
+ ctx.state.data = {
+ title: $('.Heading_h1___Cf5Y').text().trim(),
+ description: $('.intro-text').text().trim(),
+ link,
+ item: $('.AppListItem_appInfo__h9cWP')
+ .toArray()
+ .map((element) => {
+ const item = $(element);
+ const title = item.find('.Heading_h2___LwQD').text().trim();
+ const link = `${baseURL}${item.find('.Heading_h2___LwQD a').attr('href')}`;
+ const description = item.find('.AppListItem_description__wtODK').text().trim();
+
+ return {
+ title,
+ link,
+ description,
+ };
+ }),
+ };
+};
diff --git a/lib/v2/alternativeto/utils.js b/lib/v2/alternativeto/utils.js
new file mode 100644
index 000000000..1ecbc0243
--- /dev/null
+++ b/lib/v2/alternativeto/utils.js
@@ -0,0 +1,22 @@
+const baseURL = 'https://alternativeto.net';
+
+const puppeteerGet = (url, cache) =>
+ cache.tryGet(url, async () => {
+ const browser = await require('@/utils/puppeteer')();
+ const page = await browser.newPage();
+ await page.setRequestInterception(true);
+ page.on('request', (request) => {
+ request.resourceType() === 'document' ? request.continue() : request.abort();
+ });
+ await page.goto(url, {
+ waitUntil: 'domcontentloaded',
+ });
+ const html = await page.evaluate(() => document.documentElement.innerHTML);
+ browser.close();
+ return html;
+ });
+
+module.exports = {
+ baseURL,
+ puppeteerGet,
+};
diff --git a/website/docs/routes/programming.md b/website/docs/routes/programming.md
index c82c88b2f..6163e73cc 100644
--- a/website/docs/routes/programming.md
+++ b/website/docs/routes/programming.md
@@ -82,6 +82,24 @@ You have the option to utilize the main heading or use individual categories as
+## AlternativeTo {#alternativeto}
+
+### Software Alternatives {#alternativeto-software-alternatives}
+
+
+
+> routeParms can be copied from original site URL, example: `/alternativeto/software/cpp/license=opensource&platform=windows`
+
+
+
+### Platform Software {#alternativeto-platform-software}
+
+
+
+> routeParms can be copied from original site URL, example: `/alternativeto/platform/firefox/license=free`
+
+
+
## AtCoder {#atcoder}
### Present Contests {#atcoder-present-contests}