- commit
- d177fb9
- parent
- b5a85a0
- author
- Eric Bower
- date
- 2022-07-31 00:55:14 +0000 UTC
refactor: use post column slug for url By creating a new column for our url slugs, we can allow users to be able to change the slug. It also makes the logic around how to generate a post url easier since it is explicit inside the post record.
10 files changed,
+108,
-61
1@@ -0,0 +1,3 @@
2+ALTER TABLE posts ADD COLUMN slug character varying(255) NOT NULL DEFAULT '';
3+ALTER TABLE posts ADD CONSTRAINT unique_slug_for_user UNIQUE (user_id, cur_space, slug);
4+UPDATE posts SET slug = filename;
+7,
-2
1@@ -13,12 +13,13 @@ import (
2 )
3
4 type PostMetaData struct {
5+ Filename string
6+ Slug string
7 Text string
8 Title string
9 Description string
10 PublishAt *time.Time
11 Hidden bool
12- Filename string
13 }
14
15 type ScpFileHooks interface {
16@@ -88,9 +89,11 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
17 }
18
19 now := time.Now()
20+ slug := shared.SanitizeFileExt(filename)
21 metadata := PostMetaData{
22 Filename: filename,
23- Title: shared.SanitizeFileExt(filename),
24+ Slug: slug,
25+ Title: shared.ToUpper(slug),
26 PublishAt: &now,
27 }
28 if post != nil {
29@@ -122,6 +125,7 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
30 _, err = h.DBPool.InsertPost(
31 userID,
32 filename,
33+ metadata.Slug,
34 metadata.Title,
35 text,
36 metadata.Description,
37@@ -142,6 +146,7 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
38 logger.Infof("(%s) found, updating record", filename)
39 _, err = h.DBPool.UpdatePost(
40 post.ID,
41+ metadata.Slug,
42 metadata.Title,
43 text,
44 metadata.Description,
+10,
-10
1@@ -185,7 +185,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
2
3 postCollection := make([]PostItemData, 0, len(posts))
4 for _, post := range posts {
5- if post.Filename == "_header" {
6+ if post.Filename == "_header.txt" {
7 parsedText := pkg.ParseText(post.Text)
8 if parsedText.MetaData.Title != "" {
9 headerTxt.Title = parsedText.MetaData.Title
10@@ -199,7 +199,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
11 if len(headerTxt.Nav) > 0 {
12 headerTxt.HasItems = true
13 }
14- } else if post.Filename == "_readme" {
15+ } else if post.Filename == "_readme.txt" {
16 parsedText := pkg.ParseText(post.Text)
17 readmeTxt.Items = parsedText.Items
18 readmeTxt.ListType = parsedText.MetaData.ListType
19@@ -255,11 +255,11 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
20 subdomain := shared.GetSubdomain(r)
21 cfg := shared.GetCfg(r)
22
23- var filename string
24+ var slug string
25 if !cfg.IsSubdomains() || subdomain == "" {
26- filename, _ = url.PathUnescape(shared.GetField(r, 1))
27+ slug, _ = url.PathUnescape(shared.GetField(r, 1))
28 } else {
29- filename, _ = url.PathUnescape(shared.GetField(r, 0))
30+ slug, _ = url.PathUnescape(shared.GetField(r, 0))
31 }
32
33 dbpool := shared.GetDB(r)
34@@ -272,7 +272,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
35 return
36 }
37
38- header, _ := dbpool.FindPostWithFilename("_header", user.ID, cfg.Space)
39+ header, _ := dbpool.FindPostWithFilename("_header.txt", user.ID, cfg.Space)
40 blogName := GetBlogName(username)
41 if header != nil {
42 headerParsed := pkg.ParseText(header.Text)
43@@ -282,12 +282,12 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
44 }
45
46 var data PostPageData
47- post, err := dbpool.FindPostWithFilename(filename, user.ID, cfg.Space)
48+ post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
49 if err == nil {
50 parsedText := pkg.ParseText(post.Text)
51
52 // we need the blog name from the readme unfortunately
53- readme, err := dbpool.FindPostWithFilename("_readme", user.ID, cfg.Space)
54+ readme, err := dbpool.FindPostWithFilename("_readme.txt", user.ID, cfg.Space)
55 if err == nil {
56 readmeParsed := pkg.ParseText(readme.Text)
57 if readmeParsed.MetaData.Title != "" {
58@@ -318,7 +318,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
59 Items: parsedText.Items,
60 }
61 } else {
62- logger.Infof("post not found %s/%s", username, filename)
63+ logger.Infof("post not found %s/%s", username, slug)
64 data = PostPageData{
65 Site: *cfg.GetSiteData(),
66 PageTitle: "Post not found",
67@@ -481,7 +481,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
68 }
69
70 for _, post := range posts {
71- if post.Filename == "_header" {
72+ if post.Filename == "_header.txt" {
73 parsedText := pkg.ParseText(post.Text)
74 if parsedText.MetaData.Title != "" {
75 headerTxt.Title = parsedText.MetaData.Title
+8,
-8
1@@ -108,7 +108,7 @@ func blogHandler(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request
2
3 postCollection := make([]lists.PostItemData, 0, len(posts))
4 for _, post := range posts {
5- if post.Filename == "_header" {
6+ if post.Filename == "_header.txt" {
7 parsedText := pkg.ParseText(post.Text)
8 if parsedText.MetaData.Title != "" {
9 headerTxt.Title = parsedText.MetaData.Title
10@@ -122,7 +122,7 @@ func blogHandler(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request
11 if len(headerTxt.Nav) > 0 {
12 headerTxt.HasItems = true
13 }
14- } else if post.Filename == "_readme" {
15+ } else if post.Filename == "_readme.txt" {
16 parsedText := pkg.ParseText(post.Text)
17 readmeTxt.Items = parsedText.Items
18 readmeTxt.ListType = parsedText.MetaData.ListType
19@@ -233,7 +233,7 @@ func readHandler(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request
20
21 func postHandler(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request) {
22 username := GetField(ctx, 0)
23- filename, _ := url.PathUnescape(GetField(ctx, 1))
24+ slug, _ := url.PathUnescape(GetField(ctx, 1))
25
26 dbpool := GetDB(ctx)
27 logger := GetLogger(ctx)
28@@ -246,7 +246,7 @@ func postHandler(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request
29 return
30 }
31
32- header, _ := dbpool.FindPostWithFilename("_header", user.ID, cfg.Space)
33+ header, _ := dbpool.FindPostWithFilename("_header.txt", user.ID, cfg.Space)
34 blogName := lists.GetBlogName(username)
35 if header != nil {
36 headerParsed := pkg.ParseText(header.Text)
37@@ -255,9 +255,9 @@ func postHandler(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request
38 }
39 }
40
41- post, err := dbpool.FindPostWithFilename(filename, user.ID, cfg.Space)
42+ post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
43 if err != nil {
44- logger.Infof("post not found %s/%s", username, filename)
45+ logger.Infof("post not found %s/%s", username, slug)
46 w.WriteHeader(gemini.StatusNotFound, "post not found")
47 return
48 }
49@@ -265,7 +265,7 @@ func postHandler(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request
50 parsedText := pkg.ParseText(post.Text)
51
52 // we need the blog name from the readme unfortunately
53- readme, err := dbpool.FindPostWithFilename("_readme", user.ID, cfg.Space)
54+ readme, err := dbpool.FindPostWithFilename("_readme.txt", user.ID, cfg.Space)
55 if err == nil {
56 readmeParsed := pkg.ParseText(readme.Text)
57 if readmeParsed.MetaData.Title != "" {
58@@ -379,7 +379,7 @@ func rssBlogHandler(ctx context.Context, w gemini.ResponseWriter, r *gemini.Requ
59 }
60
61 for _, post := range posts {
62- if post.Filename == "_header" {
63+ if post.Filename == "_header.txt" {
64 parsedText := pkg.ParseText(post.Text)
65 if parsedText.MetaData.Title != "" {
66 headerTxt.Title = parsedText.MetaData.Title
+10,
-10
1@@ -211,11 +211,11 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
2 subdomain := shared.GetSubdomain(r)
3 cfg := shared.GetCfg(r)
4
5- var filename string
6+ var slug string
7 if !cfg.IsSubdomains() || subdomain == "" {
8- filename, _ = url.PathUnescape(shared.GetField(r, 1))
9+ slug, _ = url.PathUnescape(shared.GetField(r, 1))
10 } else {
11- filename, _ = url.PathUnescape(shared.GetField(r, 0))
12+ slug, _ = url.PathUnescape(shared.GetField(r, 0))
13 }
14
15 dbpool := shared.GetDB(r)
16@@ -231,7 +231,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
17 blogName := GetBlogName(username)
18
19 var data PostPageData
20- post, err := dbpool.FindPostWithFilename(filename, user.ID, cfg.Space)
21+ post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
22 if err == nil {
23 parsedText, err := ParseText(post.Filename, post.Text)
24 if err != nil {
25@@ -253,7 +253,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
26 Contents: template.HTML(parsedText),
27 }
28 } else {
29- logger.Infof("post not found %s/%s", username, filename)
30+ logger.Infof("post not found %s/%s", username, slug)
31 data = PostPageData{
32 Site: *cfg.GetSiteData(),
33 PageTitle: "Paste not found",
34@@ -288,11 +288,11 @@ func postHandlerRaw(w http.ResponseWriter, r *http.Request) {
35 subdomain := shared.GetSubdomain(r)
36 cfg := shared.GetCfg(r)
37
38- var filename string
39+ var slug string
40 if !cfg.IsSubdomains() || subdomain == "" {
41- filename, _ = url.PathUnescape(shared.GetField(r, 1))
42+ slug, _ = url.PathUnescape(shared.GetField(r, 1))
43 } else {
44- filename, _ = url.PathUnescape(shared.GetField(r, 0))
45+ slug, _ = url.PathUnescape(shared.GetField(r, 0))
46 }
47
48 dbpool := shared.GetDB(r)
49@@ -305,9 +305,9 @@ func postHandlerRaw(w http.ResponseWriter, r *http.Request) {
50 return
51 }
52
53- post, err := dbpool.FindPostWithFilename(filename, user.ID, cfg.Space)
54+ post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
55 if err != nil {
56- logger.Infof("post not found %s/%s", username, filename)
57+ logger.Infof("post not found %s/%s", username, slug)
58 http.Error(w, "post not found", http.StatusNotFound)
59 return
60 }
+2,
-0
1@@ -24,5 +24,7 @@ func (p *FileHooks) FileValidate(text string, filename string) (bool, error) {
2 }
3
4 func (p *FileHooks) FileMeta(text string, data *filehandlers.PostMetaData) error {
5+ // we want the slug to be the filename for pastes
6+ data.Slug = data.Filename
7 return nil
8 }
+15,
-15
1@@ -160,7 +160,7 @@ func blogStyleHandler(w http.ResponseWriter, r *http.Request) {
2 http.Error(w, "blog not found", http.StatusNotFound)
3 return
4 }
5- styles, err := dbpool.FindPostWithFilename("_styles", user.ID, cfg.Space)
6+ styles, err := dbpool.FindPostWithFilename("_styles.css", user.ID, cfg.Space)
7 if err != nil {
8 logger.Infof("css not found for: %s", username)
9 http.Error(w, "css not found", http.StatusNotFound)
10@@ -220,9 +220,9 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
11 hasCSS := false
12 postCollection := make([]PostItemData, 0, len(posts))
13 for _, post := range posts {
14- if post.Filename == "_styles" && len(post.Text) > 0 {
15+ if post.Filename == "_styles.css" && len(post.Text) > 0 {
16 hasCSS = true
17- } else if post.Filename == "_readme" {
18+ } else if post.Filename == "_readme.md" {
19 parsedText, err := ParseText(post.Text)
20 if err != nil {
21 logger.Error(err)
22@@ -287,11 +287,11 @@ func postRawHandler(w http.ResponseWriter, r *http.Request) {
23 subdomain := shared.GetSubdomain(r)
24 cfg := shared.GetCfg(r)
25
26- var filename string
27+ var slug string
28 if !cfg.IsSubdomains() || subdomain == "" {
29- filename, _ = url.PathUnescape(shared.GetField(r, 1))
30+ slug, _ = url.PathUnescape(shared.GetField(r, 1))
31 } else {
32- filename, _ = url.PathUnescape(shared.GetField(r, 0))
33+ slug, _ = url.PathUnescape(shared.GetField(r, 0))
34 }
35
36 dbpool := shared.GetDB(r)
37@@ -304,7 +304,7 @@ func postRawHandler(w http.ResponseWriter, r *http.Request) {
38 return
39 }
40
41- post, err := dbpool.FindPostWithFilename(filename, user.ID, cfg.Space)
42+ post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
43 if err != nil {
44 logger.Infof("post not found")
45 http.Error(w, "post not found", http.StatusNotFound)
46@@ -325,11 +325,11 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
47 subdomain := shared.GetSubdomain(r)
48 cfg := shared.GetCfg(r)
49
50- var filename string
51+ var slug string
52 if !cfg.IsSubdomains() || subdomain == "" {
53- filename, _ = url.PathUnescape(shared.GetField(r, 1))
54+ slug, _ = url.PathUnescape(shared.GetField(r, 1))
55 } else {
56- filename, _ = url.PathUnescape(shared.GetField(r, 0))
57+ slug, _ = url.PathUnescape(shared.GetField(r, 0))
58 }
59
60 dbpool := shared.GetDB(r)
61@@ -351,7 +351,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
62
63 hasCSS := false
64 var data PostPageData
65- post, err := dbpool.FindPostWithFilename(filename, user.ID, cfg.Space)
66+ post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
67 if err == nil {
68 parsedText, err := ParseText(post.Text)
69 if err != nil {
70@@ -359,7 +359,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
71 }
72
73 // we need the blog name from the readme unfortunately
74- readme, err := dbpool.FindPostWithFilename("_readme", user.ID, cfg.Space)
75+ readme, err := dbpool.FindPostWithFilename("_readme.md", user.ID, cfg.Space)
76 if err == nil {
77 readmeParsed, err := ParseText(readme.Text)
78 if err != nil {
79@@ -371,7 +371,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
80 }
81
82 // we need the blog name from the readme unfortunately
83- css, err := dbpool.FindPostWithFilename("_styles", user.ID, cfg.Space)
84+ css, err := dbpool.FindPostWithFilename("_styles.css", user.ID, cfg.Space)
85 if err == nil {
86 if len(css.Text) > 0 {
87 hasCSS = true
88@@ -414,7 +414,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
89 BlogName: blogName,
90 Contents: "Oops! we can't seem to find this post.",
91 }
92- logger.Infof("post not found %s/%s", username, filename)
93+ logger.Infof("post not found %s/%s", username, slug)
94 }
95
96 ts, err := renderTemplate(cfg, []string{
97@@ -579,7 +579,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
98 }
99
100 for _, post := range posts {
101- if post.Filename == "_readme" {
102+ if post.Filename == "_readme.md" {
103 parsedText, err := ParseText(post.Text)
104 if err != nil {
105 logger.Error(err)
1@@ -24,7 +24,11 @@ func FilenameToTitle(filename string, title string) string {
2 return title
3 }
4
5- pre := fnameRe.ReplaceAllString(title, " ")
6+ return ToUpper(title)
7+}
8+
9+func ToUpper(str string) string {
10+ pre := fnameRe.ReplaceAllString(str, " ")
11 r := []rune(pre)
12 r[0] = unicode.ToUpper(r[0])
13 return string(r)
+4,
-2
1@@ -26,6 +26,7 @@ type Post struct {
2 ID string `json:"id"`
3 UserID string `json:"user_id"`
4 Filename string `json:"filename"`
5+ Slug string `json:"slug"`
6 Title string `json:"title"`
7 Text string `json:"text"`
8 Description string `json:"description"`
9@@ -108,10 +109,11 @@ type DB interface {
10 FindPostsBeforeDate(date *time.Time, space string) ([]*Post, error)
11 FindUpdatedPostsForUser(userID string, space string) ([]*Post, error)
12 FindPostWithFilename(filename string, userID string, space string) (*Post, error)
13+ FindPostWithSlug(slug string, userID string, space string) (*Post, error)
14 FindAllPosts(pager *Pager, space string) (*Paginate[*Post], error)
15 FindAllUpdatedPosts(pager *Pager, space string) (*Paginate[*Post], error)
16- InsertPost(userID string, filename string, title string, text string, description string, publishAt *time.Time, hidden bool, space string) (*Post, error)
17- UpdatePost(postID string, title string, text string, description string, publishAt *time.Time) (*Post, error)
18+ InsertPost(userID string, filename string, slug string, title string, text string, description string, publishAt *time.Time, hidden bool, space string) (*Post, error)
19+ UpdatePost(postID string, slug string, title string, text string, description string, publishAt *time.Time) (*Post, error)
20 RemovePosts(postIDs []string) error
21
22 AddViewCount(postID string) (int, error)
+44,
-13
1@@ -30,19 +30,21 @@ const (
2 sqlSelectTotalPostsAfterDate = `SELECT count(id) FROM posts WHERE created_at >= $1 AND cur_space = $2`
3 sqlSelectUsersWithPost = `SELECT count(app_users.id) FROM app_users WHERE EXISTS (SELECT 1 FROM posts WHERE user_id = app_users.id AND cur_space = $1);`
4
5- sqlSelectPosts = `SELECT id, user_id, filename, title, text, description, created_at, publish_at, updated_at, hidden FROM posts`
6- sqlSelectPostsBeforeDate = `SELECT posts.id, user_id, filename, title, text, description, publish_at, app_users.name as username, posts.updated_at FROM posts LEFT OUTER JOIN app_users ON app_users.id = posts.user_id WHERE publish_at::date <= $1 AND cur_space = $2`
7- sqlSelectPostWithFilename = `SELECT posts.id, user_id, filename, title, text, description, publish_at, app_users.name as username, posts.updated_at FROM posts LEFT OUTER JOIN app_users ON app_users.id = posts.user_id WHERE filename = $1 AND user_id = $2 AND cur_space = $3`
8- sqlSelectPost = `SELECT posts.id, user_id, filename, title, text, description, publish_at, app_users.name as username, posts.updated_at FROM posts LEFT OUTER JOIN app_users ON app_users.id = posts.user_id WHERE posts.id = $1`
9- sqlSelectPostsForUser = `SELECT posts.id, user_id, filename, title, text, description, publish_at, app_users.name as username, posts.updated_at FROM posts LEFT OUTER JOIN app_users ON app_users.id = posts.user_id WHERE user_id = $1 AND publish_at::date <= CURRENT_DATE AND cur_space = $2 ORDER BY publish_at DESC`
10- sqlSelectUpdatedPostsForUser = `SELECT posts.id, user_id, filename, title, text, description, publish_at, app_users.name as username, posts.updated_at FROM posts LEFT OUTER JOIN app_users ON app_users.id = posts.user_id WHERE user_id = $1 AND publish_at::date <= CURRENT_DATE AND cur_space = $2 ORDER BY updated_at DESC`
11- sqlSelectAllUpdatedPosts = `SELECT posts.id, user_id, filename, title, text, description, publish_at, app_users.name as username, posts.updated_at, 0 as score FROM posts LEFT OUTER JOIN app_users ON app_users.id = posts.user_id WHERE hidden = FALSE AND publish_at::date <= CURRENT_DATE AND cur_space = $3 ORDER BY updated_at DESC LIMIT $1 OFFSET $2`
12+ sqlSelectPosts = `SELECT id, user_id, filename, slug, title, text, description, created_at, publish_at, updated_at, hidden FROM posts`
13+ sqlSelectPostsBeforeDate = `SELECT posts.id, user_id, filename, slug, title, text, description, publish_at, app_users.name as username, posts.updated_at FROM posts LEFT OUTER JOIN app_users ON app_users.id = posts.user_id WHERE publish_at::date <= $1 AND cur_space = $2`
14+ sqlSelectPostWithFilename = `SELECT posts.id, user_id, filename, slug, title, text, description, publish_at, app_users.name as username, posts.updated_at FROM posts LEFT OUTER JOIN app_users ON app_users.id = posts.user_id WHERE filename = $1 AND user_id = $2 AND cur_space = $3`
15+ sqlSelectPostWithSlug = `SELECT posts.id, user_id, filename, slug, title, text, description, publish_at, app_users.name as username, posts.updated_at FROM posts LEFT OUTER JOIN app_users ON app_users.id = posts.user_id WHERE slug = $1 AND user_id = $2 AND cur_space = $3`
16+ sqlSelectPost = `SELECT posts.id, user_id, filename, slug, title, text, description, publish_at, app_users.name as username, posts.updated_at FROM posts LEFT OUTER JOIN app_users ON app_users.id = posts.user_id WHERE posts.id = $1`
17+ sqlSelectPostsForUser = `SELECT posts.id, user_id, filename, slug, title, text, description, publish_at, app_users.name as username, posts.updated_at FROM posts LEFT OUTER JOIN app_users ON app_users.id = posts.user_id WHERE user_id = $1 AND publish_at::date <= CURRENT_DATE AND cur_space = $2 ORDER BY publish_at DESC`
18+ sqlSelectUpdatedPostsForUser = `SELECT posts.id, user_id, filename, slug, title, text, description, publish_at, app_users.name as username, posts.updated_at FROM posts LEFT OUTER JOIN app_users ON app_users.id = posts.user_id WHERE user_id = $1 AND publish_at::date <= CURRENT_DATE AND cur_space = $2 ORDER BY updated_at DESC`
19+ sqlSelectAllUpdatedPosts = `SELECT posts.id, user_id, filename, slug, title, text, description, publish_at, app_users.name as username, posts.updated_at, 0 as score FROM posts LEFT OUTER JOIN app_users ON app_users.id = posts.user_id WHERE hidden = FALSE AND publish_at::date <= CURRENT_DATE AND cur_space = $3 ORDER BY updated_at DESC LIMIT $1 OFFSET $2`
20 sqlSelectPostCount = `SELECT count(id) FROM posts WHERE hidden = FALSE AND cur_space=$1`
21 sqlSelectPostsByRank = `
22 SELECT
23 posts.id,
24 user_id,
25 filename,
26+ slug,
27 title,
28 text,
29 description,
30@@ -66,10 +68,10 @@ const (
31 LIMIT $1 OFFSET $2`
32
33 sqlInsertPublicKey = `INSERT INTO public_keys (user_id, public_key) VALUES ($1, $2)`
34- sqlInsertPost = `INSERT INTO posts (user_id, filename, title, text, description, publish_at, hidden, cur_space) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`
35+ sqlInsertPost = `INSERT INTO posts (user_id, filename, slug, title, text, description, publish_at, hidden, cur_space) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`
36 sqlInsertUser = `INSERT INTO app_users DEFAULT VALUES returning id`
37
38- sqlUpdatePost = `UPDATE posts SET title = $1, text = $2, description = $3, updated_at = $4, publish_at = $5 WHERE id = $6`
39+ sqlUpdatePost = `UPDATE posts SET slug = $1, title = $2, text = $3, description = $4, updated_at = $5, publish_at = $6 WHERE id = $7`
40 sqlUpdateUserName = `UPDATE app_users SET name = $1 WHERE id = $2`
41 sqlIncrementViews = `UPDATE posts SET views = views + 1 WHERE id = $1 RETURNING views`
42
43@@ -235,6 +237,7 @@ func (me *PsqlDB) FindPostsBeforeDate(date *time.Time, space string) ([]*db.Post
44 &post.ID,
45 &post.UserID,
46 &post.Filename,
47+ &post.Slug,
48 &post.Title,
49 &post.Text,
50 &post.Description,
51@@ -350,6 +353,29 @@ func (me *PsqlDB) FindPostWithFilename(filename string, persona_id string, space
52 &post.ID,
53 &post.UserID,
54 &post.Filename,
55+ &post.Slug,
56+ &post.Title,
57+ &post.Text,
58+ &post.Description,
59+ &post.PublishAt,
60+ &post.Username,
61+ &post.UpdatedAt,
62+ )
63+ if err != nil {
64+ return nil, err
65+ }
66+
67+ return post, nil
68+}
69+
70+func (me *PsqlDB) FindPostWithSlug(slug string, user_id string, space string) (*db.Post, error) {
71+ post := &db.Post{}
72+ r := me.Db.QueryRow(sqlSelectPostWithSlug, slug, user_id, space)
73+ err := r.Scan(
74+ &post.ID,
75+ &post.UserID,
76+ &post.Filename,
77+ &post.Slug,
78 &post.Title,
79 &post.Text,
80 &post.Description,
81@@ -371,6 +397,7 @@ func (me *PsqlDB) FindPost(postID string) (*db.Post, error) {
82 &post.ID,
83 &post.UserID,
84 &post.Filename,
85+ &post.Slug,
86 &post.Title,
87 &post.Text,
88 &post.Description,
89@@ -393,6 +420,7 @@ func (me *PsqlDB) postPager(rs *sql.Rows, pageNum int, space string) (*db.Pagina
90 &post.ID,
91 &post.UserID,
92 &post.Filename,
93+ &post.Slug,
94 &post.Title,
95 &post.Text,
96 &post.Description,
97@@ -441,9 +469,9 @@ func (me *PsqlDB) FindAllUpdatedPosts(page *db.Pager, space string) (*db.Paginat
98 return me.postPager(rs, page.Num, space)
99 }
100
101-func (me *PsqlDB) InsertPost(userID string, filename string, title string, text string, description string, publishAt *time.Time, hidden bool, space string) (*db.Post, error) {
102+func (me *PsqlDB) InsertPost(userID, filename, slug, title, text, description string, publishAt *time.Time, hidden bool, space string) (*db.Post, error) {
103 var id string
104- err := me.Db.QueryRow(sqlInsertPost, userID, filename, title, text, description, publishAt, hidden, space).Scan(&id)
105+ err := me.Db.QueryRow(sqlInsertPost, userID, filename, slug, title, text, description, publishAt, hidden, space).Scan(&id)
106 if err != nil {
107 return nil, err
108 }
109@@ -451,8 +479,8 @@ func (me *PsqlDB) InsertPost(userID string, filename string, title string, text
110 return me.FindPost(id)
111 }
112
113-func (me *PsqlDB) UpdatePost(postID string, title string, text string, description string, publishAt *time.Time) (*db.Post, error) {
114- _, err := me.Db.Exec(sqlUpdatePost, title, text, description, time.Now(), publishAt, postID)
115+func (me *PsqlDB) UpdatePost(postID, slug, title, text, description string, publishAt *time.Time) (*db.Post, error) {
116+ _, err := me.Db.Exec(sqlUpdatePost, slug, title, text, description, time.Now(), publishAt, postID)
117 if err != nil {
118 return nil, err
119 }
120@@ -478,6 +506,7 @@ func (me *PsqlDB) FindPostsForUser(userID string, space string) ([]*db.Post, err
121 &post.ID,
122 &post.UserID,
123 &post.Filename,
124+ &post.Slug,
125 &post.Title,
126 &post.Text,
127 &post.Description,
128@@ -509,6 +538,7 @@ func (me *PsqlDB) FindPosts() ([]*db.Post, error) {
129 &post.ID,
130 &post.UserID,
131 &post.Filename,
132+ &post.Slug,
133 &post.Title,
134 &post.Text,
135 &post.Description,
136@@ -541,6 +571,7 @@ func (me *PsqlDB) FindUpdatedPostsForUser(userID string, space string) ([]*db.Po
137 &post.ID,
138 &post.UserID,
139 &post.Filename,
140+ &post.Slug,
141 &post.Title,
142 &post.Text,
143 &post.Description,