├── .gitignore ├── README.md ├── index.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and not Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Stores VSCode versions used for testing VSCode extensions 107 | .vscode-test 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hexo-web-push-notification 2 | 3 |  4 | 5 |  6 | 7 | A hexo plugin helps you **automatically** notify readers new post update **everytime** you deploy new post. Subscribed readers can receive browser notification about your latest post. The notification will contain title and excerpt. Clicking it will bring readers to your latest post. 8 | 9 | [开发经历](https://www.inevitable.tech/posts/a1b574bb/) 10 | 11 | [Example site](https://www.inevitable.tech) 12 | 13 | Reminder: If you choose not to receive the notification, the prompt will not shown until 15 days later. 14 | 15 | ## Requirement 16 | 17 | This plugin relies on [webPushr](https://www.webpushr.com/), which is a **free** web push notification service. Thus make sure you have already registered and add your site into the webpushr. If you want notifications to work for Safari, remember to set up the Safari Certificate. Here is the [教程](https://www.inevitable.tech/posts/98ae9e55/). Do not worry about integrating webpushr into your site. This plugin will make this happen for you. 18 | 19 | ## Install 20 | 21 | ```js 22 | npm i hexo-web-push-notification --save 23 | ``` 24 | 25 | ## Usage 26 | 27 | Add the configuration to `_config.yml` in hexo root dir. 28 | 29 | ```yml 30 | webPushNotification: 31 | webpushrKey: "your webpushr rest api key" 32 | webpushrAuthToken: "your webpushr authorize token" 33 | trackingCode: "AEGlpbdgvBCWXqXI6PtsUzobY7TLV9gwJU8bzMktrwfrSERg_xnLVbjpCw8x2GmFmi1ZcLTz0ni6OnX5MAwoM88" 34 | ``` 35 | 36 | The `trackingCode` is a little bit harder to find. Go to your webpushr site dashboard, and go to Setup>TrackingCode. The tracking code look like this: 37 | 38 | ```js 39 | 40 | 43 | 44 | ``` 45 | 46 | `AEGlpbdgvBCWXqXI6PtsUzobY7TLV9gwJU8bzMktrwfrSERg_xnLVbjpCw8x2GmFmi1ZcLTz0ni6OnX5MAwoM88` in the last line is your `trackingCode`. 47 | 48 | The webpushrKey and webpushrAuthToken can be found in Integration>REST API Keys. 49 | 50 | **notice**: The ask-for-notification prompt will not appear locally. This means you will not see any ask-for-notification prompt when running `hexo server` 51 | 52 | ## Customize 53 | 54 | You can customize how your ask-for-notification prompt look like in Setup>EditCustom Prompts. 55 | 56 | ## How it works 57 | 58 | The plugin generates `newPost.json` during `hexo generate`. The `newPost.json` contains the information of latest post. It looks like this: 59 | 60 | ```json 61 | { 62 | "title": "Auto web push notification", 63 | "id": "posts/afd56cf2/", 64 | "date_published": "02/24/2020", 65 | "summary": "如何自动通知读者有更新了?即只要正常更新博客,读者便可以在第一时间收到关于新文章的通知。", 66 | "url": "https://www.inevitable.tech/posts/afd56cf2/", 67 | "tags": ["hexo", "push notifications", "自动化", "CI"], 68 | "categories": ["开发"] 69 | } 70 | ``` 71 | 72 | The summary part is from the excerpt of your post. Plase ensure that your post have **excerpt**. 73 | 74 | ``` 75 | --- 76 | title: Hexo使用Web Push Notification 浏览器通知推送 77 | tags: 78 | - hexo 79 | - 服务器推送技术 80 | - push notifications 81 | categories: 82 | - 开发 83 | comments: true 84 | abbrlink: 98ae9e55 85 | date: 2020-02-26 10:00:00 86 | --- 87 | 88 | Web Push Notification 是怎么工作的?个人博客为什么要使用它?如何使用它? 89 | 90 | 91 | ``` 92 | 93 | The content between `---` and `` is excerpt. 94 | 95 | When you call `hexo deploy`, the plugin will compare the `newPost.json` from your online site and from your local machine. If the id values are different, the plugin will trigger the push notification from [webPush](https://www.webpushr.com/). 96 | 97 | ## Future work 98 | 99 | - [ ] Maybe support more web push notification services. 100 | 101 | The roadmap needs your feedbacks. Feel free to open the issue. 102 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* global hexo */ 2 | /* eslint no-param-reassign:0, strict:0 */ 3 | 'use strict'; 4 | 5 | const util = require('hexo-util'); 6 | const fs = require('hexo-fs'); 7 | const fetch = require("node-fetch"); 8 | const url = require("url") 9 | var request = require('request'); 10 | var moment = require('moment'); 11 | 12 | // triggered after hexo generate. 13 | // this output the newPost.json into public/. 14 | hexo.on('generateAfter', async function (post) { 15 | var posts = hexo.locals.get('posts').data 16 | var dateSortedPosts = posts.sort(function (a, b) { return b.date - a.date }).map(function (v) { return v }) 17 | var newPost = dateSortedPosts[0] 18 | var JSONFeed = { 19 | 'title': newPost.title, 20 | 'id': newPost.path, 21 | 'date_published': newPost.date.format('L'), 22 | 'summary': util.stripHTML(newPost.excerpt), 23 | 'url': newPost.permalink, 24 | 'tags': newPost.tags.data.map(function (v) { return v.name }), 25 | 'categories': newPost.categories.data.map(function (v) { return v.name }) 26 | } 27 | fs.writeFile('public/newPost.json', JSON.stringify(JSONFeed), function (err, data) { hexo.log.info("Generated: newPost.json") }) 28 | }) 29 | 30 | //triggered before hexo deploy. 31 | //it compare the newPost.json from your site and local to decide whether push the notification. 32 | // DeployAfter event has encode issue 33 | hexo.on("deployAfter", async function (post) { 34 | // Get newPost.json from your site. 35 | var newPostOnlineSite = await fetch(url.resolve(hexo.config.url, "newPost.json")); 36 | var newPostOnlineSite = await newPostOnlineSite.json(); 37 | newPostOnlineSite = JSON.parse(JSON.stringify(newPostOnlineSite)); 38 | // Get newPost.json from your local. 39 | var newPostLocal = await fs.readFileSync('public/newPost.json') 40 | // Get newPost.json from local 41 | newPostLocal = JSON.parse(newPostLocal); 42 | // console.table({ 43 | // "From online site": newPostOnlineSite, 44 | // "From Local": newPostLocal 45 | // }); 46 | //determine whether to push web notification 47 | if (newPostOnlineSite.id != newPostLocal.id) { 48 | // push new Post notification 49 | var payload = { 50 | title: newPostLocal.title, 51 | message: newPostLocal.summary, 52 | target_url: new URL(newPostLocal.url).pathname, 53 | // segment: [4205], 54 | send_at: moment().add(10, 'minutes').format() 55 | // sid: '4557611' 56 | }; 57 | var headers = { 58 | webpushrKey: hexo.config.webPushNotification.webpushrKey, 59 | webpushrAuthToken: hexo.config.webPushNotification.webpushrAuthToken, 60 | "Content-Type": "application/json" 61 | }; 62 | var options = { 63 | url: 'https://api.webpushr.com/v1/notification/send/all', 64 | method: 'POST', 65 | headers: headers, 66 | body: JSON.stringify(payload) 67 | }; 68 | function callback(error, response, body) { 69 | if (!error && response.statusCode == 200) { 70 | hexo.log.info("Successfully send notifications"); 71 | hexo.log.info(body); 72 | 73 | } 74 | else { 75 | hexo.log.error("Fail to send notifications"); 76 | hexo.log.error(body); 77 | } 78 | } 79 | hexo.log.info(JSON.stringify(payload)) 80 | request(options, callback); 81 | 82 | // const response = await fetch( 83 | // "https://app.webpushr.com/api/v1/notification/send/segment", 84 | // { 85 | // method: "POST", 86 | // headers: { 87 | // webpushrKey: hexo.config.webPushNotification.webpushrKey, 88 | // webpushrAuthToken: hexo.config.webPushNotification.webpushrAuthToken, 89 | // "Content-Type": "application/json" 90 | // }, 91 | // body: JSON.stringify(payload) 92 | // } 93 | // ); 94 | // // const data = await response.json(); 95 | // console.log(response.status) 96 | // if (!response.ok) { 97 | // // NOT res.status >= 200 && res.status < 300 98 | // hexo.log.error("Push Notification failed " + response.status.toString()+' '+response.statusText); 99 | // } else { 100 | // hexo.log.info("Successfully push notification"); 101 | // } 102 | } else { 103 | hexo.log.info("No New Post detected."); 104 | } 105 | }) 106 | 107 | //insert webpushr tracking code 108 | hexo.extend.filter.register('after_render:html', data => { 109 | var payload = `(function (w, d, s, id) { 110 | if (typeof (w.webpushr) !== 'undefined') return; w.webpushr = w.webpushr || function () { (w.webpushr.q = w.webpushr.q || []).push(arguments) }; var js, fjs = d.getElementsByTagName(s)[0]; js = d.createElement(s); js.id = id; js.async = 1; js.src = "https://cdn.webpushr.com/app.min.js";fjs.parentNode.appendChild(js);}(window, document, 'script', 'webpushr-jssdk'));webpushr('setup', { 'key': '${hexo.config.webPushNotification.trackingCode}' });` 111 | 112 | // return data.replace(/
(?!<\/body>).+?<\/body>/s, str => str.replace('', "