├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
└── workflows
│ ├── codeql-analysis.yml
│ ├── dependency-review.yml
│ ├── lint.yml
│ └── nodejs.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── app.json
├── lib
├── auto-config.js
├── config.js
├── handler.js
├── main.js
├── sites.js
└── utils.js
├── package-lock.json
├── package.json
├── renovate.json
├── sample.env
└── test
├── .eslintrc
├── handler.spec.js
└── test.env
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | indent_style = space
7 | indent_size = 2
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /coverage/**/*
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@voxpelli",
3 | "root": true
4 | }
5 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 | schedule:
9 | - cron: '0 10 * * 0'
10 |
11 | jobs:
12 | analyze:
13 | uses: voxpelli/ghatemplates/.github/workflows/codeql-analysis.yml@main
14 |
--------------------------------------------------------------------------------
/.github/workflows/dependency-review.yml:
--------------------------------------------------------------------------------
1 | name: 'Dependency Review'
2 |
3 | on: [pull_request]
4 |
5 | permissions:
6 | contents: read
7 |
8 | jobs:
9 | dependency-review:
10 | uses: voxpelli/ghatemplates/.github/workflows/dependency-review.yml@main
11 |
12 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Linting
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | tags:
8 | - '*'
9 | pull_request:
10 | branches:
11 | - main
12 |
13 | permissions:
14 | contents: read
15 |
16 | jobs:
17 | lint:
18 | uses: voxpelli/ghatemplates/.github/workflows/lint.yml@main
19 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | name: Node CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | tags:
8 | - '*'
9 | pull_request:
10 | branches:
11 | - main
12 |
13 | permissions:
14 | contents: read
15 |
16 | jobs:
17 | test:
18 | uses: voxpelli/ghatemplates/.github/workflows/test.yml@main
19 | with:
20 | node-versions: '14,16,18,20'
21 | os: 'ubuntu-latest'
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Basic ones
2 | /coverage
3 | /docs
4 | /node_modules
5 | /.env
6 | /.nyc_output
7 |
8 | # When using npm, skip the yarn.lock
9 | /yarn.lock
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 | All notable changes to this project will be documented in this file.
3 | This project adheres to [Semantic Versioning](http://semver.org/).
4 |
5 | ## 0.7.0 – YYYY-MM-DD
6 |
7 | * **Breaking** – now requires at least Node.js 8.x
8 |
9 | ## 0.6.1 – 2018-01-20
10 |
11 | * **Feature** – layout names are now configurable and can also be disabled. Fixes #29
12 |
13 | ## 0.6.0 – 2017-05-16
14 |
15 | * **Breaking** – the format for configuring content-dependent categories have changed, now a JSON array
16 | * **Feature** – permalink styles and filename styles can now be content-dependent just like categories
17 |
18 | ## 0.5.0 – 2017-05-14
19 |
20 | * **Feature** – post file names are now configurable
21 | * **Feature** – media file names are now configurable
22 | * **Bug fix** – corrected resolving of site options
23 | * **Bug fix** – corrected combined use of `MICROPUB_SITES_JSON` and `MICROPUB_SITE_URL`
24 | * **Updated** – dependencies has been updated
25 |
26 | ## 0.4.4 – 2017-02-14
27 |
28 | * **Improvement** – includes updated `micropub-express` module that supports the [now standard](https://github.com/voxpelli/node-micropub-express/issues/7) `create` IndieAuth scope, that eg. Quill [now uses](https://github.com/aaronpk/Quill/commit/eab1a65f63f227bae126a554e3bf93aa05c70695)
29 | * **Updated** – dependencies has been updated
30 |
31 | ## 0.4.3 – 2017-01-28
32 |
33 | * **Feature** – support `config` queries and include syndication targets in it
34 |
35 | ## 0.4.2 – 2017-01-28
36 |
37 | * **Feature** – configurable syndication targets, see `sample.env` and #9
38 | * **Improvement** – using new [microformat-express](https://github.com/voxpelli/node-micropub-express) version that is more spec compliant: JSON by default in query responses, better errors and support for space separated scopes
39 | * **Updated** – dependencies has been updated
40 |
41 | ## 0.4.1 – 2017-01-21
42 |
43 | * **Improvement** – using new `format-microformat` version that by default publish all posts in the past by 15 seconds to avoid time sync issues with build servers
44 | * **Improvement** – now includes a Yarn lock file
45 | * **Updated** – dependencies has been updated
46 |
47 | ## 0.4.0 - 2016-08-17
48 |
49 | * **Feature** – derviced categories are now configurable
50 | * **Somewhat breaking** – this project now requires Node.js 6.x
51 |
52 | ## 0.3.1 - 2016-07-30
53 |
54 | * **Bug fix** – the caching of the auto-configuration didn't work
55 |
56 | ## 0.3.0 - 2016-07-30
57 |
58 | * **Feature** – permalinks are now configurable
59 | * **Feature** – this project now autoconfigures eg. the permalink style based on a sites `_config.yml` file
60 | * **Somewhat breaking** – as this project now respects the actual permalink configuration it will now default to other permalink styles than before
61 |
62 | ## 0.2.0 - 2016-07-06
63 |
64 | - Initial public release
65 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Pelle Wessman
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 | # Micropub to GitHub
2 |
3 | [](https://indieweb.org/)
4 | [](https://github.com/voxpelli/eslint-config)
5 | [](https://mastodon.social/@voxpelli)
6 |
7 | An endpoint that accepts [Micropub](http://micropub.net/) requests, formats them into [Jekyll](http://jekyllrb.com/) posts before pushing them to a configured GitHub repository. This enables updating a Jekyll blog through a [Micropub client](https://indieweb.org/Micropub/Clients).
8 |
9 | ### _Early alpha_
10 |
11 | Supported:
12 | * Creation of posts
13 | * Uploading of media
14 | * Replacing an existing post with a new version
15 |
16 | Unsupported:
17 | * Partial update
18 | * Deletes
19 |
20 | ## Requirements
21 |
22 | Requires at least Node.js 14.0.0.
23 |
24 | ## Installation
25 |
26 | Install as a normal Node.js application. Add the required [configuration](#configuration) values via environment variables or similar mechanism. Or deploy to Heroku:
27 |
28 | [](https://heroku.com/deploy?template=https://github.com/voxpelli/webpage-micropub-to-github)
29 |
30 | ## Endpoint discovery
31 |
32 | Once deployed, your Micropub endpoint can be found at `/micropub/main` e.g. `https://example.com/micropub/main`.
33 |
34 | If you specified more than one site using the `MICROPUB_SITES_JSON` variable, then each endpoint will be available under the name of its respective key, i.e. `/micropub/key-name`.
35 |
36 | To enable automatic discovery for your [Micropub endpoint](https://indieweb.org/micropub#Endpoint_Discovery) and [token endpoint](https://indieweb.org/obtaining-an-access-token#Discovery), you will need to add the following values to your site's `
`:
37 |
38 | ```
39 |
40 |
41 | ```
42 |
43 | ## Configuration
44 |
45 | ### Required values
46 |
47 | The following variables are required to enable a Micropub client to push content to your GitHub repository.
48 |
49 | Variable | Description
50 | -------- | -----------
51 | `MICROPUB_TOKEN_ENDPOINT` | URL to verify Micropub token. Example: `https://tokens.indieauth.com/token`
52 | `MICROPUB_TOKEN_ME` | URL to identify Micropub user. Example: `https://johndoe.example`
53 | `MICROPUB_GITHUB_TOKEN` | [GitHub access token](https://github.com/settings/tokens) to grant access to repository. Example: `12345abcde67890fghij09876klmno54321pqrst`
54 | `MICROPUB_GITHUB_USER` | Username/organisation that owns repository. Example: `johndoe`
55 | `MICROPUB_SITE_GITHUB_REPO` | GitHub repository in which site files are found. Example: `johndoe.github.io`
56 | `MICROPUB_SITE_URL` | URL where site is published. Example: `https://johndoe.example`
57 |
58 | ### Syndication
59 |
60 | The following variables can be used to set [syndication target(s)](https://www.w3.org/TR/micropub/#syndication-targets).
61 |
62 | Variable | Description
63 | -------- | -----------
64 | `MICROPUB_SITE_SYNDICATE_TO_UID` | Unique identifier of syndication target. Example: `https://social.example/johndoe`
65 | `MICROPUB_SITE_SYNDICATE_TO_NAME` | User readable name of syndication target. Example: `@johndoe on Example Social Network`
66 | `MICROPUB_SITE_SYNDICATE_TO` | Complex syndication target. Provided as a JSON array, e.g.: `[{"uid":"https://social.example/johndoe","name":"@johndoe on Example Social Network","service":{"name":"Example Social Network","url":"https://social.example/","photo":"https://social.example/icon.png"},"user":{"name":"johndoe","url":"https://social.example/johndoe","photo":"https://social.example/johndoe/photo.jpg"}}]`. Not compatible with `MICROPUB_SITES_JSON`.
67 |
68 | ### Output style
69 |
70 | The following variables allow you to configure the name and destination for files pushed to your repository. These variables will also accept conditional values ([described below](#conditional-values)).
71 |
72 | Variable | Description
73 | -------- | -----------
74 | `MICROPUB_FILENAME_STYLE` | File name and path for post. Example: `_posts/:year-:month-:day-:slug`
75 | `MICROPUB_MEDIA_FILES_STYLE` | File name and path for media files. Example: `media/:year-:month-:slug/:filesslug`
76 | `MICROPUB_PERMALINK_STYLE` | [Jekyll permalink style](http://jekyllrb.com/docs/permalinks/). Example: `/:categories/:year/:month/:title/`
77 | `MICROPUB_LAYOUT_NAME` | The name of the Jekyll layout to use for the posts. Set to `false` to have no layout be added. Defaults to `microblogpost`
78 | `MICROPUB_OPTION_DERIVE_CATEGORY` | Override the default category
79 | `MICROPUB_GITHUB_BRANCH` | Branch to use for pushes. Useful to test out if things end up where you want them to. Example: `micropub`
80 |
81 | #### Complex output styles
82 |
83 | These configuration options can all be given different values for different types of content by setting up conditions under which each configuration applies. See [conditional values](#conditional-values).
84 |
85 | ### Complex configuration
86 |
87 | Variable | Description
88 | -------- | -----------
89 | `MICROPUB_SITES_JSON` | Complex settings and/or multiple sites (including their syndication targets) provided as JSON, e.g.: `'{"site1":{"url":"https://site1.example/","github":{"repo":"site1"},"token":[{"endpoint":"https://tokens.indieauth.com/token","me":"https://site1.example/"}]},"site2":{"url":"http://site2.example/","github":{"repo":"site2"},"token":[{"endpoint":"https://tokens.indieauth.com/token","me":"http://site2.example/"}]}}'`
90 | `MICROPUB_OPTION_NO_AUTO_CONFIGURE` | Auto-configure permalink status from the Jekyll repo config. Boolean
91 | `MICROPUB_OPTION_DERIVE_LANGUAGES` | Comma separated list of language codes to auto-detect. Example `eng,swe`
92 | `MICROPUB_HOST` | Domain name to enforce. Will redirect requests to all other domain names and IP addresses that the endpoint can be accessed on.
93 | `MICROPUB_ENCODE_HTML` | (_non-standard_) Option to opt out of HTML-encoding of text content if set to `false`. Defaults to `true`.
94 |
95 | ### Conditional values
96 |
97 | Conditions are set up by assessing the environment variables using a JSON object of the format:
98 |
99 | ```json
100 | [
101 | {
102 | "condition": "bookmark OR name",
103 | "value": "value-one"
104 | },
105 | {
106 | "condition": "bookmark OR name",
107 | "value": "value-two"
108 | }
109 | ]
110 | ```
111 |
112 | Conditions are [fulfills expressions](https://github.com/voxpelli/node-fulfills#condition-syntax) that apply to the properties of the document being saved. Pretty much any property that can be inserted into a YAML front matter can be matched against. All values explicitly set in the Micropub request are available, but some defaults and derived values may not be available, depending on the option configured.
113 |
114 | _Please [open an issue](https://github.com/voxpelli/webpage-micropub-to-github/issues/new) and let me know what conditions you would like to set up._
115 |
116 | ## Modules used
117 |
118 | * [micropub-express](https://github.com/voxpelli/node-micropub-express) – an [Express](http://expressjs.com/) Micropub endpoint that accepts and verifies Micropub requests and calls a callback with a parsed `micropubDocument`.
119 | * [format-microformat](https://github.com/voxpelli/node-format-microformat) – a module that takes a `micropubDocument` as its input, and converts this data into a standard that can be published elsewhere. Currently supports the Jekyll format.
120 | * [github-publish](https://github.com/voxpelli/node-github-publish) – a module that takes a filename and content and publishes it to a GitHub repository. The formatted data generated by `format-microformat` can be published to a Jekyll blog hosted on a GitHub, or a [GitHub Pages](https://pages.github.com/) site.
121 |
122 | ## Related
123 |
124 | * [My 2015 in IndieWeb](http://voxpelli.com/2016/03/my-2015-in-indieweb/) – post from 2016-03-12 by @voxpelli
125 | * [miklb/jekyll-indieweb](https://github.com/miklb/jekyll-indieweb) – a Jekyll theme built with the IndieWeb in mind
126 | * [voxpelli/voxpelli.github.com](https://github.com/voxpelli/voxpelli.github.com) – first Jekyll blog to use this Micropub endpoint
127 | * [webmention.herokuapp.com](https://webmention.herokuapp.com/) – another IndieWeb project suited for Jekyll, this one for [Webmention](https://indieweb.org/webmention)
128 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Micropub to GitHub",
3 | "description": "An endpoint that accepts Micropub requests, formats them into Jekyll posts and pushes them to a configured GitHub repository.",
4 | "keywords": [
5 | "indieweb",
6 | "micropub",
7 | "api",
8 | "jekyll"
9 | ],
10 | "repository": "https://github.com/voxpelli/webpage-micropub-to-github",
11 | "env": {
12 | "MICROPUB_GITHUB_TOKEN": {
13 | "description": "An access token for the GitHub API. Get one at: https://github.com/settings/tokens",
14 | "required": true
15 | },
16 | "MICROPUB_GITHUB_USER": {
17 | "description": "The GitHub user to which the configured repositories belongs.",
18 | "required": true
19 | },
20 | "MICROPUB_GITHUB_BRANCH": {
21 | "description": "Branch to use when pushing. Leave empty/blank to use default.",
22 | "required": false
23 | },
24 |
25 | "MICROPUB_SITE_GITHUB_REPO": {
26 | "description": "The name of your GitHub repository.",
27 | "required": true
28 | },
29 | "MICROPUB_SITE_URL": {
30 | "description": "The URL to your site.",
31 | "required": true
32 | },
33 |
34 | "MICROPUB_TOKEN_ENDPOINT": {
35 | "description": "The token endpoint to verify the micropub token against. See: https://indieweb.org/token-endpoint",
36 | "required": true,
37 | "value": "https://tokens.indieauth.com/token"
38 | },
39 | "MICROPUB_TOKEN_ME": {
40 | "description": "Your personal domain name that the micropub token should represent. Defaults to site URL.",
41 | "required": false
42 | },
43 |
44 | "MICROPUB_OPTION_DERIVE_LANGUAGES": {
45 | "description": "A comma separated list of languages to autodetect. Eg: eng,swe",
46 | "required": false
47 | }
48 | },
49 | "buildpacks": [
50 | {
51 | "url": "heroku/nodejs"
52 | }
53 | ]
54 | }
55 |
--------------------------------------------------------------------------------
/lib/auto-config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const yaml = require('js-yaml');
4 |
5 | const cache = {};
6 |
7 | const setCache = (key, value, maxAge) => {
8 | cache[JSON.stringify(key)] = {
9 | value,
10 | expire: Date.now() + maxAge
11 | };
12 | };
13 |
14 | const getCache = (key, value, maxAge) => {
15 | key = JSON.stringify(key);
16 |
17 | if (!cache[key]) {
18 | return;
19 | } else if (cache[key].expire < Date.now()) {
20 | delete cache[key];
21 | return;
22 | }
23 |
24 | return cache[key].value;
25 | };
26 |
27 | module.exports = function (publisher) {
28 | const cacheKey = [publisher.user, publisher.repo, publisher.branch];
29 | const cache = getCache(cacheKey);
30 |
31 | if (cache) {
32 | return cache.then(options => Object.assign({}, options));
33 | }
34 |
35 | const lookup = publisher.retrieve('_config.yml')
36 | .then(result => yaml.safeLoad(result.content || ''))
37 | .then(config => {
38 | const options = {};
39 |
40 | config = config || {};
41 |
42 | if (config.permalink) {
43 | options.permalinkStyle = config.permalink;
44 | }
45 |
46 | return options;
47 | });
48 |
49 | setCache(cacheKey, lookup, 60 * 60 * 1000);
50 |
51 | return lookup.then(options => Object.assign({}, options));
52 | };
53 |
--------------------------------------------------------------------------------
/lib/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { deepFreeze } = require('./utils');
4 |
5 | const env = process.env;
6 | const pkg = require('../package.json');
7 |
8 | let prefix = 'MICROPUB_';
9 |
10 | prefix = env[prefix + 'PREFIX'] || prefix;
11 |
12 | // Load the dotenv file
13 | if (process.env.DOTENV_FILE) {
14 | const dotEnvResult = require('dotenv').config({
15 | path: process.env.DOTENV_FILE
16 | });
17 | if (dotEnvResult.error) { throw dotEnvResult.error; }
18 | } else {
19 | // Fail silently if we assume default dotenv file location
20 | require('dotenv').config();
21 | }
22 |
23 | /**
24 | * @template T
25 | * @param {T} value
26 | * @param {boolean} defaultToValue
27 | * @returns {T|any[]|Object}
28 | */
29 | const parseJSON = (value, defaultToValue) => {
30 | let result;
31 |
32 | if (value) {
33 | try {
34 | result = JSON.parse(value);
35 | } catch (err) {}
36 | }
37 |
38 | if (!result && defaultToValue) {
39 | return value;
40 | }
41 | };
42 |
43 | const parseJSONList = (value) => [].concat(parseJSON(value) || []);
44 |
45 | const config = {
46 | version: pkg.version,
47 | env: env.NODE_ENV || 'production',
48 | port: env.PORT || 8080,
49 | host: env[prefix + 'HOST'],
50 | github: {
51 | user: env[prefix + 'GITHUB_USER'],
52 | token: env[prefix + 'GITHUB_TOKEN'],
53 | branch: env[prefix + 'GITHUB_BRANCH']
54 | },
55 | site: {
56 | url: env[prefix + 'SITE_URL'],
57 | repo: env[prefix + 'SITE_GITHUB_REPO'],
58 | syndicateToUid: env[prefix + 'SITE_SYNDICATE_TO_UID'],
59 | syndicateToName: env[prefix + 'SITE_SYNDICATE_TO_NAME'],
60 | syndicateTo: parseJSONList(env[prefix + 'SITE_SYNDICATE_TO'])
61 | },
62 | sites: parseJSON(env[prefix + 'SITES_JSON']) || false,
63 | token: env[prefix + 'TOKEN_ENDPOINT']
64 | ? [{
65 | endpoint: env[prefix + 'TOKEN_ENDPOINT'],
66 | me: env[prefix + 'TOKEN_ME'] || env[prefix + 'SITE_URL']
67 | }]
68 | : [],
69 | handlerOptions: {
70 | noAutoConfigure: !!env[prefix + 'OPTION_NO_AUTO_CONFIGURE'],
71 | deriveCategory: parseJSON(env[prefix + 'OPTION_DERIVE_CATEGORY']) || false,
72 | deriveLanguages: (env[prefix + 'OPTION_DERIVE_LANGUAGES'] || '').split(',').filter(item => !!item),
73 | layoutName: parseJSON(env[prefix + 'LAYOUT_NAME'], true),
74 | filenameStyle: parseJSON(env[prefix + 'FILENAME_STYLE'], true),
75 | mediaFilesStyle: parseJSON(env[prefix + 'MEDIA_FILES_STYLE'], true),
76 | permalinkStyle: parseJSON(env[prefix + 'PERMALINK_STYLE'], true),
77 | encodeHTML: parseJSON(env[prefix + 'ENCODE_HTML'])
78 | }
79 | };
80 |
81 | config.userAgent = pkg.name + '/' + config.version + ' (' + pkg.homepage + ')';
82 |
83 | module.exports = deepFreeze(config);
84 |
--------------------------------------------------------------------------------
/lib/handler.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const GitHubPublisher = require('github-publish');
4 | const MicropubFormatter = require('format-microformat');
5 | const fulfills = require('fulfills');
6 |
7 | const autoConfigure = require('./auto-config');
8 |
9 | const removeEmptyValues = function (obj, exclude = []) {
10 | const result = {};
11 | Object.keys(obj).forEach(key => {
12 | if (obj[key] || exclude.includes(key)) {
13 | result[key] = obj[key];
14 | }
15 | });
16 | return result;
17 | };
18 |
19 | const matchPropertiesToConditions = function (conditions, properties) {
20 | let result;
21 |
22 | conditions.some(({ condition, value }) => {
23 | if (fulfills(properties, condition)) {
24 | result = value;
25 | return true;
26 | }
27 | });
28 |
29 | return result;
30 | };
31 |
32 | module.exports = function (githubTarget, micropubDocument, siteUrl, options) {
33 | options = removeEmptyValues(options || {}, ['layoutName', 'encodeHTML']);
34 |
35 | const publisher = new GitHubPublisher(githubTarget.token, githubTarget.user, githubTarget.repo, githubTarget.branch);
36 |
37 | let force = false;
38 |
39 | let categoryDeriver;
40 |
41 | if (options.deriveCategory) {
42 | categoryDeriver = (properties) => matchPropertiesToConditions(options.deriveCategory, properties);
43 | }
44 |
45 | return Promise.resolve(
46 | options.noAutoConfigure
47 | ? options
48 | : autoConfigure(publisher).then(autoConfig => Object.assign(autoConfig, options))
49 | )
50 | .then(options => {
51 | // Resolve any condition based options to functions that will resolve values based on the conditions
52 | [
53 | 'permalinkStyle',
54 | 'filenameStyle',
55 | 'mediaFilesStyle',
56 | 'layoutName'
57 | ].forEach(key => {
58 | // Save the original value as we will need it when matching the properties to the conditions
59 | const value = options[key];
60 |
61 | if (Array.isArray(value)) {
62 | options[key] = (properties) => matchPropertiesToConditions(value, properties);
63 | }
64 | });
65 |
66 | return options;
67 | })
68 | .then(options => new MicropubFormatter({
69 | relativeTo: siteUrl,
70 | deriveCategory: categoryDeriver,
71 | deriveLanguages: options.deriveLanguages,
72 | permalinkStyle: options.permalinkStyle,
73 | filenameStyle: options.filenameStyle,
74 | filesStyle: options.mediaFilesStyle,
75 | layoutName: options.layoutName,
76 | encodeHTML: options.encodeHTML
77 | }))
78 | .then(formatter => formatter.formatAll(micropubDocument))
79 | .then(formatted => {
80 | if (formatted.raw.url === formatted.url) {
81 | force = true;
82 | }
83 |
84 | return Promise.all(
85 | (formatted.files || []).map(file => {
86 | return publisher.publish(file.filename, file.buffer, {
87 | force,
88 | message: 'uploading media'
89 | })
90 | .then(result => {
91 | // TODO: Do something more than just logging
92 | if (!result) { console.log('Failed to upload media'); }
93 | });
94 | })
95 | )
96 | .then(() => formatted);
97 | })
98 | .then(formatted => {
99 | let category = formatted.raw.derived.category || 'article';
100 |
101 | if (category === 'social') {
102 | category = 'social interaction';
103 | }
104 |
105 | if (options.verbose) {
106 | console.log('Formatted content:\n' + formatted.content);
107 | }
108 |
109 | return publisher.publish(formatted.filename, formatted.content, {
110 | force,
111 | message: 'uploading ' + category
112 | })
113 | .then(function (result) {
114 | return result ? formatted.url : false;
115 | });
116 | });
117 | };
118 |
--------------------------------------------------------------------------------
/lib/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const express = require('express');
4 | const logger = require('bunyan-adaptor')();
5 | const micropub = require('micropub-express');
6 |
7 | const handler = require('./handler');
8 |
9 | const config = require('./config');
10 | const supportedSites = require('./sites');
11 |
12 | const app = express();
13 |
14 | app.disable('x-powered-by');
15 | app.set('env', config.env);
16 | app.set('port', config.port);
17 | // Activate when on Heroku?
18 | // app.enable('trust proxy');
19 |
20 | if (config.host) {
21 | app.use((req, res, next) => {
22 | let portSuffix = (req.get('x-forwarded-port') || req.app.get('port')) + '';
23 | portSuffix = (portSuffix === '80' ? '' : ':' + portSuffix);
24 | if (req.hostname + portSuffix === config.host) { return next(); }
25 | res.redirect(307, req.protocol + '://' + config.host + req.url);
26 | });
27 | }
28 |
29 | app.param('targetsite', (req, res, next, id) => {
30 | if (supportedSites[id]) {
31 | req.targetsite = supportedSites[id];
32 | next();
33 | } else {
34 | res.sendStatus(404);
35 | }
36 | });
37 |
38 | app.use('/micropub/:targetsite', micropub({
39 | logger,
40 | userAgent: config.userAgent,
41 | tokenReference: req => (req.targetsite.token || []).concat(config.token),
42 | queryHandler: (q, req) => {
43 | if (q === 'config') {
44 | const config = {};
45 |
46 | if (req.targetsite.syndicateTo) { config['syndicate-to'] = req.targetsite.syndicateTo; }
47 |
48 | return config;
49 | } else if (q === 'syndicate-to') {
50 | return req.targetsite.syndicateTo ? { 'syndicate-to': req.targetsite.syndicateTo } : undefined;
51 | }
52 | },
53 | handler: (micropubDocument, req) => {
54 | logger.debug({ micropubDocument: JSON.stringify(micropubDocument, null, 2), date: Date().toString() }, 'Received a Micropub document');
55 |
56 | const options = Object.assign({}, config.handlerOptions, req.targetsite.options || {});
57 |
58 | return handler(
59 | Object.assign({}, config.github, req.targetsite.github),
60 | micropubDocument,
61 | req.targetsite.url,
62 | options
63 | ).then(function (url) {
64 | if (url) {
65 | return { url };
66 | }
67 | });
68 | }
69 | }));
70 |
71 | // TODO: Add a proper graceful shutdown mechanism here? Probably still needed in Express 4 – it probably needs to be
72 | app.listen(config.port);
73 |
74 | logger.info('Started and listens on ' + config.port);
75 |
--------------------------------------------------------------------------------
/lib/sites.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { deepFreeze } = require('./utils');
4 |
5 | const config = require('./config');
6 |
7 | const sites = Object.assign({}, config.sites || {});
8 |
9 | if (config.site.url) {
10 | const {
11 | url,
12 | repo,
13 | syndicateTo,
14 | syndicateToUid,
15 | syndicateToName
16 | } = config.site;
17 |
18 | sites.main = {
19 | url,
20 | github: { repo },
21 | syndicateTo
22 | };
23 |
24 | if (syndicateToUid) {
25 | sites.main.syndicateTo = [
26 | {
27 | uid: syndicateToUid,
28 | name: syndicateToName
29 | }
30 | ];
31 | }
32 | }
33 |
34 | module.exports = deepFreeze(sites);
35 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const deepFreeze = function (obj) {
4 | Object.keys(obj).forEach(key => {
5 | const item = obj[key];
6 | if (typeof item === 'object' && !Object.isFrozen(item)) {
7 | obj[key] = deepFreeze(item);
8 | }
9 | });
10 | return Object.freeze(obj);
11 | };
12 |
13 | module.exports = {
14 | deepFreeze
15 | };
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "micropub-to-github",
3 | "version": "0.6.1",
4 | "license": "MIT",
5 | "private": true,
6 | "description": "An endpoint that accepts Micropub requests, formats them into Jekyll posts and pushes them to a configured GitHub repository.",
7 | "author": "Pelle Wessman (http://kodfabrik.se/)",
8 | "homepage": "https://github.com/voxpelli/webpage-micropub-to-github",
9 | "repository": {
10 | "type": "git",
11 | "url": "git://github.com/voxpelli/webpage-micropub-to-github.git"
12 | },
13 | "main": "./lib/main",
14 | "directories": {
15 | "lib": "./lib"
16 | },
17 | "scripts": {
18 | "check:dependency-check": "dependency-check 'lib/**/*.js' 'test/**/*.js' --no-dev",
19 | "check:installed-check": "installed-check --engine-no-dev",
20 | "check:lint": "eslint .",
21 | "check": "run-p check:*",
22 | "start": "node .",
23 | "sync-gh-actions": "ghat",
24 | "test:mocha": "NODE_ENV=test DOTENV_FILE=test/test.env nyc --reporter=lcov --reporter text mocha 'test/**/*.spec.js'",
25 | "test-ci": "run-s test:*",
26 | "test": "run-s check test:*"
27 | },
28 | "husky": {
29 | "hooks": {
30 | "pre-push": "npm test"
31 | }
32 | },
33 | "engines": {
34 | "node": ">=14.0.0"
35 | },
36 | "devDependencies": {
37 | "@voxpelli/eslint-config": "^4.0.0",
38 | "chai": "^4.2.0",
39 | "chai-as-promised": "^7.1.1",
40 | "dependency-check": "^4.1.0",
41 | "eslint": "^6.8.0",
42 | "eslint-config-standard": "^14.1.0",
43 | "eslint-plugin-import": "^2.18.2",
44 | "eslint-plugin-jsdoc": "^21.0.0",
45 | "eslint-plugin-node": "^11.0.0",
46 | "eslint-plugin-promise": "^4.2.1",
47 | "eslint-plugin-standard": "^4.0.0",
48 | "ghat": "^0.14.0",
49 | "husky": "^4.3.8",
50 | "installed-check": "^7.0.0",
51 | "mocha": "^7.0.1",
52 | "nock": "^11.7.2",
53 | "npm-run-all2": "^6.0.5",
54 | "nyc": "^15.0.0",
55 | "sinon": "^8.1.1"
56 | },
57 | "dependencies": {
58 | "bunyan-adaptor": "^4.0.0",
59 | "dotenv": "^8.2.0",
60 | "express": "^4.17.1",
61 | "format-microformat": "^0.11.1",
62 | "fulfills": "^2.1.0",
63 | "github-publish": "^3.0.0",
64 | "js-yaml": "^3.12.1",
65 | "micropub-express": "^0.8.1"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "github>voxpelli/renovate-config:app"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/sample.env:
--------------------------------------------------------------------------------
1 | # See README.md for list of available configuration options
2 |
3 | MICROPUB_TOKEN_ENDPOINT="https://tokens.indieauth.com/token"
4 | MICROPUB_TOKEN_ME="https://johndoe.example"
5 |
6 | MICROPUB_GITHUB_TOKEN="12345abcde67890fghij09876klmno54321pqrst"
7 | MICROPUB_GITHUB_USER="johndoe"
8 |
9 | MICROPUB_SITE_URL="https://johndoe.example"
10 | MICROPUB_SITE_GITHUB_REPO="johndoe.github.io"
11 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/test/handler.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const chai = require('chai');
4 | const chaiAsPromised = require('chai-as-promised');
5 | const nock = require('nock');
6 | const sinon = require('sinon');
7 |
8 | chai.use(chaiAsPromised);
9 | chai.should();
10 |
11 | describe('Handler', () => {
12 | const handler = require('../lib/handler.js');
13 | let handlerConfig;
14 | let clock;
15 |
16 | const basicTest = function ({ content, message, finalUrl, filename, contentInput } = {}) {
17 | const token = 'abc123';
18 | const user = 'username';
19 | const repo = 'repo';
20 |
21 | filename = filename || '_posts/2015-06-30-awesomeness-is-awesome.md';
22 |
23 | const path = '/repos/' + user + '/' + repo + '/contents/' + filename;
24 |
25 | const encodedContent = Buffer.from(content || (
26 | '---\n' +
27 | 'layout: micropubpost\n' +
28 | 'date: \'2015-06-30T14:19:45.000Z\'\n' +
29 | 'title: awesomeness is awesome\n' +
30 | 'lang: en\n' +
31 | 'slug: awesomeness-is-awesome\n' +
32 | '---\n' +
33 | 'hello world\n'
34 | )).toString('base64');
35 |
36 | const mock = nock('https://api.github.com/')
37 | .matchHeader('authorization', val => val && val[0] === 'Bearer ' + token)
38 | .put(path, {
39 | message: message || 'uploading article',
40 | content: encodedContent
41 | })
42 | .reply(201, { content: { sha: 'abc123' } });
43 |
44 | return handler(
45 | {
46 | token,
47 | user,
48 | repo
49 | },
50 | {
51 | type: ['h-entry'],
52 | properties: {
53 | content: [contentInput || 'hello world'],
54 | name: ['awesomeness is awesome'],
55 | lang: ['en']
56 | }
57 | },
58 | 'http://example.com/foo/',
59 | handlerConfig
60 | )
61 | .then(url => {
62 | mock.done();
63 | url.should.equal(finalUrl || 'http://example.com/foo/2015/06/awesomeness-is-awesome/');
64 | });
65 | };
66 |
67 | beforeEach(() => {
68 | nock.cleanAll();
69 | nock.disableNetConnect();
70 | clock = sinon.useFakeTimers(1435674000000);
71 | handlerConfig = {
72 | // Enable to help with debugging of wrongly formatted content
73 | // verbose: true,
74 | noAutoConfigure: true,
75 | permalinkStyle: '/:categories/:year/:month/:title/'
76 | };
77 | });
78 |
79 | afterEach(() => {
80 | clock.restore();
81 | sinon.restore();
82 | if (!nock.isDone()) {
83 | throw new Error('pending nock mocks: ' + nock.pendingMocks());
84 | }
85 | });
86 |
87 | describe('main', () => {
88 | it('should format and send content', () => {
89 | return basicTest();
90 | });
91 |
92 | it('should upload files prior to content', () => {
93 | const token = 'abc123';
94 | const user = 'username';
95 | const repo = 'repo';
96 | const repoPath = '/repos/' + user + '/' + repo + '/contents/';
97 | const mediaFilename = 'foo.jpg';
98 |
99 | const fileContent = Buffer.from('abc123');
100 |
101 | const encodedContent = Buffer.from(
102 | '---\n' +
103 | 'layout: micropubpost\n' +
104 | 'date: \'2015-06-30T14:19:45.000Z\'\n' +
105 | 'title: awesomeness is awesome\n' +
106 | 'lang: en\n' +
107 | 'slug: awesomeness-is-awesome\n' +
108 | 'mf-photo:\n' +
109 | ' - \'http://example.com/foo/media/2015-06-awesomeness-is-awesome/' + mediaFilename + '\'\n' +
110 | '---\n' +
111 | 'hello world\n'
112 | );
113 |
114 | const mock = nock('https://api.github.com/')
115 | .matchHeader('authorization', val => val && val[0] === 'Bearer ' + token)
116 | // Upload of the media
117 | .put(repoPath + 'media/2015-06-awesomeness-is-awesome/' + mediaFilename, {
118 | // TODO: Change this commit message to at least be "new media" instead
119 | message: 'uploading media',
120 | content: fileContent.toString('base64')
121 | })
122 | .reply(201, { content: { sha: 'abc123' } })
123 | // Upload of the content
124 | .put(repoPath + '_posts/2015-06-30-awesomeness-is-awesome.md', {
125 | message: 'uploading article',
126 | content: encodedContent.toString('base64')
127 | })
128 | .reply(201, { content: { sha: 'abc123' } });
129 |
130 | return handler(
131 | {
132 | token,
133 | user,
134 | repo
135 | }, {
136 | type: ['h-entry'],
137 | properties: {
138 | content: ['hello world'],
139 | name: ['awesomeness is awesome'],
140 | lang: ['en']
141 | },
142 | files: {
143 | photo: [{ filename: 'foo.jpg', buffer: fileContent }]
144 | }
145 | },
146 | 'http://example.com/foo/',
147 | handlerConfig
148 | )
149 | .then(url => {
150 | mock.done();
151 | url.should.equal('http://example.com/foo/2015/06/awesomeness-is-awesome/');
152 | });
153 | });
154 |
155 | it('should override existing content if matching URL', () => {
156 | const token = 'abc123';
157 | const user = 'username';
158 | const repo = 'repo';
159 | const path = '/repos/' + user + '/' + repo + '/contents/_posts/2015-06-30-awesomeness-is-awesome.md';
160 | const sha = 'abc123';
161 |
162 | const encodedContent = Buffer.from(
163 | '---\n' +
164 | 'layout: micropubpost\n' +
165 | 'date: \'2015-06-30T14:19:45.000Z\'\n' +
166 | 'title: awesomeness is awesome\n' +
167 | 'lang: en\n' +
168 | 'slug: awesomeness-is-awesome\n' +
169 | '---\n' +
170 | 'hello world\n'
171 | );
172 | const base64 = encodedContent.toString('base64');
173 |
174 | const mock = nock('https://api.github.com/')
175 | .put(path, {
176 | message: 'uploading article',
177 | content: base64
178 | })
179 | .reply(422, {})
180 |
181 | .get(path)
182 | .reply(200, { sha })
183 |
184 | .put(path, {
185 | message: 'uploading article',
186 | content: base64,
187 | sha
188 | })
189 | .reply(201, { content: { sha: 'xyz789' } });
190 |
191 | return handler(
192 | {
193 | token,
194 | user,
195 | repo
196 | }, {
197 | type: ['h-entry'],
198 | url: 'http://example.com/foo/2015/06/awesomeness-is-awesome/',
199 | properties: {
200 | content: ['hello world'],
201 | name: ['awesomeness is awesome'],
202 | lang: ['en']
203 | }
204 | },
205 | 'http://example.com/foo/',
206 | handlerConfig
207 | )
208 | .then(url => {
209 | mock.done();
210 | url.should.equal('http://example.com/foo/2015/06/awesomeness-is-awesome/');
211 | });
212 | });
213 |
214 | it('should not override existing content if no matching URL', () => {
215 | const token = 'abc123';
216 | const user = 'username';
217 | const repo = 'repo';
218 | const path = '/repos/' + user + '/' + repo + '/contents/_posts/2015-06-30-awesomeness-is-awesome.md';
219 |
220 | const encodedContent = Buffer.from(
221 | '---\n' +
222 | 'layout: micropubpost\n' +
223 | 'date: \'2015-06-30T14:19:45.000Z\'\n' +
224 | 'title: awesomeness is awesome\n' +
225 | 'lang: en\n' +
226 | 'slug: awesomeness-is-awesome\n' +
227 | '---\n' +
228 | 'hello world\n'
229 | );
230 | const base64 = encodedContent.toString('base64');
231 |
232 | const mock = nock('https://api.github.com/')
233 | .put(path, {
234 | message: 'uploading article',
235 | content: base64
236 | })
237 | .reply(422, {});
238 |
239 | return handler(
240 | {
241 | token,
242 | user,
243 | repo
244 | }, {
245 | type: ['h-entry'],
246 | properties: {
247 | content: ['hello world'],
248 | name: ['awesomeness is awesome'],
249 | lang: ['en']
250 | }
251 | },
252 | 'http://example.com/foo/',
253 | handlerConfig
254 | )
255 | .then(url => {
256 | mock.done();
257 | url.should.equal(false);
258 | });
259 | });
260 |
261 | it('should set a custom commit message when formatter returns a category', () => {
262 | const token = 'abc123';
263 | const user = 'username';
264 | const repo = 'repo';
265 | const path = '/repos/' + user + '/' + repo + '/contents/_posts/2015-06-30-51585.md';
266 |
267 | const encodedContent = Buffer.from(
268 | '---\n' +
269 | 'layout: micropubpost\n' +
270 | 'date: \'2015-06-30T14:19:45.000Z\'\n' +
271 | 'title: \'\'\n' +
272 | 'lang: en\n' +
273 | 'slug: \'51585\'\n' +
274 | 'category: social\n' +
275 | '---\n' +
276 | 'hello world\n'
277 | );
278 | const base64 = encodedContent.toString('base64');
279 |
280 | const mock = nock('https://api.github.com/')
281 | .matchHeader('authorization', val => val && val[0] === 'Bearer ' + token)
282 | .put(path, {
283 | message: 'uploading social interaction',
284 | content: base64
285 | })
286 | .reply(201, { content: { sha: 'abc123' } });
287 |
288 | return handler(
289 | {
290 | token,
291 | user,
292 | repo
293 | }, {
294 | type: ['h-entry'],
295 | properties: {
296 | content: ['hello world'],
297 | lang: ['en']
298 | }
299 | },
300 | 'http://example.com/foo/',
301 | handlerConfig
302 | )
303 | .then(url => {
304 | mock.done();
305 | url.should.equal('http://example.com/foo/social/2015/06/51585/');
306 | });
307 | });
308 |
309 | it('should format HTML to Markdown and send content', () => {
310 | const token = 'abc123';
311 | const user = 'username';
312 | const repo = 'repo';
313 | const path = '/repos/' + user + '/' + repo + '/contents/_posts/2015-06-30-awesomeness-is-awesome.md';
314 |
315 | const encodedContent = Buffer.from(
316 | '---\n' +
317 | 'layout: micropubpost\n' +
318 | 'date: \'2015-06-30T14:19:45.000Z\'\n' +
319 | 'title: awesomeness is awesome\n' +
320 | 'lang: en\n' +
321 | 'slug: awesomeness-is-awesome\n' +
322 | '---\n' +
323 | '**hello world**\n'
324 | );
325 |
326 | const mock = nock('https://api.github.com/')
327 | .matchHeader('authorization', val => val && val[0] === 'Bearer ' + token)
328 | .put(path, {
329 | message: 'uploading article',
330 | content: encodedContent.toString('base64')
331 | })
332 | .reply(201, { content: { sha: 'abc123' } });
333 |
334 | return handler(
335 | {
336 | token,
337 | user,
338 | repo
339 | }, {
340 | type: ['h-entry'],
341 | properties: {
342 | content: [{
343 | html: 'hello world ',
344 | value: 'hello world'
345 | }],
346 | name: ['awesomeness is awesome'],
347 | lang: ['en']
348 | }
349 | },
350 | 'http://example.com/foo/',
351 | handlerConfig
352 | )
353 | .then(url => {
354 | mock.done();
355 | url.should.equal('http://example.com/foo/2015/06/awesomeness-is-awesome/');
356 | });
357 | });
358 |
359 | it('should support category deriving', () => {
360 | handlerConfig.deriveCategory = [
361 | { value: 'foo', condition: 'abc = 123 AND foo = bar' },
362 | { value: 'xyz', condition: 'content[] = "hello world"' }
363 | ];
364 |
365 | return basicTest({
366 | content: (
367 | '---\n' +
368 | 'layout: micropubpost\n' +
369 | 'date: \'2015-06-30T14:19:45.000Z\'\n' +
370 | 'title: awesomeness is awesome\n' +
371 | 'lang: en\n' +
372 | 'slug: awesomeness-is-awesome\n' +
373 | 'category: xyz\n' +
374 | '---\n' +
375 | 'hello world\n'
376 | ),
377 | message: 'uploading xyz',
378 | finalUrl: 'http://example.com/foo/xyz/2015/06/awesomeness-is-awesome/'
379 | });
380 | });
381 |
382 | it('should support custom layout deriving', () => {
383 | handlerConfig.layoutName = [
384 | { value: 'foo', condition: 'abc = 123 AND foo = bar' },
385 | { value: 'xyz', condition: 'content[] = "hello world"' }
386 | ];
387 |
388 | return basicTest({
389 | content: (
390 | '---\n' +
391 | 'layout: xyz\n' +
392 | 'date: \'2015-06-30T14:19:45.000Z\'\n' +
393 | 'title: awesomeness is awesome\n' +
394 | 'lang: en\n' +
395 | 'slug: awesomeness-is-awesome\n' +
396 | '---\n' +
397 | 'hello world\n'
398 | )
399 | });
400 | });
401 |
402 | it('should support simple custom layout', () => {
403 | handlerConfig.layoutName = 'simple';
404 |
405 | return basicTest({
406 | content: (
407 | '---\n' +
408 | 'layout: simple\n' +
409 | 'date: \'2015-06-30T14:19:45.000Z\'\n' +
410 | 'title: awesomeness is awesome\n' +
411 | 'lang: en\n' +
412 | 'slug: awesomeness-is-awesome\n' +
413 | '---\n' +
414 | 'hello world\n'
415 | )
416 | });
417 | });
418 |
419 | it('should support layout-less', () => {
420 | handlerConfig.layoutName = false;
421 |
422 | return basicTest({
423 | content: (
424 | '---\n' +
425 | 'date: \'2015-06-30T14:19:45.000Z\'\n' +
426 | 'title: awesomeness is awesome\n' +
427 | 'lang: en\n' +
428 | 'slug: awesomeness-is-awesome\n' +
429 | '---\n' +
430 | 'hello world\n'
431 | )
432 | });
433 | });
434 |
435 | it('should support callback based permalink style', () => {
436 | handlerConfig.permalinkStyle = [
437 | { value: 'first/:slug', condition: 'content[] = "hello world"' },
438 | { value: 'second/:slug', condition: 'abc = 123 AND foo = bar' }
439 | ];
440 |
441 | return basicTest({
442 | content: (
443 | '---\n' +
444 | 'layout: micropubpost\n' +
445 | 'date: \'2015-06-30T14:19:45.000Z\'\n' +
446 | 'title: awesomeness is awesome\n' +
447 | 'lang: en\n' +
448 | 'slug: awesomeness-is-awesome\n' +
449 | '---\n' +
450 | 'hello world\n'
451 | ),
452 | finalUrl: 'http://example.com/foo/first/awesomeness-is-awesome'
453 | });
454 | });
455 |
456 | it('should support callback based filename style', () => {
457 | handlerConfig.filenameStyle = [
458 | { value: 'first/:slug', condition: 'abc = 123 AND foo = bar' },
459 | { value: 'second/:slug', condition: 'content[] = "hello world"' }
460 | ];
461 |
462 | return basicTest({
463 | content: (
464 | '---\n' +
465 | 'layout: micropubpost\n' +
466 | 'date: \'2015-06-30T14:19:45.000Z\'\n' +
467 | 'title: awesomeness is awesome\n' +
468 | 'lang: en\n' +
469 | 'slug: awesomeness-is-awesome\n' +
470 | '---\n' +
471 | 'hello world\n'
472 | ),
473 | filename: 'second/awesomeness-is-awesome.md'
474 | });
475 | });
476 |
477 | it('should correctly HTML-encode text input', () => {
478 | return basicTest({
479 | contentInput: 'world < hello',
480 | content: (
481 | '---\n' +
482 | 'layout: micropubpost\n' +
483 | 'date: \'2015-06-30T14:19:45.000Z\'\n' +
484 | 'title: awesomeness is awesome\n' +
485 | 'lang: en\n' +
486 | 'slug: awesomeness-is-awesome\n' +
487 | '---\n' +
488 | 'world < hello\n'
489 | )
490 | });
491 | });
492 |
493 | it('should support HTML-encode opt out', () => {
494 | handlerConfig.encodeHTML = false;
495 |
496 | return basicTest({
497 | contentInput: 'world < hello',
498 | content: (
499 | '---\n' +
500 | 'layout: micropubpost\n' +
501 | 'date: \'2015-06-30T14:19:45.000Z\'\n' +
502 | 'title: awesomeness is awesome\n' +
503 | 'lang: en\n' +
504 | 'slug: awesomeness-is-awesome\n' +
505 | '---\n' +
506 | 'world < hello\n'
507 | )
508 | });
509 | });
510 | });
511 | });
512 |
--------------------------------------------------------------------------------
/test/test.env:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/voxpelli/webpage-micropub-to-github/f71d6dc3aa8549ee0036118d794867c6dc706607/test/test.env
--------------------------------------------------------------------------------