mirror of https://github.com/jlelse/GoBlog
Add map feature
This commit is contained in:
parent
9369305c7d
commit
09804d7640
|
@ -70,6 +70,7 @@ type configBlog struct {
|
|||
PostAsHome bool `mapstructure:"postAsHome"`
|
||||
RandomPost *randomPost `mapstructure:"randomPost"`
|
||||
Comments *comments `mapstructure:"comments"`
|
||||
Map *configMap `mapstructure:"map"`
|
||||
}
|
||||
|
||||
type section struct {
|
||||
|
@ -145,6 +146,11 @@ type comments struct {
|
|||
Enabled bool `mapstructure:"enabled"`
|
||||
}
|
||||
|
||||
type configMap struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Path string `mapstructure:"path"`
|
||||
}
|
||||
|
||||
type configUser struct {
|
||||
Nick string `mapstructure:"nick"`
|
||||
Name string `mapstructure:"name"`
|
||||
|
|
2
geo.go
2
geo.go
|
@ -12,6 +12,8 @@ import (
|
|||
"github.com/thoas/go-funk"
|
||||
)
|
||||
|
||||
const geoParam = "location"
|
||||
|
||||
func (a *goBlog) geoTitle(g *gogeouri.Geo, lang string) string {
|
||||
if name, ok := g.Parameters["name"]; ok && len(name) > 0 && name[0] != "" {
|
||||
return name[0]
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) {
|
||||
blog := r.Context().Value(blogContextKey).(string)
|
||||
|
||||
allPostsWithLocation, err := a.db.getPosts(&postsRequestConfig{
|
||||
blog: blog,
|
||||
status: statusPublished,
|
||||
parameter: geoParam,
|
||||
withOnlyParameters: []string{geoParam},
|
||||
})
|
||||
if err != nil {
|
||||
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(allPostsWithLocation) == 0 {
|
||||
a.render(w, r, templateGeoMap, &renderData{
|
||||
BlogString: blog,
|
||||
Data: map[string]interface{}{
|
||||
"nolocations": true,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
type templateLocation struct {
|
||||
Lat float64
|
||||
Lon float64
|
||||
Post string
|
||||
}
|
||||
|
||||
var locations []*templateLocation
|
||||
for _, p := range allPostsWithLocation {
|
||||
if g := p.GeoURI(); g != nil {
|
||||
locations = append(locations, &templateLocation{
|
||||
Lat: g.Latitude,
|
||||
Lon: g.Longitude,
|
||||
Post: p.Path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
jb, err := json.Marshal(locations)
|
||||
if err != nil {
|
||||
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Manipulate CSP header
|
||||
w.Header().Set(cspHeader, w.Header().Get(cspHeader)+" https://unpkg.com/ https://tile.openstreetmap.org")
|
||||
|
||||
a.render(w, r, templateGeoMap, &renderData{
|
||||
BlogString: blog,
|
||||
Data: map[string]interface{}{
|
||||
"locations": string(jb),
|
||||
},
|
||||
})
|
||||
}
|
14
http.go
14
http.go
|
@ -538,6 +538,16 @@ func (a *goBlog) buildDynamicRouter() (*chi.Mux, error) {
|
|||
r.Get(brPath+".opml", a.serveBlogrollExport)
|
||||
})
|
||||
}
|
||||
|
||||
// Geo map
|
||||
if mc := blogConfig.Map; mc != nil && mc.Enabled {
|
||||
mapPath := mc.Path
|
||||
if mc.Path == "" {
|
||||
mapPath = "/map"
|
||||
}
|
||||
r.With(a.privateModeHandler...).With(a.cache.cacheMiddleware, sbm).Get(blogConfig.getRelativePath(mapPath), a.serveGeoMap)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Sitemap
|
||||
|
@ -573,6 +583,8 @@ func (a *goBlog) refreshCSPDomains() {
|
|||
}
|
||||
}
|
||||
|
||||
const cspHeader = "Content-Security-Policy"
|
||||
|
||||
func (a *goBlog) securityHeaders(next http.Handler) http.Handler {
|
||||
a.refreshCSPDomains()
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -581,7 +593,7 @@ func (a *goBlog) securityHeaders(next http.Handler) http.Handler {
|
|||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
|
||||
w.Header().Set("X-Xss-Protection", "1; mode=block")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'self'"+a.cspDomains)
|
||||
w.Header().Set(cspHeader, "default-src 'self'"+a.cspDomains)
|
||||
if a.cfg.Server.Tor && a.torAddress != "" {
|
||||
w.Header().Set("Onion-Location", fmt.Sprintf("http://%v%v", a.torAddress, r.RequestURI))
|
||||
}
|
||||
|
|
|
@ -231,6 +231,10 @@ footer {
|
|||
transition: transform 2s ease;
|
||||
}
|
||||
|
||||
#map {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
/* Print */
|
||||
@media print {
|
||||
html {
|
||||
|
|
24
postsDb.go
24
postsDb.go
|
@ -227,6 +227,7 @@ type postsRequestConfig struct {
|
|||
publishedYear, publishedMonth, publishedDay int
|
||||
randomOrder bool
|
||||
withoutParameters bool
|
||||
withOnlyParameters []string
|
||||
}
|
||||
|
||||
func buildPostsQuery(c *postsRequestConfig, selection string) (query string, args []interface{}) {
|
||||
|
@ -303,11 +304,28 @@ func buildPostsQuery(c *postsRequestConfig, selection string) (query string, arg
|
|||
return query, args
|
||||
}
|
||||
|
||||
func (d *database) getPostParameters(path string) (params map[string][]string, err error) {
|
||||
rows, err := d.query("select parameter, value from post_parameters where path = @path order by id", sql.Named("path", path))
|
||||
func (d *database) getPostParameters(path string, parameters ...string) (params map[string][]string, err error) {
|
||||
var sqlArgs []interface{}
|
||||
// Parameter filter
|
||||
paramFilter := ""
|
||||
if len(parameters) > 0 {
|
||||
paramFilter = " and parameter in ("
|
||||
for i, p := range parameters {
|
||||
if i > 0 {
|
||||
paramFilter += ", "
|
||||
}
|
||||
named := fmt.Sprintf("param%v", i)
|
||||
paramFilter += "@" + named
|
||||
sqlArgs = append(sqlArgs, sql.Named(named, p))
|
||||
}
|
||||
paramFilter += ")"
|
||||
}
|
||||
// Query
|
||||
rows, err := d.query("select parameter, value from post_parameters where path = @path"+paramFilter+" order by id", append(sqlArgs, sql.Named("path", path))...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Result
|
||||
var name, value string
|
||||
params = map[string][]string{}
|
||||
for rows.Next() {
|
||||
|
@ -343,7 +361,7 @@ func (d *database) getPosts(config *postsRequestConfig) (posts []*post, err erro
|
|||
Status: postStatus(status),
|
||||
}
|
||||
if !config.withoutParameters {
|
||||
if p.Parameters, err = d.getPostParameters(path); err != nil {
|
||||
if p.Parameters, err = d.getPostParameters(path, config.withOnlyParameters...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ const (
|
|||
templateNotificationsAdmin = "notificationsadmin"
|
||||
templateWebmentionAdmin = "webmentionadmin"
|
||||
templateBlogroll = "blogroll"
|
||||
templateGeoMap = "geomap"
|
||||
)
|
||||
|
||||
func (a *goBlog) initRendering() error {
|
||||
|
|
|
@ -195,6 +195,10 @@ footer * {
|
|||
transition: transform 2s ease;
|
||||
}
|
||||
|
||||
#map {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
/* Print */
|
||||
@media print {
|
||||
html {
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
(function () {
|
||||
let mapEl = document.getElementById('map')
|
||||
let locations = JSON.parse(mapEl.dataset.locations)
|
||||
|
||||
let map = L.map('map')
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map)
|
||||
|
||||
let markers = []
|
||||
locations.forEach(loc => {
|
||||
let marker = [loc.Lat, loc.Lon]
|
||||
L.marker(marker).addTo(map).on('click', function () {
|
||||
window.open(loc.Post, '_blank').focus()
|
||||
})
|
||||
markers.push(marker)
|
||||
})
|
||||
|
||||
map.fitBounds(markers)
|
||||
map.zoomOut(2, { animate: false })
|
||||
})()
|
|
@ -0,0 +1,31 @@
|
|||
{{ define "title" }}
|
||||
<title>{{ .Blog.Title }}</title>
|
||||
{{ if not .Data.nolocations }}
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
|
||||
integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
|
||||
crossorigin=""
|
||||
/>
|
||||
<script
|
||||
src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
|
||||
integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
|
||||
crossorigin=""
|
||||
></script>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
<main>
|
||||
{{ if .Data.nolocations }}
|
||||
<p>{{ string .Blog.Lang "nolocations" }}</p>
|
||||
{{ else }}
|
||||
<div class="p" id="map" data-locations="{{ .Data.locations }}"></div>
|
||||
<script defer src="{{ asset "js/geomap.js" }}"></script>
|
||||
{{ end }}
|
||||
</main>
|
||||
{{ end }}
|
||||
|
||||
{{ define "geomap" }}
|
||||
{{ template "base" . }}
|
||||
{{ end }}
|
|
@ -23,6 +23,7 @@ locationnotsupported: "Die Standort-API wird von diesem Browser nicht unterstüt
|
|||
mediafiles: "Medien-Dateien"
|
||||
next: "Weiter"
|
||||
nofiles: "Keine Dateien"
|
||||
nolocations: "Keine Posts mit Standorten"
|
||||
noposts: "Hier sind keine Posts."
|
||||
oldcontent: "⚠️ Dieser Eintrag ist bereits über ein Jahr alt. Er ist möglicherweise nicht mehr aktuell. Meinungen können sich geändert haben."
|
||||
posts: "Posts"
|
||||
|
|
|
@ -33,6 +33,7 @@ mediafiles: "Media files"
|
|||
nameopt: "Name (optional)"
|
||||
next: "Next"
|
||||
nofiles: "No files"
|
||||
nolocations: "No posts with locations"
|
||||
noposts: "There are no posts here."
|
||||
notifications: "Notifications"
|
||||
oldcontent: "⚠️ This entry is already over one year old. It may no longer be up to date. Opinions may have changed."
|
||||
|
|
Loading…
Reference in New Issue