Restructure the go package

This commit is contained in:
2021-12-16 21:57:05 +01:00
parent a9b5ac4df2
commit 39ef200967
26 changed files with 111 additions and 98 deletions
+63
View File
@@ -0,0 +1,63 @@
package config
import (
"strings"
"github.com/sirupsen/logrus"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
// Backend contains backend connection-specific configuration
type Backend struct {
Addr string `mapstructure:"addr"`
Secured bool `mapstructure:"secured"`
Headers map[string]string `mapstructure:"headers"`
}
// Content contains content-specific configuration
type Content struct {
Backend Backend `mapstructure:"backend"`
Key string `mapstructure:"key"`
Pinned string `mapstructure:"pinned"`
PostsPerPage int `mapstructure:"postsPerPage"`
}
// Config contains application configuration
type Config struct {
Verbose string `mapstructure:"verbose"`
Base string `mapstructure:"base"`
Addr string `mapstructure:"addr"`
Unix string `mapstructure:"unix"`
Content Content `mapstructure:"content"`
}
// initialize default values on app-start
func init() {
pflag.BoolP("version", "V", false, "get application version")
pflag.BoolP("verbose", "v", false, "enable verbose logging")
pflag.StringP("config", "c", string("frontend.yaml"), "config file path")
pflag.String("addr", "127.0.0.1:8000", "tcp addr to listen")
pflag.String("unix", "", "unix socket path to listen")
pflag.String("base", "", "http URI prefix")
pflag.StringToString("content.backend.headers", nil, "map of additional headers to send")
pflag.String("content.backend.addr", "demo.ghost.io:443", "ghost backend addr")
pflag.Bool("content.backend.secured", true, "is ghost backend secured")
pflag.String("content.key", "22444f78447824223cefc48062", "ghost content api key")
pflag.String("content.pinned", "contact", "pinned page slug")
pflag.Int("content.postsPerPage", 5, "amount of posts per page")
pflag.Parse()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
viper.BindPFlags(pflag.CommandLine)
if viper.GetBool("verbose") {
logrus.SetLevel(logrus.DebugLevel)
logrus.Debug("Verbose mode")
}
}
+25
View File
@@ -0,0 +1,25 @@
package content
import (
"fmt"
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost/data"
)
// Blog content data
type Blog struct {
_ interface{} `template:"blog.go.tmpl"`
data.Meta
Pinned []data.Post
Posts []data.Post
}
// Title returns blog content title
func (i Blog) Title() string {
return fmt.Sprintf("... %d of %d", i.Meta.Pagination.Page, i.Meta.Pagination.Pages)
}
// Description returns blog content description
func (i Blog) Description() string {
return "TODO:"
}
+18
View File
@@ -0,0 +1,18 @@
package content
// Error content data
type Error struct {
_ interface{} `template:"error.go.tmpl"`
Message string
}
// Title returns error title
func (e Error) Title() string {
return e.Message
}
// Description returns error description
func (e Error) Description() string {
return e.Message
}
+29
View File
@@ -0,0 +1,29 @@
package content
import "code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost/data"
// Index content data
type Index struct {
_ interface{} `template:"index.go.tmpl"`
data.Meta
Pinned []data.Post
Posts []data.Post
}
// Title returns index title
func (i Index) Title() string {
if len(i.Pinned) > 0 {
return i.Pinned[0].Title
}
if len(i.Posts) > 0 {
return i.Posts[0].Title
}
return "UNKNOWN:"
}
// Description returns index description
func (i Index) Description() string {
return "TODO:"
}
+44
View File
@@ -0,0 +1,44 @@
package data
//go:generate $GOPATH/bin/easyjson -pkg -no_std_marshalers
import "html/template"
// Pages are ghost pages data
//easyjson:json
type Pages struct {
Pages []Post `json:"pages"`
Meta Meta `json:"meta"`
}
// Post contains ghost post data
//easyjson:json
type Post struct {
ID string `json:"id"`
UUID string `json:"uuid"`
Title string `json:"title"`
HTML template.HTML `json:"html"`
FImage template.URL `json:"feature_image"`
}
// Meta contains ghost result metadata
//easyjson:json
type Meta struct {
Pagination Pagination `json:"pagination"`
}
// Pagination contains ghost pagination data
//easyjson:json
type Pagination struct {
Page int `json:"page"`
Limit int `json:"limit"`
Pages int `json:"pages"`
Total int `json:"total"`
}
// Posts are ghost posts data
//easyjson:json
type Posts struct {
Posts []Post `json:"posts"`
Meta Meta `json:"meta"`
}
+407
View File
@@ -0,0 +1,407 @@
// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.
package data
import (
json "encoding/json"
easyjson "github.com/mailru/easyjson"
jlexer "github.com/mailru/easyjson/jlexer"
jwriter "github.com/mailru/easyjson/jwriter"
template "html/template"
)
// suppress unused package warning
var (
_ *json.RawMessage
_ *jlexer.Lexer
_ *jwriter.Writer
_ easyjson.Marshaler
)
func easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData(in *jlexer.Lexer, out *Posts) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeFieldName(false)
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "posts":
if in.IsNull() {
in.Skip()
out.Posts = nil
} else {
in.Delim('[')
if out.Posts == nil {
if !in.IsDelim(']') {
out.Posts = make([]Post, 0, 0)
} else {
out.Posts = []Post{}
}
} else {
out.Posts = (out.Posts)[:0]
}
for !in.IsDelim(']') {
var v1 Post
(v1).UnmarshalEasyJSON(in)
out.Posts = append(out.Posts, v1)
in.WantComma()
}
in.Delim(']')
}
case "meta":
(out.Meta).UnmarshalEasyJSON(in)
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData(out *jwriter.Writer, in Posts) {
out.RawByte('{')
first := true
_ = first
{
const prefix string = ",\"posts\":"
out.RawString(prefix[1:])
if in.Posts == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
out.RawString("null")
} else {
out.RawByte('[')
for v2, v3 := range in.Posts {
if v2 > 0 {
out.RawByte(',')
}
(v3).MarshalEasyJSON(out)
}
out.RawByte(']')
}
}
{
const prefix string = ",\"meta\":"
out.RawString(prefix)
(in.Meta).MarshalEasyJSON(out)
}
out.RawByte('}')
}
// MarshalEasyJSON supports easyjson.Marshaler interface
func (v Posts) MarshalEasyJSON(w *jwriter.Writer) {
easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData(w, v)
}
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *Posts) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData(l, v)
}
func easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData1(in *jlexer.Lexer, out *Post) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeFieldName(false)
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "id":
out.ID = string(in.String())
case "uuid":
out.UUID = string(in.String())
case "title":
out.Title = string(in.String())
case "html":
out.HTML = template.HTML(in.String())
case "feature_image":
out.FImage = template.URL(in.String())
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData1(out *jwriter.Writer, in Post) {
out.RawByte('{')
first := true
_ = first
{
const prefix string = ",\"id\":"
out.RawString(prefix[1:])
out.String(string(in.ID))
}
{
const prefix string = ",\"uuid\":"
out.RawString(prefix)
out.String(string(in.UUID))
}
{
const prefix string = ",\"title\":"
out.RawString(prefix)
out.String(string(in.Title))
}
{
const prefix string = ",\"html\":"
out.RawString(prefix)
out.String(string(in.HTML))
}
{
const prefix string = ",\"feature_image\":"
out.RawString(prefix)
out.String(string(in.FImage))
}
out.RawByte('}')
}
// MarshalEasyJSON supports easyjson.Marshaler interface
func (v Post) MarshalEasyJSON(w *jwriter.Writer) {
easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData1(w, v)
}
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *Post) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData1(l, v)
}
func easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData2(in *jlexer.Lexer, out *Pagination) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeFieldName(false)
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "page":
out.Page = int(in.Int())
case "limit":
out.Limit = int(in.Int())
case "pages":
out.Pages = int(in.Int())
case "total":
out.Total = int(in.Int())
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData2(out *jwriter.Writer, in Pagination) {
out.RawByte('{')
first := true
_ = first
{
const prefix string = ",\"page\":"
out.RawString(prefix[1:])
out.Int(int(in.Page))
}
{
const prefix string = ",\"limit\":"
out.RawString(prefix)
out.Int(int(in.Limit))
}
{
const prefix string = ",\"pages\":"
out.RawString(prefix)
out.Int(int(in.Pages))
}
{
const prefix string = ",\"total\":"
out.RawString(prefix)
out.Int(int(in.Total))
}
out.RawByte('}')
}
// MarshalEasyJSON supports easyjson.Marshaler interface
func (v Pagination) MarshalEasyJSON(w *jwriter.Writer) {
easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData2(w, v)
}
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *Pagination) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData2(l, v)
}
func easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData3(in *jlexer.Lexer, out *Pages) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeFieldName(false)
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "pages":
if in.IsNull() {
in.Skip()
out.Pages = nil
} else {
in.Delim('[')
if out.Pages == nil {
if !in.IsDelim(']') {
out.Pages = make([]Post, 0, 0)
} else {
out.Pages = []Post{}
}
} else {
out.Pages = (out.Pages)[:0]
}
for !in.IsDelim(']') {
var v4 Post
(v4).UnmarshalEasyJSON(in)
out.Pages = append(out.Pages, v4)
in.WantComma()
}
in.Delim(']')
}
case "meta":
(out.Meta).UnmarshalEasyJSON(in)
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData3(out *jwriter.Writer, in Pages) {
out.RawByte('{')
first := true
_ = first
{
const prefix string = ",\"pages\":"
out.RawString(prefix[1:])
if in.Pages == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
out.RawString("null")
} else {
out.RawByte('[')
for v5, v6 := range in.Pages {
if v5 > 0 {
out.RawByte(',')
}
(v6).MarshalEasyJSON(out)
}
out.RawByte(']')
}
}
{
const prefix string = ",\"meta\":"
out.RawString(prefix)
(in.Meta).MarshalEasyJSON(out)
}
out.RawByte('}')
}
// MarshalEasyJSON supports easyjson.Marshaler interface
func (v Pages) MarshalEasyJSON(w *jwriter.Writer) {
easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData3(w, v)
}
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *Pages) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData3(l, v)
}
func easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData4(in *jlexer.Lexer, out *Meta) {
isTopLevel := in.IsStart()
if in.IsNull() {
if isTopLevel {
in.Consumed()
}
in.Skip()
return
}
in.Delim('{')
for !in.IsDelim('}') {
key := in.UnsafeFieldName(false)
in.WantColon()
if in.IsNull() {
in.Skip()
in.WantComma()
continue
}
switch key {
case "pagination":
(out.Pagination).UnmarshalEasyJSON(in)
default:
in.SkipRecursive()
}
in.WantComma()
}
in.Delim('}')
if isTopLevel {
in.Consumed()
}
}
func easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData4(out *jwriter.Writer, in Meta) {
out.RawByte('{')
first := true
_ = first
{
const prefix string = ",\"pagination\":"
out.RawString(prefix[1:])
(in.Pagination).MarshalEasyJSON(out)
}
out.RawByte('}')
}
// MarshalEasyJSON supports easyjson.Marshaler interface
func (v Meta) MarshalEasyJSON(w *jwriter.Writer) {
easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData4(w, v)
}
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
func (v *Meta) UnmarshalEasyJSON(l *jlexer.Lexer) {
easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData4(l, v)
}
+16
View File
@@ -0,0 +1,16 @@
package ghost
import (
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost/data"
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost/params"
)
// Client is the ghost backend client
type Client interface {
// GetPosts returns blog posts according to query params
GetPosts(queryParams ...params.Modifier) (posts *data.Posts, err error)
// GetPostBySlug returns a single post by its slug title and query params
GetPostBySlug(slug string, queryParams ...params.Modifier) (posts *data.Posts, err error)
// GetPageBySlug returns a single page by its slug title and query params
GetPageBySlug(slug string, queryParams ...params.Modifier) (pages *data.Pages, err error)
}
+39
View File
@@ -0,0 +1,39 @@
package params
// Params are generics query argument
type Params struct {
Limit int
Page int
}
// Modifier function takes params and makes some changes
type Modifier func(params Params) Params
// Modifiers is a list of modifier
type Modifiers []Modifier
// Apply function modifies params
func (ms Modifiers) Apply(params Params) Params {
for _, m := range ms {
params = m(params)
}
return params
}
// WithLimit modifier setups the limit
func WithLimit(limit int) Modifier {
return func(params Params) Params {
params.Limit = limit
return params
}
}
// WithPage modifier setups the page
func WithPage(page int) Modifier {
return func(params Params) Params {
params.Page = page
return params
}
}
+171
View File
@@ -0,0 +1,171 @@
package httpclient
import (
"fmt"
"strconv"
"sync"
"time"
"github.com/mailru/easyjson"
"github.com/valyala/fasthttp"
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost"
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost/data"
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost/params"
)
var _ ghost.Client = (*HTTPClient)(nil)
// Ghost content data URIs:
const (
ghostAPIPrefix = "/ghost/api/v3/"
ghostAPIGetPosts = ghostAPIPrefix + "content/posts/"
ghostAPIGetPostBySlug = ghostAPIPrefix + "content/posts/slug/%s/"
ghostAPIGetPageBySlug = ghostAPIPrefix + "content/pages/slug/%s/"
)
// HTTPClient implements the ghost http client
type HTTPClient struct {
QueryTimeout time.Duration
ContentKey string
Addr string
Secured bool
Headers map[string]string
client *fasthttp.HostClient
setupClientOnce sync.Once
}
// setupClient creates the default http client
func (g *HTTPClient) setupClient() {
g.client = &fasthttp.HostClient{
Addr: g.Addr,
IsTLS: g.Secured,
DisableHeaderNamesNormalizing: true,
DisablePathNormalizing: true,
}
}
// doQuery does the method and unmarshals the result into the easyjson Unmarshaler
func (g *HTTPClient) doQuery(path string, v easyjson.Unmarshaler, params params.Params) (err error) {
g.setupClientOnce.Do(g.setupClient)
req := fasthttp.AcquireRequest()
res := fasthttp.AcquireResponse()
defer func() {
fasthttp.ReleaseResponse(res)
fasthttp.ReleaseRequest(req)
}()
g.setupRequest(path, req)
g.applyParams(params, req)
err = g.client.DoTimeout(req, res, g.QueryTimeout)
if err != nil {
return
}
if res.StatusCode() != fasthttp.StatusOK {
return fmt.Errorf("non OK status code: %d", res.StatusCode())
}
resBytes := res.Body()
if resBytes == nil && v == nil {
return fmt.Errorf("nothing to unmarshal")
}
if resBytes == nil {
return
}
err = easyjson.Unmarshal(resBytes, v)
return
}
// setupRequest does the necessary initial configuration to the http request
func (g *HTTPClient) setupRequest(path string, req *fasthttp.Request) {
uri := req.URI()
scheme := "http"
if g.Secured {
scheme = "https"
}
uri.SetHost(g.Addr)
uri.SetPath(path)
uri.SetScheme(scheme)
uri.QueryArgs().Add("key", g.ContentKey)
for hKey, hValue := range g.Headers {
req.Header.Add(hKey, hValue)
}
}
// applyParams function additionally configure the http request using params
func (g *HTTPClient) applyParams(p params.Params, req *fasthttp.Request) (err error) {
uri := req.URI()
limit := p.Limit
if limit > 0 {
uri.QueryArgs().Add("limit", strconv.Itoa(limit))
}
page := p.Page
if page > 1 {
uri.QueryArgs().Add("page", strconv.Itoa(page))
}
return
}
// GetPageBySlug returns the only one page using slug filter
func (g *HTTPClient) GetPageBySlug(slug string, queryModifiers ...params.Modifier) (pages *data.Pages, err error) {
pages = &data.Pages{}
defaultParams := params.Params{}
method := fmt.Sprintf(ghostAPIGetPageBySlug, slug)
err = g.doQuery(method, pages, defaultParams)
if err != nil {
pages = nil
}
return
}
// GetPosts returns posts
func (g *HTTPClient) GetPosts(queryModifiers ...params.Modifier) (posts *data.Posts, err error) {
posts = &data.Posts{}
defaultParams := params.Params{}
combinedParams := params.Modifiers(queryModifiers).Apply(defaultParams)
err = g.doQuery(ghostAPIGetPosts, posts, combinedParams)
if err != nil {
posts = nil
}
return
}
// GetPostBySlug returns the only one post using slug filter
func (g *HTTPClient) GetPostBySlug(slug string, queryModifiers ...params.Modifier) (posts *data.Posts, err error) {
posts = &data.Posts{}
defaultParams := params.Params{}
combinedParams := params.Modifiers(queryModifiers).Apply(defaultParams)
method := fmt.Sprintf(ghostAPIGetPostBySlug, slug)
err = g.doQuery(method, posts, combinedParams)
if err != nil {
posts = nil
}
return
}
+30
View File
@@ -0,0 +1,30 @@
package routes
import (
routing "github.com/jackwhelpton/fasthttp-routing/v2"
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/content"
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost/params"
)
// blog handler renders blog data
func (r *Routes) blog(c *routing.Context) (err error) {
postsPerPage := r.ContentConfig.PostsPerPage
currentPage := c.QueryArgs().GetUintOrZero("page")
latestPosts, err := r.GhostClient.GetPosts(
params.WithLimit(postsPerPage),
params.WithPage(currentPage),
)
if err != nil {
return
}
blogContent := content.Blog{
Meta: latestPosts.Meta,
Posts: latestPosts.Posts,
}
return c.Write(blogContent)
}
+55
View File
@@ -0,0 +1,55 @@
package routes
import (
"fmt"
"net/http"
routing "github.com/jackwhelpton/fasthttp-routing/v2"
"github.com/sirupsen/logrus"
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/content"
)
// errorNotFound renders http error-404 template
func (r *Routes) errorNotFound(c *routing.Context) (err error) {
errorContent := content.Error{Message: "not found"}
return c.Write(errorContent)
}
// useErrorHandler is the middleware that catch handlers errors and render error template
func (r *Routes) useErrorHandler(c *routing.Context) (err error) {
worker := func() (err error) {
defer func() {
r := recover()
if r == nil {
return
}
err = routing.NewHTTPError(http.StatusInternalServerError,
fmt.Sprintf("panic:\n%v", r))
}()
err = c.Next()
return
}
err = worker()
if err == nil {
return
}
c.Abort()
logrus.Warnf("Cannot process request, %v", err)
errorContent := content.Error{
Message: err.Error(),
}
return c.Write(errorContent)
}
+44
View File
@@ -0,0 +1,44 @@
package routes
import (
"net/http"
routing "github.com/jackwhelpton/fasthttp-routing/v2"
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/content"
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost/params"
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/templates"
)
// rootRedirect redirects the root url to the index using http redirect
func (r *Routes) rootRedirect(c *routing.Context) (err error) {
c.Redirect(templates.URLIndex, http.StatusFound)
return
}
// index handler renders index data
func (r *Routes) index(c *routing.Context) (err error) {
pinnedPageSlug := r.ContentConfig.Pinned
postsPerPage := r.ContentConfig.PostsPerPage
pinnedPages, err := r.GhostClient.GetPageBySlug(pinnedPageSlug)
if err != nil {
return
}
latestPosts, err := r.GhostClient.GetPosts(params.WithLimit(postsPerPage))
if err != nil {
return
}
indexContent := content.Index{
Pinned: pinnedPages.Pages,
Meta: latestPosts.Meta,
Posts: latestPosts.Posts,
}
return c.Write(indexContent)
}
+39
View File
@@ -0,0 +1,39 @@
package routes
import (
"io"
routing "github.com/jackwhelpton/fasthttp-routing/v2"
"github.com/valyala/fasthttp"
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/templates"
)
var _ routing.DataWriter = (*TemplateWriter)(nil)
// staticWriter is thread-safe static instance of template writer
var staticWriter = &TemplateWriter{}
// TemplateWriter is the fasthttp data writer that loads and executes template using the content
type TemplateWriter struct{}
// SetHeader sets the content type to HTML since all templates are HTML
func (tw *TemplateWriter) SetHeader(rh *fasthttp.ResponseHeader) {
rh.SetContentType(routing.MIME_HTML)
}
// Write executes the template and writes result to the response writer
func (tw *TemplateWriter) Write(w io.Writer, content interface{}) error {
template := templates.GetTemplateOf(content)
return template.Execute(w, content)
}
// useTemplateWriter is the routing middleware to set the default data writer
func (r *Routes) useTemplateWriter(c *routing.Context) (err error) {
c.SetDataWriter(staticWriter)
return
}
+49
View File
@@ -0,0 +1,49 @@
package routes
import (
"sync"
routing "github.com/jackwhelpton/fasthttp-routing/v2"
"github.com/valyala/fasthttp"
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/config"
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost"
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/templates"
)
// Routes is the main handler that contains all routes handlers
type Routes struct {
GhostClient ghost.Client
ContentConfig config.Content
Base string
router *routing.Router
handler fasthttp.RequestHandler
initOnce sync.Once
}
// Handler invokes the lazy once-initializer and then does the request
func (r *Routes) Handler(ctx *fasthttp.RequestCtx) {
r.initOnce.Do(r.init)
r.handler(ctx)
}
// init has the renderer initialization
func (r *Routes) init() {
router := routing.New()
router.Use(r.useTemplateWriter)
router.Use(r.useErrorHandler)
router.NotFound(r.errorNotFound)
root := router.Group(r.Base)
root.Get(templates.URLRoot, r.rootRedirect)
root.Get(templates.URLIndex, r.index)
root.Get(templates.URLBlog, r.blog)
r.router = router
r.handler = router.HandleRequest
}
+72
View File
@@ -0,0 +1,72 @@
<!doctype html>
<html lang="en_us">
<!-- render head -->
{{ template "head.go.tmpl" . }}
<body>
<section id="this-is-blog" class="root-container blog-container">
<!-- render menu -->
<div class="menu-container">
{{ template "menu.go.tmpl" . }}
</div>
<!-- render top pagination -->
{{ if (gt .Pagination.Page 1) }}
<div class="pagination-container-top">
<ul>
{{ if (gt .Pagination.Page 1) }}
<li>
<a href="{{ getBlogURL }}?page={{ sub .Pagination.Page }}">{{ sub .Pagination.Page }}</a>
</li>
{{ end }}
<li>
{{ .Pagination.Page }} ←
</li>
{{ if (lt .Pagination.Page .Pagination.Total) }}
<li>
<a href="{{ getBlogURL }}?page={{ add .Pagination.Page }}">{{ add .Pagination.Page }}</a>
</li>
{{ end }}
</ul>
</div>
{{ end }}
<!-- render posts -->
<div class="posts-container">
{{ range .Posts }}
{{ template "post.go.tmpl" . }}
{{ end }}
</div>
<!-- render bottom pagination -->
{{ if (gt .Pagination.Total 1) }}
<div class="pagination-container-bottom">
<ul>
{{ if (gt .Pagination.Page 1) }}
<li>
<a href="{{ getBlogURL }}?page={{ sub .Pagination.Page }}">{{ sub .Pagination.Page }}</a>
</li>
{{ end }}
<li>
{{ .Pagination.Page }} ←
</li>
{{ if (lt .Pagination.Page .Pagination.Total) }}
<li>
<a href="{{ getBlogURL }}?page={{ add .Pagination.Page }}">{{ add .Pagination.Page }}</a>
</li>
{{ end }}
</ul>
</div>
{{ end }}
</section>
</body>
</html>
+82
View File
@@ -0,0 +1,82 @@
<!doctype html>
<html lang="en_us">
<!-- render head -->
{{ template "head.go.tmpl" . }}
<body>
<!-- render menu -->
<div class="root-container menu-container">
{{ template "menu.go.tmpl" . }}
</div>
<!-- render error -->
<code class="language-c">
/* $NetBSD: yes.c,v 1.5 1997/10/19 14:28:27 mrg Exp $ */
/*
* Copyright (c) 1987, 1993
* The Regents of the University of California. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* This product includes software developed by the University of
* California, Berkeley and its contributors.
* 4. Neither the name of the University nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#include <sys/cdefs.h>
#ifndef lint
__COPYRIGHT("@(#) Copyright (c) 1987, 1993\n\
The Regents of the University of California. All rights reserved.\n");
#endif /* not lint */
#ifndef lint
#if 0
static char sccsid[] = "@(#)yes.c 8.1 (Berkeley) 6/6/93";
#endif
__RCSID("$NetBSD: yes.c,v 1.5 1997/10/19 14:28:27 mrg Exp $");
#endif /* not lint */
#include <stdio.h>
int main __P((int, char **));
int
main(argc, argv)
int argc;
char **argv;
{
if (argc > 1)
for(;;)
(void)puts(argv[1]);
else for (;;)
(void)puts("y");
}
</code>
</body>
</html>
+24
View File
@@ -0,0 +1,24 @@
package templates
import "html/template"
// UseFuncs returns a func map with template helpers functions
func UseFuncs() template.FuncMap {
return template.FuncMap{
"add": func(i int) int {
return i + 1
},
"sub": func(i int) int {
return i - 1
},
"getJSAppURL": func() string {
return URLJSApp
},
"getIndexURL": func() string {
return URLIndex
},
"getBlogURL": func() string {
return URLBlog
},
}
}
+9
View File
@@ -0,0 +1,9 @@
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="description" content="{{ .Description }}"/>
<title>{{ .Title }}</title>
<script type="text/javascript" src="{{ getJSAppURL }}" async></script>
</head>
+57
View File
@@ -0,0 +1,57 @@
<!doctype html>
<html lang="en_us">
<!-- render head -->
{{ template "head.go.tmpl" . }}
<body>
<section class="root-container welcome-container">
<!-- render menu -->
<div class="menu-container">
{{ template "menu.go.tmpl" . }}
</div>
<!-- render pinned -->
<div class="pinned-container">
{{ range .Pinned }}
<div class="pinned-post">
{{ template "post.go.tmpl" . }}
</div>
{{ end }}
</div>
<div class="scroll-container">
<a href="#this-is-blog"><span></span></a>
</div>
</section>
<section id="this-is-blog" class="root-container blog-container">
<!-- render posts -->
<div class="posts-container">
{{ range .Posts }}
{{ template "post.go.tmpl" . }}
{{ end }}
</div>
<!-- render bottom pagination -->
{{ if (gt .Pagination.Total 1) }}
<div class="pagination-container-bottom">
<ul>
<li>
{{ .Pagination.Page }} ←
</li>
<li>
<a href="{{ getBlogURL }}?page={{ add .Pagination.Page }}">{{ add .Pagination.Page }}</a>
</li>
</ul>
</div>
{{ end }}
</section>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
<div class="menu">
<div class="menu-logo"><a href="{{ getIndexURL }}"><span>nikita</span>.tokarch.uk</a></div>
<ul class="menu-nav">
<li class="menu-nav-li1"><a href="#this-is-blog"><i class="bi bi-journal-arrow-down"></i> blog</a></li>
<li class="menu-nav-li2"><a class="external" target="_blank" href="https://github.com/mainnika"><i class="bi bi-github"></i> github</a></li>
<li class="menu-nav-li3"><a class="external" target="_blank" href="https://www.linkedin.com/in/mainnika"><i class="bi bi-linkedin"></i> linkedin</a></li>
<li class="menu-nav-li4"><a class="external" target="_blank" href="https://www.instagram.com/mainnika"><i class="bi bi-instagram"></i> instagram</a></li>
</ul>
</div>
+24
View File
@@ -0,0 +1,24 @@
<div class="blog-post">
<!-- render post header -->
<div class="blog-post-head">
<h1>{{ .Title }}</h1>
</div>
{{ if .FImage }}
<div class="blog-post-head-image">
<img src="{{ .FImage }}"></img>
</div>
{{ end }}
<!-- render post -->
<div class="blog-post-content">
{{ .HTML }}
</div>
<!-- render post tailer -->
<div class="blog-post-tailer">
<h3>… … …</h3>
</div>
</div>
+91
View File
@@ -0,0 +1,91 @@
package templates
import (
"bytes"
"embed"
"fmt"
"html/template"
"io/fs"
"reflect"
"github.com/sirupsen/logrus"
)
// Static go-templates source
//go:embed blog.go.tmpl
//go:embed error.go.tmpl
//go:embed head.go.tmpl
//go:embed index.go.tmpl
//go:embed menu.go.tmpl
//go:embed post.go.tmpl
var content embed.FS
// List of compiled go-templates
var Templates *template.Template
// Load embeded templates
func init() {
Templates = template.New("")
Templates.Funcs(UseFuncs())
tmplNames, err := fs.Glob(content, "*.go.tmpl")
if err != nil {
panic(err)
}
buf := bytes.NewBuffer(nil)
for _, name := range tmplNames {
buf.Reset()
tmplContent, err := content.Open(name)
if err != nil {
panic(err)
}
size, err := buf.ReadFrom(tmplContent)
if err != nil {
panic(err)
}
tmpl, err := Templates.New(name).Parse(buf.String())
if err != nil {
panic(err)
}
logrus.Debugf("Found template: %s, size:%d", tmpl.Name(), size)
}
logrus.Debugf("Templates loading complete%s", Templates.DefinedTemplates())
}
// MustLookup wraps lookup function for the root template namespace
func MustLookup(name string) *template.Template {
tmpl := Templates.Lookup(name)
if tmpl == nil {
panic(fmt.Errorf("cannot find template %s", name))
}
return tmpl
}
// GetTemplateOf returns template which is mapped to the content data
func GetTemplateOf(content interface{}) (template *template.Template) {
el := reflect.TypeOf(content)
numField := el.NumField()
for i := 0; i < numField; i++ {
field := el.Field(i)
tag := field.Tag
found, ok := tag.Lookup("template")
if !ok {
continue
}
return MustLookup(found)
}
panic(fmt.Errorf("content %v does not have a template tag", content))
}
+9
View File
@@ -0,0 +1,9 @@
package templates
const (
URLRoot = "/"
URLIndex = "/index.aspx"
URLBlog = "/blog.aspx"
URLPost = "/post.aspx"
URLJSApp = "/js-bin/app.js"
)