Browse Source

Blog front-end, first version

Vova Tkach 6 years ago
parent
commit
8e3fa2b323

+ 193 - 0
engine/fetdata/blog.go

@@ -0,0 +1,193 @@
+package fetdata
+
+import (
+	"math"
+	"strconv"
+	"strings"
+
+	"golang-fave/engine/sqlw"
+	"golang-fave/utils"
+)
+
+func (this *FERData) postsGetCount(buf string, cat int) (int, int) {
+	if cat == 0 {
+		var num int
+		if err := this.wrap.DB.QueryRow(`
+			SELECT
+				COUNT(*)
+			FROM
+				blog_posts
+			WHERE
+				active = 1
+			;
+		`).Scan(&num); err == nil {
+			pear_page := 2
+			max_pages := int(math.Ceil(float64(num) / float64(pear_page)))
+			curr_page := 1
+			p := this.wrap.R.URL.Query().Get("p")
+			if p != "" {
+				pi, err := strconv.Atoi(p)
+				if err != nil {
+					curr_page = 1
+				} else {
+					if pi < 1 {
+						curr_page = 1
+					} else if pi > max_pages {
+						curr_page = max_pages
+					} else {
+						curr_page = pi
+					}
+				}
+			}
+			limit_offset := curr_page*pear_page - pear_page
+			return limit_offset, pear_page
+		}
+	} else {
+		var num int
+		if err := this.wrap.DB.QueryRow(`
+			SELECT
+				COUNT(blog_posts.id)
+			FROM
+				blog_posts
+				LEFT JOIN blog_cat_post_rel ON blog_cat_post_rel.post_id = blog_posts.id
+			WHERE
+				blog_posts.active = 1 AND
+				blog_cat_post_rel.category_id = ?
+			;
+		`, cat).Scan(&num); err == nil {
+			pear_page := 2
+			max_pages := int(math.Ceil(float64(num) / float64(pear_page)))
+			curr_page := 1
+			p := this.wrap.R.URL.Query().Get("p")
+			if p != "" {
+				pi, err := strconv.Atoi(p)
+				if err != nil {
+					curr_page = 1
+				} else {
+					if pi < 1 {
+						curr_page = 1
+					} else if pi > max_pages {
+						curr_page = max_pages
+					} else {
+						curr_page = pi
+					}
+				}
+			}
+			limit_offset := curr_page*pear_page - pear_page
+			return limit_offset, pear_page
+		}
+	}
+	return 0, 0
+}
+
+func (this *FERData) postsToBuffer(buf string, cat int, order string) {
+	if this.bufferPosts == nil {
+		this.bufferPosts = map[string][]*BlogPost{}
+	}
+	if _, ok := this.bufferPosts[buf]; !ok {
+		var posts []*BlogPost
+
+		limit_offset, pear_page := this.postsGetCount(buf, cat)
+
+		var rows *sqlw.Rows
+		var err error
+
+		if cat == 0 {
+			rows, err = this.wrap.DB.Query(`
+				SELECT
+					blog_posts.id,
+					blog_posts.user,
+					blog_posts.name,
+					blog_posts.alias,
+					blog_posts.content,
+					UNIX_TIMESTAMP(blog_posts.datetime) AS datetime,
+					blog_posts.active
+				FROM
+					blog_posts
+				WHERE
+					blog_posts.active = 1
+				ORDER BY
+					blog_posts.id `+order+`
+				LIMIT ?, ?;
+			`, limit_offset, pear_page)
+		} else {
+			rows, err = this.wrap.DB.Query(`
+				SELECT
+					blog_posts.id,
+					blog_posts.user,
+					blog_posts.name,
+					blog_posts.alias,
+					blog_posts.content,
+					UNIX_TIMESTAMP(blog_posts.datetime) AS datetime,
+					blog_posts.active
+				FROM
+					blog_posts
+					LEFT JOIN blog_cat_post_rel ON blog_cat_post_rel.post_id = blog_posts.id
+				WHERE
+					blog_posts.active = 1 AND
+					blog_cat_post_rel.category_id = ?
+				ORDER BY
+					blog_posts.id `+order+`
+				LIMIT ?, ?;
+			`, cat, limit_offset, pear_page)
+		}
+
+		if err == nil {
+			var f_id int
+			var f_user int
+			var f_name string
+			var f_alias string
+			var f_content string
+			var f_datetime int
+			var f_active int
+			for rows.Next() {
+				err = rows.Scan(&f_id, &f_user, &f_name, &f_alias, &f_content, &f_datetime, &f_active)
+				if err == nil {
+					posts = append(posts, &BlogPost{
+						id:       f_id,
+						user:     f_user,
+						name:     f_name,
+						alias:    f_alias,
+						content:  f_content,
+						datetime: f_datetime,
+						active:   f_active,
+					})
+				}
+			}
+			rows.Close()
+		}
+		this.bufferPosts[buf] = posts
+	}
+}
+
+func (this *FERData) BlogPosts() []*BlogPost {
+	return this.BlogPostsOrder("DESC")
+}
+
+func (this *FERData) BlogPostsOrder(order string) []*BlogPost {
+	posts_order := "DESC"
+
+	if strings.ToLower(order) == "asc" {
+		posts_order = "ASC"
+	}
+
+	buf := "posts_" + posts_order
+	this.postsToBuffer(buf, 0, posts_order)
+	return this.bufferPosts[buf]
+}
+
+func (this *FERData) BlogPostsOfCat(cat int) []*BlogPost {
+	return this.BlogPostsOfCatOrder(cat, "DESC")
+}
+
+func (this *FERData) BlogPostsOfCatOrder(cat int, order string) []*BlogPost {
+	posts_order := "DESC"
+
+	if strings.ToLower(order) == "asc" {
+		posts_order = "ASC"
+	}
+
+	buf := "posts_" + posts_order + "_" + utils.IntToStr(cat)
+	this.postsToBuffer(buf, cat, posts_order)
+	return this.bufferPosts[buf]
+}

+ 44 - 0
engine/fetdata/blog_post.go

@@ -0,0 +1,44 @@
+package fetdata
+
+import (
+	"html/template"
+	"time"
+)
+
+type BlogPost struct {
+	id       int
+	user     int
+	name     string
+	alias    string
+	content  string
+	datetime int
+	active   int
+}
+
+func (this *BlogPost) Id() int {
+	return this.id
+}
+
+func (this *BlogPost) Name() string {
+	return this.name
+}
+
+func (this *BlogPost) Alias() string {
+	return this.alias
+}
+
+func (this *BlogPost) Permalink() string {
+	return "/blog/" + this.alias + "/"
+}
+
+func (this *BlogPost) Content() template.HTML {
+	return template.HTML(this.content)
+}
+
+func (this *BlogPost) DateTime() int {
+	return this.datetime
+}
+
+func (this *BlogPost) DateTimeFormat(format string) string {
+	return time.Unix(int64(this.datetime), 0).Format(format)
+}

+ 57 - 0
engine/fetdata/content.go

@@ -7,10 +7,35 @@ import (
 	"golang-fave/utils"
 )
 
+func (this *FERData) Id() int {
+	if this.dataRow != nil {
+		if this.wrap.CurrModule == "index" {
+			return this.dataRow.(*utils.MySql_page).A_id
+		} else if this.wrap.CurrModule == "blog" {
+			if len(this.wrap.UrlArgs) >= 2 && this.wrap.UrlArgs[0] == "blog" && this.wrap.UrlArgs[1] == "category" {
+				// Blog category
+				return this.dataRow.(*utils.MySql_blog_category).A_id
+			} else {
+				// Blog post
+				return this.dataRow.(*utils.MySql_blog_posts).A_id
+			}
+		}
+	}
+	return 0
+}
+
 func (this *FERData) Name() string {
 	if this.dataRow != nil {
 		if this.wrap.CurrModule == "index" {
 			return this.dataRow.(*utils.MySql_page).A_name
+		} else if this.wrap.CurrModule == "blog" {
+			if len(this.wrap.UrlArgs) >= 2 && this.wrap.UrlArgs[0] == "blog" && this.wrap.UrlArgs[1] == "category" {
+				// Blog category
+				return this.dataRow.(*utils.MySql_blog_category).A_name
+			} else {
+				// Blog post
+				return this.dataRow.(*utils.MySql_blog_posts).A_name
+			}
 		}
 	}
 	return ""
@@ -20,6 +45,14 @@ func (this *FERData) Alias() string {
 	if this.dataRow != nil {
 		if this.wrap.CurrModule == "index" {
 			return this.dataRow.(*utils.MySql_page).A_alias
+		} else if this.wrap.CurrModule == "blog" {
+			if len(this.wrap.UrlArgs) >= 2 && this.wrap.UrlArgs[0] == "blog" && this.wrap.UrlArgs[1] == "category" {
+				// Blog category
+				return this.dataRow.(*utils.MySql_blog_category).A_alias
+			} else {
+				// Blog post
+				return this.dataRow.(*utils.MySql_blog_posts).A_alias
+			}
 		}
 	}
 	return ""
@@ -29,6 +62,14 @@ func (this *FERData) Content() template.HTML {
 	if this.dataRow != nil {
 		if this.wrap.CurrModule == "index" {
 			return template.HTML(this.dataRow.(*utils.MySql_page).A_content)
+		} else if this.wrap.CurrModule == "blog" {
+			if len(this.wrap.UrlArgs) >= 2 && this.wrap.UrlArgs[0] == "blog" && this.wrap.UrlArgs[1] == "category" {
+				// Blog category
+				return template.HTML("")
+			} else {
+				// Blog post
+				return template.HTML(this.dataRow.(*utils.MySql_blog_posts).A_content)
+			}
 		}
 	}
 	return template.HTML("")
@@ -38,6 +79,14 @@ func (this *FERData) DateTime() int {
 	if this.dataRow != nil {
 		if this.wrap.CurrModule == "index" {
 			return this.dataRow.(*utils.MySql_page).A_datetime
+		} else if this.wrap.CurrModule == "blog" {
+			if len(this.wrap.UrlArgs) >= 2 && this.wrap.UrlArgs[0] == "blog" && this.wrap.UrlArgs[1] == "category" {
+				// Blog category
+				return 0
+			} else {
+				// Blog post
+				return this.dataRow.(*utils.MySql_blog_posts).A_datetime
+			}
 		}
 	}
 	return 0
@@ -47,6 +96,14 @@ func (this *FERData) DateTimeFormat(format string) string {
 	if this.dataRow != nil {
 		if this.wrap.CurrModule == "index" {
 			return time.Unix(int64(this.dataRow.(*utils.MySql_page).A_datetime), 0).Format(format)
+		} else if this.wrap.CurrModule == "blog" {
+			if len(this.wrap.UrlArgs) >= 2 && this.wrap.UrlArgs[0] == "blog" && this.wrap.UrlArgs[1] == "category" {
+				// Blog category
+				return ""
+			} else {
+				// Blog post
+				return time.Unix(int64(this.dataRow.(*utils.MySql_blog_posts).A_datetime), 0).Format(format)
+			}
 		}
 	}
 	return ""

+ 31 - 3
engine/fetdata/fetdata.go

@@ -1,6 +1,8 @@
 package fetdata
 
 import (
+	"time"
+
 	"golang-fave/engine/wrapper"
 	"golang-fave/utils"
 )
@@ -10,7 +12,8 @@ type FERData struct {
 	dataRow interface{}
 	is404   bool
 
-	bufferUser *utils.MySql_user
+	bufferUser  *utils.MySql_user
+	bufferPosts map[string][]*BlogPost
 }
 
 func New(wrap *wrapper.Wrapper, drow interface{}, is404 bool) *FERData {
@@ -33,6 +36,31 @@ func (this *FERData) init() *FERData {
 	return this
 }
 
-func (this *FERData) Is404() bool {
-	return this.is404
+func (this *FERData) Module() string {
+	if this.is404 {
+		return "404"
+	}
+
+	var mod string
+	if this.wrap.CurrModule == "index" {
+		mod = "index"
+	} else if this.wrap.CurrModule == "blog" {
+		if len(this.wrap.UrlArgs) >= 2 && this.wrap.UrlArgs[0] == "blog" && this.wrap.UrlArgs[1] == "category" {
+			mod = "blog-category"
+		} else if len(this.wrap.UrlArgs) >= 2 && this.wrap.UrlArgs[0] == "blog" && this.wrap.UrlArgs[1] != "" {
+			mod = "blog-post"
+		} else {
+			mod = "blog"
+		}
+	}
+
+	return mod
+}
+
+func (this *FERData) CurrentDateTime() int {
+	return int(time.Now().Unix())
+}
+
+func (this *FERData) CurrentDateTimeFormat(format string) string {
+	return time.Unix(int64(time.Now().Unix()), 0).Format(format)
 }

+ 33 - 0
engine/fetdata/meta_data.go

@@ -8,6 +8,17 @@ func (this *FERData) MetaTitle() string {
 	if this.dataRow != nil {
 		if this.wrap.CurrModule == "index" {
 			return this.dataRow.(*utils.MySql_page).A_meta_title
+		} else if this.wrap.CurrModule == "blog" {
+			if len(this.wrap.UrlArgs) >= 2 && this.wrap.UrlArgs[0] == "blog" && this.wrap.UrlArgs[1] == "category" {
+				// Blog category
+				return ""
+			} else if len(this.wrap.UrlArgs) >= 2 && this.wrap.UrlArgs[0] == "blog" && this.wrap.UrlArgs[1] != "" {
+				// Blog post
+				return ""
+			} else {
+				// Blog
+				return ""
+			}
 		}
 	}
 	return ""
@@ -17,6 +28,17 @@ func (this *FERData) MetaKeywords() string {
 	if this.dataRow != nil {
 		if this.wrap.CurrModule == "index" {
 			return this.dataRow.(*utils.MySql_page).A_meta_keywords
+		} else if this.wrap.CurrModule == "blog" {
+			if len(this.wrap.UrlArgs) >= 2 && this.wrap.UrlArgs[0] == "blog" && this.wrap.UrlArgs[1] == "category" {
+				// Blog category
+				return ""
+			} else if len(this.wrap.UrlArgs) >= 2 && this.wrap.UrlArgs[0] == "blog" && this.wrap.UrlArgs[1] != "" {
+				// Blog post
+				return ""
+			} else {
+				// Blog
+				return ""
+			}
 		}
 	}
 	return ""
@@ -26,6 +48,17 @@ func (this *FERData) MetaDescription() string {
 	if this.dataRow != nil {
 		if this.wrap.CurrModule == "index" {
 			return this.dataRow.(*utils.MySql_page).A_meta_description
+		} else if this.wrap.CurrModule == "blog" {
+			if len(this.wrap.UrlArgs) >= 2 && this.wrap.UrlArgs[0] == "blog" && this.wrap.UrlArgs[1] == "category" {
+				// Blog category
+				return ""
+			} else if len(this.wrap.UrlArgs) >= 2 && this.wrap.UrlArgs[0] == "blog" && this.wrap.UrlArgs[1] != "" {
+				// Blog post
+				return ""
+			} else {
+				// Blog
+				return ""
+			}
 		}
 	}
 	return ""

+ 21 - 0
hosts/localhost/template/blog-category.html

@@ -0,0 +1,21 @@
+{{template "header.html" .}}
+<div class="card mb-4">
+	{{range $.Data.BlogPostsOfCat $.Data.Id}}
+		<div class="post">
+			<div class="card-body">
+				<h2 class="card-title">
+					<a href="{{.Permalink}}">
+						{{.Name}}
+					</a>
+				</h2>
+				<div class="post-content">
+					{{.Content}}
+				</div>
+				<div class="post-date">
+					<small>Published on {{.DateTimeFormat "02/01/2006, 15:04:05"}}</small>
+				</div>
+			</div>
+		</div>
+	{{end}}
+</div>
+{{template "footer.html" .}}

+ 13 - 0
hosts/localhost/template/blog-post.html

@@ -0,0 +1,13 @@
+{{template "header.html" .}}
+<div class="card mb-4">
+	<div class="card-body">
+		<h2 class="card-title">{{$.Data.Name}}</h2>
+		<div class="page-content">
+			{{$.Data.Content}}
+		</div>
+	</div>
+	<div class="card-footer text-muted">
+		Published on {{$.Data.DateTimeFormat "02/01/2006, 15:04:05"}}
+	</div>
+</div>
+{{template "footer.html" .}}

+ 21 - 0
hosts/localhost/template/blog.html

@@ -0,0 +1,21 @@
+{{template "header.html" .}}
+<div class="card mb-4">
+	{{range $.Data.BlogPosts}}
+		<div class="post">
+			<div class="card-body">
+				<h2 class="card-title">
+					<a href="{{.Permalink}}">
+						{{.Name}}
+					</a>
+				</h2>
+				<div class="post-content">
+					{{.Content}}
+				</div>
+				<div class="post-date">
+					<small>Published on {{.DateTimeFormat "02/01/2006, 15:04:05"}}</small>
+				</div>
+			</div>
+		</div>
+	{{end}}
+</div>
+{{template "footer.html" .}}

+ 7 - 1
hosts/localhost/template/footer.html

@@ -8,7 +8,13 @@
 		</div>
 		<footer class="bg-light py-4">
 			<div class="container">
-				<p class="m-0 text-center text-black">Copyright © Your Website 2019</p>
+				<p class="m-0 text-center text-black">
+					Copyright © Your Website {{if eq ($.Data.CurrentDateTimeFormat "2006") "2019"}}
+						{{$.Data.CurrentDateTimeFormat "2006"}}
+					{{else}}
+						2019-{{$.Data.CurrentDateTimeFormat "2006"}}
+					{{end}}
+				</p>
 			</div>
 		</footer>
 		<!-- Optional JavaScript -->

+ 36 - 7
hosts/localhost/template/header.html

@@ -9,7 +9,23 @@
 		<!-- Bootstrap CSS -->
 		<link rel="stylesheet" href="{{$.System.PathCssBootstrap}}">
 
-		<title>{{if not $.Data.Is404}}{{$.Data.MetaTitle}}{{else}}Error 404{{end}}</title>
+		<title>
+			{{if not (eq $.Data.Module "404")}}
+				{{if eq $.Data.Module "index"}}
+					{{$.Data.MetaTitle}}
+				{{else if or (eq $.Data.Module "blog") (eq $.Data.Module "blog-post") (eq $.Data.Module "blog-category")}}
+					{{if eq $.Data.Module "blog-category"}}
+						Posts of category "{{$.Data.Name}}" | Blog
+					{{else if eq $.Data.Module "blog-post"}}
+						{{$.Data.Name}} | Blog
+					{{else}}
+						Latest posts | Blog
+					{{end}}
+				{{end}}
+			{{else}}
+				Error 404
+			{{end}}
+		</title>
 		<meta name="keywords" content="{{$.Data.MetaKeywords}}" />
 		<meta name="description" content="{{$.Data.MetaDescription}}" />
 		<link rel="shortcut icon" href="{{$.System.PathIcoFav}}" type="image/x-icon" />
@@ -40,7 +56,10 @@
 								<a class="nav-link{{if eq $.Data.Alias "/about/"}} active{{end}}" href="/about/">About</a>
 							</li>
 							<li class="nav-item">
-								<a class="nav-link{{if $.Data.Is404}} active{{end}}" href="/not-existent-page/">404</a>
+								<a class="nav-link{{if eq $.Data.Module "404"}} active{{end}}" href="/not-existent-page/">404</a>
+							</li>
+							<li class="nav-item">
+								<a class="nav-link{{if or (eq $.Data.Module "blog") (eq $.Data.Module "blog-post") (eq $.Data.Module "blog-category")}} active{{end}}" href="/blog/">Blog</a>
 							</li>
 						</ul>
 					</div>
@@ -50,11 +69,21 @@
 				<div class="bg-fave">
 					<div class="container">
 						<h1 class="text-left text-white m-0 p-0 py-5">
-							{{if not $.Data.Is404}}
-								{{if eq $.Data.Alias "/"}}
-									Welcome to home page
-								{{else}}
-									Welcome to some another page
+							{{if not (eq $.Data.Module "404")}}
+								{{if eq $.Data.Module "index"}}
+									{{if eq $.Data.Alias "/"}}
+										Welcome to home page
+									{{else}}
+										Welcome to some another page
+									{{end}}
+								{{else if or (eq $.Data.Module "blog") (eq $.Data.Module "blog-post") (eq $.Data.Module "blog-category")}}
+									{{if eq $.Data.Module "blog-category"}}
+										Blog category
+									{{else if eq $.Data.Module "blog-post"}}
+										Blog post
+									{{else}}
+										Blog
+									{{end}}
 								{{end}}
 							{{else}}
 								Oops, page is not found...

+ 120 - 1
modules/module_blog.go

@@ -2,11 +2,13 @@ package modules
 
 import (
 	"html"
+	"net/http"
 	"strings"
 
 	"golang-fave/assets"
 	"golang-fave/consts"
 	"golang-fave/engine/builder"
+	"golang-fave/engine/fetdata"
 	"golang-fave/engine/sqlw"
 	"golang-fave/engine/wrapper"
 	"golang-fave/utils"
@@ -29,7 +31,118 @@ func (this *Modules) RegisterModule_Blog() *Module {
 			{Mount: "categories-add", Name: "Add new category", Show: true, Icon: assets.SysSvgIconPlus},
 			{Mount: "categories-modify", Name: "Modify category", Show: false},
 		},
-	}, nil, func(wrap *wrapper.Wrapper) (string, string, string) {
+	}, func(wrap *wrapper.Wrapper) {
+		if len(wrap.UrlArgs) == 3 && wrap.UrlArgs[0] == "blog" && wrap.UrlArgs[1] == "category" && wrap.UrlArgs[2] != "" {
+			// Blog category
+			row := &utils.MySql_blog_category{}
+			err := wrap.DB.QueryRow(`
+				SELECT
+					id,
+					user,
+					name,
+					alias,
+					lft,
+					rgt
+				FROM
+					blog_cats
+				WHERE
+					alias = ? AND
+					id > 1
+				LIMIT 1;`,
+				wrap.UrlArgs[2],
+			).Scan(
+				&row.A_id,
+				&row.A_user,
+				&row.A_name,
+				&row.A_alias,
+				&row.A_lft,
+				&row.A_rgt,
+			)
+
+			if err != nil && err != wrapper.ErrNoRows {
+				// System error 500
+				utils.SystemErrorPageEngine(wrap.W, err)
+				return
+			} else if err == wrapper.ErrNoRows {
+				// User error 404 page
+				wrap.RenderFrontEnd("404", fetdata.New(wrap, nil, true), http.StatusNotFound)
+				return
+			}
+
+			// Fix url
+			if wrap.R.URL.Path[len(wrap.R.URL.Path)-1] != '/' {
+				http.Redirect(wrap.W, wrap.R, wrap.R.URL.Path+"/"+utils.ExtractGetParams(wrap.R.RequestURI), 301)
+				return
+			}
+
+			// Render template
+			wrap.RenderFrontEnd("blog-category", fetdata.New(wrap, row, false), http.StatusOK)
+			return
+		} else if len(wrap.UrlArgs) == 2 && wrap.UrlArgs[0] == "blog" && wrap.UrlArgs[1] != "" {
+			// Blog post
+			row := &utils.MySql_blog_posts{}
+			err := wrap.DB.QueryRow(`
+				SELECT
+					id,
+					user,
+					name,
+					alias,
+					content,
+					UNIX_TIMESTAMP(datetime) as datetime,
+					active
+				FROM
+					blog_posts
+				WHERE
+					active = 1 and
+					alias = ?
+				LIMIT 1;`,
+				wrap.UrlArgs[1],
+			).Scan(
+				&row.A_id,
+				&row.A_user,
+				&row.A_name,
+				&row.A_alias,
+				&row.A_content,
+				&row.A_datetime,
+				&row.A_active,
+			)
+
+			if err != nil && err != wrapper.ErrNoRows {
+				// System error 500
+				utils.SystemErrorPageEngine(wrap.W, err)
+				return
+			} else if err == wrapper.ErrNoRows {
+				// User error 404 page
+				wrap.RenderFrontEnd("404", fetdata.New(wrap, nil, true), http.StatusNotFound)
+				return
+			}
+
+			// Fix url
+			if wrap.R.URL.Path[len(wrap.R.URL.Path)-1] != '/' {
+				http.Redirect(wrap.W, wrap.R, wrap.R.URL.Path+"/"+utils.ExtractGetParams(wrap.R.RequestURI), 301)
+				return
+			}
+
+			// Render template
+			wrap.RenderFrontEnd("blog-post", fetdata.New(wrap, row, false), http.StatusOK)
+			return
+		} else if len(wrap.UrlArgs) == 1 && wrap.UrlArgs[0] == "blog" {
+			// Blog
+
+			// Fix url
+			if wrap.R.URL.Path[len(wrap.R.URL.Path)-1] != '/' {
+				http.Redirect(wrap.W, wrap.R, wrap.R.URL.Path+"/"+utils.ExtractGetParams(wrap.R.RequestURI), 301)
+				return
+			}
+
+			// Render template
+			wrap.RenderFrontEnd("blog", fetdata.New(wrap, nil, false), http.StatusOK)
+			return
+		}
+
+		// User error 404 page
+		wrap.RenderFrontEnd("404", fetdata.New(wrap, nil, true), http.StatusNotFound)
+	}, func(wrap *wrapper.Wrapper) (string, string, string) {
 		content := ""
 		sidebar := ""
 		if wrap.CurrSubModule == "" || wrap.CurrSubModule == "default" {
@@ -143,6 +256,12 @@ func (this *Modules) RegisterModule_Blog() *Module {
 				},
 				func(values *[]string) string {
 					return builder.DataTableAction(&[]builder.DataTableActionRow{
+						{
+							Icon:   assets.SysSvgIconView,
+							Href:   `/blog/category/` + (*values)[3] + `/`,
+							Hint:   "View",
+							Target: "_blank",
+						},
 						{
 							Icon: assets.SysSvgIconEdit,
 							Href: "/cp/" + wrap.CurrModule + "/categories-modify/" + (*values)[0] + "/",