├── .gitignore ├── config_example.yml ├── example.png ├── screenshot.png ├── .eslintrc.js ├── run.js ├── serverless.yml ├── package.json ├── LICENSE ├── README.md └── handler.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .serverless 3 | config.yml 4 | -------------------------------------------------------------------------------- /config_example.yml: -------------------------------------------------------------------------------- 1 | webhookUrl: https://hooks.slack.com/services/ 2 | postCount: 10 -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ranrib/producthunt-digest/HEAD/example.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ranrib/producthunt-digest/HEAD/screenshot.png -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'airbnb-base', 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 12, 12 | }, 13 | rules: { 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /run.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const yaml = require('js-yaml'); 3 | const handler = require('./handler'); 4 | 5 | const fileContents = fs.readFileSync('./config.yml', 'utf8'); 6 | const config = yaml.load(fileContents); 7 | 8 | process.env.WEBHOOK_URL = config.webhookUrl; 9 | process.env.POST_COUNT = config.postCount; 10 | 11 | handler.run(); 12 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: ph-digest 2 | 3 | frameworkVersion: '2' 4 | 5 | 6 | provider: 7 | name: aws 8 | runtime: nodejs14.x 9 | lambdaHashingVersion: 20201221 10 | 11 | functions: 12 | sendDigest: 13 | handler: handler.run 14 | environment: 15 | WEBHOOK_URL: ${file(config.yml):webhookUrl} 16 | POST_COUNT: ${file(config.yml):postCount} 17 | events: 18 | - schedule: rate(1 day) 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "producthunt-digest", 3 | "version": "1.0.0", 4 | "homepage": "https://github.com/ranrib/producthunt-digest", 5 | "bugs": { 6 | "url": "https://github.com/ranrib/producthunt-digest/issues" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/ranrib/producthunt-digest.git" 11 | }, 12 | "description": "Generate a daily digest of top posts from Product Hunt", 13 | "author": "Ran Ribenzaft ", 14 | "license": "MIT", 15 | "keywords": [ 16 | "producthunt", 17 | "digest" 18 | ], 19 | "dependencies": { 20 | "axios": "^0.21.4", 21 | "js-yaml": "^4.1.0" 22 | }, 23 | "scripts": { 24 | "start": "node ./run.js", 25 | "lint": "eslint .", 26 | "lint:fix": "eslint . --fix", 27 | "deploy": "sls deploy", 28 | "invoke": "sls invoke -f sendDigest" 29 | }, 30 | "devDependencies": { 31 | "eslint": "^7.32.0", 32 | "eslint-config-airbnb-base": "^14.2.1", 33 | "eslint-plugin-import": "^2.24.2", 34 | "eslint-plugin-react": "^7.26.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ran Ribenzaft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Product Hunt Digest 2 | 3 | Product Hunt Digest - An unofficial daily digest of top products from PH to Slack | Product Hunt 4 | Product Hunt Digest - An unofficial daily digest of top products from PH to Slack | Product Hunt 5 | 6 | This open-source project generates a daily digest of top performing products on Product Hunt directly to your Slack. 7 | 8 | Setup is very simple, and takes a few minutes. 9 | 10 | ![Screenshot](screenshot.png) 11 | 12 | ## Installation 13 | 14 | Make sure you have installed on your environment Git, Node, and npm. 15 | 16 | This code is running on AWS Lambda function, so you'll need an AWS account as well. 17 | 18 | Ultimately this code sends the digest to Slack, so make sure you got an [incoming webhook URL](https://api.slack.com/messaging/webhooks) set. 19 | 20 | ```bash 21 | git clone git@github.com:ranrib/producthunt-digest.git 22 | cd producthunt-digest 23 | npm install 24 | cp config_example.yml config.yml 25 | ``` 26 | 27 | Now edit `config.yml` file and set the Slack webhook URL, and the number of posts you would like to get on the daily digest. 28 | 29 | ## Running Locally 30 | 31 | ```bash 32 | npm run start 33 | ``` 34 | 35 | The script will send the current digest into your Slack. 36 | 37 | ## Deployment 38 | 39 | ```bash 40 | npm run deploy 41 | ``` 42 | 43 | To test that the Lambda function is running properly: 44 | ```bash 45 | npm run invoke 46 | ``` 47 | 48 | ## License 49 | 50 | Provided under the MIT license. See LICENSE for details. 51 | 52 | Copyright 2021, Ran Ribenzaft. 53 | -------------------------------------------------------------------------------- /handler.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | const producyHuntUrl = 'https://www.producthunt.com/frontend/graphql'; 4 | const graphqlQuery = `query LegacyHomePage( 5 | $cursor: String 6 | $postCursor: String 7 | ) { 8 | sections(first: 1, after: $cursor) { 9 | edges { 10 | cursor 11 | node { 12 | id 13 | date 14 | posts(after: $postCursor) { 15 | edges { 16 | node { 17 | ...PostItemList 18 | featuredComment { 19 | id 20 | body: bodyText 21 | user { 22 | id 23 | __typename 24 | } 25 | __typename 26 | } 27 | __typename 28 | } 29 | __typename 30 | } 31 | pageInfo { 32 | endCursor 33 | hasNextPage 34 | __typename 35 | } 36 | __typename 37 | } 38 | __typename 39 | } 40 | __typename 41 | } 42 | pageInfo { 43 | endCursor 44 | hasNextPage 45 | __typename 46 | } 47 | __typename 48 | } 49 | } 50 | fragment PostItemList on Post { 51 | id 52 | ...PostItem 53 | __typename 54 | } 55 | fragment PostItem on Post { 56 | id 57 | _id 58 | commentsCount 59 | name 60 | shortenedUrl 61 | slug 62 | tagline 63 | updatedAt 64 | pricingType 65 | topics(first: 1) { 66 | edges { 67 | node { 68 | id 69 | name 70 | slug 71 | __typename 72 | } 73 | __typename 74 | } 75 | __typename 76 | } 77 | ...PostThumbnail 78 | ...PostVoteButton 79 | __typename 80 | } 81 | fragment PostThumbnail on Post { 82 | id 83 | name 84 | thumbnailImageUuid 85 | ...PostStatusIcons 86 | __typename 87 | } 88 | fragment PostStatusIcons on Post { 89 | name 90 | productState 91 | __typename 92 | } 93 | fragment PostVoteButton on Post { 94 | createdAt 95 | ... on Votable { 96 | id 97 | votesCount 98 | __typename 99 | } 100 | __typename 101 | }`; 102 | const productHuntBody = { 103 | operationName: 'LegacyHomePage', 104 | variables: { cursor: 'MA==' }, 105 | query: graphqlQuery, 106 | }; 107 | 108 | module.exports.run = async () => { 109 | const response = await axios.post(producyHuntUrl, productHuntBody); 110 | const slackMsg = response.data.data.sections.edges[0].node.posts.edges.sort( 111 | (a, b) => b.node.votesCount - a.node.votesCount, 112 | ).slice(0, process.env.POST_COUNT).map((post) => ({ 113 | type: 'section', 114 | text: { 115 | type: 'mrkdwn', 116 | text: `**\n:star: ${post.node.votesCount} votes\n ${post.node.tagline}`, 117 | }, 118 | accessory: { 119 | type: 'image', 120 | image_url: `https://ph-files.imgix.net/${post.node.thumbnailImageUuid}`, 121 | alt_text: post.node.name, 122 | }, 123 | })); 124 | await axios.post(process.env.WEBHOOK_URL, { blocks: slackMsg, text: 'Product Hunt Daily Digest' }); 125 | }; 126 | --------------------------------------------------------------------------------