ref: c825a7312131b4afa67ee90d593640dee3525d98
parent: 7198ea8a1e575644886c4fe027a41b43cfc1ea5b
author: Bjørn Erik Pedersen <[email protected]>
date: Mon Jun 26 17:34:16 EDT 2017
Support open "current content page" in browser This commit adds a new `--navigateToChanged` and config setting with the same name, that, when running the Hugo server with live reload enabled, will navigate to the current content file's URL on save. This is really useful for site-wide content changes (copyedits etc.). Fixes #3643
--- a/commands/hugo.go
+++ b/commands/hugo.go
@@ -981,8 +981,28 @@
}
if !buildWatch && !c.Cfg.GetBool("disableLiveReload") {
- // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized
- livereload.ForceRefresh()
+
+ navigate := c.Cfg.GetBool("navigateToChanged")
+
+ var p *hugolib.Page
+
+ if navigate {
+
+ // It is probably more confusing than useful
+ // to navigate to a new URL on RENAME etc.
+ // so for now we use the WRITE event only.
+ name := pickOneWritePath(dynamicEvents)
+
+ if name != "" {
+ p = Hugo.GetContentPage(name)
+ }
+ }
+
+ if p != nil {
+ livereload.NavigateToPath(p.RelPermalink())
+ } else {
+ livereload.ForceRefresh()
+ }
}
}
case err := <-watcher.Errors:
@@ -1005,6 +1025,16 @@
wg.Wait()
return nil
+}
+
+func pickOneWritePath(events []fsnotify.Event) string {
+ for _, ev := range events {
+ if ev.Op&fsnotify.Write == fsnotify.Write {
+ return ev.Name
+ }
+ }
+
+ return ""
}
func (c *commandeer) isStatic(path string) bool {
--- a/commands/server.go
+++ b/commands/server.go
@@ -33,6 +33,7 @@
var (
disableLiveReload bool
+ navigateToChanged bool
renderToDisk bool
serverAppend bool
serverInterface string
@@ -87,6 +88,7 @@
serverCmd.Flags().BoolVarP(&serverWatch, "watch", "w", true, "watch filesystem for changes and recreate as needed")
serverCmd.Flags().BoolVarP(&serverAppend, "appendPort", "", true, "append port to baseURL")
serverCmd.Flags().BoolVar(&disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild")
+ serverCmd.Flags().BoolVar(&navigateToChanged, "navigateToChanged", false, "navigate to changed content file on live browser reload")
serverCmd.Flags().BoolVar(&renderToDisk, "renderToDisk", false, "render to Destination path (default is render to memory & serve from there)")
serverCmd.Flags().String("memstats", "", "log memory usage to this file")
serverCmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".")
@@ -108,6 +110,10 @@
if cmd.Flags().Changed("disableLiveReload") {
c.Set("disableLiveReload", disableLiveReload)
+ }
+
+ if cmd.Flags().Changed("navigateToChanged") {
+ c.Set("navigateToChanged", navigateToChanged)
}
if serverWatch {
--- a/hugolib/hugo_sites.go
+++ b/hugolib/hugo_sites.go
@@ -19,6 +19,8 @@
"strings"
"sync"
+ "path/filepath"
+
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/helpers"
@@ -36,6 +38,27 @@
multilingual *Multilingual
*deps.Deps
+}
+
+// GetContentPage finds a Page with content given the absolute filename.
+// Returns nil if none found.
+func (h *HugoSites) GetContentPage(filename string) *Page {
+ s := h.Sites[0]
+ contendDir := filepath.Join(s.PathSpec.AbsPathify(s.Cfg.GetString("contentDir")))
+ if !strings.HasPrefix(filename, contendDir) {
+ return nil
+ }
+
+ rel := strings.TrimPrefix(filename, contendDir)
+ rel = strings.TrimPrefix(rel, helpers.FilePathSeparator)
+
+ pos := s.rawAllPages.findPagePosByFilePath(rel)
+
+ if pos == -1 {
+ return nil
+ }
+ return s.rawAllPages[pos]
+
}
// NewHugoSites creates a new collection of sites given the input sites, building
--- a/hugolib/hugo_sites_build_test.go
+++ b/hugolib/hugo_sites_build_test.go
@@ -223,6 +223,12 @@
require.NotNil(t, s.disabledKinds)
}
+ gp1 := sites.GetContentPage(filepath.FromSlash("content/sect/doc1.en.md"))
+ require.NotNil(t, gp1)
+ require.Equal(t, "doc1", gp1.Title)
+ gp2 := sites.GetContentPage(filepath.FromSlash("content/sect/notfound.md"))
+ require.Nil(t, gp2)
+
enSite := sites.Sites[0]
enSiteHome := enSite.getPage(KindHome)
require.True(t, enSiteHome.IsTranslated())
--- a/livereload/livereload.go
+++ b/livereload/livereload.go
@@ -37,6 +37,7 @@
package livereload
import (
+ "fmt"
"net/http"
"path/filepath"
@@ -43,6 +44,9 @@
"github.com/gorilla/websocket"
)
+// Prefix to signal to LiveReload that we need to navigate to another path.
+const hugoNavigatePrefix = "__hugo_navigate"
+
var upgrader = &websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024}
// Handler is a HandlerFunc handling the livereload
@@ -69,6 +73,12 @@
RefreshPath("/x.js")
}
+// NavigateToPath tells livereload to navigate to the given path.
+// This translates to `window.location.href = path` in the client.
+func NavigateToPath(path string) {
+ RefreshPath(hugoNavigatePrefix + path)
+}
+
// RefreshPath tells livereload to refresh only the given path.
// If that path points to a CSS stylesheet or an image, only the changes
// will be updated in the browser, not the entire page.
@@ -81,9 +91,42 @@
// ServeJS serves the liverreload.js who's reference is injected into the page.
func ServeJS(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/javascript")
- w.Write(livereloadJS)
+ w.Write(liveReloadJS())
}
-// livereloadJS contains the output of `uglifyjs livereload.js -m toplevel`
-// Source: https://github.com/livereload/livereload-js/archive/v2.2.1.tar.gz
-var livereloadJS = []byte(`(function e(t,n,o){function i(s,l){if(!n[s]){if(!t[s]){var c=typeof require=="function"&&require;if(!l&&c)return c(s,!0);if(r)return r(s,!0);var a=new Error("Cannot find module '"+s+"'");throw a.code="MODULE_NOT_FOUND",a}var u=n[s]={exports:{}};t[s][0].call(u.exports,function(e){var n=t[s][1][e];return i(n?n:e)},u,u.exports,e,t,n,o)}return n[s].exports}var r=typeof require=="function"&&require;for(var s=0;s<o.length;s++)i(o[s]);return i})({1:[function(e,t,n){(function(){var t,o,i,r,s,l;l=e("./protocol"),r=l.Parser,o=l.PROTOCOL_6,i=l.PROTOCOL_7;s="2.2.1";n.Connector=t=function(){function e(e,t,n,o){this.options=e;this.WebSocket=t;this.Timer=n;this.handlers=o;this._uri="ws://"+this.options.host+":"+this.options.port+"/livereload";this._nextDelay=this.options.mindelay;this._connectionDesired=false;this.protocol=0;this.protocolParser=new r({connected:function(e){return function(t){e.protocol=t;e._handshakeTimeout.stop();e._nextDelay=e.options.mindelay;e._disconnectionReason="broken";return e.handlers.connected(t)}}(this),error:function(e){return function(t){e.handlers.error(t);return e._closeOnError()}}(this),message:function(e){return function(t){return e.handlers.message(t)}}(this)});this._handshakeTimeout=new n(function(e){return function(){if(!e._isSocketConnected()){return}e._disconnectionReason="handshake-timeout";return e.socket.close()}}(this));this._reconnectTimer=new n(function(e){return function(){if(!e._connectionDesired){return}return e.connect()}}(this));this.connect()}e.prototype._isSocketConnected=function(){return this.socket&&this.socket.readyState===this.WebSocket.OPEN};e.prototype.connect=function(){this._connectionDesired=true;if(this._isSocketConnected()){return}this._reconnectTimer.stop();this._disconnectionReason="cannot-connect";this.protocolParser.reset();this.handlers.connecting();this.socket=new this.WebSocket(this._uri);this.socket.onopen=function(e){return function(t){return e._onopen(t)}}(this);this.socket.onclose=function(e){return function(t){return e._onclose(t)}}(this);this.socket.onmessage=function(e){return function(t){return e._onmessage(t)}}(this);return this.socket.onerror=function(e){return function(t){return e._onerror(t)}}(this)};e.prototype.disconnect=function(){this._connectionDesired=false;this._reconnectTimer.stop();if(!this._isSocketConnected()){return}this._disconnectionReason="manual";return this.socket.close()};e.prototype._scheduleReconnection=function(){if(!this._connectionDesired){return}if(!this._reconnectTimer.running){this._reconnectTimer.start(this._nextDelay);return this._nextDelay=Math.min(this.options.maxdelay,this._nextDelay*2)}};e.prototype.sendCommand=function(e){if(this.protocol==null){return}return this._sendCommand(e)};e.prototype._sendCommand=function(e){return this.socket.send(JSON.stringify(e))};e.prototype._closeOnError=function(){this._handshakeTimeout.stop();this._disconnectionReason="error";return this.socket.close()};e.prototype._onopen=function(e){var t;this.handlers.socketConnected();this._disconnectionReason="handshake-failed";t={command:"hello",protocols:[o,i]};t.ver=s;if(this.options.ext){t.ext=this.options.ext}if(this.options.extver){t.extver=this.options.extver}if(this.options.snipver){t.snipver=this.options.snipver}this._sendCommand(t);return this._handshakeTimeout.start(this.options.handshake_timeout)};e.prototype._onclose=function(e){this.protocol=0;this.handlers.disconnected(this._disconnectionReason,this._nextDelay);return this._scheduleReconnection()};e.prototype._onerror=function(e){};e.prototype._onmessage=function(e){return this.protocolParser.process(e.data)};return e}()}).call(this)},{"./protocol":6}],2:[function(e,t,n){(function(){var e;e={bind:function(e,t,n){if(e.addEventListener){return e.addEventListener(t,n,false)}else if(e.attachEvent){e[t]=1;return e.attachEvent("onpropertychange",function(e){if(e.propertyName===t){return n()}})}else{throw new Error("Attempt to attach custom event "+t+" to something which isn't a DOMElement")}},fire:function(e,t){var n;if(e.addEventListener){n=document.createEvent(
\ No newline at end of file
+func liveReloadJS() []byte {
+ return []byte(livereloadJS + hugoLiveReloadPlugin)
+}
+
+var (
+ // This is temporary patched with this PR (enables sensible error messages):
+ // https://github.com/livereload/livereload-js/pull/64
+ // TODO(bep) replace with distribution once merged.
+ livereloadJS = `(function e(t,n,o){function i(s,l){if(!n[s]){if(!t[s]){var c=typeof require=="function"&&require;if(!l&&c)return c(s,!0);if(r)return r(s,!0);var a=new Error("Cannot find module '"+s+"'");throw a.code="MODULE_NOT_FOUND",a}var h=n[s]={exports:{}};t[s][0].call(h.exports,function(e){var n=t[s][1][e];return i(n?n:e)},h,h.exports,e,t,n,o)}return n[s].exports}var r=typeof require=="function"&&require;for(var s=0;s<o.length;s++)i(o[s]);return i})({1:[function(e,t,n){(function(){var t,o,i,r,s,l;l=e("./protocol"),r=l.Parser,o=l.PROTOCOL_6,i=l.PROTOCOL_7;s="2.2.2";n.Connector=t=function(){function e(e,t,n,o){this.options=e;this.WebSocket=t;this.Timer=n;this.handlers=o;this._uri="ws"+(this.options.https?"s":"")+"://"+this.options.host+":"+this.options.port+"/livereload";this._nextDelay=this.options.mindelay;this._connectionDesired=false;this.protocol=0;this.protocolParser=new r({connected:function(e){return function(t){e.protocol=t;e._handshakeTimeout.stop();e._nextDelay=e.options.mindelay;e._disconnectionReason="broken";return e.handlers.connected(t)}}(this),error:function(e){return function(t){e.handlers.error(t);return e._closeOnError()}}(this),message:function(e){return function(t){return e.handlers.message(t)}}(this)});this._handshakeTimeout=new n(function(e){return function(){if(!e._isSocketConnected()){return}e._disconnectionReason="handshake-timeout";return e.socket.close()}}(this));this._reconnectTimer=new n(function(e){return function(){if(!e._connectionDesired){return}return e.connect()}}(this));this.connect()}e.prototype._isSocketConnected=function(){return this.socket&&this.socket.readyState===this.WebSocket.OPEN};e.prototype.connect=function(){this._connectionDesired=true;if(this._isSocketConnected()){return}this._reconnectTimer.stop();this._disconnectionReason="cannot-connect";this.protocolParser.reset();this.handlers.connecting();this.socket=new this.WebSocket(this._uri);this.socket.onopen=function(e){return function(t){return e._onopen(t)}}(this);this.socket.onclose=function(e){return function(t){return e._onclose(t)}}(this);this.socket.onmessage=function(e){return function(t){return e._onmessage(t)}}(this);return this.socket.onerror=function(e){return function(t){return e._onerror(t)}}(this)};e.prototype.disconnect=function(){this._connectionDesired=false;this._reconnectTimer.stop();if(!this._isSocketConnected()){return}this._disconnectionReason="manual";return this.socket.close()};e.prototype._scheduleReconnection=function(){if(!this._connectionDesired){return}if(!this._reconnectTimer.running){this._reconnectTimer.start(this._nextDelay);return this._nextDelay=Math.min(this.options.maxdelay,this._nextDelay*2)}};e.prototype.sendCommand=function(e){if(this.protocol==null){return}return this._sendCommand(e)};e.prototype._sendCommand=function(e){return this.socket.send(JSON.stringify(e))};e.prototype._closeOnError=function(){this._handshakeTimeout.stop();this._disconnectionReason="error";return this.socket.close()};e.prototype._onopen=function(e){var t;this.handlers.socketConnected();this._disconnectionReason="handshake-failed";t={command:"hello",protocols:[o,i]};t.ver=s;if(this.options.ext){t.ext=this.options.ext}if(this.options.extver){t.extver=this.options.extver}if(this.options.snipver){t.snipver=this.options.snipver}this._sendCommand(t);return this._handshakeTimeout.start(this.options.handshake_timeout)};e.prototype._onclose=function(e){this.protocol=0;this.handlers.disconnected(this._disconnectionReason,this._nextDelay);return this._scheduleReconnection()};e.prototype._onerror=function(e){};e.prototype._onmessage=function(e){return this.protocolParser.process(e.data)};return e}()}).call(this)},{"./protocol":6}],2:[function(e,t,n){(function(){var e;e={bind:function(e,t,n){if(e.addEventListener){return e.addEventListener(t,n,false)}else if(e.attachEvent){e[t]=1;return e.attachEvent("onpropertychange",function(e){if(e.propertyName===t){return n()}})}else{throw new Error("Attempt to attach custom event "+t+" to something which isn't a DOMElement")}},fire:function(e,t){var n;if(e.addEventList
\ No newline at end of file
+ hugoLiveReloadPlugin = fmt.Sprintf(`
+/*
+Hugo adds a specific prefix, "__hugo_navigate", to the path in certain situations to signal
+navigation to another content page.
+*/
+
+function HugoReload() {}
+
+HugoReload.identifier = 'hugoReloader';
+HugoReload.version = '0.9';
+
+HugoReload.prototype.reload = function(path, options) {
+ var prefix = %q;
+
+ if (path.lastIndexOf(prefix, 0) !== 0) {
+ return false
+ }
+
+ path = path.substring(prefix.length);
+ window.location.href = path;
+
+ return true;
+};
+
+LiveReload.addPlugin(HugoReload)
+`, hugoNavigatePrefix)
+)