feat: JSON-LD graph, breadcrumbs and hreflang in the resolve answer (tracer)

- classes/schema builds one @graph per page (WebPage, article, profile,
  service, BreadcrumbList, VideoObject, WebSite, publisher) linked by @id
- breadcrumb trail through the primary parent chain, hreflang alternates
  with x-default, html_lang and og:locale in the resolve answer
- migration 202610280004 adds the publisher, local business, home crumb
  and pagination settings columns
This commit is contained in:
Jakub Zych
2026-11-04 14:06:57 +01:00
parent df93c86001
commit ea2ce24954
12 changed files with 1676 additions and 2 deletions

View File

@@ -63,6 +63,23 @@ type VerifyFile struct {
Body string `json:"body"`
}
// The publisher types (D-45) and the page address forms (D-40).
const (
PublisherOrganization = "organization"
PublisherPerson = "person"
PageFormQuery = "query"
PageFormPath = "path"
DefaultLocalType = "LocalBusiness"
)
// OpeningHours is one opening-hours row of a local business: a day (mon
// … sun) and its opening and closing time (24-hour HH:MM).
type OpeningHours struct {
Day string `json:"day"`
Opens string `json:"opens"`
Closes string `json:"closes"`
}
// DefaultQueryParams is the seeded D-07 list.
func DefaultQueryParams() []QueryParam {
return []QueryParam{
@@ -105,6 +122,38 @@ type Settings struct {
LinksNewTab bool `gorm:"column:links_new_tab"`
NofollowDomains string `gorm:"column:nofollow_domains"`
SponsoredDomains string `gorm:"column:sponsored_domains"`
// BreadcrumbHome is the home crumb's name (translatable; empty: the
// per-locale default copy).
BreadcrumbHome string `gorm:"column:breadcrumb_home"`
// PageForm is the paginated address form, query (?page=N) or path
// (/<segment>/N); PageSegment the path segment, translatable per
// locale; the suffixes are added on page 2 and later (D-40, D-41).
PageForm string `gorm:"column:page_form"`
PageSegment string `gorm:"column:page_segment"`
PageTitleSuffix string `gorm:"column:page_title_suffix"`
PageDescriptionSuffix string `gorm:"column:page_description_suffix"`
// The publisher of the site (D-45): organization or person, its name
// (translatable; empty: the site name), logo or photo, website, email,
// phone and sameAs profiles (one https URL per line).
PublisherType string `gorm:"column:publisher_type"`
PublisherName string `gorm:"column:publisher_name"`
PublisherLogo string `gorm:"column:publisher_logo"`
PublisherURL string `gorm:"column:publisher_url"`
PublisherEmail string `gorm:"column:publisher_email"`
PublisherPhone string `gorm:"column:publisher_phone"`
PublisherSameAs string `gorm:"column:publisher_same_as"`
// The local business data of an organisation publisher (D-45).
LocalEnabled bool `gorm:"column:local_enabled"`
LocalType string `gorm:"column:local_type"`
LocalPriceRange string `gorm:"column:local_price_range"`
LocalStreet string `gorm:"column:local_street"`
LocalPostalCode string `gorm:"column:local_postal_code"`
LocalCity string `gorm:"column:local_city"`
LocalRegion string `gorm:"column:local_region"`
LocalCountry string `gorm:"column:local_country"`
LocalLatitude string `gorm:"column:local_latitude"`
LocalLongitude string `gorm:"column:local_longitude"`
LocalHours lagoon.Jsonable[[]OpeningHours] `gorm:"column:local_hours"`
// RoutesReportedAt is the time of the newest route report (D-33),
// written by the report only.
RoutesReportedAt *time.Time `gorm:"column:routes_reported_at"`
@@ -142,9 +191,15 @@ func DefaultSettings() Settings {
URLHost: HostKeep,
URLSlash: SlashKeep,
LinksNewTab: true,
PageForm: PageFormQuery,
PublisherType: PublisherOrganization,
LocalType: DefaultLocalType,
}
}
// Hours returns the local business's opening hours.
func (s Settings) Hours() []OpeningHours { return s.LocalHours.Get() }
// Params returns the query-parameter list.
func (s Settings) Params() []QueryParam { return s.QueryParams.Get() }