Split status in status and visibility

Closes #38

This aligns the implementation more with the IndieWeb standards and allows setting and updating status and visibility individually.
master
Jan-Lukas Else 5 days ago
parent c7da67b581
commit 37a9e1f29c
  1. 10
      authentication.go
  2. 4
      blogstats.go
  3. 42
      blogstats_test.go
  4. 7
      check.go
  5. 11
      dbmigrations/00031.sql
  6. 19
      editor.go
  7. 24
      geoMap.go
  8. 2
      go.mod
  9. 4
      go.sum
  10. 32
      http.go
  11. 77
      micropub.go
  12. 4
      micropub_test.go
  13. 3
      nodeinfo.go
  14. 52
      posts.go
  15. 89
      postsDb.go
  16. 36
      postsDb_test.go
  17. 2
      postsDeleter.go
  18. 19
      postsFuncs.go
  19. 4
      postsScheduler.go
  20. 21
      postsScheduler_test.go
  21. 2
      reactions_test.go
  22. 3
      sitemap.go
  23. 7
      strings/de.yaml
  24. 7
      strings/default.yaml
  25. 3
      strings/es.yaml
  26. 3
      strings/pt-br.yaml
  27. 10
      telegram_test.go
  28. 8
      uiComponents.go
  29. 2
      webmentionSending.go

@ -176,9 +176,13 @@ func (a *goBlog) serveLogout(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusFound)
}
func (a *goBlog) getDefaultPostStatusse(r *http.Request) []postStatus {
func (a *goBlog) getDefaultPostStates(r *http.Request) (status []postStatus, visibility []postVisibility) {
if a.isLoggedIn(r) {
return []postStatus{statusPublished, statusUnlisted, statusPrivate}
status = []postStatus{statusPublished}
visibility = []postVisibility{visibilityPublic, visibilityUnlisted, visibilityPrivate}
} else {
status = []postStatus{statusPublished}
visibility = []postVisibility{visibilityPublic}
}
return []postStatus{statusPublished}
return
}

@ -65,7 +65,7 @@ with filtered as (
tolocal(published) as pub,
mdtext(coalesce(content, '')) as content
from posts
where status = @status and blog = @blog
where status = @status and visibility = @visibility and blog = @blog
)
)
select *
@ -154,7 +154,7 @@ func (db *database) getBlogStats(blog string) (data *blogStatsData, err error) {
Months: map[string][]blogStatsRow{},
}
// Query and scan
rows, err := db.Query(blogStatsSql, sql.Named("status", statusPublished), sql.Named("blog", blog))
rows, err := db.Query(blogStatsSql, sql.Named("status", statusPublished), sql.Named("visibility", visibilityPublic), sql.Named("blog", blog))
if err != nil {
return nil, err
}

@ -41,20 +41,42 @@ func Test_blogStats(t *testing.T) {
// Insert post
err := app.createPost(&post{
Content: "This is a simple **test** post",
Blog: "en",
Section: "test",
Published: "2020-06-01",
Status: statusPublished,
Content: "This is a simple **test** post",
Blog: "en",
Section: "test",
Published: "2020-06-01",
Status: statusPublished,
Visibility: visibilityPublic,
})
require.NoError(t, err)
err = app.createPost(&post{
Content: "This is another simple **test** post",
Blog: "en",
Section: "test",
Published: "2021-05-01",
Status: statusPublished,
Content: "This is another simple **test** post",
Blog: "en",
Section: "test",
Published: "2021-05-01",
Status: statusPublished,
Visibility: visibilityPublic,
})
require.NoError(t, err)
err = app.createPost(&post{
Content: "This is a private post, that doesn't count",
Blog: "en",
Section: "test",
Published: "2021-05-01",
Status: statusPublished,
Visibility: visibilityPrivate,
})
require.NoError(t, err)
err = app.createPost(&post{
Content: "Unlisted posts don't count as well",
Blog: "en",
Section: "test",
Published: "2021-05-01",
Status: statusPublished,
Visibility: visibilityUnlisted,
})
require.NoError(t, err)

@ -18,8 +18,11 @@ import (
)
func (a *goBlog) checkAllExternalLinks() {
// Get all published posts without parameters
posts, err := a.getPosts(&postsRequestConfig{status: statusPublished, withoutParameters: true})
posts, err := a.getPosts(&postsRequestConfig{
status: []postStatus{statusPublished},
visibility: []postVisibility{visibilityPublic, visibilityUnlisted},
withoutParameters: true,
})
if err != nil {
log.Println(err.Error())
return

@ -0,0 +1,11 @@
alter table posts add visibility text not null default '';
update posts set status = 'published', visibility = 'public' where status = 'published';
update posts set status = 'published-deleted', visibility = 'public' where status = 'published-deleted';
update posts set status = 'draft', visibility = 'public' where status = 'draft';
update posts set status = 'draft-deleted', visibility = 'public' where status = 'draft-deleted';
update posts set status = 'scheduled', visibility = 'public' where status = 'scheduled';
update posts set status = 'scheduled-deleted', visibility = 'public' where status = 'scheduled-deleted';
update posts set status = 'published', visibility = 'private' where status = 'private';
update posts set status = 'published-deleted', visibility = 'private' where status = 'private-deleted';
update posts set status = 'published', visibility = 'unlisted' where status = 'unlisted';
update posts set status = 'published-deleted', visibility = 'unlisted' where status = 'unlisted-deleted';

@ -194,6 +194,7 @@ func (*goBlog) editorPostTemplate(blog string, bc *configBlog) string {
marsh("blog", blog)
marsh("section", bc.DefaultSection)
marsh("status", statusDraft)
marsh("visibility", visibilityPublic)
marsh("priority", 0)
marsh("slug", "")
marsh("title", "")
@ -206,8 +207,8 @@ func (*goBlog) editorPostTemplate(blog string, bc *configBlog) string {
func (a *goBlog) editorPostDesc(bc *configBlog) string {
t := a.ts.GetTemplateStringVariant(bc.Lang, "editorpostdesc")
paramBuilder, statusBuilder := bufferpool.Get(), bufferpool.Get()
defer bufferpool.Put(paramBuilder, statusBuilder)
paramBuilder, statusBuilder, visibilityBuilder := bufferpool.Get(), bufferpool.Get(), bufferpool.Get()
defer bufferpool.Put(paramBuilder, statusBuilder, visibilityBuilder)
for i, param := range []string{
"published",
"updated",
@ -236,7 +237,7 @@ func (a *goBlog) editorPostDesc(bc *configBlog) string {
paramBuilder.WriteByte('`')
}
for i, status := range []postStatus{
statusDraft, statusPublished, statusUnlisted, statusScheduled, statusPrivate,
statusPublished, statusDraft, statusScheduled,
} {
if i > 0 {
statusBuilder.WriteString(", ")
@ -245,5 +246,15 @@ func (a *goBlog) editorPostDesc(bc *configBlog) string {
statusBuilder.WriteString(string(status))
statusBuilder.WriteByte('`')
}
return fmt.Sprintf(t, paramBuilder.String(), "status", statusBuilder.String())
for i, visibility := range []postVisibility{
visibilityPublic, visibilityUnlisted, visibilityPrivate,
} {
if i > 0 {
visibilityBuilder.WriteString(", ")
}
visibilityBuilder.WriteByte('`')
visibilityBuilder.WriteString(string(visibility))
visibilityBuilder.WriteByte('`')
}
return fmt.Sprintf(t, paramBuilder.String(), "status", "visibility", statusBuilder.String(), visibilityBuilder.String())
}

@ -17,12 +17,14 @@ func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) {
mapPath := bc.getRelativePath(defaultIfEmpty(bc.Map.Path, defaultGeoMapPath))
canonical := a.getFullAddress(mapPath)
allPostsWithLocation, err := a.db.countPosts(&postsRequestConfig{
allPostsWithLocationRequestConfig := &postsRequestConfig{
blog: blog,
statusse: a.getDefaultPostStatusse(r),
parameters: []string{a.cfg.Micropub.LocationParam, gpxParameter},
withOnlyParameters: []string{a.cfg.Micropub.LocationParam, gpxParameter},
})
}
allPostsWithLocationRequestConfig.status, allPostsWithLocationRequestConfig.visibility = a.getDefaultPostStates(r)
allPostsWithLocation, err := a.db.countPosts(allPostsWithLocationRequestConfig)
if err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
@ -55,14 +57,16 @@ const geoMapTracksSubpath = "/tracks.json"
func (a *goBlog) serveGeoMapTracks(w http.ResponseWriter, r *http.Request) {
blog, _ := a.getBlog(r)
allPostsWithTracks, err := a.getPosts(&postsRequestConfig{
allPostsWithTracksRequestConfig := &postsRequestConfig{
blog: blog,
statusse: a.getDefaultPostStatusse(r),
parameters: []string{gpxParameter},
withOnlyParameters: []string{gpxParameter},
excludeParameter: showRouteParam,
excludeParameterValue: "false", // Don't show hidden route tracks
})
}
allPostsWithTracksRequestConfig.status, allPostsWithTracksRequestConfig.visibility = a.getDefaultPostStates(r)
allPostsWithTracks, err := a.getPosts(allPostsWithTracksRequestConfig)
if err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
@ -101,12 +105,14 @@ const geoMapLocationsSubpath = "/locations.json"
func (a *goBlog) serveGeoMapLocations(w http.ResponseWriter, r *http.Request) {
blog, _ := a.getBlog(r)
allPostsWithLocations, err := a.getPosts(&postsRequestConfig{
allPostsWithLocationRequestConfig := &postsRequestConfig{
blog: blog,
statusse: a.getDefaultPostStatusse(r),
parameters: []string{a.cfg.Micropub.LocationParam},
withOnlyParameters: []string{a.cfg.Micropub.LocationParam},
})
}
allPostsWithLocationRequestConfig.status, allPostsWithLocationRequestConfig.visibility = a.getDefaultPostStates(r)
allPostsWithLocations, err := a.getPosts(allPostsWithLocationRequestConfig)
if err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return

@ -59,7 +59,7 @@ require (
// master
github.com/yuin/goldmark-emoji v1.0.2-0.20210607094911-0487583eca38
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
golang.org/x/net v0.0.0-20220920203100-d0c6ba3f52d9
golang.org/x/net v0.0.0-20220921203646-d300de134e69
golang.org/x/sync v0.0.0-20220907140024-f12130a52804
golang.org/x/text v0.3.7
gopkg.in/yaml.v3 v3.0.1

@ -624,8 +624,8 @@ golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220920203100-d0c6ba3f52d9 h1:asZqf0wXastQr+DudYagQS8uBO8bHKeYD1vbAvGmFL8=
golang.org/x/net v0.0.0-20220920203100-d0c6ba3f52d9/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20220921203646-d300de134e69 h1:hUJpGDpnfwdJW8iNypFjmSY0sCBEL+spFTZ2eO+Sfps=
golang.org/x/net v0.0.0-20220921203646-d300de134e69/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=

@ -259,16 +259,16 @@ func (a *goBlog) servePostsAliasesRedirects() http.HandlerFunc {
path := r.URL.Path
row, err := a.db.QueryRow(`
-- normal posts
select 'post', status, 200 from posts where path = @path
select 'post', status, visibility, 200 from posts where path = @path
union all
-- short paths
select 'alias', path, 301 from shortpath where printf('/s/%x', id) = @path
select 'alias', path, '', 301 from shortpath where printf('/s/%x', id) = @path
union all
-- post aliases
select 'alias', path, 302 from post_parameters where parameter = 'aliases' and value = @path
select 'alias', path, '', 302 from post_parameters where parameter = 'aliases' and value = @path
union all
-- deleted posts
select 'deleted', '', 410 from deleted where path = @path
select 'deleted', '', '', 410 from deleted where path = @path
-- just select the first result
limit 1
`, sql.Named("path", path))
@ -276,9 +276,9 @@ func (a *goBlog) servePostsAliasesRedirects() http.HandlerFunc {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
var pathType, value string
var pathType, value1, value2 string
var status int
err = row.Scan(&pathType, &value, &status)
err = row.Scan(&pathType, &value1, &value2, &status)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
// Error
@ -290,26 +290,32 @@ func (a *goBlog) servePostsAliasesRedirects() http.HandlerFunc {
// Found post or alias
switch pathType {
case "post":
// Is post, check status
switch postStatus(value) {
case statusPublished, statusUnlisted:
alicePrivate.Append(a.checkActivityStreamsRequest, a.cacheMiddleware).ThenFunc(a.servePost).ServeHTTP(w, r)
// Check status
switch postStatus(value1) {
case statusPublished:
// Check visibility
switch postVisibility(value2) {
case visibilityPublic, visibilityUnlisted:
alicePrivate.Append(a.checkActivityStreamsRequest, a.cacheMiddleware).ThenFunc(a.servePost).ServeHTTP(w, r)
default: // private, etc.
alice.New(a.authMiddleware).ThenFunc(a.servePost).ServeHTTP(w, r)
}
return
case statusPublishedDeleted, statusUnlistedDeleted:
case statusPublishedDeleted:
if a.isLoggedIn(r) {
a.servePost(w, r)
return
}
alicePrivate.Append(a.cacheMiddleware).ThenFunc(a.serve410).ServeHTTP(w, r)
return
default: // private, draft, scheduled, etc.
default: // draft, scheduled, etc.
alice.New(a.authMiddleware).ThenFunc(a.servePost).ServeHTTP(w, r)
return
}
case "alias":
// Is alias, redirect
alicePrivate.Append(cacheLoggedIn, a.cacheMiddleware).ThenFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, value, status)
http.Redirect(w, r, value1, status)
}).ServeHTTP(w, r)
return
case "deleted":

@ -168,19 +168,17 @@ func (a *goBlog) micropubParseValuePostParamsValueMap(entry *post, values map[st
delete(values, "mp-channel")
}
// Status
statusStr := ""
if status, ok := values["post-status"]; ok && len(status) > 0 {
statusStr = status[0]
statusStr := status[0]
entry.Status = micropubStatus(statusStr)
delete(values, "post-status")
}
visibilityStr := ""
// Visibility
if visibility, ok := values["visibility"]; ok && len(visibility) > 0 {
visibilityStr = visibility[0]
visibilityStr := visibility[0]
entry.Visibility = micropubVisibility(visibilityStr)
delete(values, "visibility")
}
if finalStatus := micropubStatus(statusNil, statusStr, visibilityStr); finalStatus != statusNil {
entry.Status = finalStatus
}
// Parameter
if name, ok := values["name"]; ok {
entry.Parameters["title"] = name
@ -297,16 +295,14 @@ func (a *goBlog) micropubParsePostParamsMfItem(entry *post, mf *microformatItem)
entry.setChannel(mf.Properties.MpChannel[0])
}
// Status
status := ""
if len(mf.Properties.PostStatus) > 0 {
status = mf.Properties.PostStatus[0]
status := mf.Properties.PostStatus[0]
entry.Status = micropubStatus(status)
}
visibility := ""
// Visibility
if len(mf.Properties.Visibility) > 0 {
visibility = mf.Properties.Visibility[0]
}
if finalStatus := micropubStatus(statusNil, status, visibility); finalStatus != statusNil {
entry.Status = finalStatus
visibility := mf.Properties.Visibility[0]
entry.Visibility = micropubVisibility(visibility)
}
// Parameter
if len(mf.Properties.Name) > 0 {
@ -398,6 +394,10 @@ func (a *goBlog) extractParamsFromContent(p *post) error {
p.Status = postStatus(status[0])
delete(p.Parameters, "status")
}
if visibility := p.Parameters["visibility"]; len(visibility) == 1 {
p.Visibility = postVisibility(visibility[0])
delete(p.Parameters, "visibility")
}
if priority := p.Parameters["priority"]; len(priority) == 1 {
p.Priority = cast.ToInt(priority[0])
delete(p.Parameters, "priority")
@ -513,13 +513,14 @@ func (a *goBlog) micropubUpdate(w http.ResponseWriter, r *http.Request, u string
return
}
// Check if post is marked as deleted
if strings.HasSuffix(string(p.Status), statusDeletedSuffix) {
if p.Deleted() {
a.serveError(w, r, "post is marked as deleted, undelete it first", http.StatusBadRequest)
return
}
// Update post
oldPath := p.Path
oldStatus := p.Status
oldVisibility := p.Visibility
a.micropubUpdateReplace(p, mf.Replace)
a.micropubUpdateAdd(p, mf.Add)
a.micropubUpdateDelete(p, mf.Delete)
@ -528,7 +529,7 @@ func (a *goBlog) micropubUpdate(w http.ResponseWriter, r *http.Request, u string
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
err = a.replacePost(p, oldPath, oldStatus)
err = a.replacePost(p, oldPath, oldStatus, oldVisibility)
if err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
@ -547,16 +548,14 @@ func (a *goBlog) micropubUpdateReplace(p *post, replace map[string][]any) {
p.Updated = cast.ToStringSlice(updated)[0]
}
// Status
statusStr := ""
if status, ok := replace["post-status"]; ok && len(status) > 0 {
statusStr = cast.ToStringSlice(status)[0]
statusStr := cast.ToStringSlice(status)[0]
p.Status = micropubStatus(statusStr)
}
visibilityStr := ""
// Visibility
if visibility, ok := replace["visibility"]; ok && len(visibility) > 0 {
visibilityStr = cast.ToStringSlice(visibility)[0]
}
if finalStatus := micropubStatus(p.Status, statusStr, visibilityStr); finalStatus != statusNil {
p.Status = finalStatus
visibilityStr := cast.ToStringSlice(visibility)[0]
p.Visibility = micropubVisibility(visibilityStr)
}
// Parameters
if name, ok := replace["name"]; ok && name != nil {
@ -671,24 +670,22 @@ func (a *goBlog) micropubUpdateDelete(p *post, del any) {
}
}
func micropubStatus(defaultStatus postStatus, status string, visibility string) (final postStatus) {
final = defaultStatus
func micropubStatus(status string) postStatus {
switch status {
case "published":
final = statusPublished
case "draft":
final = statusDraft
}
if final != statusDraft {
// Only override status if it's not a draft
switch visibility {
case "public":
final = statusPublished
case "unlisted":
final = statusUnlisted
case "private":
final = statusPrivate
}
return statusDraft
default:
return statusPublished
}
}
func micropubVisibility(visibility string) postVisibility {
switch visibility {
case "unlisted":
return visibilityUnlisted
case "private":
return visibilityPrivate
default:
return visibilityPublic
}
return final
}

@ -44,12 +44,12 @@ func Test_micropubQuery(t *testing.T) {
},
{
query: "source&url=http://localhost:8080/test/post",
want: "{\"type\":[\"h-entry\"],\"properties\":{\"published\":[\"\"],\"updated\":[\"\"],\"post-status\":[\"published\"],\"visibility\":[\"public\"],\"category\":[\"test\",\"test2\"],\"content\":[\"---\\nblog: default\\npath: /test/post\\npriority: 0\\npublished: \\\"\\\"\\nsection: \\\"\\\"\\nstatus: published\\ntags:\\n - test\\n - test2\\nupdated: \\\"\\\"\\n---\\nTest post\"],\"url\":[\"http://localhost:8080/test/post\"],\"mp-slug\":[\"\"],\"mp-channel\":[\"default\"]}}",
want: "{\"type\":[\"h-entry\"],\"properties\":{\"published\":[\"\"],\"updated\":[\"\"],\"post-status\":[\"published\"],\"visibility\":[\"public\"],\"category\":[\"test\",\"test2\"],\"content\":[\"---\\nblog: default\\npath: /test/post\\npriority: 0\\npublished: \\\"\\\"\\nsection: \\\"\\\"\\nstatus: published\\ntags:\\n - test\\n - test2\\nupdated: \\\"\\\"\\nvisibility: public\\n---\\nTest post\"],\"url\":[\"http://localhost:8080/test/post\"],\"mp-slug\":[\"\"],\"mp-channel\":[\"default\"]}}",
wantStatus: http.StatusOK,
},
{
query: "source",
want: "{\"items\":[{\"type\":[\"h-entry\"],\"properties\":{\"published\":[\"\"],\"updated\":[\"\"],\"post-status\":[\"published\"],\"visibility\":[\"public\"],\"category\":[\"test\",\"test2\"],\"content\":[\"---\\nblog: default\\npath: /test/post\\npriority: 0\\npublished: \\\"\\\"\\nsection: \\\"\\\"\\nstatus: published\\ntags:\\n - test\\n - test2\\nupdated: \\\"\\\"\\n---\\nTest post\"],\"url\":[\"http://localhost:8080/test/post\"],\"mp-slug\":[\"\"],\"mp-channel\":[\"default\"]}}]}",
want: "{\"items\":[{\"type\":[\"h-entry\"],\"properties\":{\"published\":[\"\"],\"updated\":[\"\"],\"post-status\":[\"published\"],\"visibility\":[\"public\"],\"category\":[\"test\",\"test2\"],\"content\":[\"---\\nblog: default\\npath: /test/post\\npriority: 0\\npublished: \\\"\\\"\\nsection: \\\"\\\"\\nstatus: published\\ntags:\\n - test\\n - test2\\nupdated: \\\"\\\"\\nvisibility: public\\n---\\nTest post\"],\"url\":[\"http://localhost:8080/test/post\"],\"mp-slug\":[\"\"],\"mp-channel\":[\"default\"]}}]}",
wantStatus: http.StatusOK,
},
{

@ -32,7 +32,8 @@ func (a *goBlog) serveNodeInfoDiscover(w http.ResponseWriter, r *http.Request) {
func (a *goBlog) serveNodeInfo(w http.ResponseWriter, r *http.Request) {
localPosts, _ := a.db.countPosts(&postsRequestConfig{
status: statusPublished,
status: []postStatus{statusPublished},
visibility: []postVisibility{visibilityPublic},
})
buf := bufferpool.Get()
defer bufferpool.Put(buf)

@ -25,6 +25,7 @@ type post struct {
Blog string
Section string
Status postStatus
Visibility postVisibility
Priority int
// Not persisted
Slug string
@ -32,21 +33,23 @@ type post struct {
}
type postStatus string
type postVisibility string
const (
statusDeletedSuffix string = "-deleted"
statusDeletedSuffix postStatus = "-deleted"
statusNil postStatus = ""
statusPublished postStatus = "published"
statusPublishedDeleted postStatus = "published-deleted"
statusPublishedDeleted postStatus = statusPublished + statusDeletedSuffix
statusDraft postStatus = "draft"
statusDraftDeleted postStatus = "draft-deleted"
statusPrivate postStatus = "private"
statusPrivateDeleted postStatus = "private-deleted"
statusUnlisted postStatus = "unlisted"
statusUnlistedDeleted postStatus = "unlisted-deleted"
statusDraftDeleted postStatus = statusDraft + statusDeletedSuffix
statusScheduled postStatus = "scheduled"
statusScheduledDeleted postStatus = "scheduled-deleted"
statusScheduledDeleted postStatus = statusScheduled + statusDeletedSuffix
visibilityNil postVisibility = ""
visibilityPublic postVisibility = "public"
visibilityUnlisted postVisibility = "unlisted"
visibilityPrivate postVisibility = "private"
)
func (a *goBlog) servePost(w http.ResponseWriter, r *http.Request) {
@ -76,7 +79,7 @@ func (a *goBlog) servePost(w http.ResponseWriter, r *http.Request) {
}
w.Header().Add("Link", fmt.Sprintf("<%s>; rel=shortlink", a.shortPostURL(p)))
status := http.StatusOK
if strings.HasSuffix(string(p.Status), statusDeletedSuffix) {
if p.Deleted() {
status = http.StatusGone
}
a.renderWithStatusCode(w, r, status, renderMethod, &renderData{
@ -154,7 +157,7 @@ func (a *goBlog) serveDrafts(w http.ResponseWriter, r *http.Request) {
path: bc.getRelativePath("/editor/drafts"),
title: a.ts.GetTemplateStringVariant(bc.Lang, "drafts"),
description: a.ts.GetTemplateStringVariant(bc.Lang, "draftsdesc"),
status: statusDraft,
status: []postStatus{statusDraft},
})))
}
@ -164,7 +167,8 @@ func (a *goBlog) servePrivate(w http.ResponseWriter, r *http.Request) {
path: bc.getRelativePath("/editor/private"),
title: a.ts.GetTemplateStringVariant(bc.Lang, "privateposts"),
description: a.ts.GetTemplateStringVariant(bc.Lang, "privatepostsdesc"),
status: statusPrivate,
status: []postStatus{statusPublished},
visibility: []postVisibility{visibilityPrivate},
})))
}
@ -174,7 +178,8 @@ func (a *goBlog) serveUnlisted(w http.ResponseWriter, r *http.Request) {
path: bc.getRelativePath("/editor/unlisted"),
title: a.ts.GetTemplateStringVariant(bc.Lang, "unlistedposts"),
description: a.ts.GetTemplateStringVariant(bc.Lang, "unlistedpostsdesc"),
status: statusUnlisted,
status: []postStatus{statusPublished},
visibility: []postVisibility{visibilityUnlisted},
})))
}
@ -184,7 +189,7 @@ func (a *goBlog) serveScheduled(w http.ResponseWriter, r *http.Request) {
path: bc.getRelativePath("/editor/scheduled"),
title: a.ts.GetTemplateStringVariant(bc.Lang, "scheduledposts"),
description: a.ts.GetTemplateStringVariant(bc.Lang, "scheduledpostsdesc"),
status: statusScheduled,
status: []postStatus{statusScheduled},
})))
}
@ -194,7 +199,7 @@ func (a *goBlog) serveDeleted(w http.ResponseWriter, r *http.Request) {
path: bc.getRelativePath("/editor/deleted"),
title: a.ts.GetTemplateStringVariant(bc.Lang, "deletedposts"),
description: a.ts.GetTemplateStringVariant(bc.Lang, "deletedpostsdesc"),
statusse: []postStatus{statusPublishedDeleted, statusDraftDeleted, statusScheduledDeleted, statusPrivateDeleted, statusUnlistedDeleted},
status: []postStatus{statusPublishedDeleted, statusDraftDeleted, statusScheduledDeleted},
})))
}
@ -253,8 +258,8 @@ type indexConfig struct {
title string
description string
summaryTemplate summaryTyp
status postStatus
statusse []postStatus
status []postStatus
visibility []postVisibility
}
const defaultPhotosPath = "/photos"
@ -277,12 +282,14 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
sections = append(sections, sectionKey)
}
}
statusse := ic.statusse
if ic.status != statusNil {
statusse = []postStatus{ic.status}
defaultStatus, defaultVisibility := a.getDefaultPostStates(r)
status := ic.status
if len(status) == 0 {
status = defaultStatus
}
if len(statusse) == 0 {
statusse = a.getDefaultPostStatusse(r)
visibility := ic.visibility
if len(visibility) == 0 {
visibility = defaultVisibility
}
p := paginator.New(&postPaginationAdapter{config: &postsRequestConfig{
blog: blog,
@ -294,7 +301,8 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
publishedYear: ic.year,
publishedMonth: ic.month,
publishedDay: ic.day,
statusse: statusse,
status: status,
visibility: visibility,
priorityOrder: true,
}, a: a}, bc.Pagination)
p.SetPage(stringToInt(chi.URLParam(r, "page")))

@ -59,7 +59,7 @@ func (a *goBlog) checkPost(p *post) (err error) {
// Fix content
p.Content = strings.TrimSuffix(strings.TrimPrefix(p.Content, "\n"), "\n")
// Check status
if p.Status == "" {
if p.Status == statusNil {
p.Status = statusPublished
if p.Published != "" {
// If published time is in the future, set status to scheduled
@ -72,6 +72,10 @@ func (a *goBlog) checkPost(p *post) (err error) {
}
}
}
// Check visibility
if p.Visibility == visibilityNil {
p.Visibility = visibilityPublic
}
// Cleanup params
for pk, pvs := range p.Parameters {
pvs = lo.Filter(pvs, func(s string, _ int) bool { return s != "" })
@ -126,14 +130,15 @@ func (a *goBlog) createPost(p *post) error {
return a.createOrReplacePost(p, &postCreationOptions{new: true})
}
func (a *goBlog) replacePost(p *post, oldPath string, oldStatus postStatus) error {
return a.createOrReplacePost(p, &postCreationOptions{new: false, oldPath: oldPath, oldStatus: oldStatus})
func (a *goBlog) replacePost(p *post, oldPath string, oldStatus postStatus, oldVisibility postVisibility) error {
return a.createOrReplacePost(p, &postCreationOptions{new: false, oldPath: oldPath, oldStatus: oldStatus, oldVisibility: oldVisibility})
}
type postCreationOptions struct {
new bool
oldPath string
oldStatus postStatus
new bool
oldPath string
oldStatus postStatus
oldVisibility postVisibility
}
func (a *goBlog) createOrReplacePost(p *post, o *postCreationOptions) error {
@ -152,8 +157,8 @@ func (a *goBlog) createOrReplacePost(p *post, o *postCreationOptions) error {
return err
}
// Trigger hooks
if p.Status == statusPublished || p.Status == statusUnlisted {
if o.new || (o.oldStatus != statusPublished && o.oldStatus != statusUnlisted) {
if p.Status == statusPublished && (p.Visibility == visibilityPublic || p.Visibility == visibilityUnlisted) {
if o.new || (o.oldStatus != statusPublished && o.oldVisibility != visibilityPublic && o.oldVisibility != visibilityUnlisted) {
defer a.postPostHooks(p)
} else {
defer a.postUpdateHooks(p)
@ -183,15 +188,15 @@ func (db *database) savePost(p *post, o *postCreationOptions) error {
// Update or create post
if o.new {
// New post, create it
sqlBuilder.WriteString("insert into posts (path, content, published, updated, blog, section, status, priority) values (?, ?, ?, ?, ?, ?, ?, ?);")
sqlArgs = append(sqlArgs, p.Path, p.Content, toUTCSafe(p.Published), toUTCSafe(p.Updated), p.Blog, p.Section, p.Status, p.Priority)
sqlBuilder.WriteString("insert into posts (path, content, published, updated, blog, section, status, visibility, priority) values (?, ?, ?, ?, ?, ?, ?, ?, ?);")
sqlArgs = append(sqlArgs, p.Path, p.Content, toUTCSafe(p.Published), toUTCSafe(p.Updated), p.Blog, p.Section, p.Status, p.Visibility, p.Priority)
} else {
// Delete post parameters
sqlBuilder.WriteString("delete from post_parameters where path = ?;")
sqlArgs = append(sqlArgs, o.oldPath)
// Update old post
sqlBuilder.WriteString("update posts set path = ?, content = ?, published = ?, updated = ?, blog = ?, section = ?, status = ?, priority = ? where path = ?;")
sqlArgs = append(sqlArgs, p.Path, p.Content, toUTCSafe(p.Published), toUTCSafe(p.Updated), p.Blog, p.Section, p.Status, p.Priority, o.oldPath)
sqlBuilder.WriteString("update posts set path = ?, content = ?, published = ?, updated = ?, blog = ?, section = ?, status = ?, visibility = ?, priority = ? where path = ?;")
sqlArgs = append(sqlArgs, p.Path, p.Content, toUTCSafe(p.Published), toUTCSafe(p.Updated), p.Blog, p.Section, p.Status, p.Visibility, p.Priority, o.oldPath)
}
// Insert post parameters
for param, value := range p.Parameters {
@ -227,7 +232,7 @@ func (a *goBlog) deletePost(path string) error {
return err
}
// Post exists, check if it's already marked as deleted
if strings.HasSuffix(string(p.Status), statusDeletedSuffix) {
if p.Deleted() {
// Post is already marked as deleted, delete it from database
if _, err = a.db.Exec(
`begin; delete from posts where path = ?; insert or ignore into deleted (path) values (?); commit;`,
@ -242,7 +247,7 @@ func (a *goBlog) deletePost(path string) error {
a.deleteReactionsCache(p.Path)
} else {
// Update post status
p.Status = postStatus(string(p.Status) + statusDeletedSuffix)
p.Status = p.Status + statusDeletedSuffix
// Add parameter
deletedTime := utcNowString()
if p.Parameters == nil {
@ -279,7 +284,7 @@ func (a *goBlog) undeletePost(path string) error {
return err
}
// Post exists, update status and parameters
p.Status = postStatus(strings.TrimSuffix(string(p.Status), statusDeletedSuffix))
p.Status = postStatus(strings.TrimSuffix(string(p.Status), string(statusDeletedSuffix)))
// Remove parameter
p.Parameters["deleted"] = nil
// Update database
@ -337,8 +342,8 @@ type postsRequestConfig struct {
limit int
offset int
sections []string
status postStatus
statusse []postStatus
status []postStatus
visibility []postVisibility
taxonomy *configTaxonomy
taxonomyValue string
parameters []string // Ignores parameterValue
@ -375,13 +380,9 @@ func buildPostsQuery(c *postsRequestConfig, selection string) (query string, arg
queryBuilder.WriteString(" and path = @path")
args = append(args, sql.Named("path", c.path))
}
if c.status != "" && c.status != statusNil {
queryBuilder.WriteString(" and status = @status")
args = append(args, sql.Named("status", c.status))
}
if c.statusse != nil && len(c.statusse) > 0 {
if c.status != nil && len(c.status) > 0 {
queryBuilder.WriteString(" and status in (")
for i, status := range c.statusse {
for i, status := range c.status {
if i > 0 {
queryBuilder.WriteString(", ")
}
@ -392,6 +393,19 @@ func buildPostsQuery(c *postsRequestConfig, selection string) (query string, arg
}
queryBuilder.WriteByte(')')
}
if c.visibility != nil && len(c.visibility) > 0 {
queryBuilder.WriteString(" and visibility in (")
for i, visibility := range c.visibility {
if i > 0 {
queryBuilder.WriteString(", ")
}
named := "visibility" + strconv.Itoa(i)
queryBuilder.WriteByte('@')
queryBuilder.WriteString(named)
args = append(args, sql.Named(named, visibility))
}
queryBuilder.WriteByte(')')
}
if c.blog != "" {
queryBuilder.WriteString(" and blog = @blog")
args = append(args, sql.Named("blog", c.blog))
@ -541,28 +555,29 @@ func (d *database) loadPostParameters(posts []*post, parameters ...string) (err
func (a *goBlog) getPosts(config *postsRequestConfig) (posts []*post, err error) {
// Query posts
query, queryParams := buildPostsQuery(config, "path, coalesce(content, ''), coalesce(published, ''), coalesce(updated, ''), blog, coalesce(section, ''), status, priority")
query, queryParams := buildPostsQuery(config, "path, coalesce(content, ''), coalesce(published, ''), coalesce(updated, ''), blog, coalesce(section, ''), status, visibility, priority")
rows, err := a.db.Query(query, queryParams...)
if err != nil {
return nil, err
}
// Prepare row scanning
var path, content, published, updated, blog, section, status string
var path, content, published, updated, blog, section, status, visibility string
var priority int
for rows.Next() {
if err = rows.Scan(&path, &content, &published, &updated, &blog, &section, &status, &priority); err != nil {
if err = rows.Scan(&path, &content, &published, &updated, &blog, &section, &status, &visibility, &priority); err != nil {
return nil, err
}
// Create new post, fill and add to list
p := &post{
Path: path,
Content: content,
Published: toLocalSafe(published),
Updated: toLocalSafe(updated),
Blog: blog,
Section: section,
Status: postStatus(status),
Priority: priority,
Path: path,
Content: content,
Published: toLocalSafe(published),
Updated: toLocalSafe(updated),
Blog: blog,
Section: section,
Status: postStatus(status),
Visibility: postVisibility(visibility),
Priority: priority,
}
posts = append(posts, p)
}
@ -620,8 +635,10 @@ func (a *goBlog) getRandomPostPath(blog string) (path string, err error) {
}
func (d *database) allTaxonomyValues(blog string, taxonomy string) ([]string, error) {
// TODO: Query posts the normal way
rows, err := d.Query("select distinct value from post_parameters where parameter = @tax and length(coalesce(value, '')) > 0 and path in (select path from posts where blog = @blog and status = @status) order by value", sql.Named("tax", taxonomy), sql.Named("blog", blog), sql.Named("status", statusPublished))
rows, err := d.Query(
"select distinct value from post_parameters where parameter = @tax and length(coalesce(value, '')) > 0 and path in (select path from posts where blog = @blog and status = @status and visibility = @visibility) order by value",
sql.Named("tax", taxonomy), sql.Named("blog", blog), sql.Named("status", statusPublished), sql.Named("visibility", visibilityPublic),
)
if err != nil {
return nil, err
}

@ -34,13 +34,14 @@ func Test_postsDb(t *testing.T) {
// Save post
err := app.db.savePost(&post{
Path: "/test/abc",
Content: "ABC",
Published: now,
Updated: nowPlus1Hour,
Blog: "en",
Section: "test",
Status: statusDraft,
Path: "/test/abc",
Content: "ABC",
Published: now,
Updated: nowPlus1Hour,
Blog: "en",
Section: "test",
Status: statusDraft,
Visibility: visibilityPublic,
Parameters: map[string][]string{
"title": {"Title"},
"tags": {"C", "A", "B"},
@ -65,7 +66,7 @@ func Test_postsDb(t *testing.T) {
// Check drafts
drafts, _ := app.getPosts(&postsRequestConfig{
blog: "en",
status: statusDraft,
status: []postStatus{statusDraft},
})
is.Len(drafts, 1)
@ -97,10 +98,10 @@ func Test_postsDb(t *testing.T) {
must.NoError(err)
// Check if post is marked as deleted
count, err = app.db.countPosts(&postsRequestConfig{status: statusDraft})
count, err = app.db.countPosts(&postsRequestConfig{status: []postStatus{statusDraft}})
must.NoError(err)
is.Equal(0, count)
count, err = app.db.countPosts(&postsRequestConfig{status: statusDraftDeleted})
count, err = app.db.countPosts(&postsRequestConfig{status: []postStatus{statusDraftDeleted}})
must.NoError(err)
is.Equal(1, count)
@ -115,13 +116,14 @@ func Test_postsDb(t *testing.T) {
// Save published post
err = app.db.savePost(&post{
Path: "/test/abc",
Content: "ABC",
Published: "2021-06-10 10:00:00",
Updated: "2021-06-15 10:00:00",
Blog: "en",
Section: "test",
Status: statusPublished,
Path: "/test/abc",
Content: "ABC",
Published: "2021-06-10 10:00:00",
Updated: "2021-06-15 10:00:00",
Blog: "en",
Section: "test",
Status: statusPublished,
Visibility: visibilityPublic,
Parameters: map[string][]string{
"tags": {"Test", "Blog", "A"},
},

@ -16,7 +16,7 @@ func (a *goBlog) initPostsDeleter() {
func (a *goBlog) checkDeletedPosts() {
// Get all posts with `deleted` parameter and a deleted status
postsToDelete, err := a.getPosts(&postsRequestConfig{
statusse: []postStatus{statusPublishedDeleted, statusDraftDeleted, statusPrivateDeleted, statusUnlistedDeleted, statusScheduledDeleted},
status: []postStatus{statusPublishedDeleted, statusDraftDeleted, statusScheduledDeleted},
parameter: "deleted",
})
if err != nil {

@ -142,7 +142,7 @@ func (a *goBlog) postTranslations(p *post) []*post {
}
func (p *post) isPublishedSectionPost() bool {
return p.Published != "" && p.Section != "" && p.Status == statusPublished
return p.Published != "" && p.Section != "" && p.Status == statusPublished && p.Visibility == visibilityPublic
}
func (a *goBlog) postToMfItem(p *post) *microformatItem {
@ -150,20 +150,18 @@ func (a *goBlog) postToMfItem(p *post) *microformatItem {
switch p.Status {
case statusDraft:
mfStatus = "draft"
case statusPublished, statusScheduled, statusUnlisted, statusPrivate:
case statusPublished, statusScheduled:
mfStatus = "published"
case statusPublishedDeleted, statusDraftDeleted, statusPrivateDeleted, statusUnlistedDeleted, statusScheduledDeleted:
case statusPublishedDeleted, statusDraftDeleted, statusScheduledDeleted:
mfStatus = "deleted"
}
switch p.Status {
case statusDraft, statusScheduled, statusPublished:
switch p.Visibility {
case visibilityPublic:
mfVisibility = "public"
case statusUnlisted:
case visibilityUnlisted:
mfVisibility = "unlisted"
case statusPrivate:
case visibilityPrivate:
mfVisibility = "private"
case statusPublishedDeleted, statusDraftDeleted, statusPrivateDeleted, statusUnlistedDeleted, statusScheduledDeleted:
mfVisibility = "deleted"
}
return &microformatItem{
Type: []string{"h-entry"},
@ -244,6 +242,7 @@ func (p *post) contentWithParams() string {
params["published"] = p.Published
params["updated"] = p.Updated
params["status"] = string(p.Status)
params["visibility"] = string(p.Visibility)
params["priority"] = p.Priority
pb, _ := yaml.Marshal(params)
return fmt.Sprintf("---\n%s---\n%s", string(pb), p.Content)
@ -290,5 +289,5 @@ func (p *post) TTS() string {
}
func (p *post) Deleted() bool {
return strings.HasSuffix(string(p.Status), statusDeletedSuffix)
return strings.HasSuffix(string(p.Status), string(statusDeletedSuffix))
}

@ -27,7 +27,7 @@ func (a *goBlog) startPostsScheduler() {
func (a *goBlog) checkScheduledPosts() {
postsToPublish, err := a.getPosts(&postsRequestConfig{
status: statusScheduled,
status: []postStatus{statusScheduled},
publishedBefore: time.Now(),
})
if err != nil {
@ -36,7 +36,7 @@ func (a *goBlog) checkScheduledPosts() {
}
for _, post := range postsToPublish {
post.Status = statusPublished
err := a.replacePost(post, post.Path, statusScheduled)
err := a.replacePost(post, post.Path, statusScheduled, post.Visibility)
if err != nil {
log.Println("Error publishing scheduled post:", err)
continue

@ -26,23 +26,24 @@ func Test_postsScheduler(t *testing.T) {
_ = app.initCache()
err := app.db.savePost(&post{
Path: "/test/abc",
Content: "ABC",
Published: toLocalSafe(time.Now().Add(-1 * time.Hour).String()),
Blog: "en",
Section: "test",
Status: statusScheduled,
Path: "/test/abc",
Content: "ABC",
Published: toLocalSafe(time.Now().Add(-1 * time.Hour).String()),
Blog: "en",
Section: "test",
Status: statusScheduled,
Visibility: visibilityPublic,
}, &postCreationOptions{new: true})
require.NoError(t, err)
count, err := app.db.countPosts(&postsRequestConfig{status: statusPublished})
count, err := app.db.countPosts(&postsRequestConfig{status: []postStatus{statusScheduled}})
require.NoError(t, err)
assert.Equal(t, 0, count)
assert.Equal(t, 1, count)
app.checkScheduledPosts()
count, err = app.db.countPosts(&postsRequestConfig{status: statusPublished})
count, err = app.db.countPosts(&postsRequestConfig{status: []postStatus{statusScheduled}})
require.NoError(t, err)
assert.Equal(t, 1, count)
assert.Equal(t, 0, count)
}

@ -50,7 +50,7 @@ func Test_reactionsLowLevel(t *testing.T) {
Path: "/newpost",
Content: "test",
Status: statusPublished,
}, "/testpost", statusPublished)
}, "/testpost", statusPublished, visibilityPublic)
require.NoError(t, err)
// Check if reaction count is 4

@ -155,7 +155,8 @@ func (a *goBlog) serveSitemapBlogPosts(w http.ResponseWriter, r *http.Request) {
// Request posts
blog, _ := a.getBlog(r)
posts, _ := a.getPosts(&postsRequestConfig{
status: statusPublished,
status: []postStatus{statusPublished},
visibility: []postVisibility{visibilityPublic},
blog: blog,
withoutParameters: true,
})

@ -19,7 +19,7 @@ download: "Herunterladen"
drafts: "Entwürfe"
draftsdesc: "Posts mit dem Status `draft`."
editor: "Editor"
editorpostdesc: "💡 Leere Parameter werden automatisch entfernt. Mehr mögliche Parameter: %s. Mögliche Zustände für `%s`: %s."
editorpostdesc: "💡 Leere Parameter werden automatisch entfernt. Mehr mögliche Parameter: %s. Mögliche Zustände für `%s` und `%s`: %s und %s."
emailopt: "E-Mail (optional)"
fileuses: "Datei-Verwendungen"
general: "Allgemein"
@ -50,7 +50,7 @@ posts: "Posts"
postsections: "Post-Bereiche"
prev: "Zurück"
privateposts: "Private Posts"
privatepostsdesc: "Posts mit dem Status `private`, die nur eingeloggt sichtbar sind."
privatepostsdesc: "Veröffentlichte Posts mit der Sichtbarkeit `private`, die nur eingeloggt sichtbar sind."
publishedon: "Veröffentlicht am"
replyto: "Antwort an"
scheduledposts: "Geplante Posts"
@ -74,11 +74,12 @@ translate: "Übersetzen"
translations: "Übersetzungen"
undelete: "Wiederherstellen"
unlistedposts: "Ungelistete Posts"
unlistedpostsdesc: "Posts mit dem Status `unlisted`, die nicht in Archiven angezeigt werden."
unlistedpostsdesc: "Veröffentlichte Posts mit der Sichtbarkeit `unlisted`, die nicht in Archiven angezeigt werden."
update: "Aktualisieren"
updatedon: "Aktualisiert am"
upload: "Hochladen"
view: "Anschauen"
visibility: "Sichtbarkeit"
whatistor: "Was ist Tor?"
withoutdate: "Ohne Datum"
words: "Wörter"

@ -22,7 +22,7 @@ download: "Download"
drafts: "Drafts"
draftsdesc: "Posts with status `draft`."
editor: "Editor"
editorpostdesc: "💡 Empty parameters are removed automatically. More possible parameters: %s. Possible states for `%s`: %s."
editorpostdesc: "💡 Empty parameters are removed automatically. More possible parameters: %s. Possible states for `%s` and `%s`: %s and %s."
emailopt: "Email (optional)"
feed: "Feed"
fileuses: "file uses"
@ -60,7 +60,7 @@ posts: "Posts"
postsections: "Post sections"
prev: "Previous"
privateposts: "Private posts"
privatepostsdesc: "Posts with status `private` that are visible only when logged in."
privatepostsdesc: "Published posts with visibility `private` that are visible only when logged in."
publishedon: "Published on"
replyto: "Reply to"
reverify: "Reverify"
@ -87,13 +87,14 @@ translate: "Translate"
translations: "Translations"
undelete: "Undelete"
unlistedposts: "Unlisted posts"
unlistedpostsdesc: "Posts with status `unlisted` that are not displayed in archives."
unlistedpostsdesc: "Published posts with visibility `unlisted` that are not displayed in archives."
update: "Update"
updatedon: "Updated on"
upload: "Upload"
username: "Username"
verified: "Verified"
view: "View"
visibility: "Visibility"
webmentions: "Webmentions"
websiteopt: "Website (optional)"
whatistor: "What is Tor?"

@ -22,7 +22,6 @@ download: "Descargar"
drafts: "Borradores"
draftsdesc: "Posts con status `draft` (borrador)."
editor: "Editor"
editorpostdesc: "💡 Los parámetros vacíos se eliminan automáticamente. Más parámetros posibles: %s. Variables para (estado) status `%s`: %s."
emailopt: "Email (opcional)"
feed: "Feed"
fileuses: "usos de archivo"
@ -57,7 +56,6 @@ posts: "Posts"
postsections: "Secciones de Posts"
prev: "Anterior"