From 353558ce9bd7495d3ea1f470d7322bdb6c873ebd Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Wed, 5 Aug 2026 01:10:31 +0530 Subject: [PATCH] add a public URL per help center and fix code review issues Each help center gets an optional public_url. When set, canonical links, hreflang alternates, og:url, the sitemap and robots.txt point at that host instead of the app root URL. Stored media URLs are rewritten to root-relative paths so logos, favicons, header images and article images resolve on whichever host serves the page. The URL is validated on both the form and the backend, and trailing slashes are trimmed. Also fixes issues found in code review: - the help center template used a locale link as the data root inside the alternates loop, which broke every public page on a help center with two or more locales - creating an article posted the clicked collection id, so changing the collection in the sheet filed the article in the wrong place - the collection field's error message was hidden when the locale had no collections, so Create did nothing with no error shown - switching the header type or unticking popular articles dropped those theme values on save, because vee-validate unsets fields that unmount - closing a new collection sheet with Esc left the parent id set, so the next collection you edited got re-parented - a table row with th labels in the first column was treated as a header row when preparing article content for embeddings - the img tag regex stopped at a > inside an attribute value and injected loading/decoding in the middle of an attribute --- cmd/helpcenter.go | 72 ++++++--- .../admin/help-center/ArticleEditSheet.vue | 8 +- .../admin/help-center/HelpCenterForm.vue | 140 +++++++++--------- .../admin/help-center/helpCenterFormSchema.js | 4 + .../admin/help-center/HelpCenterTree.vue | 10 +- i18n/en-US.json | 3 + internal/helpcenter/helpcenter.go | 25 +++- internal/helpcenter/models/models.go | 1 + internal/helpcenter/queries.sql | 14 +- internal/migrations/v2.7.0.go | 6 +- internal/stringutil/htmlembedprep.go | 13 +- internal/stringutil/htmlimages.go | 2 +- schema.sql | 3 +- static/public/web-templates/help-center.html | 8 +- 14 files changed, 192 insertions(+), 117 deletions(-) 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.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 }}