feat: public API, SEO and existing surfaces
This commit is contained in:
1 parent
45fbbc33b1
commit
c390fcfe8d
103 files changed
+4476
-231
No files matched your search
@@ -0,0 +1,13 @@
|
||||
package article
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrLanguageNotAvailable = errors.New("no published version in that language")
|
||||
|
||||
type Alternate struct {
|
||||
ID uint32 `json:"id"`
|
||||
Lang string `json:"lang"`
|
||||
StoryID uint32 `json:"story_id"`
|
||||
Slug string `json:"slug"`
|
||||
URL *string `json:"url"`
|
||||
}
|
||||
@@ -38,6 +38,7 @@ type Filters struct {
|
||||
Q string
|
||||
Status string
|
||||
Lang *string
|
||||
Langs *[]string
|
||||
AuthorID *uint32
|
||||
Sort Sort
|
||||
Unpaged bool
|
||||
@@ -101,6 +102,7 @@ type Repository interface {
|
||||
ListByStories(storyIDs []uint32) ([]Article, error)
|
||||
Get(id uint32) (Article, error)
|
||||
GetByName(name string) (Article, error)
|
||||
GetByLangSlug(siteID uint32, lang string, slug string) (Article, error)
|
||||
GetAll() ([]Article, error)
|
||||
ListForAccount(creatorID uint32, authorID *uint32) ([]Article, error)
|
||||
Create(post Article) (Article, error)
|
||||
|
||||
@@ -11,6 +11,7 @@ const (
|
||||
SEOSourceSocial = "social"
|
||||
SEOSourceCover = "cover"
|
||||
SEOSourceCanonicalURL = "canonical_url"
|
||||
SEOSourcePattern = "pattern"
|
||||
)
|
||||
|
||||
type SEOOpenGraph struct {
|
||||
@@ -22,6 +23,8 @@ type SEOOpenGraph struct {
|
||||
URL string `json:"url"`
|
||||
SiteName string `json:"site_name"`
|
||||
Locale string `json:"locale"`
|
||||
|
||||
LocaleAlternates []string `json:"locale_alternates"`
|
||||
}
|
||||
|
||||
type SEOTwitter struct {
|
||||
@@ -49,6 +52,8 @@ type SEOFallbacks struct {
|
||||
Description string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
Canonical string `json:"canonical"`
|
||||
|
||||
SiteTextSkipped bool `json:"site_text_skipped"`
|
||||
}
|
||||
|
||||
type SEOJSONLDPerson struct {
|
||||
@@ -88,4 +93,6 @@ type ResolvedSEO struct {
|
||||
ModifiedAt string `json:"modified_at"`
|
||||
Fallbacks SEOFallbacks `json:"fallbacks"`
|
||||
JSONLD SEOJSONLD `json:"json_ld"`
|
||||
Lang string `json:"lang"`
|
||||
Alternates []Alternate `json:"alternates"`
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
|
||||
var socialLocalePattern = regexp.MustCompile(`^([A-Za-z]{2,3})_([A-Za-z]{2})$`)
|
||||
|
||||
var regionPattern = regexp.MustCompile(`^[A-Za-z]{2}$`)
|
||||
|
||||
func NormalizeSocialLocale(tag string, social *string) (*string, error) {
|
||||
if social == nil {
|
||||
return nil, nil
|
||||
@@ -25,25 +27,38 @@ func NormalizeSocialLocale(tag string, social *string) (*string, error) {
|
||||
return &normalized, nil
|
||||
}
|
||||
|
||||
func SocialLocaleState(row Row) string {
|
||||
func SocialLocaleState(row Row, siteLocale *string) string {
|
||||
if row.SocialLocale != nil {
|
||||
return SocialLocaleSet
|
||||
}
|
||||
if _, region, found := strings.Cut(row.Tag, "-"); found && region != "" {
|
||||
if EffectiveSocialLocale(row, siteLocale) != nil {
|
||||
return SocialLocaleDerived
|
||||
}
|
||||
return SocialLocaleNotSet
|
||||
}
|
||||
|
||||
func EffectiveSocialLocale(row Row) *string {
|
||||
switch SocialLocaleState(row) {
|
||||
case SocialLocaleSet:
|
||||
func EffectiveSocialLocale(row Row, siteLocale *string) *string {
|
||||
if row.SocialLocale != nil {
|
||||
return row.SocialLocale
|
||||
case SocialLocaleDerived:
|
||||
}
|
||||
if _, region, found := strings.Cut(row.Tag, "-"); found && region != "" {
|
||||
derived := strings.Replace(row.Tag, "-", "_", 1)
|
||||
return &derived
|
||||
}
|
||||
return nil
|
||||
return borrowedSocialLocale(row.Tag, siteLocale)
|
||||
}
|
||||
|
||||
func borrowedSocialLocale(tag string, siteLocale *string) *string {
|
||||
if siteLocale == nil {
|
||||
return nil
|
||||
}
|
||||
tagLanguage, _, _ := strings.Cut(tag, "-")
|
||||
parts := strings.Split(*siteLocale, "-")
|
||||
if len(parts) < 2 || !strings.EqualFold(parts[0], tagLanguage) || !regionPattern.MatchString(parts[1]) {
|
||||
return nil
|
||||
}
|
||||
borrowed := strings.ToLower(parts[0]) + "_" + strings.ToUpper(parts[1])
|
||||
return &borrowed
|
||||
}
|
||||
|
||||
func normalizeEntries(entries []Entry) ([]Entry, error) {
|
||||
|
||||
@@ -49,7 +49,7 @@ func Available() []Option {
|
||||
return options
|
||||
}
|
||||
|
||||
func BuildView(rows []Row, counts map[string]VersionCount) View {
|
||||
func BuildView(rows []Row, counts map[string]VersionCount, siteLocale *string) View {
|
||||
view := View{Revision: Revision(rows), NeedsSetup: NeedsSetup(rows), Languages: []LanguageView{}, Available: Available()}
|
||||
if view.NeedsSetup {
|
||||
return view
|
||||
@@ -68,7 +68,7 @@ func BuildView(rows []Row, counts map[string]VersionCount) View {
|
||||
VersionCount: counts[row.Tag].Total,
|
||||
PublishedCount: counts[row.Tag].Published,
|
||||
SocialLocale: row.SocialLocale,
|
||||
SocialLocaleState: SocialLocaleState(row),
|
||||
SocialLocaleState: SocialLocaleState(row, siteLocale),
|
||||
})
|
||||
}
|
||||
return view
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package site
|
||||
|
||||
import "strings"
|
||||
|
||||
func (s Site) PublicURL(lang string, slug string) *string {
|
||||
if s.PublicURLPattern == nil || lang == "" {
|
||||
return nil
|
||||
}
|
||||
filled := strings.NewReplacer(LangPlaceholder, lang, SlugPlaceholder, slug).Replace(*s.PublicURLPattern)
|
||||
return &filled
|
||||
}
|
||||
|
||||
func (s Site) ArticleURL(lang string, slug string) (string, bool) {
|
||||
if localized := s.PublicURL(lang, slug); localized != nil {
|
||||
return *localized, true
|
||||
}
|
||||
return s.LegacyURL(slug), false
|
||||
}
|
||||
|
||||
func (s Site) LegacyURL(slug string) string {
|
||||
return s.BaseURL + "/" + slug
|
||||
}
|
||||
Reference in new issue
Block a user