Compare commits
7 Commits
2558a14d8c
...
07fdaa0a9e
Author | SHA1 | Date |
---|---|---|
Maxim Lebedev | 07fdaa0a9e | |
Maxim Lebedev | 0558cf72d2 | |
Maxim Lebedev | aa7cba5e14 | |
Maxim Lebedev | 7f294a1f80 | |
Maxim Lebedev | f9ab36076a | |
Maxim Lebedev | cde901b708 | |
Maxim Lebedev | d623d3a60f |
|
@ -23,9 +23,13 @@ To assign global settings to a site you need to create an `index.md` in `$HOME_C
|
|||
```markdown
|
||||
---
|
||||
# Known properties:
|
||||
title: "ACME Inc" # Site title/name
|
||||
baseUrl: "https://example.com/" # Prefix of all relative URL's
|
||||
# Site name for multilingual site, homepage title for monolingual
|
||||
title: "ACME Inc"
|
||||
baseUrl: "https://example.com/" # Prefix of all absolute URL's
|
||||
timeZone: "UTC" # Time zone name
|
||||
# Fallback for unsupported language request to / path for multilang site, base
|
||||
# language for monolingual
|
||||
defaultLanguage: "en"
|
||||
|
||||
# Any other key-value sets will be treated as secondary parameters and accessed through the `.Site.Params`:
|
||||
tokens:
|
||||
|
|
|
@ -23,9 +23,14 @@
|
|||
```markdown
|
||||
---
|
||||
# Известные параметры:
|
||||
title: "ACME Inc" # Название сайта
|
||||
baseUrl: "https://example.com/" # Префикс всех относительных URL
|
||||
# Имя сайта для мультиязычных сайтов, название домашней страницы для
|
||||
# моноязычного
|
||||
title: "ACME Inc"
|
||||
baseUrl: "https://example.com/" # Префикс для всех абсолютных URL
|
||||
timeZone: "UTC" # Имя часового пояса
|
||||
# Язык для перенаправления запросов с неподдерживаемой локализацией к пути /
|
||||
# для мультиязычных сайтов, базовый язык для моноязычных
|
||||
defaultLanguage: "en"
|
||||
|
||||
# Любые другие наборы ключей-значений будут восприниматься как второстепенные параметры и доступные через `.Site.Params`:
|
||||
tokens:
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package middleware
|
||||
|
||||
import "net/http"
|
||||
|
||||
type (
|
||||
// Interceptor intercepts an HTTP handler invocation, it is passed both
|
||||
// response writer and request which after interception can be passed
|
||||
// onto the handler function.
|
||||
Interceptor func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)
|
||||
|
||||
// HandlerFunc builds on top of http.HandlerFunc, and exposes API to
|
||||
// intercept with Interceptor. This allows building complex long chains
|
||||
// without complicated struct manipulation.
|
||||
HandlerFunc http.HandlerFunc
|
||||
|
||||
// Chain is a collection of interceptors that will be invoked in there
|
||||
// index order.
|
||||
Chain []Interceptor
|
||||
|
||||
// Skipper is a requests checker for middleware configurations. true
|
||||
// return force middleware to skip any actions with current request.
|
||||
Skipper func(r *http.Request) bool
|
||||
)
|
||||
|
||||
var DefaultSkipper Skipper = func(_ *http.Request) bool { return false }
|
||||
|
||||
// Intercept returns back a continuation that will call install middleware to
|
||||
// intercept the continuation call.
|
||||
func (hf HandlerFunc) Intercept(i Interceptor) HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
i(w, r, http.HandlerFunc(hf))
|
||||
}
|
||||
}
|
||||
|
||||
// Handler allows hooking multiple middleware in single call.
|
||||
func (c Chain) Handler(hf http.HandlerFunc) http.Handler {
|
||||
current := HandlerFunc(hf)
|
||||
|
||||
for i := len(c) - 1; i >= 0; i-- {
|
||||
current = current.Intercept(c[i])
|
||||
}
|
||||
|
||||
return http.HandlerFunc(current)
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type (
|
||||
RedirectConfig struct {
|
||||
Skipper Skipper
|
||||
Code int
|
||||
}
|
||||
|
||||
redirectLogic func(u *url.URL) (url string, ok bool)
|
||||
)
|
||||
|
||||
func Redirect(config RedirectConfig, redirect redirectLogic) Interceptor {
|
||||
if config.Skipper == nil {
|
||||
config.Skipper = DefaultSkipper
|
||||
}
|
||||
|
||||
if config.Code == 0 {
|
||||
config.Code = http.StatusMovedPermanently
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
if config.Skipper(r) {
|
||||
next(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: r.Host,
|
||||
Path: r.RequestURI,
|
||||
}
|
||||
|
||||
if r.TLS != nil {
|
||||
u.Scheme += "s"
|
||||
}
|
||||
|
||||
if target, ok := redirect(u); ok {
|
||||
http.RedirectHandler(target, config.Code).ServeHTTP(w, r)
|
||||
} else {
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,6 +33,10 @@ func (ucase *siteUseCase) Do(ctx context.Context, lang domain.Language) (*domain
|
|||
}
|
||||
}
|
||||
|
||||
if !out.IsMultiLingual() {
|
||||
out.Language = out.DefaultLanguage
|
||||
}
|
||||
|
||||
sub, err := ucase.sites.Get(ctx, lang)
|
||||
if err != nil {
|
||||
return out, nil
|
||||
|
|
203
main.go
203
main.go
|
@ -13,8 +13,10 @@ import (
|
|||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
@ -28,6 +30,7 @@ import (
|
|||
|
||||
"source.toby3d.me/toby3d/home/internal/common"
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/middleware"
|
||||
"source.toby3d.me/toby3d/home/internal/page"
|
||||
pagefsrepo "source.toby3d.me/toby3d/home/internal/page/repository/fs"
|
||||
pageucase "source.toby3d.me/toby3d/home/internal/page/usecase"
|
||||
|
@ -80,117 +83,151 @@ func NewApp(ctx context.Context, config *domain.Config) (*App, error) {
|
|||
pages := pagefsrepo.NewFileSystemPageRepository(contentDir)
|
||||
pager := pageucase.NewPageUseCase(pages, resources)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: config.AddrPort().String(),
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO(toby3d): use exists static use case or split that on static and resource modules?
|
||||
// INFO(toby3d): any static file is public and unprotected by design, so it's safe to search it
|
||||
// first before deep down to any page or it's resource which might be secured by middleware or
|
||||
// something else.
|
||||
static, err := statics.Get(r.Context(), strings.TrimPrefix(r.URL.Path, "/"))
|
||||
if err == nil {
|
||||
http.ServeContent(w, r, static.Name(), domain.ResourceModTime(static), static)
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO(toby3d): use exists static use case or split that on static and resource modules?
|
||||
// INFO(toby3d): any static file is public and unprotected by design, so it's safe to search it first
|
||||
// before deep down to any page or it's resource which might be secured by middleware or something else.
|
||||
static, err := statics.Get(r.Context(), strings.TrimPrefix(r.URL.Path, "/"))
|
||||
if err == nil {
|
||||
http.ServeContent(w, r, static.Name(), domain.ResourceModTime(static), static)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
lang := domain.LanguageUnd
|
||||
|
||||
s, err := siter.Do(r.Context(), lang)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if s.IsMultiLingual() {
|
||||
head, tail := urlutil.ShiftPath(r.URL.Path)
|
||||
|
||||
if head == "" {
|
||||
supported := make([]language.Tag, len(s.Languages))
|
||||
for i := range s.Languages {
|
||||
supported[i] = language.Make(s.Languages[i].Lang())
|
||||
}
|
||||
|
||||
if s.DefaultLanguage != domain.LanguageUnd {
|
||||
supported = append(
|
||||
[]language.Tag{language.Make(s.DefaultLanguage.Code())},
|
||||
supported...,
|
||||
)
|
||||
}
|
||||
|
||||
requested, _, err := language.ParseAcceptLanguage(
|
||||
r.Header.Get(common.HeaderAcceptLanguage))
|
||||
if err != nil || len(requested) == 0 {
|
||||
requested = append(requested, language.English)
|
||||
}
|
||||
|
||||
matched, _, _ := language.NewMatcher(supported).Match(requested...)
|
||||
lang = domain.NewLanguage(matched.String())
|
||||
|
||||
http.Redirect(w, r, "/"+lang.Lang()+"/", http.StatusSeeOther)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
lang := domain.LanguageUnd
|
||||
lang = domain.NewLanguage(head)
|
||||
r.URL.Path = tail
|
||||
}
|
||||
|
||||
s, err := siter.Do(r.Context(), lang)
|
||||
if err != nil {
|
||||
if s, err = siter.Do(r.Context(), lang); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
p, err := pager.Do(r.Context(), lang, r.URL.Path)
|
||||
if err != nil {
|
||||
if !errors.Is(err, page.ErrNotExist) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if s.IsMultiLingual() {
|
||||
head, tail := urlutil.ShiftPath(r.URL.Path)
|
||||
|
||||
if head == "" {
|
||||
supported := make([]language.Tag, len(s.Languages))
|
||||
for i := range s.Languages {
|
||||
supported[i] = language.Make(s.Languages[i].Lang())
|
||||
}
|
||||
|
||||
if s.DefaultLanguage != domain.LanguageUnd {
|
||||
supported = append(
|
||||
[]language.Tag{language.Make(s.DefaultLanguage.Code())},
|
||||
supported...,
|
||||
)
|
||||
}
|
||||
|
||||
requested, _, err := language.ParseAcceptLanguage(
|
||||
r.Header.Get(common.HeaderAcceptLanguage))
|
||||
if err != nil || len(requested) == 0 {
|
||||
requested = append(requested, language.English)
|
||||
}
|
||||
|
||||
matched, _, _ := language.NewMatcher(supported).Match(requested...)
|
||||
lang = domain.NewLanguage(matched.String())
|
||||
|
||||
http.Redirect(w, r, "/"+lang.Lang()+"/", http.StatusSeeOther)
|
||||
res, err := resourcer.Do(r.Context(), r.URL.Path)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
lang = domain.NewLanguage(head)
|
||||
r.URL.Path = tail
|
||||
}
|
||||
|
||||
if s, err = siter.Do(r.Context(), lang); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
p, err := pager.Do(r.Context(), lang, r.URL.Path)
|
||||
if err != nil {
|
||||
if !errors.Is(err, page.ErrNotExist) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.ServeContent(w, r, res.Name(), domain.ResourceModTime(res), res)
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
contentLanguage := make([]string, len(p.Translations))
|
||||
for i := range p.Translations {
|
||||
contentLanguage[i] = p.Translations[i].Language.Code()
|
||||
}
|
||||
|
||||
w.Header().Set(common.HeaderContentLanguage, strings.Join(contentLanguage, ", "))
|
||||
|
||||
tpl, err := themer.Do(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8)
|
||||
if err = tpl.Execute(w, &Context{
|
||||
Site: s,
|
||||
Page: p,
|
||||
}); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
|
||||
mw := middleware.Chain{
|
||||
middleware.Redirect(middleware.RedirectConfig{
|
||||
Code: http.StatusSeeOther,
|
||||
Skipper: func(r *http.Request) bool {
|
||||
return r.UserAgent() != "TelegramBot (like TwitterBot)"
|
||||
},
|
||||
}, func(u *url.URL) (string, bool) {
|
||||
s, err := siter.Do(ctx, domain.LanguageUnd)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if ivHash, ok := s.Params["instantViewHash"].(string); ok {
|
||||
target := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "t.me",
|
||||
Path: path.Join("/share", "url"),
|
||||
}
|
||||
|
||||
res, err := resourcer.Do(r.Context(), r.URL.Path)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
q := target.Query()
|
||||
|
||||
return
|
||||
}
|
||||
q.Set("url", u.String())
|
||||
q.Set("hash", ivHash)
|
||||
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
target.RawQuery = q.Encode()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeContent(w, r, res.Name(), domain.ResourceModTime(res), res)
|
||||
|
||||
return
|
||||
return target.String(), ok
|
||||
}
|
||||
|
||||
contentLanguage := make([]string, len(p.Translations))
|
||||
for i := range p.Translations {
|
||||
contentLanguage[i] = p.Translations[i].Language.Code()
|
||||
}
|
||||
|
||||
w.Header().Set(common.HeaderContentLanguage, strings.Join(contentLanguage, ", "))
|
||||
|
||||
tpl, err := themer.Do(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8)
|
||||
if err = tpl.Execute(w, &Context{
|
||||
Site: s,
|
||||
Page: p,
|
||||
}); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return "", false
|
||||
}),
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: config.AddrPort().String(),
|
||||
Handler: mw.Handler(handler),
|
||||
ErrorLog: logger,
|
||||
BaseContext: func(ln net.Listener) context.Context { return ctx },
|
||||
WriteTimeout: 500 * time.Millisecond,
|
||||
|
|
Loading…
Reference in New Issue