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

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

View file

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

View file

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

View file

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

View file

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

View file

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