Compare commits
5 commits
master
...
run-in-sub
Author | SHA1 | Date | |
---|---|---|---|
|
adcf2b4e46 | ||
|
c49836bec7 | ||
|
badcebeb35 | ||
|
be12c945fa | ||
|
ae1aacd17c |
7 changed files with 80 additions and 23 deletions
14
README.md
14
README.md
|
@ -52,6 +52,9 @@ files, and for your Ghost API interface. The parameters to set are:
|
||||||
* `GHOST_URL` - URL of your Ghost installation
|
* `GHOST_URL` - URL of your Ghost installation
|
||||||
* `GHOST_VERSION` - 'v2' or 'v3' depending on which version you're using
|
* `GHOST_VERSION` - 'v2' or 'v3' depending on which version you're using
|
||||||
* `GHOST_ADMIN_KEY` - 'Admi API key for Ghost'
|
* `GHOST_ADMIN_KEY` - 'Admi API key for Ghost'
|
||||||
|
* `BASE_PATH` - Subdomain to serve the website on (eg. `/seance`).
|
||||||
|
Default: `/`. If you're not planning to use the Seance server, then
|
||||||
|
you can safely ignore this option.
|
||||||
|
|
||||||
In case you're wondering about the WebDAV server: that's the setup we
|
In case you're wondering about the WebDAV server: that's the setup we
|
||||||
use at Snipette. We'd like to eventually let you upload directly through
|
use at Snipette. We'd like to eventually let you upload directly through
|
||||||
|
@ -103,7 +106,16 @@ file via the "Labs" section. This is required becaues Ghost doesn't
|
||||||
let you directly add users; it only lets you import them.
|
let you directly add users; it only lets you import them.
|
||||||
|
|
||||||
Seance also attempts to fetch the Medium user's profile image and upload
|
Seance also attempts to fetch the Medium user's profile image and upload
|
||||||
it via WebDAV. The JSON file will link to the WebDAV-uploaded image
|
it via WebDAV. The JSON file will link to the WebDAV-uploaded image.
|
||||||
|
|
||||||
|
## Run the Seance Server
|
||||||
|
|
||||||
|
The server is mainly needed when Seance'ing draft Medium posts, because
|
||||||
|
Medium doesn't let you access those anonymously so you need to resort
|
||||||
|
to bookmarklets instead. But it also comes in handy as a nicer,
|
||||||
|
friendlier interface for sending out posts for those who don't want to
|
||||||
|
use the command-line. The engine is more or less the same; it's just
|
||||||
|
the interface that's different!
|
||||||
|
|
||||||
# credits
|
# credits
|
||||||
|
|
||||||
|
|
20
cli.js
20
cli.js
|
@ -136,6 +136,26 @@ program.command('setup')
|
||||||
config.ghost.admin_key = res.admin_key
|
config.ghost.admin_key = res.admin_key
|
||||||
console.log(`Right. So that's Ghost ${config.ghost.version} running at ${config.ghost.url} with key ${config.ghost.admin_key}`)
|
console.log(`Right. So that's Ghost ${config.ghost.version} running at ${config.ghost.url} with key ${config.ghost.admin_key}`)
|
||||||
|
|
||||||
|
console.log('\n\nNow, a bit about the server.')
|
||||||
|
console.log(
|
||||||
|
'This server usually runs on the main path (eg. example.com/).' +
|
||||||
|
'If you want to host seance on a different subpath (eg. ' +
|
||||||
|
'example.com/seance) then please enter that last part of the' +
|
||||||
|
'string now (eg. /seance).'
|
||||||
|
)
|
||||||
|
res = await prompt.get([
|
||||||
|
{ name: 'base_path', default: config.basePath || '/' },
|
||||||
|
])
|
||||||
|
if (res.base_path == '/') {
|
||||||
|
console.log('Okay. Serving on the root path then :)')
|
||||||
|
} else {
|
||||||
|
config.basePath = res.base_path
|
||||||
|
console.log(
|
||||||
|
'Right, so we\'ll be serving at yoursite.com' +
|
||||||
|
`${config.basePath} then :)`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'\n\nA final thing. Do you have a "scissors" or other image ' +
|
'\n\nA final thing. Do you have a "scissors" or other image ' +
|
||||||
'used as a separator in your article? If so, enter the path ' +
|
'used as a separator in your article? If so, enter the path ' +
|
||||||
|
|
|
@ -95,6 +95,13 @@ let config = convict({
|
||||||
env: 'SEPARATOR_IMAGE',
|
env: 'SEPARATOR_IMAGE',
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
basePath: {
|
||||||
|
doc: 'Base subpath on which Seance is hosted (eg. /seance)',
|
||||||
|
format: 'String', // TODO: validate by checking path
|
||||||
|
env: 'BASE_PATH',
|
||||||
|
default: null,
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Load configs from home directory, if present
|
// Load configs from home directory, if present
|
||||||
|
|
42
server.js
42
server.js
|
@ -5,6 +5,7 @@ const slugify = require('underscore.string/slugify')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
|
|
||||||
const { Seance } = require ('./seance')
|
const { Seance } = require ('./seance')
|
||||||
|
const config = require('./config')
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
var expressWs = require('express-ws')(app)
|
var expressWs = require('express-ws')(app)
|
||||||
|
@ -18,8 +19,12 @@ app.use(bodyParser.urlencoded({
|
||||||
app.use(bodyParser('json'))
|
app.use(bodyParser('json'))
|
||||||
|
|
||||||
// Enable static files
|
// Enable static files
|
||||||
app.use(express.static('public'))
|
app.use('/', express.static('public')) // basePath is prefixed later
|
||||||
app.use(express.static('static'))
|
app.use(config.basePath || '/', express.static('static'))
|
||||||
|
|
||||||
|
// Router
|
||||||
|
var router = express.Router()
|
||||||
|
app.use(config.basePath || '/', router)
|
||||||
|
|
||||||
// Set up VueXpress
|
// Set up VueXpress
|
||||||
let options = {
|
let options = {
|
||||||
|
@ -29,12 +34,15 @@ let options = {
|
||||||
metaInfo: {
|
metaInfo: {
|
||||||
title: 'Seance',
|
title: 'Seance',
|
||||||
script: [
|
script: [
|
||||||
{ type: 'text/javascript', src: '/app.js' },
|
{
|
||||||
|
type: 'text/javascript',
|
||||||
|
src: (config.basePath || '') + '/app.js'
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
extractCSS: true,
|
extractCSS: true,
|
||||||
cssOutputPath: '/css/styles.css',
|
cssOutputPath: (config.basePath || '') + '/css/styles.css',
|
||||||
publicPath: 'public',
|
publicPath: 'public' + (config.basePath || ''),
|
||||||
compilerConfig: {
|
compilerConfig: {
|
||||||
// custom webpack config
|
// custom webpack config
|
||||||
},
|
},
|
||||||
|
@ -46,19 +54,22 @@ let options = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderer = vueRenderer(options)
|
const renderer = vueRenderer(options)
|
||||||
app.use(renderer)
|
router.use(renderer)
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
|
|
||||||
app.get('/', (req, res) => {
|
router.get('/', (req, res) => {
|
||||||
res.render('index', {
|
res.render('index', {
|
||||||
baseUrl: req.hostname.startsWith('localhost')
|
seanceUrl: req.hostname.startsWith('localhost')
|
||||||
? req.protocol + '://' + req.headers.host
|
? req.headers.host + req.baseUrl
|
||||||
: '//' + req.hostname, // auto choose http or https
|
: req.hostname + req.baseUrl,
|
||||||
|
protocol: req.hostname.startsWith('localhost')
|
||||||
|
? req.protocol + '://'
|
||||||
|
: '//',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post('/fetch', (req, res) => {
|
router.post('/fetch', (req, res) => {
|
||||||
var json
|
var json
|
||||||
var post
|
var post
|
||||||
|
|
||||||
|
@ -110,6 +121,9 @@ app.post('/fetch', (req, res) => {
|
||||||
|
|
||||||
// render the final post
|
// render the final post
|
||||||
res.render('fetch-medium', {
|
res.render('fetch-medium', {
|
||||||
|
seanceUrl: req.hostname.startsWith('localhost')
|
||||||
|
? req.headers.host + req.baseUrl
|
||||||
|
: req.hostname + req.baseUrl,
|
||||||
post: {
|
post: {
|
||||||
title: post.title,
|
title: post.title,
|
||||||
subtitle: post.content.subtitle,
|
subtitle: post.content.subtitle,
|
||||||
|
@ -125,14 +139,14 @@ app.get('/fetch', (req, res) => {
|
||||||
res.redirect(303, '/')
|
res.redirect(303, '/')
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get('/api', (req, res) => {
|
router.get('/api', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
message: 'Welcome to the Seance API :)',
|
message: 'Welcome to the Seance API :)',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.ws('/ws/fetch-medium', (ws, req) => {
|
router.ws('/ws/fetch-medium', (ws, req) => {
|
||||||
ws.on('message', async(msg) => {
|
ws.on('message', async(msg) => {
|
||||||
|
|
||||||
command = msg.split(' ')
|
command = msg.split(' ')
|
||||||
|
@ -182,7 +196,7 @@ app.ws('/ws/fetch-medium', (ws, req) => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.ws('/ws/push-ghost', (ws, req) => {
|
router.ws('/ws/push-ghost', (ws, req) => {
|
||||||
ws.on('message', async(msg) => {
|
ws.on('message', async(msg) => {
|
||||||
|
|
||||||
// respond to keepalive
|
// respond to keepalive
|
||||||
|
|
|
@ -11,12 +11,13 @@ function addNotification(msg, className='is-warning') {
|
||||||
function mediumFetch(e) {
|
function mediumFetch(e) {
|
||||||
if(e) { e.preventDefault() }
|
if(e) { e.preventDefault() }
|
||||||
|
|
||||||
|
const baseUrl = document.querySelector('[data-seance-url]').dataset.seanceUrl
|
||||||
const postSlug = document.querySelector('[data-post-slug]').dataset.postSlug
|
const postSlug = document.querySelector('[data-post-slug]').dataset.postSlug
|
||||||
const mediumUrl = document.querySelector('[data-post-mediumurl]').dataset.postMediumurl
|
const mediumUrl = document.querySelector('[data-post-mediumurl]').dataset.postMediumurl
|
||||||
var password = document.querySelector('input[name=password]').value
|
var password = document.querySelector('input[name=password]').value
|
||||||
console.log('Fetching ' + postSlug)
|
console.log('Fetching ' + postSlug)
|
||||||
const socketProtocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:')
|
const socketProtocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:')
|
||||||
const socketUrl = socketProtocol + '//' + window.location.host + '/ws/fetch-medium/'
|
const socketUrl = socketProtocol + '//' + baseUrl + '/ws/fetch-medium/'
|
||||||
const socket = new WebSocket(socketUrl)
|
const socket = new WebSocket(socketUrl)
|
||||||
|
|
||||||
// set up the div
|
// set up the div
|
||||||
|
@ -96,11 +97,12 @@ function mediumFetch(e) {
|
||||||
function ghostPush(e) {
|
function ghostPush(e) {
|
||||||
if(e) { e.preventDefault() }
|
if(e) { e.preventDefault() }
|
||||||
|
|
||||||
|
const baseUrl = document.querySelector('[data-seance-url]').dataset.seanceUrl
|
||||||
let postSlug = document.querySelector('[data-post-slug]').dataset.postSlug
|
let postSlug = document.querySelector('[data-post-slug]').dataset.postSlug
|
||||||
var password = document.querySelector('input[name=password]').value
|
var password = document.querySelector('input[name=password]').value
|
||||||
console.log('Pushing ' + postSlug)
|
console.log('Pushing ' + postSlug)
|
||||||
const socketProtocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:')
|
const socketProtocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:')
|
||||||
const socketUrl = socketProtocol + '//' + window.location.host + '/ws/push-ghost/'
|
const socketUrl = socketProtocol + '//' + baseUrl + '/ws/push-ghost/'
|
||||||
const socket = new WebSocket(socketUrl)
|
const socket = new WebSocket(socketUrl)
|
||||||
|
|
||||||
// set up the div
|
// set up the div
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<nav class="navbar has-background-light" role="navigation" aria-label="main navigation">
|
<nav class="navbar has-background-light" role="navigation" aria-label="main navigation">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<a class="navbar-item is-size-5" href="/">Seance</a>
|
<a class="navbar-item is-size-5" :href="seanceUrl + '/'">Seance</a>
|
||||||
|
|
||||||
<a role="button" class="navbar navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="navbarMain">
|
<a role="button" class="navbar navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="navbarMain">
|
||||||
<span aria-hidden="true"></span>
|
<span aria-hidden="true"></span>
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
</div>
|
</div>
|
||||||
<footer class="card-footer" id="statusbar">
|
<footer class="card-footer" id="statusbar">
|
||||||
<a :href="post.mediumUrl" v-if="post.mediumUrl" class="card-footer-item">View on Medium</a>
|
<a :href="post.mediumUrl" v-if="post.mediumUrl" class="card-footer-item">View on Medium</a>
|
||||||
<a href="#" class="card-footer-item" onclick="mediumFetch(event)" :data-post-slug="post.slug" :data-post-mediumurl="post.mediumUrl">Fetch</a>
|
<a href="#" class="card-footer-item" onclick="mediumFetch(event)" :data-post-slug="post.slug" :data-post-mediumurl="post.mediumUrl" :data-seance-url="seanceUrl">Fetch</a>
|
||||||
<a href="#" class="card-footer-item" onclick="ghostPush(event)">Push to Ghost</a>
|
<a href="#" class="card-footer-item" onclick="ghostPush(event)">Push to Ghost</a>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,6 +62,7 @@
|
||||||
title: 'Seance',
|
title: 'Seance',
|
||||||
user: null,
|
user: null,
|
||||||
host: 'http://localhost:4000',
|
host: 'http://localhost:4000',
|
||||||
|
seanceUrl: 'http://localhost:4000',
|
||||||
post: {
|
post: {
|
||||||
title: '(untitled)',
|
title: '(untitled)',
|
||||||
subtitle: '',
|
subtitle: '',
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<nav class="navbar has-background-light" role="navigation" aria-label="main navigation">
|
<nav class="navbar has-background-light" role="navigation" aria-label="main navigation">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<a class="navbar-item is-size-5" href="/">Seance</a>
|
<a class="navbar-item is-size-5" :href="'//' + seanceUrl + '/'">Seance</a>
|
||||||
|
|
||||||
<a role="button" class="navbar navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="navbarMain">
|
<a role="button" class="navbar navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="navbarMain">
|
||||||
<span aria-hidden="true"></span>
|
<span aria-hidden="true"></span>
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
|
|
||||||
<div class="column is-half is-offset-one-quarter has-text-centered" style="margin-top: 10%;">
|
<div class="column is-half is-offset-one-quarter has-text-centered" style="margin-top: 10%;">
|
||||||
<h1 class="title">{{ title }}</h1>
|
<h1 class="title">{{ title }}</h1>
|
||||||
<form action="/fetch/medium/" method="post">
|
<form :action="'/' + seanceUrl + '/fetch/medium/'" method="post">
|
||||||
<input type="search" class="input is-medium is-rounded is-focused" placeholder="Enter a Medium link..."/>
|
<input type="search" class="input is-medium is-rounded is-focused" placeholder="Enter a Medium link..."/>
|
||||||
<p>or, use the bookmarklet <a class="button is-primary is-small" :href="bookmarklet">Add to Seance</a></p>
|
<p>or, use the bookmarklet <a class="button is-primary is-small" :href="bookmarklet">Add to Seance</a></p>
|
||||||
</form>
|
</form>
|
||||||
|
@ -43,12 +43,13 @@
|
||||||
return {
|
return {
|
||||||
title: 'Seance',
|
title: 'Seance',
|
||||||
user: null,
|
user: null,
|
||||||
baseUrl: 'http://localhost:4000',
|
seanceUrl: 'localhost:4000',
|
||||||
|
protocol: '//',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookmarklet () {
|
bookmarklet () {
|
||||||
return `javascript:(()=>{document.title='[Loading...] '+document.title;s=document.createElement('script');s.src='${this.baseUrl}/bookmarklet.js';s.dataset.seanceUrl='${this.baseUrl}';document.body.appendChild(s)})()`
|
return `javascript:(()=>{document.title='[Loading...] '+document.title;s=document.createElement('script');s.src='${this.protocol}${this.seanceUrl}/bookmarklet.js';s.dataset.seanceUrl='${this.protocol}${this.seanceUrl}';document.body.appendChild(s)})()`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue