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_VERSION` - 'v2' or 'v3' depending on which version you're using
|
||||
* `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
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
|
|
20
cli.js
20
cli.js
|
@ -136,6 +136,26 @@ program.command('setup')
|
|||
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('\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(
|
||||
'\n\nA final thing. Do you have a "scissors" or other image ' +
|
||||
'used as a separator in your article? If so, enter the path ' +
|
||||
|
|
|
@ -95,6 +95,13 @@ let config = convict({
|
|||
env: 'SEPARATOR_IMAGE',
|
||||
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
|
||||
|
|
42
server.js
42
server.js
|
@ -5,6 +5,7 @@ const slugify = require('underscore.string/slugify')
|
|||
const fs = require('fs')
|
||||
|
||||
const { Seance } = require ('./seance')
|
||||
const config = require('./config')
|
||||
|
||||
const app = express()
|
||||
var expressWs = require('express-ws')(app)
|
||||
|
@ -18,8 +19,12 @@ app.use(bodyParser.urlencoded({
|
|||
app.use(bodyParser('json'))
|
||||
|
||||
// Enable static files
|
||||
app.use(express.static('public'))
|
||||
app.use(express.static('static'))
|
||||
app.use('/', express.static('public')) // basePath is prefixed later
|
||||
app.use(config.basePath || '/', express.static('static'))
|
||||
|
||||
// Router
|
||||
var router = express.Router()
|
||||
app.use(config.basePath || '/', router)
|
||||
|
||||
// Set up VueXpress
|
||||
let options = {
|
||||
|
@ -29,12 +34,15 @@ let options = {
|
|||
metaInfo: {
|
||||
title: 'Seance',
|
||||
script: [
|
||||
{ type: 'text/javascript', src: '/app.js' },
|
||||
{
|
||||
type: 'text/javascript',
|
||||
src: (config.basePath || '') + '/app.js'
|
||||
},
|
||||
],
|
||||
},
|
||||
extractCSS: true,
|
||||
cssOutputPath: '/css/styles.css',
|
||||
publicPath: 'public',
|
||||
cssOutputPath: (config.basePath || '') + '/css/styles.css',
|
||||
publicPath: 'public' + (config.basePath || ''),
|
||||
compilerConfig: {
|
||||
// custom webpack config
|
||||
},
|
||||
|
@ -46,19 +54,22 @@ let options = {
|
|||
}
|
||||
|
||||
const renderer = vueRenderer(options)
|
||||
app.use(renderer)
|
||||
router.use(renderer)
|
||||
|
||||
// Views
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
router.get('/', (req, res) => {
|
||||
res.render('index', {
|
||||
baseUrl: req.hostname.startsWith('localhost')
|
||||
? req.protocol + '://' + req.headers.host
|
||||
: '//' + req.hostname, // auto choose http or https
|
||||
seanceUrl: req.hostname.startsWith('localhost')
|
||||
? req.headers.host + req.baseUrl
|
||||
: req.hostname + req.baseUrl,
|
||||
protocol: req.hostname.startsWith('localhost')
|
||||
? req.protocol + '://'
|
||||
: '//',
|
||||
})
|
||||
})
|
||||
|
||||
app.post('/fetch', (req, res) => {
|
||||
router.post('/fetch', (req, res) => {
|
||||
var json
|
||||
var post
|
||||
|
||||
|
@ -110,6 +121,9 @@ app.post('/fetch', (req, res) => {
|
|||
|
||||
// render the final post
|
||||
res.render('fetch-medium', {
|
||||
seanceUrl: req.hostname.startsWith('localhost')
|
||||
? req.headers.host + req.baseUrl
|
||||
: req.hostname + req.baseUrl,
|
||||
post: {
|
||||
title: post.title,
|
||||
subtitle: post.content.subtitle,
|
||||
|
@ -125,14 +139,14 @@ app.get('/fetch', (req, res) => {
|
|||
res.redirect(303, '/')
|
||||
})
|
||||
|
||||
app.get('/api', (req, res) => {
|
||||
router.get('/api', (req, res) => {
|
||||
res.json({
|
||||
status: 'success',
|
||||
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) => {
|
||||
|
||||
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) => {
|
||||
|
||||
// respond to keepalive
|
||||
|
|
|
@ -11,12 +11,13 @@ function addNotification(msg, className='is-warning') {
|
|||
function mediumFetch(e) {
|
||||
if(e) { e.preventDefault() }
|
||||
|
||||
const baseUrl = document.querySelector('[data-seance-url]').dataset.seanceUrl
|
||||
const postSlug = document.querySelector('[data-post-slug]').dataset.postSlug
|
||||
const mediumUrl = document.querySelector('[data-post-mediumurl]').dataset.postMediumurl
|
||||
var password = document.querySelector('input[name=password]').value
|
||||
console.log('Fetching ' + postSlug)
|
||||
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)
|
||||
|
||||
// set up the div
|
||||
|
@ -96,11 +97,12 @@ function mediumFetch(e) {
|
|||
function ghostPush(e) {
|
||||
if(e) { e.preventDefault() }
|
||||
|
||||
const baseUrl = document.querySelector('[data-seance-url]').dataset.seanceUrl
|
||||
let postSlug = document.querySelector('[data-post-slug]').dataset.postSlug
|
||||
var password = document.querySelector('input[name=password]').value
|
||||
console.log('Pushing ' + postSlug)
|
||||
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)
|
||||
|
||||
// set up the div
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div id="app">
|
||||
<nav class="navbar has-background-light" role="navigation" aria-label="main navigation">
|
||||
<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">
|
||||
<span aria-hidden="true"></span>
|
||||
|
@ -44,7 +44,7 @@
|
|||
</div>
|
||||
<footer class="card-footer" id="statusbar">
|
||||
<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>
|
||||
</footer>
|
||||
</div>
|
||||
|
@ -62,6 +62,7 @@
|
|||
title: 'Seance',
|
||||
user: null,
|
||||
host: 'http://localhost:4000',
|
||||
seanceUrl: 'http://localhost:4000',
|
||||
post: {
|
||||
title: '(untitled)',
|
||||
subtitle: '',
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div id="app">
|
||||
<nav class="navbar has-background-light" role="navigation" aria-label="main navigation">
|
||||
<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">
|
||||
<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%;">
|
||||
<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..."/>
|
||||
<p>or, use the bookmarklet <a class="button is-primary is-small" :href="bookmarklet">Add to Seance</a></p>
|
||||
</form>
|
||||
|
@ -43,12 +43,13 @@
|
|||
return {
|
||||
title: 'Seance',
|
||||
user: null,
|
||||
baseUrl: 'http://localhost:4000',
|
||||
seanceUrl: 'localhost:4000',
|
||||
protocol: '//',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
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