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
@@ -29,6 +29,8 @@ type MediaUsage struct {
|
||||
Title string
|
||||
Published *bool
|
||||
Fields []string
|
||||
StoryID *uint32
|
||||
Lang *string
|
||||
|
||||
ContentTypeKey string
|
||||
ContentTypeName string
|
||||
@@ -138,7 +140,8 @@ func usageFor(byID map[uint32]*MediaUsage, found article.Article) *MediaUsage {
|
||||
return entry
|
||||
}
|
||||
published := found.IsOnline
|
||||
entry := &MediaUsage{Type: UsageTypeArticle, ID: found.ID, Title: found.Title, Published: &published, Fields: []string{}}
|
||||
storyID, lang := found.StoryID, found.Lang
|
||||
entry := &MediaUsage{Type: UsageTypeArticle, ID: found.ID, Title: found.Title, Published: &published, Fields: []string{}, StoryID: &storyID, Lang: &lang}
|
||||
byID[found.ID] = entry
|
||||
return entry
|
||||
}
|
||||
@@ -173,3 +176,23 @@ func (u *GetMediaUsageUseCase) siteUsages(img image.Image) ([]MediaUsage, error)
|
||||
func articleMentionsImage(a article.Article, img image.Image) bool {
|
||||
return image.ContainsStoredName(a.Body, image.StoredName(img.Path)) || a.UsesImage(img.ID)
|
||||
}
|
||||
|
||||
type articleGroup struct {
|
||||
story uint32
|
||||
article uint32
|
||||
}
|
||||
|
||||
func groupOf(art article.Article) articleGroup {
|
||||
if art.Lang == "" || art.StoryID == 0 {
|
||||
return articleGroup{article: art.ID}
|
||||
}
|
||||
return articleGroup{story: art.StoryID}
|
||||
}
|
||||
|
||||
func distinctArticleCount(articles []article.Article) int {
|
||||
groups := map[articleGroup]bool{}
|
||||
for _, art := range articles {
|
||||
groups[groupOf(art)] = true
|
||||
}
|
||||
return len(groups)
|
||||
}
|
||||
@@ -19,17 +19,17 @@ func (g *GetSiteLanguagesUseCase) Get(key string) (localization.View, error) {
|
||||
if err != nil {
|
||||
return localization.View{}, err
|
||||
}
|
||||
return readSiteLanguages(g.localization, found.ID)
|
||||
return readSiteLanguages(g.localization, found)
|
||||
}
|
||||
|
||||
func readSiteLanguages(repository localization.Repository, siteID uint32) (localization.View, error) {
|
||||
rows, err := repository.Rows(siteID)
|
||||
func readSiteLanguages(repository localization.Repository, owner site.Site) (localization.View, error) {
|
||||
rows, err := repository.Rows(owner.ID)
|
||||
if err != nil {
|
||||
return localization.View{}, err
|
||||
}
|
||||
counts, err := repository.VersionCounts(siteID)
|
||||
counts, err := repository.VersionCounts(owner.ID)
|
||||
if err != nil {
|
||||
return localization.View{}, err
|
||||
}
|
||||
return localization.BuildView(rows, counts), nil
|
||||
return localization.BuildView(rows, counts, owner.Locale), nil
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
@@ -160,11 +161,11 @@ func (l *ListMediaUseCase) articleUsageCount(img image.Image) (int, error) {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
distinct := map[uint32]bool{}
|
||||
distinct := map[uint32]article.Article{}
|
||||
for _, found := range slices.Concat(inBody, referencing) {
|
||||
distinct[found.ID] = true
|
||||
distinct[found.ID] = found
|
||||
}
|
||||
return len(distinct), nil
|
||||
return distinctArticleCount(slices.Collect(maps.Values(distinct))), nil
|
||||
}
|
||||
|
||||
func (l *ListMediaUseCase) usageCounter() (func(image.Image) int, error) {
|
||||
@@ -177,13 +178,13 @@ func (l *ListMediaUseCase) usageCounter() (func(image.Image) int, error) {
|
||||
return nil, err
|
||||
}
|
||||
return func(img image.Image) int {
|
||||
used := siteUsage[img.ID]
|
||||
mentioning := []article.Article{}
|
||||
for _, a := range articles {
|
||||
if articleMentionsImage(a, img) {
|
||||
used++
|
||||
mentioning = append(mentioning, a)
|
||||
}
|
||||
}
|
||||
return used
|
||||
return siteUsage[img.ID] + distinctArticleCount(mentioning)
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/language"
|
||||
"RenewCMS/internal/domain/localization"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"time"
|
||||
)
|
||||
@@ -9,16 +11,18 @@ import (
|
||||
type ListSitemapUseCase struct {
|
||||
articleRepository article.Repository
|
||||
sites site.Repository
|
||||
localization localization.Repository
|
||||
}
|
||||
|
||||
func NewListSitemapUseCase(articleRepository article.Repository, sites site.Repository) *ListSitemapUseCase {
|
||||
return &ListSitemapUseCase{articleRepository: articleRepository, sites: sites}
|
||||
func NewListSitemapUseCase(articleRepository article.Repository, sites site.Repository, localizationRepository localization.Repository) *ListSitemapUseCase {
|
||||
return &ListSitemapUseCase{articleRepository: articleRepository, sites: sites, localization: localizationRepository}
|
||||
}
|
||||
|
||||
type SitemapEntry struct {
|
||||
URL string
|
||||
ModifiedAt time.Time
|
||||
Noindex bool
|
||||
Lang string
|
||||
}
|
||||
|
||||
type SitemapPage struct {
|
||||
@@ -31,10 +35,21 @@ func (u *ListSitemapUseCase) List(siteKey string, page int, size int) (SitemapPa
|
||||
if err != nil {
|
||||
return SitemapPage{}, err
|
||||
}
|
||||
rows, err := u.localization.Rows(owner.ID)
|
||||
if err != nil {
|
||||
return SitemapPage{}, err
|
||||
}
|
||||
langs := []string{language.Unassigned}
|
||||
for _, row := range rows {
|
||||
if row.Enabled {
|
||||
langs = append(langs, row.Tag)
|
||||
}
|
||||
}
|
||||
online := true
|
||||
found, err := u.articleRepository.FindPage(article.Filters{
|
||||
IsOnline: &online,
|
||||
SiteID: &owner.ID,
|
||||
Langs: &langs,
|
||||
Page: page,
|
||||
Size: size,
|
||||
Sort: article.Sort{Field: article.SortByID, Direction: article.SortAsc},
|
||||
@@ -44,7 +59,12 @@ func (u *ListSitemapUseCase) List(siteKey string, page int, size int) (SitemapPa
|
||||
}
|
||||
entries := make([]SitemapEntry, 0, len(found.Items))
|
||||
for _, item := range found.Items {
|
||||
entries = append(entries, SitemapEntry{URL: articleURL(owner, item.Slug), ModifiedAt: item.UpdatedAt, Noindex: item.Noindex})
|
||||
entries = append(entries, SitemapEntry{URL: sitemapURL(owner, item), ModifiedAt: item.UpdatedAt, Noindex: item.Noindex, Lang: item.Lang})
|
||||
}
|
||||
return SitemapPage{Entries: entries, Total: found.Total}, nil
|
||||
}
|
||||
|
||||
func sitemapURL(owner site.Site, item article.Article) string {
|
||||
url, _ := owner.ArticleURL(item.Lang, item.Slug)
|
||||
return url
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package useCases
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/localization"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"errors"
|
||||
"strings"
|
||||
@@ -26,10 +27,53 @@ type ResolveArticleSEOUseCase struct {
|
||||
articleRepository article.Repository
|
||||
sites site.Repository
|
||||
images image.Repository
|
||||
localization localization.Repository
|
||||
}
|
||||
|
||||
func NewResolveArticleSEOUseCase(articleRepository article.Repository, sites site.Repository, images image.Repository) *ResolveArticleSEOUseCase {
|
||||
return &ResolveArticleSEOUseCase{articleRepository: articleRepository, sites: sites, images: images}
|
||||
func NewResolveArticleSEOUseCase(articleRepository article.Repository, sites site.Repository, images image.Repository, localizationRepository localization.Repository) *ResolveArticleSEOUseCase {
|
||||
return &ResolveArticleSEOUseCase{articleRepository: articleRepository, sites: sites, images: images, localization: localizationRepository}
|
||||
}
|
||||
|
||||
type seoLanguage struct {
|
||||
defaultTag string
|
||||
locale string
|
||||
alternates []article.Alternate
|
||||
localeAlternates []string
|
||||
}
|
||||
|
||||
func (u *ResolveArticleSEOUseCase) languageOf(art article.Article, owner site.Site) (seoLanguage, error) {
|
||||
if art.Lang == "" {
|
||||
locale := ""
|
||||
if owner.Locale != nil {
|
||||
locale = *owner.Locale
|
||||
}
|
||||
return seoLanguage{alternates: []article.Alternate{}, localeAlternates: []string{}, locale: locale}, nil
|
||||
}
|
||||
rows, err := u.localization.Rows(art.SiteID)
|
||||
if err != nil {
|
||||
return seoLanguage{}, err
|
||||
}
|
||||
versions, err := storyVersions(u.articleRepository, art)
|
||||
if err != nil {
|
||||
return seoLanguage{}, err
|
||||
}
|
||||
siblings := eligibleSiblings(rows, versions, art)
|
||||
localeAlternates := []string{}
|
||||
for _, sibling := range siblings {
|
||||
if resolved := socialLocaleOf(rows, sibling.Lang, owner.Locale); resolved != "" {
|
||||
localeAlternates = append(localeAlternates, resolved)
|
||||
}
|
||||
}
|
||||
locale := socialLocaleOf(rows, art.Lang, owner.Locale)
|
||||
if locale == "" {
|
||||
localeAlternates = []string{}
|
||||
}
|
||||
return seoLanguage{
|
||||
defaultTag: confirmedDefault(rows),
|
||||
locale: locale,
|
||||
alternates: alternatesOf(owner, siblings),
|
||||
localeAlternates: localeAlternates,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u *ResolveArticleSEOUseCase) Resolve(id uint32, caller ContentCaller) (article.ResolvedSEO, error) {
|
||||
@@ -48,7 +92,11 @@ func (u *ResolveArticleSEOUseCase) Resolve(id uint32, caller ContentCaller) (art
|
||||
if err != nil {
|
||||
return article.ResolvedSEO{}, err
|
||||
}
|
||||
return buildResolvedSEO(art, owner, picked), nil
|
||||
spoken, err := u.languageOf(art, owner)
|
||||
if err != nil {
|
||||
return article.ResolvedSEO{}, err
|
||||
}
|
||||
return buildResolvedSEO(art, owner, picked, spoken), nil
|
||||
}
|
||||
|
||||
func (u *ResolveArticleSEOUseCase) resolveImage(art article.Article, owner site.Site) (article.SEOImage, error) {
|
||||
@@ -96,11 +144,11 @@ func resolveCanonical(art article.Article, owner site.Site) (string, string) {
|
||||
if art.CanonicalURL != "" {
|
||||
return art.CanonicalURL, article.SEOSourceCanonicalURL
|
||||
}
|
||||
return articleURL(owner, art.Slug), article.SEOSourceSite
|
||||
}
|
||||
|
||||
func articleURL(owner site.Site, slug string) string {
|
||||
return owner.BaseURL + "/" + slug
|
||||
url, patterned := owner.ArticleURL(art.Lang, art.Slug)
|
||||
if patterned {
|
||||
return url, article.SEOSourcePattern
|
||||
}
|
||||
return url, article.SEOSourceSite
|
||||
}
|
||||
|
||||
func resolveAuthors(art article.Article) []article.SEOAuthor {
|
||||
@@ -144,10 +192,12 @@ func buildJSONLD(headline string, description string, picked article.SEOImage, a
|
||||
return ld
|
||||
}
|
||||
|
||||
func buildResolvedSEO(art article.Article, owner site.Site, picked article.SEOImage) article.ResolvedSEO {
|
||||
func buildResolvedSEO(art article.Article, owner site.Site, picked article.SEOImage, spoken seoLanguage) article.ResolvedSEO {
|
||||
meta, titleSource := metaTitle(art)
|
||||
title := applyTitleTemplate(owner.TitleTemplate, meta, owner.SiteName)
|
||||
description, descriptionSource := resolveDescription(art, owner, analyzeBody(art.Body))
|
||||
siteTextAllowed := art.Lang == "" || art.Lang == spoken.defaultTag
|
||||
description, descriptionSource := resolveDescription(art, owner, analyzeBody(art.Body), siteTextAllowed)
|
||||
siteTextSkipped := descriptionSource == article.SEOSourceNone && owner.DefaultDescription != ""
|
||||
canonical, canonicalSource := resolveCanonical(art, owner)
|
||||
authors := resolveAuthors(art)
|
||||
shareTitle := firstNonEmpty(art.SocialTitle, title)
|
||||
@@ -156,10 +206,6 @@ func buildResolvedSEO(art article.Article, owner site.Site, picked article.SEOIm
|
||||
if picked.URL != "" {
|
||||
card = twitterCardLarge
|
||||
}
|
||||
locale := ""
|
||||
if owner.Locale != nil {
|
||||
locale = *owner.Locale
|
||||
}
|
||||
var published *string
|
||||
if art.IsOnline {
|
||||
formatted := formatSEOTime(art.CreatedAt)
|
||||
@@ -172,14 +218,16 @@ func buildResolvedSEO(art article.Article, owner site.Site, picked article.SEOIm
|
||||
Robots: resolveRobots(art, owner),
|
||||
OG: article.SEOOpenGraph{
|
||||
Type: openGraphArticleType, Title: shareTitle, Description: shareDescription, Image: picked.URL, ImageAlt: picked.Alt,
|
||||
URL: canonical, SiteName: owner.SiteName, Locale: locale,
|
||||
URL: canonical, SiteName: owner.SiteName, Locale: spoken.locale, LocaleAlternates: spoken.localeAlternates,
|
||||
},
|
||||
Twitter: article.SEOTwitter{Card: card, Title: shareTitle, Description: shareDescription, Image: picked.URL, ImageAlt: picked.Alt},
|
||||
Image: picked,
|
||||
Authors: authors,
|
||||
PublishedAt: published,
|
||||
ModifiedAt: formatSEOTime(art.UpdatedAt),
|
||||
Fallbacks: article.SEOFallbacks{Title: titleSource, Description: descriptionSource, Image: picked.Source, Canonical: canonicalSource},
|
||||
Fallbacks: article.SEOFallbacks{Title: titleSource, Description: descriptionSource, Image: picked.Source, Canonical: canonicalSource, SiteTextSkipped: siteTextSkipped},
|
||||
JSONLD: buildJSONLD(meta, description, picked, authors, art, canonical),
|
||||
Lang: art.Lang,
|
||||
Alternates: spoken.alternates,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/language"
|
||||
"RenewCMS/internal/domain/localization"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type ResolvePublicArticleUseCase struct {
|
||||
articles article.Repository
|
||||
sites site.Repository
|
||||
localization localization.Repository
|
||||
bodyRenderer article.BodyRenderer
|
||||
}
|
||||
|
||||
func NewResolvePublicArticleUseCase(articles article.Repository, sites site.Repository, localizationRepository localization.Repository, bodyRenderer article.BodyRenderer) *ResolvePublicArticleUseCase {
|
||||
return &ResolvePublicArticleUseCase{articles: articles, sites: sites, localization: localizationRepository, bodyRenderer: bodyRenderer}
|
||||
}
|
||||
|
||||
type PublicArticle struct {
|
||||
Article article.Article
|
||||
Viewer ContentCaller
|
||||
Context ArticleContext
|
||||
Alternates []article.Alternate
|
||||
RequestedLang string
|
||||
Fallback bool
|
||||
}
|
||||
|
||||
type PublicStoryQuery struct {
|
||||
SiteKey string
|
||||
StoryID uint32
|
||||
Lang *string
|
||||
FallbackDefault bool
|
||||
Format article.BodyFormat
|
||||
}
|
||||
|
||||
type PublicLookupQuery struct {
|
||||
SiteKey string
|
||||
Lang string
|
||||
Slug string
|
||||
Format article.BodyFormat
|
||||
}
|
||||
|
||||
func (u *ResolvePublicArticleUseCase) ByLangSlug(query PublicLookupQuery) (PublicArticle, error) {
|
||||
tag, err := language.Normalize(query.Lang)
|
||||
if errors.Is(err, language.ErrInvalidLanguage) {
|
||||
return PublicArticle{}, err
|
||||
}
|
||||
unsupported := errors.Is(err, language.ErrUnsupportedLanguage)
|
||||
owner, err := u.sites.GetByKey(query.SiteKey)
|
||||
if err != nil {
|
||||
return PublicArticle{}, err
|
||||
}
|
||||
if unsupported {
|
||||
return PublicArticle{}, article.ErrArticleNotFound
|
||||
}
|
||||
found, err := u.articles.GetByLangSlug(owner.ID, tag, query.Slug)
|
||||
if err != nil {
|
||||
return PublicArticle{}, err
|
||||
}
|
||||
if !found.IsOnline {
|
||||
return PublicArticle{}, article.ErrArticleNotFound
|
||||
}
|
||||
rows, err := u.localization.Rows(owner.ID)
|
||||
if err != nil {
|
||||
return PublicArticle{}, err
|
||||
}
|
||||
versions, err := storyVersions(u.articles, found)
|
||||
if err != nil {
|
||||
return PublicArticle{}, err
|
||||
}
|
||||
return u.present(owner, rows, versions, found, query.Format)
|
||||
}
|
||||
|
||||
func (u *ResolvePublicArticleUseCase) ByStory(query PublicStoryQuery) (PublicArticle, error) {
|
||||
requested := ""
|
||||
unsupported := false
|
||||
if query.Lang != nil {
|
||||
tag, err := language.Normalize(*query.Lang)
|
||||
if errors.Is(err, language.ErrInvalidLanguage) {
|
||||
return PublicArticle{}, err
|
||||
}
|
||||
requested = tag
|
||||
unsupported = errors.Is(err, language.ErrUnsupportedLanguage)
|
||||
}
|
||||
owner, err := u.sites.GetByKey(query.SiteKey)
|
||||
if err != nil {
|
||||
return PublicArticle{}, err
|
||||
}
|
||||
rows, err := u.localization.Rows(owner.ID)
|
||||
if err != nil {
|
||||
return PublicArticle{}, err
|
||||
}
|
||||
if unsupported {
|
||||
return PublicArticle{}, article.ErrLanguageNotAvailable
|
||||
}
|
||||
defaultTag := confirmedDefault(rows)
|
||||
if query.Lang == nil {
|
||||
requested = defaultTag
|
||||
}
|
||||
versions, err := u.articles.ListByStory(query.StoryID)
|
||||
if err != nil {
|
||||
return PublicArticle{}, err
|
||||
}
|
||||
chosen, fallback, ok := chooseStoryVersion(owner, versions, requested, defaultTag, query.FallbackDefault)
|
||||
if !ok {
|
||||
return PublicArticle{}, article.ErrLanguageNotAvailable
|
||||
}
|
||||
presented, err := u.present(owner, rows, versions, chosen, query.Format)
|
||||
if err != nil {
|
||||
return PublicArticle{}, err
|
||||
}
|
||||
presented.RequestedLang = requested
|
||||
presented.Fallback = fallback
|
||||
return presented, nil
|
||||
}
|
||||
|
||||
func chooseStoryVersion(owner site.Site, versions []article.Article, requested string, defaultTag string, fallbackDefault bool) (article.Article, bool, bool) {
|
||||
if requested == "" {
|
||||
return article.Article{}, false, false
|
||||
}
|
||||
if found, ok := publishedVersion(owner, versions, requested); ok {
|
||||
return found, false, true
|
||||
}
|
||||
if fallbackDefault && defaultTag != "" && defaultTag != requested {
|
||||
if found, ok := publishedVersion(owner, versions, defaultTag); ok {
|
||||
return found, true, true
|
||||
}
|
||||
}
|
||||
return article.Article{}, false, false
|
||||
}
|
||||
|
||||
func publishedVersion(owner site.Site, versions []article.Article, tag string) (article.Article, bool) {
|
||||
for _, version := range versions {
|
||||
if version.SiteID == owner.ID && version.Lang == tag && version.IsOnline {
|
||||
return version, true
|
||||
}
|
||||
}
|
||||
return article.Article{}, false
|
||||
}
|
||||
|
||||
func (u *ResolvePublicArticleUseCase) present(owner site.Site, rows []localization.Row, versions []article.Article, found article.Article, format article.BodyFormat) (PublicArticle, error) {
|
||||
rendered, err := renderArticle(u.bodyRenderer, found, format)
|
||||
if err != nil {
|
||||
return PublicArticle{}, err
|
||||
}
|
||||
viewer := ContentCaller{}
|
||||
described := ArticleContext{Capabilities: viewer.capabilitiesFor(found), Translations: []article.Translation{}}
|
||||
return PublicArticle{Article: rendered, Viewer: viewer, Context: described, Alternates: alternatesOf(owner, eligibleSiblings(rows, versions, found))}, nil
|
||||
}
|
||||
@@ -70,7 +70,7 @@ func (u *UpdateSiteLanguagesUseCase) Update(ctx context.Context, command UpdateS
|
||||
return err
|
||||
}
|
||||
}
|
||||
view, err = readSiteLanguages(tx.Localization, found.ID)
|
||||
view, err = readSiteLanguages(tx.Localization, found)
|
||||
return err
|
||||
})
|
||||
if errors.Is(err, transaction.ErrLockContention) {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/localization"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"slices"
|
||||
)
|
||||
|
||||
func confirmedDefault(rows []localization.Row) string {
|
||||
if localization.NeedsSetup(rows) {
|
||||
return ""
|
||||
}
|
||||
return localization.DefaultTag(rows)
|
||||
}
|
||||
|
||||
func eligibleSiblings(rows []localization.Row, versions []article.Article, served article.Article) []article.Article {
|
||||
defaultTag := confirmedDefault(rows)
|
||||
siblings := []article.Article{}
|
||||
for _, version := range versions {
|
||||
if version.ID == served.ID || version.SiteID != served.SiteID || !version.IsOnline || version.Lang == "" {
|
||||
continue
|
||||
}
|
||||
if localization.EnabledTag(rows, version.Lang) {
|
||||
siblings = append(siblings, version)
|
||||
}
|
||||
}
|
||||
slices.SortFunc(siblings, func(left article.Article, right article.Article) int {
|
||||
return localization.CompareTags(defaultTag, left.Lang, right.Lang)
|
||||
})
|
||||
return siblings
|
||||
}
|
||||
|
||||
func storyVersions(repository article.Repository, art article.Article) ([]article.Article, error) {
|
||||
if art.StoryID == 0 {
|
||||
return []article.Article{}, nil
|
||||
}
|
||||
return repository.ListByStory(art.StoryID)
|
||||
}
|
||||
|
||||
func alternatesOf(owner site.Site, siblings []article.Article) []article.Alternate {
|
||||
alternates := make([]article.Alternate, 0, len(siblings))
|
||||
for _, sibling := range siblings {
|
||||
alternates = append(alternates, article.Alternate{ID: sibling.ID, Lang: sibling.Lang, StoryID: sibling.StoryID, Slug: sibling.Slug, URL: owner.PublicURL(sibling.Lang, sibling.Slug)})
|
||||
}
|
||||
return alternates
|
||||
}
|
||||
|
||||
func rowOf(rows []localization.Row, tag string) localization.Row {
|
||||
for _, row := range rows {
|
||||
if row.Tag == tag {
|
||||
return row
|
||||
}
|
||||
}
|
||||
return localization.Row{Tag: tag}
|
||||
}
|
||||
|
||||
func socialLocaleOf(rows []localization.Row, tag string, siteLocale *string) string {
|
||||
if resolved := localization.EffectiveSocialLocale(rowOf(rows, tag), siteLocale); resolved != nil {
|
||||
return *resolved
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -15,7 +15,9 @@ func metaTitle(art article.Article) (string, string) {
|
||||
return art.Title, article.SEOSourceTitle
|
||||
}
|
||||
|
||||
func resolveDescription(art article.Article, owner site.Site, facts bodyFacts) (string, string) {
|
||||
const withSiteText = true
|
||||
|
||||
func resolveDescription(art article.Article, owner site.Site, facts bodyFacts, siteTextAllowed bool) (string, string) {
|
||||
if art.SEODescription != "" {
|
||||
return art.SEODescription, article.SEOSourceSEODescription
|
||||
}
|
||||
@@ -25,7 +27,7 @@ func resolveDescription(art article.Article, owner site.Site, facts bodyFacts) (
|
||||
if sentence := firstSentence(facts.spacedText); sentence != "" {
|
||||
return sentence, article.SEOSourceBody
|
||||
}
|
||||
if owner.DefaultDescription != "" {
|
||||
if siteTextAllowed && owner.DefaultDescription != "" {
|
||||
return owner.DefaultDescription, article.SEOSourceSite
|
||||
}
|
||||
return "", article.SEOSourceNone
|
||||
@@ -45,7 +47,7 @@ func rawSuggestions(art article.Article, owner site.Site, facts bodyFacts) []art
|
||||
if strings.TrimSpace(title) == "" {
|
||||
suggestions = append(suggestions, suggestion(article.DismissTitleEmpty, article.SEOFieldTitle, map[string]int{}))
|
||||
}
|
||||
description, _ := resolveDescription(art, owner, facts)
|
||||
description, _ := resolveDescription(art, owner, facts, withSiteText)
|
||||
descriptionRunes := utf8.RuneCountInString(description)
|
||||
if descriptionRunes < article.SEODescriptionMinRunes {
|
||||
suggestions = append(suggestions, suggestion(article.DismissDescShort, article.SEOFieldDescription, map[string]int{"length": descriptionRunes, "min": article.SEODescriptionMinRunes}))
|
||||
|
||||
Reference in new issue
Block a user