Compare commits

...

5 commits

Author SHA1 Message Date
Hippo adcf2b4e46 Respect baseUrl while pushing to Ghost 2021-07-26 23:20:36 +05:30
Hippo c49836bec7 Force-render CSS on basePath
This is a very hacky way of doing it: a base path of /seance will
lead to rendering in,

  public/seance/seance/css/styles.css

but it seems to be the only way out for now 🙁

Hopefully in future we'll come up with a more elegant solution.
2021-07-26 23:09:40 +05:30
Hippo badcebeb35 Make CSS output URL respect baseUrl setting
A typo was preventing this from happening: it was always trying
to load CSS from the root domain instead of one relative to the
base path.
2021-07-26 22:59:04 +05:30
Hippo be12c945fa Account for null values in config.basePath
Instead of reading them as a literal string 'null' it now blanks
them out. Otherwise we were getting errors like "publicnull does
not exist"! 😬
2021-07-26 22:43:25 +05:30
Hippo ae1aacd17c Allow Seance to be hosted on a subpath
Now, instead of having to host Seance right at example.com, you
can also host it underneath at example.com/seance by changing the
appropriate setting!
2021-07-25 21:46:03 +05:30
7 changed files with 80 additions and 23 deletions

View file

@ -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
View file

@ -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 ' +

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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: '',

View file

@ -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)})()`
} }
} }
} }