Skip to content

Commit 2e04657

Browse files
committed
Add multilingual multihost support
This commit adds multihost support when more than one language is configured and `baseURL` is set per language. Updates #4027
1 parent 6233ddf commit 2e04657

File tree

14 files changed

+349
-79
lines changed

14 files changed

+349
-79
lines changed

‎commands/commandeer.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ func (c *commandeer) PathSpec() *helpers.PathSpec {
4141
return c.pathSpec
4242
}
4343

44+
func (c *commandeer) languages() helpers.Languages {
45+
return c.Cfg.Get("languagesSorted").(helpers.Languages)
46+
}
47+
4448
func (c *commandeer) initFs(fs *hugofs.Fs) error {
4549
c.DepsCfg.Fs = fs
4650
ps, err := helpers.NewPathSpec(fs, c.Cfg)

‎commands/hugo.go‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,7 @@ func (c *commandeer) watchConfig() {
526526

527527
func (c *commandeer) build(watches ...bool) error {
528528
if err := c.copyStatic(); err != nil {
529+
// TODO(bep) multihost
529530
return fmt.Errorf("Error copying static files to %s: %s", c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")), err)
530531
}
531532
watch := false
@@ -593,6 +594,24 @@ func (c *commandeer) getStaticSourceFs() afero.Fs {
593594

594595
func (c *commandeer) copyStatic() error {
595596
publishDir := c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")) + helpers.FilePathSeparator
597+
roots := c.roots()
598+
599+
if len(roots) == 0 {
600+
return c.copyStaticTo(publishDir)
601+
}
602+
603+
for _, root := range roots {
604+
dir := filepath.Join(publishDir, root)
605+
if err := c.copyStaticTo(dir); err != nil {
606+
return err
607+
}
608+
}
609+
610+
return nil
611+
612+
}
613+
614+
func (c *commandeer) copyStaticTo(publishDir string) error {
596615

597616
// If root, remove the second '/'
598617
if publishDir == "//" {
@@ -893,6 +912,7 @@ func (c *commandeer) newWatcher(port int) error {
893912

894913
if c.Cfg.GetBool("forceSyncStatic") {
895914
c.Logger.FEEDBACK.Printf("Syncing all static files\n")
915+
// TODO(bep) multihost
896916
err := c.copyStatic()
897917
if err != nil {
898918
utils.StopOnErr(c.Logger, err, fmt.Sprintf("Error copying static files to %s", publishDir))

‎commands/server.go‎

Lines changed: 139 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ import (
1919
"net/http"
2020
"net/url"
2121
"os"
22+
"path/filepath"
2223
"runtime"
2324
"strconv"
2425
"strings"
2526
"time"
2627

2728
"github.com/gohugoio/hugo/config"
29+
2830
"github.com/gohugoio/hugo/helpers"
2931
"github.com/spf13/afero"
3032
"github.com/spf13/cobra"
@@ -137,34 +139,58 @@ func server(cmd *cobra.Command, args []string) error {
137139
c.watchConfig()
138140
}
139141

140-
l, err := net.Listen("tcp", net.JoinHostPort(serverInterface, strconv.Itoa(serverPort)))
141-
if err == nil {
142-
l.Close()
143-
} else {
144-
if serverCmd.Flags().Changed("port") {
145-
// port set explicitly by user -- he/she probably meant it!
146-
return newSystemErrorF("Server startup failed: %s", err)
147-
}
148-
jww.ERROR.Println("port", serverPort, "already in use, attempting to use an available port")
149-
sp, err := helpers.FindAvailablePort()
150-
if err != nil {
151-
return newSystemError("Unable to find alternative port to use:", err)
142+
languages := c.languages()
143+
serverPorts := make([]int, 1)
144+
145+
if languages.IsMultihost() {
146+
serverPorts = make([]int, len(languages))
147+
}
148+
149+
currentServerPort := serverPort
150+
151+
for i := 0; i < len(serverPorts); i++ {
152+
l, err := net.Listen("tcp", net.JoinHostPort(serverInterface, strconv.Itoa(currentServerPort)))
153+
if err == nil {
154+
l.Close()
155+
serverPorts[i] = currentServerPort
156+
} else {
157+
if i == 0 && serverCmd.Flags().Changed("port") {
158+
// port set explicitly by user -- he/she probably meant it!
159+
return newSystemErrorF("Server startup failed: %s", err)
160+
}
161+
jww.ERROR.Println("port", serverPort, "already in use, attempting to use an available port")
162+
sp, err := helpers.FindAvailablePort()
163+
if err != nil {
164+
return newSystemError("Unable to find alternative port to use:", err)
165+
}
166+
serverPorts[i] = sp.Port
152167
}
153-
serverPort = sp.Port
168+
169+
currentServerPort = serverPorts[i] + 1
154170
}
155171

156172
c.Set("port", serverPort)
157173
if liveReloadPort != -1 {
158174
c.Set("liveReloadPort", liveReloadPort)
159175
} else {
160-
c.Set("liveReloadPort", serverPort)
176+
c.Set("liveReloadPort", serverPorts[0])
161177
}
162178

163-
baseURL, err = fixURL(c.Cfg, baseURL)
164-
if err != nil {
165-
return err
179+
if languages.IsMultihost() {
180+
for i, language := range languages {
181+
baseURL, err = fixURL(language, baseURL, serverPorts[i])
182+
if err != nil {
183+
return err
184+
}
185+
language.Set("baseURL", baseURL)
186+
}
187+
} else {
188+
baseURL, err = fixURL(c.Cfg, baseURL, serverPorts[0])
189+
if err != nil {
190+
return err
191+
}
192+
c.Cfg.Set("baseURL", baseURL)
166193
}
167-
c.Set("baseURL", baseURL)
168194

169195
if err := memStats(); err != nil {
170196
jww.ERROR.Println("memstats error:", err)
@@ -208,28 +234,52 @@ func server(cmd *cobra.Command, args []string) error {
208234
}
209235
}
210236

211-
c.serve(serverPort)
212-
213237
return nil
214238
}
215239

216-
func (c *commandeer) serve(port int) {
240+
type fileServer struct {
241+
basePort int
242+
baseURLs []string
243+
roots []string
244+
c *commandeer
245+
}
246+
247+
func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, error) {
248+
baseURL := f.baseURLs[i]
249+
root := f.roots[i]
250+
port := f.basePort + i
251+
252+
publishDir := f.c.Cfg.GetString("publishDir")
253+
254+
if root != "" {
255+
publishDir = filepath.Join(publishDir, root)
256+
}
257+
258+
absPublishDir := f.c.PathSpec().AbsPathify(publishDir)
259+
260+
// TODO(bep) multihost unify feedback
217261
if renderToDisk {
218-
jww.FEEDBACK.Println("Serving pages from " + c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")))
262+
jww.FEEDBACK.Println("Serving pages from " + absPublishDir)
219263
} else {
220264
jww.FEEDBACK.Println("Serving pages from memory")
221265
}
222266

223-
httpFs := afero.NewHttpFs(c.Fs.Destination)
224-
fs := filesOnlyFs{httpFs.Dir(c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")))}
267+
httpFs := afero.NewHttpFs(f.c.Fs.Destination)
268+
fs := filesOnlyFs{httpFs.Dir(absPublishDir)}
225269

226-
doLiveReload := !buildWatch && !c.Cfg.GetBool("disableLiveReload")
227-
fastRenderMode := doLiveReload && !c.Cfg.GetBool("disableFastRender")
270+
doLiveReload := !buildWatch && !f.c.Cfg.GetBool("disableLiveReload")
271+
fastRenderMode := doLiveReload && !f.c.Cfg.GetBool("disableFastRender")
228272

229273
if fastRenderMode {
230274
jww.FEEDBACK.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender")
231275
}
232276

277+
// We're only interested in the path
278+
u, err := url.Parse(baseURL)
279+
if err != nil {
280+
return nil, "", fmt.Errorf("Invalid baseURL: %s", err)
281+
}
282+
233283
decorate := func(h http.Handler) http.Handler {
234284
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
235285
if noHTTPCache {
@@ -240,40 +290,86 @@ func (c *commandeer) serve(port int) {
240290
if fastRenderMode {
241291
p := r.RequestURI
242292
if strings.HasSuffix(p, "/") || strings.HasSuffix(p, "html") || strings.HasSuffix(p, "htm") {
243-
c.visitedURLs.Add(p)
293+
f.c.visitedURLs.Add(p)
244294
}
245295
}
246296
h.ServeHTTP(w, r)
247297
})
248298
}
249299

250300
fileserver := decorate(http.FileServer(fs))
301+
mu := http.NewServeMux()
251302

252-
// We're only interested in the path
253-
u, err := url.Parse(c.Cfg.GetString("baseURL"))
254-
if err != nil {
255-
jww.ERROR.Fatalf("Invalid baseURL: %s", err)
256-
}
257303
if u.Path == "" || u.Path == "/" {
258-
http.Handle("/", fileserver)
304+
mu.Handle("/", fileserver)
259305
} else {
260-
http.Handle(u.Path, http.StripPrefix(u.Path, fileserver))
306+
mu.Handle(u.Path, http.StripPrefix(u.Path, fileserver))
261307
}
262308

263-
jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", u.String(), serverInterface)
264-
jww.FEEDBACK.Println("Press Ctrl+C to stop")
265-
266309
endpoint := net.JoinHostPort(serverInterface, strconv.Itoa(port))
267-
err = http.ListenAndServe(endpoint, nil)
268-
if err != nil {
269-
jww.ERROR.Printf("Error: %s\n", err.Error())
270-
os.Exit(1)
310+
311+
return mu, endpoint, nil
312+
}
313+
314+
func (c *commandeer) roots() []string {
315+
var roots []string
316+
languages := c.languages()
317+
isMultiHost := languages.IsMultihost()
318+
if !isMultiHost {
319+
return roots
320+
}
321+
322+
for _, l := range languages {
323+
roots = append(roots, l.Lang)
324+
}
325+
return roots
326+
}
327+
328+
func (c *commandeer) serve(port int) {
329+
// TODO(bep) multihost
330+
isMultiHost := Hugo.IsMultihost()
331+
332+
var (
333+
baseURLs []string
334+
roots []string
335+
)
336+
337+
if isMultiHost {
338+
for _, s := range Hugo.Sites {
339+
baseURLs = append(baseURLs, s.BaseURL.String())
340+
roots = append(roots, s.Language.Lang)
341+
}
342+
} else {
343+
baseURLs = []string{Hugo.Sites[0].BaseURL.String()}
344+
roots = []string{""}
271345
}
346+
347+
srv := &fileServer{
348+
basePort: port,
349+
baseURLs: baseURLs,
350+
roots: roots,
351+
c: c,
352+
}
353+
354+
for i, _ := range baseURLs {
355+
mu, endpoint, err := srv.createEndpoint(i)
356+
357+
go func() {
358+
err = http.ListenAndServe(endpoint, mu)
359+
if err != nil {
360+
jww.ERROR.Printf("Error: %s\n", err.Error())
361+
os.Exit(1)
362+
}
363+
}()
364+
}
365+
366+
// TODO(bep) multihost jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", u.String(), serverInterface)
367+
jww.FEEDBACK.Println("Press Ctrl+C to stop")
272368
}
273369

274370
// fixURL massages the baseURL into a form needed for serving
275371
// all pages correctly.
276-
func fixURL(cfg config.Provider, s string) (string, error) {
372+
func fixURL(cfg config.Provider, s string, port int) (string, error) {
277373
useLocalhost := false
278374
if s == "" {
279375
s = cfg.GetString("baseURL")
@@ -315,7 +411,7 @@ func fixURL(cfg config.Provider, s string) (string, error) {
315411
return "", fmt.Errorf("Failed to split baseURL hostpost: %s", err)
316412
}
317413
}
318-
u.Host += fmt.Sprintf(":%d", serverPort)
414+
u.Host += fmt.Sprintf(":%d", port)
319415
}
320416

321417
return u.String(), nil

‎commands/server_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func TestFixURL(t *testing.T) {
4747
v.Set("baseURL", test.CfgBaseURL)
4848
serverAppend = test.AppendPort
4949
serverPort = test.Port
50-
result, err := fixURL(v, baseURL)
50+
result, err := fixURL(v, baseURL, serverPort)
5151
if err != nil {
5252
t.Errorf("Test #%d %s: unexpected error %s", i, test.TestName, err)
5353
}

‎helpers/language.go‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,17 @@ func (l *Language) Params() map[string]interface{} {
102102
return l.params
103103
}
104104

105+
// IsMultihost returns whether the languages has baseURL specificed on the
106+
// language level.
107+
func (l Languages) IsMultihost() bool {
108+
for _, lang := range l {
109+
if lang.GetLocal("baseURL") != nil {
110+
return true
111+
}
112+
}
113+
return false
114+
}
115+
105116
// SetParam sets param with the given key and value.
106117
// SetParam is case-insensitive.
107118
func (l *Language) SetParam(k string, v interface{}) {
@@ -132,6 +143,17 @@ func (l *Language) GetStringMapString(key string) map[string]string {
132143
//
133144
// Get returns an interface. For a specific value use one of the Get____ methods.
134145
func (l *Language) Get(key string) interface{} {
146+
local := l.GetLocal(key)
147+
if local != nil {
148+
return local
149+
}
150+
return l.Cfg.Get(key)
151+
}
152+
153+
// GetLocal gets a configuration value set on language level. It will
154+
// not fall back to any global value.
155+
// It will return nil if a value with the given key cannot be found.
156+
func (l *Language) GetLocal(key string) interface{} {
135157
if l == nil {
136158
panic("language not set")
137159
}
@@ -141,7 +163,7 @@ func (l *Language) Get(key string) interface{} {
141163
return v
142164
}
143165
}
144-
return l.Cfg.Get(key)
166+
return nil
145167
}
146168

147169
// Set sets the value for the key in the language's params.

‎helpers/path.go‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,6 @@ func (p *PathSpec) AbsPathify(inPath string) string {
158158
return filepath.Clean(inPath)
159159
}
160160

161-
// TODO(bep): Consider moving workingDir to argument list
162161
return filepath.Join(p.workingDir, inPath)
163162
}
164163

0 commit comments

Comments
 (0)