Merge remote-tracking branch 'upstream/master' into codehighlight
* upstream/master: Work as a standalone server, including TLS Include About/Privacy page content in page description Show instance stats on About page Change default database name to writefreely Use and validate database type before connecting Mention Contributing Guide in README Add AUTHORS.md Fix About page link in Admin dash Include version in archives made by `make release` Remove keys.sh from make release Add make release
This commit is contained in:
commit
d63db27917
|
@ -2,4 +2,5 @@
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
|
||||||
|
build
|
||||||
config.ini
|
config.ini
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
# WriteFreely Contributors
|
||||||
|
|
||||||
|
WriteFreely is built by [Matt Baer](https://github.com/thebaer), with contributions from:
|
||||||
|
|
||||||
|
* [Jean-Francois Arseneau](https://github.com/TheJF)
|
||||||
|
* [Ben Overmyer](https://github.com/BenOvermyer)
|
||||||
|
* [Marcel van der Boom](https://github.com/mrvdb)
|
||||||
|
|
29
Makefile
29
Makefile
|
@ -13,6 +13,15 @@ all : build
|
||||||
build: deps
|
build: deps
|
||||||
cd cmd/writefreely; $(GOBUILD) -v
|
cd cmd/writefreely; $(GOBUILD) -v
|
||||||
|
|
||||||
|
build-linux: deps
|
||||||
|
cd cmd/writefreely; GOOS=linux GOARCH=amd64 $(GOBUILD) -v
|
||||||
|
|
||||||
|
build-windows: deps
|
||||||
|
cd cmd/writefreely; GOOS=windows GOARCH=amd64 $(GOBUILD) -v
|
||||||
|
|
||||||
|
build-darwin: deps
|
||||||
|
cd cmd/writefreely; GOOS=darwin GOARCH=amd64 $(GOBUILD) -v
|
||||||
|
|
||||||
test:
|
test:
|
||||||
$(GOTEST) -v ./...
|
$(GOTEST) -v ./...
|
||||||
|
|
||||||
|
@ -27,10 +36,30 @@ install : build
|
||||||
cmd/writefreely/$(BINARY_NAME) --gen-keys
|
cmd/writefreely/$(BINARY_NAME) --gen-keys
|
||||||
cd less/; $(MAKE) install $(MFLAGS)
|
cd less/; $(MAKE) install $(MFLAGS)
|
||||||
|
|
||||||
|
release : clean ui
|
||||||
|
mkdir build
|
||||||
|
cp -r templates build
|
||||||
|
cp -r pages build
|
||||||
|
cp -r static build
|
||||||
|
mkdir build/keys
|
||||||
|
cp schema.sql build
|
||||||
|
$(MAKE) build-linux
|
||||||
|
cp cmd/writefreely/$(BINARY_NAME) build
|
||||||
|
cd build; tar -cvzf ../$(BINARY_NAME)_$(GITREV)_linux_amd64.tar.gz *
|
||||||
|
rm build/$(BINARY_NAME)
|
||||||
|
$(MAKE) build-darwin
|
||||||
|
cp cmd/writefreely/$(BINARY_NAME) build
|
||||||
|
cd build; tar -cvzf ../$(BINARY_NAME)_$(GITREV)_darwin_amd64.tar.gz *
|
||||||
|
rm build/$(BINARY_NAME)
|
||||||
|
$(MAKE) build-windows
|
||||||
|
cp cmd/writefreely/$(BINARY_NAME).exe build
|
||||||
|
cd build; zip -r ../$(BINARY_NAME)_$(GITREV)_windows_amd64.zip ./*
|
||||||
|
|
||||||
ui : force_look
|
ui : force_look
|
||||||
cd less/; $(MAKE) $(MFLAGS)
|
cd less/; $(MAKE) $(MFLAGS)
|
||||||
|
|
||||||
clean :
|
clean :
|
||||||
|
-rm -rf build
|
||||||
cd less/; $(MAKE) clean $(MFLAGS)
|
cd less/; $(MAKE) clean $(MFLAGS)
|
||||||
|
|
||||||
force_look :
|
force_look :
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
WriteFreely is a beautifully pared-down blogging platform that's simple on the surface, yet powerful underneath.
|
WriteFreely is a beautifully pared-down blogging platform that's simple on the surface, yet powerful underneath.
|
||||||
|
|
||||||
It's designed to be flexible and share your writing widely, so it's built around plain text and can publish to the _fediverse_ via ActivityPub. It's easy to install and lightweight.
|
It's designed to be flexible and share your writing widely, so it's built around plain text and can publish to the _fediverse_ via ActivityPub. It's easy to install and light enough to run on a Raspberry Pi.
|
||||||
|
|
||||||
**[Start a blog on our instance](https://write.as/new/blog/federated)**
|
**[Start a blog on our instance](https://write.as/new/blog/federated)**
|
||||||
|
|
||||||
|
@ -123,6 +123,12 @@ will shut down your environment without destroying your data.
|
||||||
Write Freely doesn't yet provide an official Docker pathway to production. We're
|
Write Freely doesn't yet provide an official Docker pathway to production. We're
|
||||||
working on it, though!
|
working on it, though!
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
We gladly welcome contributions to WriteFreely, whether in the form of [code](https://github.com/writeas/writefreely/blob/master/CONTRIBUTING.md#contributing-to-writefreely), [bug reports](https://github.com/writeas/writefreely/issues/new?template=bug_report.md), [feature requests](https://discuss.write.as/c/feedback/feature-requests), [translations](https://poeditor.com/join/project/TIZ6HFRFdE), or documentation improvements.
|
||||||
|
|
||||||
|
Before contributing anything, please read our [Contributing Guide](https://github.com/writeas/writefreely/blob/master/CONTRIBUTING.md#contributing-to-writefreely). It describes the correct channels for submitting contributions and any potential requirements.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Licensed under the AGPL.
|
Licensed under the AGPL.
|
||||||
|
|
48
app.go
48
app.go
|
@ -20,6 +20,7 @@ import (
|
||||||
"github.com/gorilla/schema"
|
"github.com/gorilla/schema"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/manifoldco/promptui"
|
"github.com/manifoldco/promptui"
|
||||||
|
"github.com/writeas/go-strip-markdown"
|
||||||
"github.com/writeas/web-core/converter"
|
"github.com/writeas/web-core/converter"
|
||||||
"github.com/writeas/web-core/log"
|
"github.com/writeas/web-core/log"
|
||||||
"github.com/writeas/writefreely/config"
|
"github.com/writeas/writefreely/config"
|
||||||
|
@ -97,8 +98,11 @@ func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error {
|
||||||
func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *template.Template) error {
|
func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *template.Template) error {
|
||||||
p := struct {
|
p := struct {
|
||||||
page.StaticPage
|
page.StaticPage
|
||||||
Content template.HTML
|
Content template.HTML
|
||||||
Updated string
|
PlainContent string
|
||||||
|
Updated string
|
||||||
|
|
||||||
|
AboutStats *InstanceStats
|
||||||
}{
|
}{
|
||||||
StaticPage: pageForReq(app, r),
|
StaticPage: pageForReq(app, r),
|
||||||
}
|
}
|
||||||
|
@ -109,6 +113,11 @@ func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *te
|
||||||
|
|
||||||
if r.URL.Path == "/about" {
|
if r.URL.Path == "/about" {
|
||||||
c, err = getAboutPage(app)
|
c, err = getAboutPage(app)
|
||||||
|
|
||||||
|
// Fetch stats
|
||||||
|
p.AboutStats = &InstanceStats{}
|
||||||
|
p.AboutStats.NumPosts, _ = app.db.GetTotalPosts()
|
||||||
|
p.AboutStats.NumBlogs, _ = app.db.GetTotalCollections()
|
||||||
} else {
|
} else {
|
||||||
c, updated, err = getPrivacyPage(app)
|
c, updated, err = getPrivacyPage(app)
|
||||||
}
|
}
|
||||||
|
@ -117,6 +126,7 @@ func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *te
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.Content = template.HTML(applyMarkdown([]byte(c)))
|
p.Content = template.HTML(applyMarkdown([]byte(c)))
|
||||||
|
p.PlainContent = shortPostDescription(stripmd.Strip(c))
|
||||||
if updated != nil {
|
if updated != nil {
|
||||||
p.Updated = updated.Format("January 2, 2006")
|
p.Updated = updated.Format("January 2, 2006")
|
||||||
}
|
}
|
||||||
|
@ -357,7 +367,7 @@ func Serve() {
|
||||||
app.cfg.Database.Host = "localhost"
|
app.cfg.Database.Host = "localhost"
|
||||||
}
|
}
|
||||||
if app.cfg.Database.Database == "" {
|
if app.cfg.Database.Database == "" {
|
||||||
app.cfg.Database.Database = "writeas"
|
app.cfg.Database.Database = "writefreely"
|
||||||
}
|
}
|
||||||
|
|
||||||
connectToDatabase(app)
|
connectToDatabase(app)
|
||||||
|
@ -391,11 +401,26 @@ func Serve() {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Start web application server
|
|
||||||
http.Handle("/", r)
|
http.Handle("/", r)
|
||||||
log.Info("Serving on http://localhost:%d\n", app.cfg.Server.Port)
|
|
||||||
log.Info("---")
|
// Start web application server
|
||||||
err = http.ListenAndServe(fmt.Sprintf(":%d", app.cfg.Server.Port), nil)
|
if app.cfg.IsSecureStandalone() {
|
||||||
|
log.Info("Serving redirects on http://localhost:80")
|
||||||
|
go func() {
|
||||||
|
err = http.ListenAndServe(":80", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, app.cfg.App.Host, http.StatusMovedPermanently)
|
||||||
|
}))
|
||||||
|
log.Error("Unable to start redirect server: %v", err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Info("Serving on https://localhost:443")
|
||||||
|
log.Info("---")
|
||||||
|
err = http.ListenAndServeTLS(":443", app.cfg.Server.TLSCertPath, app.cfg.Server.TLSKeyPath, nil)
|
||||||
|
} else {
|
||||||
|
log.Info("Serving on http://localhost:%d\n", app.cfg.Server.Port)
|
||||||
|
log.Info("---")
|
||||||
|
err = http.ListenAndServe(fmt.Sprintf(":%d", app.cfg.Server.Port), nil)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to start: %v", err)
|
log.Error("Unable to start: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
@ -403,8 +428,13 @@ func Serve() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func connectToDatabase(app *app) {
|
func connectToDatabase(app *app) {
|
||||||
log.Info("Connecting to database...")
|
if app.cfg.Database.Type != "mysql" {
|
||||||
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s", app.cfg.Database.User, app.cfg.Database.Password, app.cfg.Database.Host, app.cfg.Database.Port, app.cfg.Database.Database, url.QueryEscape(time.Local.String())))
|
log.Error("Invalid database type '%s'. Only 'mysql' is supported right now.", app.cfg.Database.Type)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Connecting to %s database...", app.cfg.Database.Type)
|
||||||
|
db, err := sql.Open(app.cfg.Database.Type, fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s", app.cfg.Database.User, app.cfg.Database.Password, app.cfg.Database.Host, app.cfg.Database.Port, app.cfg.Database.Database, url.QueryEscape(time.Local.String())))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("%s", err)
|
log.Error("%s", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
writefreely
|
writefreely
|
||||||
|
writefreely.exe
|
||||||
|
|
|
@ -13,6 +13,9 @@ type (
|
||||||
HiddenHost string `ini:"hidden_host"`
|
HiddenHost string `ini:"hidden_host"`
|
||||||
Port int `ini:"port"`
|
Port int `ini:"port"`
|
||||||
|
|
||||||
|
TLSCertPath string `ini:"tls_cert_path"`
|
||||||
|
TLSKeyPath string `ini:"tls_key_path"`
|
||||||
|
|
||||||
Dev bool `ini:"-"`
|
Dev bool `ini:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,6 +79,10 @@ func New() *Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) IsSecureStandalone() bool {
|
||||||
|
return cfg.Server.Port == 443 && cfg.Server.TLSCertPath != "" && cfg.Server.TLSKeyPath != ""
|
||||||
|
}
|
||||||
|
|
||||||
func Load() (*Config, error) {
|
func Load() (*Config, error) {
|
||||||
cfg, err := ini.Load(FileName)
|
cfg, err := ini.Load(FileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -47,17 +47,80 @@ func Configure() (*SetupData, error) {
|
||||||
Selected: fmt.Sprintf(`{{.Label}} {{ . | faint }}`),
|
Selected: fmt.Sprintf(`{{.Label}} {{ . | faint }}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt := promptui.Prompt{
|
// Environment selection
|
||||||
Templates: tmpls,
|
selPrompt := promptui.Select{
|
||||||
Label: "Local port",
|
Templates: selTmpls,
|
||||||
Validate: validatePort,
|
Label: "Environment",
|
||||||
Default: fmt.Sprintf("%d", data.Config.Server.Port),
|
Items: []string{"Development", "Production, standalone", "Production, behind reverse proxy"},
|
||||||
}
|
}
|
||||||
port, err := prompt.Run()
|
_, envType, err := selPrompt.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
data.Config.Server.Port, _ = strconv.Atoi(port) // Ignore error, as we've already validated number
|
isDevEnv := envType == "Development"
|
||||||
|
isStandalone := envType == "Production, standalone"
|
||||||
|
|
||||||
|
data.Config.Server.Dev = isDevEnv
|
||||||
|
|
||||||
|
var prompt promptui.Prompt
|
||||||
|
if isDevEnv || !isStandalone {
|
||||||
|
// Running in dev environment or behind reverse proxy; ask for port
|
||||||
|
prompt = promptui.Prompt{
|
||||||
|
Templates: tmpls,
|
||||||
|
Label: "Local port",
|
||||||
|
Validate: validatePort,
|
||||||
|
Default: fmt.Sprintf("%d", data.Config.Server.Port),
|
||||||
|
}
|
||||||
|
port, err := prompt.Run()
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
data.Config.Server.Port, _ = strconv.Atoi(port) // Ignore error, as we've already validated number
|
||||||
|
}
|
||||||
|
|
||||||
|
if isStandalone {
|
||||||
|
selPrompt = promptui.Select{
|
||||||
|
Templates: selTmpls,
|
||||||
|
Label: "Web server mode",
|
||||||
|
Items: []string{"Insecure (port 80)", "Secure (port 443)"},
|
||||||
|
}
|
||||||
|
sel, _, err := selPrompt.Run()
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
if sel == 0 {
|
||||||
|
data.Config.Server.Port = 80
|
||||||
|
data.Config.Server.TLSCertPath = ""
|
||||||
|
data.Config.Server.TLSKeyPath = ""
|
||||||
|
} else if sel == 1 {
|
||||||
|
data.Config.Server.Port = 443
|
||||||
|
|
||||||
|
prompt = promptui.Prompt{
|
||||||
|
Templates: tmpls,
|
||||||
|
Label: "Certificate path",
|
||||||
|
Validate: validateNonEmpty,
|
||||||
|
Default: data.Config.Server.TLSCertPath,
|
||||||
|
}
|
||||||
|
data.Config.Server.TLSCertPath, err = prompt.Run()
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt = promptui.Prompt{
|
||||||
|
Templates: tmpls,
|
||||||
|
Label: "Key path",
|
||||||
|
Validate: validateNonEmpty,
|
||||||
|
Default: data.Config.Server.TLSKeyPath,
|
||||||
|
}
|
||||||
|
data.Config.Server.TLSKeyPath, err = prompt.Run()
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.Config.Server.TLSCertPath = ""
|
||||||
|
data.Config.Server.TLSKeyPath = ""
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
title(" Database setup ")
|
title(" Database setup ")
|
||||||
|
@ -124,7 +187,7 @@ func Configure() (*SetupData, error) {
|
||||||
title(" App setup ")
|
title(" App setup ")
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
||||||
selPrompt := promptui.Select{
|
selPrompt = promptui.Select{
|
||||||
Templates: selTmpls,
|
Templates: selTmpls,
|
||||||
Label: "Site type",
|
Label: "Site type",
|
||||||
Items: []string{"Single user blog", "Multi-user instance"},
|
Items: []string{"Single user blog", "Multi-user instance"},
|
||||||
|
|
18
database.go
18
database.go
|
@ -50,6 +50,8 @@ type writestore interface {
|
||||||
GetCollections(u *User) (*[]Collection, error)
|
GetCollections(u *User) (*[]Collection, error)
|
||||||
GetPublishableCollections(u *User) (*[]Collection, error)
|
GetPublishableCollections(u *User) (*[]Collection, error)
|
||||||
GetMeStats(u *User) userMeStats
|
GetMeStats(u *User) userMeStats
|
||||||
|
GetTotalCollections() (int64, error)
|
||||||
|
GetTotalPosts() (int64, error)
|
||||||
GetTopPosts(u *User, alias string) (*[]PublicPost, error)
|
GetTopPosts(u *User, alias string) (*[]PublicPost, error)
|
||||||
GetAnonymousPosts(u *User) (*[]PublicPost, error)
|
GetAnonymousPosts(u *User) (*[]PublicPost, error)
|
||||||
GetUserPosts(u *User) (*[]PublicPost, error)
|
GetUserPosts(u *User) (*[]PublicPost, error)
|
||||||
|
@ -1541,6 +1543,22 @@ func (db *datastore) GetMeStats(u *User) userMeStats {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *datastore) GetTotalCollections() (collCount int64, err error) {
|
||||||
|
err = db.QueryRow(`SELECT COUNT(*) FROM collections`).Scan(&collCount)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to fetch collections count: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *datastore) GetTotalPosts() (postCount int64, err error) {
|
||||||
|
err = db.QueryRow(`SELECT COUNT(*) FROM posts`).Scan(&postCount)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to fetch posts count: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (db *datastore) GetTopPosts(u *User, alias string) (*[]PublicPost, error) {
|
func (db *datastore) GetTopPosts(u *User, alias string) (*[]PublicPost, error) {
|
||||||
params := []interface{}{u.ID}
|
params := []interface{}{u.ID}
|
||||||
where := ""
|
where := ""
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
package writefreely
|
||||||
|
|
||||||
|
type InstanceStats struct {
|
||||||
|
NumPosts int64
|
||||||
|
NumBlogs int64
|
||||||
|
}
|
12
nodeinfo.go
12
nodeinfo.go
|
@ -57,12 +57,14 @@ func (r nodeInfoResolver) IsOpenRegistration() (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r nodeInfoResolver) Usage() (nodeinfo.Usage, error) {
|
func (r nodeInfoResolver) Usage() (nodeinfo.Usage, error) {
|
||||||
var collCount, postCount, activeHalfYear, activeMonth int
|
var collCount, postCount int64
|
||||||
err := r.db.QueryRow(`SELECT COUNT(*) FROM collections`).Scan(&collCount)
|
var activeHalfYear, activeMonth int
|
||||||
|
var err error
|
||||||
|
collCount, err = r.db.GetTotalCollections()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
collCount = 0
|
collCount = 0
|
||||||
}
|
}
|
||||||
err = r.db.QueryRow(`SELECT COUNT(*) FROM posts`).Scan(&postCount)
|
postCount, err = r.db.GetTotalPosts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to fetch post counts: %v", err)
|
log.Error("Unable to fetch post counts: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -88,10 +90,10 @@ WHERE collection_id IS NOT NULL
|
||||||
|
|
||||||
return nodeinfo.Usage{
|
return nodeinfo.Usage{
|
||||||
Users: nodeinfo.UsageUsers{
|
Users: nodeinfo.UsageUsers{
|
||||||
Total: collCount,
|
Total: int(collCount),
|
||||||
ActiveHalfYear: activeHalfYear,
|
ActiveHalfYear: activeHalfYear,
|
||||||
ActiveMonth: activeMonth,
|
ActiveMonth: activeMonth,
|
||||||
},
|
},
|
||||||
LocalPosts: postCount,
|
LocalPosts: int(postCount),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{{define "head"}}<title>About {{.SiteName}}</title>
|
{{define "head"}}<title>About {{.SiteName}}</title>
|
||||||
|
<meta name="description" content="{{.PlainContent}}">
|
||||||
{{end}}
|
{{end}}
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<div class="content-container snug">
|
<div class="content-container snug">
|
||||||
|
@ -6,6 +7,11 @@
|
||||||
|
|
||||||
{{.Content}}
|
{{.Content}}
|
||||||
|
|
||||||
|
{{if .Federation}}
|
||||||
|
<hr style="margin:1.5em 0;" />
|
||||||
|
<p><em>{{.SiteName}}</em> is home to <strong>{{largeNumFmt .AboutStats.NumPosts}}</strong> {{pluralize "article" "articles" .AboutStats.NumPosts}} across <strong>{{largeNumFmt .AboutStats.NumBlogs}}</strong> {{pluralize "blog" "blogs" .AboutStats.NumBlogs}}.</p>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<h2 style="margin-top:2em">About WriteFreely</h2>
|
<h2 style="margin-top:2em">About WriteFreely</h2>
|
||||||
<p><a href="https://writefreely.org">WriteFreely</a> is a self-hosted, decentralized blogging platform for publishing beautiful, simple blogs.</p>
|
<p><a href="https://writefreely.org">WriteFreely</a> is a self-hosted, decentralized blogging platform for publishing beautiful, simple blogs.</p>
|
||||||
<p>It lets you publish a single blog, or host a community of writers who can create multiple blogs under one account. You can also enable federation, which allows people in the fediverse to follow your blog, bookmark your posts, and share them with others.</p>
|
<p>It lets you publish a single blog, or host a community of writers who can create multiple blogs under one account. You can also enable federation, which allows people in the fediverse to follow your blog, bookmark your posts, and share them with others.</p>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{{define "head"}}<title>{{.SiteName}} Privacy Policy</title>
|
{{define "head"}}<title>{{.SiteName}} Privacy Policy</title>
|
||||||
|
<meta name="description" content="{{.PlainContent}}">
|
||||||
{{end}}
|
{{end}}
|
||||||
{{define "content"}}<div class="content-container snug">
|
{{define "content"}}<div class="content-container snug">
|
||||||
<h1>Privacy Policy</h1>
|
<h1>Privacy Policy</h1>
|
||||||
|
|
|
@ -56,7 +56,7 @@ function savePage(el) {
|
||||||
<h2>Site</h2>
|
<h2>Site</h2>
|
||||||
|
|
||||||
<h3 id="page-about">About page</h3>
|
<h3 id="page-about">About page</h3>
|
||||||
<p>Describe what your instance is <a href="/privacy">about</a>. <em>Accepts Markdown</em>.</p>
|
<p>Describe what your instance is <a href="/about">about</a>. <em>Accepts Markdown</em>.</p>
|
||||||
<form method="post" action="/admin/update/about" onsubmit="savePage(this)">
|
<form method="post" action="/admin/update/about" onsubmit="savePage(this)">
|
||||||
<textarea id="about-editor" class="section codable norm edit-page" name="content">{{.AboutPage}}</textarea>
|
<textarea id="about-editor" class="section codable norm edit-page" name="content">{{.AboutPage}}</textarea>
|
||||||
<input type="submit" value="Save" />
|
<input type="submit" value="Save" />
|
||||||
|
|
Loading…
Reference in New Issue