diff --git a/cmd/helpcenter.go b/cmd/helpcenter.go
index 36e63c26..44c5336f 100644
--- a/cmd/helpcenter.go
+++ b/cmd/helpcenter.go
@@ -18,6 +18,7 @@ import (
"github.com/abhinavxd/libredesk/internal/envelope"
"github.com/abhinavxd/libredesk/internal/helpcenter"
hcmodels "github.com/abhinavxd/libredesk/internal/helpcenter/models"
+ "github.com/abhinavxd/libredesk/internal/media"
"github.com/abhinavxd/libredesk/internal/stringutil"
realip "github.com/ferluci/fast-realip"
"github.com/knadh/stuffbin"
@@ -211,7 +212,7 @@ func handleHelpCenterPreview(r *fastglue.Request) error {
"Data": map[string]interface{}{
"Title": helpCenter.PageTitle,
"LandingHero": true,
- "HelpCenter": helpCenterTemplateData(helpCenter, locale),
+ "HelpCenter": helpCenterTemplateData(app, helpCenter, locale),
"Tree": tree.Tree,
"Popular": popular,
},
@@ -593,12 +594,12 @@ func handleShowHelpCenterHome(r *fastglue.Request) error {
popular = nil
}
var (
- root = helpCenterRootURL(app)
+ root = helpCenterBaseURL(app, helpCenter)
locales = helpCenterLocales(helpCenter)
pathFor = func(l string) string { return helpCenterHomePath(helpCenter.Slug, l) }
metaDescription = firstNonEmpty(tree.HelpCenter.MetaDescription, tree.HelpCenter.HeaderText)
)
- data := helpCenterTemplateData(tree.HelpCenter, locale)
+ data := helpCenterTemplateData(app, tree.HelpCenter, locale)
return renderHelpCenterPage(r, "help-center", map[string]interface{}{
"L": localeI18n(app, locale),
"Data": map[string]interface{}{
@@ -606,7 +607,7 @@ func handleShowHelpCenterHome(r *fastglue.Request) error {
"MetaDescription": metaDescription,
"CanonicalPath": pathFor(locale),
"LandingHero": true,
- "OGImage": absoluteURL(root, tree.HelpCenter.LogoURL),
+ "OGImage": absoluteURL(root, publicAssetPaths(app, tree.HelpCenter.LogoURL)),
"Alternates": helpCenterAlternates(helpCenter, locales, pathFor),
"XDefaultPath": defaultLocalePath(helpCenter, locales, pathFor),
"LocaleLinks": helpCenterLocaleLinks(helpCenter, locales, pathFor),
@@ -646,17 +647,17 @@ func handleShowHelpCenterCollection(r *fastglue.Request) error {
translated = []string{locale}
}
var (
- root = helpCenterRootURL(app)
+ root = helpCenterBaseURL(app, helpCenter)
pathFor = func(l string) string { return collectionPath(helpCenter.Slug, l, collection.Slug) }
)
- data := helpCenterTemplateData(helpCenter, locale)
+ data := helpCenterTemplateData(app, helpCenter, locale)
return renderHelpCenterPage(r, "help-collection", map[string]interface{}{
"L": localeI18n(app, locale),
"Data": map[string]interface{}{
"Title": fmt.Sprintf("%s - %s", collection.Name, helpCenter.Name),
"MetaDescription": collection.Description,
"CanonicalPath": pathFor(locale),
- "OGImage": absoluteURL(root, helpCenter.LogoURL),
+ "OGImage": absoluteURL(root, publicAssetPaths(app, helpCenter.LogoURL)),
"Alternates": helpCenterAlternates(helpCenter, translated, pathFor),
"XDefaultPath": defaultLocalePath(helpCenter, translated, pathFor),
"LocaleLinks": helpCenterLocaleLinks(helpCenter, translated, pathFor),
@@ -713,13 +714,13 @@ func handleShowHelpCenterArticle(r *fastglue.Request) error {
translated = []string{locale}
}
var (
- root = helpCenterRootURL(app)
+ root = helpCenterBaseURL(app, helpCenter)
pathFor = func(l string) string { return articlePath(helpCenter.Slug, l, article.Slug) }
metaDescription = firstNonEmpty(article.MetaDescription, article.Excerpt)
metaTitle = firstNonEmpty(article.MetaTitle, fmt.Sprintf("%s - %s", article.Title, helpCenter.Name))
- ogImage = absoluteURL(root, firstNonEmpty(article.MetaImageURL, helpCenter.LogoURL))
+ ogImage = absoluteURL(root, publicAssetPaths(app, firstNonEmpty(article.MetaImageURL, helpCenter.LogoURL)))
)
- data := helpCenterTemplateData(helpCenter, locale)
+ data := helpCenterTemplateData(app, helpCenter, locale)
return renderHelpCenterPage(r, "help-article", map[string]interface{}{
"L": localeI18n(app, locale),
"Data": map[string]interface{}{
@@ -739,7 +740,7 @@ func handleShowHelpCenterArticle(r *fastglue.Request) error {
"AuthorInitial": authorInitial(article),
"Collection": collection,
"Related": related,
- "Content": template.HTML(stringutil.DeferOffscreenImages(article.Content)),
+ "Content": template.HTML(publicAssetPaths(app, stringutil.DeferOffscreenImages(article.Content))),
},
})
}
@@ -769,7 +770,7 @@ func handleHelpCenterSearch(r *fastglue.Request) error {
}
}
var (
- data = helpCenterTemplateData(helpCenter, locale)
+ data = helpCenterTemplateData(app, helpCenter, locale)
lcl = localeI18n(app, locale)
pathFor = func(l string) string { return searchPath(helpCenter.Slug, l) }
)
@@ -813,7 +814,7 @@ func handleHelpCenterSitemap(r *fastglue.Request) error {
return sendErrorEnvelope(r, err)
}
- root := helpCenterRootURL(app)
+ root := helpCenterBaseURL(app, helpCenter)
set := urlset{Xmlns: sitemapNamespace}
set.URLs = append(set.URLs, sitemapURL{
Loc: root + helpCenterHomePath(helpCenter.Slug, locale),
@@ -841,9 +842,9 @@ func handleSitemapIndex(r *fastglue.Request) error {
if err != nil {
return sendErrorEnvelope(r, err)
}
- root := helpCenterRootURL(app)
index := sitemapIndex{Xmlns: sitemapNamespace}
for _, hc := range helpCenters {
+ root := helpCenterBaseURL(app, hc)
for _, locale := range helpCenterLocales(hc) {
index.Sitemaps = append(index.Sitemaps, sitemapRef{Loc: fmt.Sprintf("%s%s/sitemap.xml", root, helpCenterHomePath(hc.Slug, locale))})
}
@@ -860,6 +861,19 @@ func handleRobotsTxt(r *fastglue.Request) error {
fmt.Fprintf(r.RequestCtx, "Disallow: %s\n", path)
}
fmt.Fprintf(r.RequestCtx, "\nSitemap: %s/sitemap.xml\n", helpCenterRootURL(app))
+ // A help center on its own host can't be crawled through the app root's sitemap index.
+ helpCenters, err := app.helpcenter.GetActiveHelpCenters()
+ if err != nil {
+ return nil
+ }
+ for _, hc := range helpCenters {
+ if hc.PublicURL == "" {
+ continue
+ }
+ for _, locale := range helpCenterLocales(hc) {
+ fmt.Fprintf(r.RequestCtx, "Sitemap: %s%s/sitemap.xml\n", hc.PublicURL, helpCenterHomePath(hc.Slug, locale))
+ }
+ }
return nil
}
@@ -1055,6 +1069,25 @@ func helpCenterRootURL(app *App) string {
return strings.TrimRight(app.consts.Load().(*constants).AppBaseURL, "/")
}
+// helpCenterBaseURL returns the host a reader sees this help center on: its own public URL when
+// set, else the app root URL.
+func helpCenterBaseURL(app *App, hc hcmodels.HelpCenter) string {
+ if hc.PublicURL != "" {
+ return hc.PublicURL
+ }
+ return helpCenterRootURL(app)
+}
+
+// publicAssetPaths rewrites stored absolute media URLs to root-relative ones so uploads resolve
+// on whichever host serves the page.
+func publicAssetPaths(app *App, s string) string {
+ root := helpCenterRootURL(app)
+ if root == "" {
+ return s
+ }
+ return strings.ReplaceAll(s, root+media.PublicURI, media.PublicURI)
+}
+
// absoluteURL resolves a root-relative URL against the app root URL, since social and
// structured-data consumers reject relative image URLs.
func absoluteURL(root, u string) string {
@@ -1305,7 +1338,7 @@ func helpCenterLocales(hc hcmodels.HelpCenter) []string {
}
// helpCenterTemplateData shapes a help center row for the public templates.
-func helpCenterTemplateData(hc hcmodels.HelpCenter, locale string) map[string]interface{} {
+func helpCenterTemplateData(app *App, hc hcmodels.HelpCenter, locale string) map[string]interface{} {
navLinks := []hcmodels.NavLink{}
if len(hc.NavLinks) > 0 {
if err := json.Unmarshal(hc.NavLinks, &navLinks); err != nil {
@@ -1318,12 +1351,15 @@ func helpCenterTemplateData(hc hcmodels.HelpCenter, locale string) map[string]in
theme = hcmodels.DefaultTheme()
}
}
+ theme.Favicon = publicAssetPaths(app, theme.Favicon)
+ theme.Header.BackgroundImage = publicAssetPaths(app, theme.Header.BackgroundImage)
return map[string]interface{}{
"Slug": hc.Slug,
"Name": hc.Name,
+ "BaseURL": helpCenterBaseURL(app, hc),
"PageTitle": hc.PageTitle,
"HeaderText": hc.HeaderText,
- "LogoURL": hc.LogoURL,
+ "LogoURL": publicAssetPaths(app, hc.LogoURL),
"Color": hc.Color,
"DefaultLocale": hc.DefaultLocale,
"CurrentLocale": locale,
@@ -1401,7 +1437,7 @@ func renderHelpCenterNotFound(r *fastglue.Request, hc *hcmodels.HelpCenter) erro
if !ok {
locale = helpCenter.DefaultLocale
}
- data := helpCenterTemplateData(helpCenter, locale)
+ data := helpCenterTemplateData(app, helpCenter, locale)
lcl := localeI18n(app, locale)
r.RequestCtx.Response.Header.Set("X-Robots-Tag", noIndexHeader)
rerr := app.tmpl.RenderWebPage(r.RequestCtx, "help-notfound", map[string]interface{}{
@@ -1570,7 +1606,7 @@ func renderHelpCenterArticlePreview(r *fastglue.Request, helpCenter hcmodels.Hel
"Data": map[string]interface{}{
"Title": article.Title,
"ModifiedTime": article.UpdatedAt.Format(time.RFC3339),
- "HelpCenter": helpCenterTemplateData(helpCenter, locale),
+ "HelpCenter": helpCenterTemplateData(app, helpCenter, locale),
"Article": article,
"AuthorInitial": authorInitial(article),
"Collection": collection,
diff --git a/frontend/apps/main/src/features/admin/help-center/ArticleEditSheet.vue b/frontend/apps/main/src/features/admin/help-center/ArticleEditSheet.vue
index b2105cd5..cb1f940c 100644
--- a/frontend/apps/main/src/features/admin/help-center/ArticleEditSheet.vue
+++ b/frontend/apps/main/src/features/admin/help-center/ArticleEditSheet.vue
@@ -123,13 +123,9 @@
{{ t('helpCenter.noCollectionsInLanguage') }}
-
+
-
+
+
+
+ {{ t('helpCenter.publicURL') }}
+
+
+
+ {{ t('helpCenter.publicURLHint') }}
+
+
+
+
{{ t('helpCenter.pageTitle') }}
@@ -209,21 +220,19 @@
-
-
- {{ t('globals.messages.backgroundColor') }}
-
-
-
-
-
+
+
+
+ {{ t('globals.messages.backgroundColor') }}
+
+
+
+
+
+
@@ -244,24 +253,22 @@
-
-
- {{ t('helpCenter.styling.headerImage') }}
-
-
-
- {{ t('helpCenter.styling.headerImageHint') }}
-
-
-
+
+
+
+ {{ t('helpCenter.styling.headerImage') }}
+
+
+
+ {{ t('helpCenter.styling.headerImageHint') }}
+
+
+
+
@@ -338,26 +345,24 @@
-
-
- {{ t('helpCenter.styling.cardsPerRow') }}
-
-
-
-
-
+
+
+
+ {{ t('helpCenter.styling.cardsPerRow') }}
+
+
+
+
+
+
@@ -393,23 +398,21 @@
}}
-
-
- {{ t('helpCenter.styling.popularArticlesLabel') }}
-
-
-
-
-
-
+
+
+
+ {{ t('helpCenter.styling.popularArticlesLabel') }}
+
+
+
+
+
+
+
@@ -698,6 +701,7 @@ const socialPlatforms = [
const toFormValues = (hc) => ({
name: hc?.name || '',
slug: hc?.slug || '',
+ public_url: hc?.public_url || '',
page_title: hc?.page_title || '',
header_text: hc?.header_text || '',
meta_description: hc?.meta_description || '',
diff --git a/frontend/apps/main/src/features/admin/help-center/helpCenterFormSchema.js b/frontend/apps/main/src/features/admin/help-center/helpCenterFormSchema.js
index 42f615a8..beafbef2 100644
--- a/frontend/apps/main/src/features/admin/help-center/helpCenterFormSchema.js
+++ b/frontend/apps/main/src/features/admin/help-center/helpCenterFormSchema.js
@@ -39,6 +39,10 @@ export const createHelpCenterFormSchema = (t) => {
logo_url: optionalURL,
color: z.string().optional(),
nav_links: linkArray,
+ public_url: z
+ .string()
+ .refine((v) => !v || /^https?:\/\/[^"'()\s\\<>;{}]+$/.test(v), t('helpCenter.invalidPublicURL'))
+ .optional(),
custom_css: z.string().optional(),
custom_js: z.string().optional(),
default_locale: z
diff --git a/frontend/apps/main/src/views/admin/help-center/HelpCenterTree.vue b/frontend/apps/main/src/views/admin/help-center/HelpCenterTree.vue
index 5052188d..d55d58c7 100644
--- a/frontend/apps/main/src/views/admin/help-center/HelpCenterTree.vue
+++ b/frontend/apps/main/src/views/admin/help-center/HelpCenterTree.vue
@@ -119,7 +119,7 @@
{
const visitSite = () => {
const rootUrl = appSettingsStore.settings?.['app.root_url'] || window.location.origin
const locale = props.locale || helpCenter.value?.default_locale || ''
- const path = locale
- ? `/hc/${helpCenter.value?.slug}/${locale}`
- : `/hc/${helpCenter.value?.slug}`
+ const path = locale ? `/hc/${helpCenter.value?.slug}/${locale}` : `/hc/${helpCenter.value?.slug}`
window.open(`${rootUrl.replace(/\/$/, '')}${path}`, '_blank', 'noopener')
}
@@ -507,7 +505,7 @@ const handleArticleSave = async (formData) => {
if (editingArticle.value) {
await api.updateArticle(editingArticle.value.id, formData)
} else {
- await api.createArticle(createArticleCollectionId.value, formData)
+ await api.createArticle(formData.collection_id || createArticleCollectionId.value, formData)
}
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
description: t('globals.messages.savedSuccessfully')
diff --git a/i18n/en-US.json b/i18n/en-US.json
index 9b0d496b..af5166c7 100644
--- a/i18n/en-US.json
+++ b/i18n/en-US.json
@@ -1261,6 +1261,9 @@
"helpCenter.searchPlaceholder": "Search articles...",
"helpCenter.searchResults": "Results for",
"helpCenter.slugHint": "Used in the help center URL.",
+ "helpCenter.publicURL": "Public URL (optional)",
+ "helpCenter.publicURLHint": "If you want to serve this help center on its own domain, enter it here so canonical links, sitemaps and share previews point there instead of the app's root URL. That domain also needs to point at Libredesk in your DNS and reverse proxy.",
+ "helpCenter.invalidPublicURL": "Enter a full URL starting with https://, with no trailing slash.",
"imageLightbox.next": "Next image",
"imageLightbox.previous": "Previous image",
"imageLightbox.resetZoom": "Reset zoom",
diff --git a/internal/helpcenter/helpcenter.go b/internal/helpcenter/helpcenter.go
index 54c09739..8b234cbb 100644
--- a/internal/helpcenter/helpcenter.go
+++ b/internal/helpcenter/helpcenter.go
@@ -7,6 +7,7 @@ import (
"encoding/json"
"fmt"
"maps"
+ "net/url"
"regexp"
"slices"
"strings"
@@ -97,6 +98,7 @@ type HelpCenterRequest struct {
DefaultLocale string `json:"default_locale"`
AllowedLocales json.RawMessage `json:"allowed_locales"`
Theme json.RawMessage `json:"theme"`
+ PublicURL string `json:"public_url"`
}
type CollectionRequest struct {
@@ -271,7 +273,10 @@ func (m *Manager) CreateHelpCenter(req HelpCenterRequest) (models.HelpCenter, er
if err := m.validateColor(req.Color); err != nil {
return hc, err
}
- if err := m.q.InsertHelpCenter.Get(&hc, req.Name, req.Slug, req.PageTitle, req.HeaderText, req.MetaDescription, req.LogoURL, req.Color, req.NavLinks, req.CustomCSS, req.CustomJS, req.DefaultLocale, req.AllowedLocales, req.Theme); err != nil {
+ if err := m.validatePublicURL(req.PublicURL); err != nil {
+ return hc, err
+ }
+ if err := m.q.InsertHelpCenter.Get(&hc, req.Name, req.Slug, req.PageTitle, req.HeaderText, req.MetaDescription, req.LogoURL, req.Color, req.NavLinks, req.CustomCSS, req.CustomJS, req.DefaultLocale, req.AllowedLocales, req.Theme, req.PublicURL); err != nil {
if dbutil.IsUniqueViolationError(err) {
return hc, envelope.NewError(envelope.ConflictError, m.i18n.T("globals.messages.errorAlreadyExists"), nil)
}
@@ -300,6 +305,7 @@ func (m *Manager) DraftHelpCenter(id int, req HelpCenterRequest) (models.HelpCen
hc.DefaultLocale = req.DefaultLocale
hc.AllowedLocales = req.AllowedLocales
hc.Theme = req.Theme
+ hc.PublicURL = req.PublicURL
if err := m.validateColor(hc.Color); err != nil {
return hc, err
}
@@ -319,7 +325,10 @@ func (m *Manager) UpdateHelpCenter(id int, req HelpCenterRequest) (models.HelpCe
if err := m.validateColor(req.Color); err != nil {
return hc, err
}
- if err := m.q.UpdateHelpCenter.Get(&hc, id, req.Name, req.Slug, req.PageTitle, req.HeaderText, req.MetaDescription, req.LogoURL, req.Color, req.NavLinks, req.CustomCSS, req.CustomJS, req.DefaultLocale, req.AllowedLocales, req.Theme); err != nil {
+ if err := m.validatePublicURL(req.PublicURL); err != nil {
+ return hc, err
+ }
+ if err := m.q.UpdateHelpCenter.Get(&hc, id, req.Name, req.Slug, req.PageTitle, req.HeaderText, req.MetaDescription, req.LogoURL, req.Color, req.NavLinks, req.CustomCSS, req.CustomJS, req.DefaultLocale, req.AllowedLocales, req.Theme, req.PublicURL); err != nil {
if dbutil.IsUniqueViolationError(err) {
return hc, envelope.NewError(envelope.ConflictError, m.i18n.T("globals.messages.errorAlreadyExists"), nil)
}
@@ -1208,6 +1217,17 @@ func (m *Manager) validateLocales(defaultLocale string, allowed json.RawMessage)
return nil
}
+func (m *Manager) validatePublicURL(publicURL string) error {
+ if publicURL == "" {
+ return nil
+ }
+ u, err := url.Parse(publicURL)
+ if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
+ return envelope.NewError(envelope.InputError, m.i18n.T("helpCenter.invalidPublicURL"), nil)
+ }
+ return nil
+}
+
func (m *Manager) validateColor(color string) error {
if !hexColorRe.MatchString(color) {
return envelope.NewError(envelope.InputError, m.i18n.T("helpCenter.invalidColor"), nil)
@@ -1243,6 +1263,7 @@ func normalizeHelpCenterRequest(req HelpCenterRequest) HelpCenterRequest {
if req.Color == "" {
req.Color = defaultAccentColor
}
+ req.PublicURL = strings.TrimRight(strings.TrimSpace(req.PublicURL), "/")
req.LogoURL = sanitizeAssetURL(req.LogoURL)
req.NavLinks = normalizeNavLinks(req.NavLinks)
diff --git a/internal/helpcenter/models/models.go b/internal/helpcenter/models/models.go
index 36f0d2b7..8ccb3048 100644
--- a/internal/helpcenter/models/models.go
+++ b/internal/helpcenter/models/models.go
@@ -29,6 +29,7 @@ type HelpCenter struct {
AllowedLocales json.RawMessage `db:"allowed_locales" json:"allowed_locales"`
IsActive bool `db:"is_active" json:"is_active"`
Theme json.RawMessage `db:"theme" json:"theme"`
+ PublicURL string `db:"public_url" json:"public_url"`
}
// Theme holds the customizable branding for a help center's public pages.
diff --git a/internal/helpcenter/queries.sql b/internal/helpcenter/queries.sql
index 8f330a32..ca74d56c 100644
--- a/internal/helpcenter/queries.sql
+++ b/internal/helpcenter/queries.sql
@@ -1,32 +1,32 @@
-- name: get-all-help-centers
-SELECT id, created_at, updated_at, name, slug, page_title, header_text, meta_description, logo_url, color, nav_links, custom_css, custom_js, default_locale, allowed_locales, is_active, theme
+SELECT id, created_at, updated_at, name, slug, page_title, header_text, meta_description, logo_url, color, nav_links, custom_css, custom_js, default_locale, allowed_locales, is_active, theme, public_url
FROM help_centers
ORDER BY created_at DESC;
-- name: get-active-help-centers
-SELECT id, created_at, updated_at, name, slug, page_title, header_text, meta_description, logo_url, color, nav_links, custom_css, custom_js, default_locale, allowed_locales, is_active, theme
+SELECT id, created_at, updated_at, name, slug, page_title, header_text, meta_description, logo_url, color, nav_links, custom_css, custom_js, default_locale, allowed_locales, is_active, theme, public_url
FROM help_centers
WHERE is_active = true
ORDER BY created_at DESC;
-- name: get-help-center-by-id
-SELECT id, created_at, updated_at, name, slug, page_title, header_text, meta_description, logo_url, color, nav_links, custom_css, custom_js, default_locale, allowed_locales, is_active, theme
+SELECT id, created_at, updated_at, name, slug, page_title, header_text, meta_description, logo_url, color, nav_links, custom_css, custom_js, default_locale, allowed_locales, is_active, theme, public_url
FROM help_centers
WHERE id = $1;
-- name: get-help-center-by-slug
-SELECT id, created_at, updated_at, name, slug, page_title, header_text, meta_description, logo_url, color, nav_links, custom_css, custom_js, default_locale, allowed_locales, is_active, theme
+SELECT id, created_at, updated_at, name, slug, page_title, header_text, meta_description, logo_url, color, nav_links, custom_css, custom_js, default_locale, allowed_locales, is_active, theme, public_url
FROM help_centers
WHERE slug = $1 AND is_active = true;
-- name: insert-help-center
-INSERT INTO help_centers (name, slug, page_title, header_text, meta_description, logo_url, color, nav_links, custom_css, custom_js, default_locale, allowed_locales, theme)
-VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
+INSERT INTO help_centers (name, slug, page_title, header_text, meta_description, logo_url, color, nav_links, custom_css, custom_js, default_locale, allowed_locales, theme, public_url)
+VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
RETURNING *;
-- name: update-help-center
UPDATE help_centers
-SET name = $2, slug = $3, page_title = $4, header_text = $5, meta_description = $6, logo_url = $7, color = $8, nav_links = $9, custom_css = $10, custom_js = $11, default_locale = $12, allowed_locales = $13, theme = $14, updated_at = NOW()
+SET name = $2, slug = $3, page_title = $4, header_text = $5, meta_description = $6, logo_url = $7, color = $8, nav_links = $9, custom_css = $10, custom_js = $11, default_locale = $12, allowed_locales = $13, theme = $14, public_url = $15, updated_at = NOW()
WHERE id = $1
RETURNING *;
diff --git a/internal/migrations/v2.7.0.go b/internal/migrations/v2.7.0.go
index 20336336..4971cd0f 100644
--- a/internal/migrations/v2.7.0.go
+++ b/internal/migrations/v2.7.0.go
@@ -25,7 +25,8 @@ func V2_7_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
default_locale TEXT NOT NULL DEFAULT 'en',
allowed_locales JSONB NOT NULL DEFAULT '["en"]',
is_active BOOLEAN NOT NULL DEFAULT true,
- theme JSONB NOT NULL DEFAULT '{}'
+ theme JSONB NOT NULL DEFAULT '{}',
+ public_url TEXT NOT NULL DEFAULT ''
);
`); err != nil {
return err
@@ -42,6 +43,9 @@ func V2_7_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
if _, err := db.Exec(`ALTER TABLE help_centers ADD COLUMN IF NOT EXISTS meta_description TEXT NOT NULL DEFAULT '';`); err != nil {
return err
}
+ if _, err := db.Exec(`ALTER TABLE help_centers ADD COLUMN IF NOT EXISTS public_url TEXT NOT NULL DEFAULT '';`); err != nil {
+ return err
+ }
if _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS article_collections (
diff --git a/internal/stringutil/htmlembedprep.go b/internal/stringutil/htmlembedprep.go
index ffdb558d..e96d2c5d 100644
--- a/internal/stringutil/htmlembedprep.go
+++ b/internal/stringutil/htmlembedprep.go
@@ -138,10 +138,17 @@ func captionText(table *html.Node) string {
}
func isHeaderRow(row *html.Node) bool {
+ seen := false
for c := row.FirstChild; c != nil; c = c.NextSibling {
- if c.Type == html.ElementNode && c.Data == "th" {
- return true
+ if c.Type != html.ElementNode {
+ continue
+ }
+ switch c.Data {
+ case "th":
+ seen = true
+ case "td":
+ return false
}
}
- return false
+ return seen
}
diff --git a/internal/stringutil/htmlimages.go b/internal/stringutil/htmlimages.go
index 6b1f3331..8adc52a3 100644
--- a/internal/stringutil/htmlimages.go
+++ b/internal/stringutil/htmlimages.go
@@ -6,7 +6,7 @@ import (
)
var (
- imgTagRe = regexp.MustCompile(`(?i)
]*>`)
+ imgTagRe = regexp.MustCompile(`(?is)
"'])*>`)
imgLoadingAttrRe = regexp.MustCompile(`(?i)\bloading\s*=`)
diff --git a/schema.sql b/schema.sql
index 47f3e91c..2bed4c73 100644
--- a/schema.sql
+++ b/schema.sql
@@ -650,7 +650,8 @@ CREATE TABLE help_centers (
default_locale TEXT NOT NULL DEFAULT 'en',
allowed_locales JSONB NOT NULL DEFAULT '["en"]',
is_active BOOLEAN NOT NULL DEFAULT true,
- theme JSONB NOT NULL DEFAULT '{}'
+ theme JSONB NOT NULL DEFAULT '{}',
+ public_url TEXT NOT NULL DEFAULT ''
);
DROP TABLE IF EXISTS article_collections CASCADE;
diff --git a/static/public/web-templates/help-center.html b/static/public/web-templates/help-center.html
index 784fdb5c..6988c89e 100644
--- a/static/public/web-templates/help-center.html
+++ b/static/public/web-templates/help-center.html
@@ -7,15 +7,15 @@
{{ .Data.Title }}
{{ if .Data.MetaDescription }}{{ end }}
{{ if .Data.NoIndex }}{{ end }}
- {{ if .Data.CanonicalPath }}{{ end }}
- {{ range .Data.Alternates }}
- {{ end }}{{ if .Data.XDefaultPath }}{{ end }}
+ {{ if .Data.CanonicalPath }}{{ end }}
+ {{ range .Data.Alternates }}
+ {{ end }}{{ if .Data.XDefaultPath }}{{ end }}
{{ if .Data.MetaDescription }}{{ end }}
- {{ if .Data.CanonicalPath }}{{ end }}
+ {{ if .Data.CanonicalPath }}{{ end }}
{{ if .Data.OGImage }}{{ end }}
{{ if .Data.PublishedTime }}{{ end }}
{{ if .Data.ModifiedTime }}{{ end }}