diff --git a/.forestry/front_matter/templates/blog-post.yml b/.forestry/front_matter/templates/blog-post.yml index 0cd0a1de..6babd9e5 100644 --- a/.forestry/front_matter/templates/blog-post.yml +++ b/.forestry/front_matter/templates/blog-post.yml @@ -27,23 +27,16 @@ fields: - type: datetime name: date label: Date - description: '"Published on" date. Remember this is only a list date for - display; posts are not automatically published!' + description: '"Published on" date.

To schedule this post for future publishing, simply + set the date to the future.

WARNING: Data + is saved in the ipfs/ipfs-blog + public GitHub repo as soon as you hit the save button. So any sensitive information + will also be publicly available on GitHub, but hidden from the website blog.ipfs.io.' config: required: true date_format: YYYY/MM/DD export_format: YYYY-MM-DD -- name: publishing_date - type: datetime - label: Publishing Date - config: - required: false - date_format: - time_format: - display_utc: true - description: When the post should be published.
Use this to schedule publishing - the post in the future.
If you don't want to schedule, just ignore this. - default: now + display_utc: false - type: text name: permalink label: Permalink diff --git a/.forestry/front_matter/templates/newslinks.yml b/.forestry/front_matter/templates/newslinks.yml index 0a7a1e28..10a109f2 100644 --- a/.forestry/front_matter/templates/newslinks.yml +++ b/.forestry/front_matter/templates/newslinks.yml @@ -35,17 +35,23 @@ fields: export_format: YYYY-MM-DD description: For events, use the date of the event itself.
For all other link types, use the date the original item was published. - - name: publishing_date + - name: publish_date type: datetime - label: Publishing Date - description: When the link should be published.
Use this to schedule publishing - the link in the future.
If you don't want to schedule, just ignore this. + label: Publish Date + description: 'When + the link should be published.
Use this to schedule publishing the link in + the future.
If you don't want to schedule, leave this blank.

WARNING: + Data is saved in the ipfs/ipfs-blog public GitHub repo as soon as + you hit the Save button. This means anything you save will also be publicly + visible on GitHub, but not on the blog.ipfs.io site. Be careful if you are drafting something sensitive!' config: required: false date_format: time_format: display_utc: true - default: now + export_format: YYYY-MM-DDThh:mm:ssZ + default: '' - type: text name: path label: URL diff --git a/.github/workflows/scheduled-publishing.yml b/.github/workflows/scheduled-publishing.yml new file mode 100644 index 00000000..25b24736 --- /dev/null +++ b/.github/workflows/scheduled-publishing.yml @@ -0,0 +1,26 @@ +name: Check for scheduled posts to publish +on: + schedule: + - cron: '* 4,16 * * *' +jobs: + check-n-publish: + name: Check for scheduled posts and publish if any found + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v2 + - name: Install dependencies + run: npm ci + - name: Check for scheduled posts + run: echo "::set-output name=should_publish::$(${{ github.workspace }}/scripts/scheduled-publishing.js)" + id: should_publish_step + - name: Trigger Fleek Build + if: ${{ steps.should_publish_step.outputs.should_publish == 'true' }} + id: deploy + uses: fleekhq/action-deploy@v1 + with: + apiKey: ${{ secrets.FLEEK_API_KEY }} + - name: Scheduled posts published + if: ${{ steps.should_publish_step.outputs.should_publish == 'true' }} + run: echo "Check https://blog.ipfs.io" + diff --git a/package-lock.json b/package-lock.json index da1a75bb..f0a78098 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "eslint-plugin-prettier": "^3.4.0", "eslint-plugin-promise": "^5.1.0", "eslint-plugin-vue": "^7.9.0", + "gray-matter": "^4.0.2", "husky": "^6.0.0", "lint-staged": "^10.5.4", "lodash": "^4.17.21", @@ -41,6 +42,7 @@ "markdown-it-task-lists": "^2.1.1", "markdown-it-video": "^0.6.3", "npm-run-all": "^4.1.5", + "p-map": "^4.0.0", "postcss": "^7.0.35", "prettier": "^2.2.1", "slug": "^4.0.4", diff --git a/package.json b/package.json index ead31a79..b7947fc9 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "eslint-plugin-prettier": "^3.4.0", "eslint-plugin-promise": "^5.1.0", "eslint-plugin-vue": "^7.9.0", + "gray-matter": "^4.0.2", "husky": "^6.0.0", "lint-staged": "^10.5.4", "lodash": "^4.17.21", @@ -38,6 +39,7 @@ "markdown-it-task-lists": "^2.1.1", "markdown-it-video": "^0.6.3", "npm-run-all": "^4.1.5", + "p-map": "^4.0.0", "postcss": "^7.0.35", "prettier": "^2.2.1", "slug": "^4.0.4", diff --git a/scripts/scheduled-publishing.js b/scripts/scheduled-publishing.js new file mode 100755 index 00000000..3cf9f6b8 --- /dev/null +++ b/scripts/scheduled-publishing.js @@ -0,0 +1,98 @@ +#!/usr/bin/env node + +'use strict' + +/** + * Scheduled Publishing of posts and links. + * This script checks for any blog posts and links that are scheduled to + * be published, but are to be published today. If there is at least + * one found, a build is trigged in fleek, updating the website + * with the latest data. + */ + +const fs = require('fs') +const path = require('path') + +const pMap = require('p-map') +const matter = require('gray-matter') +const dayjs = require('dayjs') + +const isBetween = require('dayjs/plugin/isBetween') +dayjs.extend(isBetween) + +async function main() { + // read files contents concurrently, 10 at a time + const contentDir = path.resolve(__dirname, '../src/_blog') + const files = await pMap( + await fs.promises.readdir(contentDir), + (filename) => { + if (!filename.endsWith('.md')) { + return null + } + + return fs.promises.readFile(path.resolve(contentDir, filename), 'utf8') + }, + { concurrency: 10 } + ) + + const output = files + // exclude possible nulls + .filter((result) => !!result) + // process files into publishable items (posts or links) + .reduce((items, fileContent) => { + const metadata = matter(fileContent).data + + if (metadata.type && metadata.data) { + // links + items.push(...metadata.data) + } else { + items.push(metadata) + } + + return items + }, []) + // check if there is at least one publish date in the future + .some((item) => { + let publishDate + + // blog post + if (item.permalink) { + publishDate = item.date + } + + // links + if (item.url && item.publish_date) { + publishDate = item.publish_date + } + + if (!publishDate) { + return false + } + + // we trim to the hour to avoid counting the initial delays on triggering + // the job, which could cause the time calculations to exclude specific + // scheduled dates and times. + const now = dayjs(new Date()).startOf('hour') + const timeMatch = dayjs(new Date(publishDate)).isBetween( + now.subtract(12, 'hour'), + now + ) + + // using stderr since stdout is used for GH workflow output + if (timeMatch) { + console.error(`Found post/link to publish: "${item.title}"`) + } + + return timeMatch + }) + + if (!output) { + console.error('Nothing found to publish.') + } + + // output true or false: should trigger publish or not + // GH Workflow step reads from stdout + console.log(output) +} + +main() diff --git a/src/.vuepress/plugins/pageData.js b/src/.vuepress/plugins/pageData.js index e503fa2a..79d5e59f 100644 --- a/src/.vuepress/plugins/pageData.js +++ b/src/.vuepress/plugins/pageData.js @@ -1,4 +1,32 @@ const slug = require('slug') +const dayjs = require('dayjs') + +function shouldBeHidden(frontmatter) { + const isDateInFuture = (date) => dayjs(new Date(date)).isAfter(dayjs()) + let shouldHide = frontmatter.hidden || false + + // sitemap config + if (frontmatter.sitemap) { + shouldHide = shouldHide || frontmatter.sitemap.exclude + } + + // scheduled posts + // see auto-publishing of scheduled posts here: https://github.com/ipfs/ipfs-blog/issues/147 + if ( + !shouldHide && + frontmatter.permalink && // permalink is unique to posts + frontmatter.date + ) { + shouldHide = shouldHide || isDateInFuture(frontmatter.date) + } + + // scheduled links (path is unique to links) + if (!shouldHide && frontmatter.path && frontmatter.publish_date) { + shouldHide = shouldHide || isDateInFuture(frontmatter.publish_date) + } + + return shouldHide +} module.exports = (options, context) => ({ extendPageData($page) { @@ -18,8 +46,8 @@ module.exports = (options, context) => ({ frontmatter.authorKey = authorKey } - // exclude a page from the feed & robots if excluded from sitemap - if (frontmatter.sitemap && frontmatter.sitemap.exclude) { + // exclude hidden pages (sitemap config, future publishes, etc) + if (shouldBeHidden(frontmatter)) { frontmatter.feed = { enable: false, } @@ -30,6 +58,18 @@ module.exports = (options, context) => ({ } else { frontmatter.meta = [noIndex] } + + frontmatter.hidden = true + $page.hidden = true + } + + // set links has hidden (future publishes, etc) + if (frontmatter.data) { + frontmatter.data.forEach((item) => { + if (shouldBeHidden(item)) { + item.hidden = true + } + }) } }, }) diff --git a/src/.vuepress/theme/layouts/Blog.vue b/src/.vuepress/theme/layouts/Blog.vue index 087f5478..ad47102d 100644 --- a/src/.vuepress/theme/layouts/Blog.vue +++ b/src/.vuepress/theme/layouts/Blog.vue @@ -129,14 +129,8 @@ export default { return false } - if ( - page.frontmatter && - (page.frontmatter.sitemap ? !page.frontmatter.sitemap.exclude : true) - ) { - result.push({ - ...page, - category: defaultCategory, - }) + if (page.frontmatter && !page.frontmatter.hidden) { + result.push({ ...page, category: defaultCategory }) } }) diff --git a/src/.vuepress/theme/layouts/BlogPost.vue b/src/.vuepress/theme/layouts/BlogPost.vue index d2b5a6c9..50d4d681 100644 --- a/src/.vuepress/theme/layouts/BlogPost.vue +++ b/src/.vuepress/theme/layouts/BlogPost.vue @@ -1,5 +1,5 @@