diff --git a/djangocms_tacc_header_logo/__init__.py b/djangocms_tacc_header_logo/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/djangocms_tacc_header_logo/apps.py b/djangocms_tacc_header_logo/apps.py
new file mode 100644
index 000000000..803ac60b5
--- /dev/null
+++ b/djangocms_tacc_header_logo/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class TaccsiteHeaderLogoConfig(AppConfig):
+ name = 'djangocms_tacc_header_logo'
+ verbose_name = 'Header logo'
diff --git a/djangocms_tacc_header_logo/cms_plugins.py b/djangocms_tacc_header_logo/cms_plugins.py
new file mode 100644
index 000000000..b10afd0fe
--- /dev/null
+++ b/djangocms_tacc_header_logo/cms_plugins.py
@@ -0,0 +1,31 @@
+from cms.plugin_pool import plugin_pool
+from django.utils.translation import gettext_lazy as _
+
+from djangocms_picture.cms_plugins import PicturePlugin
+from djangocms_picture.models import Picture
+
+from .forms import TaccsiteHeaderLogoForm
+
+HEADER_LOGO_ID = 'header-logo'
+HEADER_LOGO_CLASS = 'navbar-brand'
+
+
+@plugin_pool.register_plugin
+class TaccsiteHeaderLogoPlugin(PicturePlugin):
+ """
+ Components > "Header logo" Plugin
+ """
+ model = Picture
+ form = TaccsiteHeaderLogoForm
+ module = 'TACC Site'
+ name = _('Header logo')
+
+ def render(self, context, instance, placeholder):
+ if not instance.attributes:
+ instance.attributes = {}
+
+ existing_class = instance.attributes.get('class', '')
+ instance.attributes['class'] = f'{HEADER_LOGO_CLASS} {existing_class}'.strip()
+ instance.attributes['id'] = HEADER_LOGO_ID
+
+ return super().render(context, instance, placeholder)
diff --git a/djangocms_tacc_header_logo/forms.py b/djangocms_tacc_header_logo/forms.py
new file mode 100644
index 000000000..6e0cd6dcc
--- /dev/null
+++ b/djangocms_tacc_header_logo/forms.py
@@ -0,0 +1,38 @@
+from cms.models import Page
+from django.conf import settings
+from django.contrib.sites.models import Site
+from djangocms_picture.forms import PictureForm
+
+HEADER_LOGO_TEMPLATE_WITH_PORTAL = 'portal_logo'
+
+
+def default_home_page():
+ site = Site.objects.get_current()
+ return Page.objects.filter(is_home=True, node__site=site).first()
+
+
+class TaccsiteHeaderLogoForm(PictureForm):
+ """
+ Defaults for new Header logo plugins only (admin add form).
+
+ On a TACC/Core-Portal instance, defaults to Portal logo picture template.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ if self.instance.pk:
+ return
+
+ if settings.PORTAL_IS_TACC_CORE_PORTAL:
+ self.initial.setdefault('template', HEADER_LOGO_TEMPLATE_WITH_PORTAL)
+
+ home = default_home_page()
+ if home is not None:
+ self.initial.setdefault('link_page', home.pk)
+
+ self.initial.setdefault('use_no_cropping', True)
+ self.initial.setdefault('use_automatic_scaling', False)
+
+ attributes = dict(self.initial.get('attributes') or {})
+ attributes.setdefault('alt', 'Project logo')
+ self.initial['attributes'] = attributes
diff --git a/docs/gh-999-editable-header-plan.md b/docs/gh-999-editable-header-plan.md
new file mode 100644
index 000000000..dd0adfe8d
--- /dev/null
+++ b/docs/gh-999-editable-header-plan.md
@@ -0,0 +1,67 @@
+# GH-999: Editable Header
+
+Related:
+
+- [#999](https://github.com/TACC/Core-CMS/issues/999)
+- [#1083](https://github.com/TACC/Core-CMS/pull/1083)
+
+## Status as of 2026-06
+
+| State | Description |
+| ------------------- | --------------------------------- |
+| one placeholder | `header-logo` static placeholder |
+| if plugin is added | `TaccsiteHeaderLogoPlugin` is only option |
+| if plugin not added | renders settings-based logo |
+
+The branding and navs and search are still settings-based.
+
+## Known Limitations
+
+| Issue | Summary | Why Allow This? |
+| --- | --- | --- |
+| [#1185](https://github.com/TACC/Core-CMS/issues/1185) | Header logo without a link is **missing** `class="portal-logo"` on `
`. | Default form links logo to home, so typical sites are unaffected. |
+
+## How to Add Other Header Plugins
+
+Requirements:
+
+- (for [CMS editor UX](#cms-editor-ux)) plugins render where `{% static_placeholder %}` sits
+- (to skip [Failed Ideas](#failed-ideas)) pick one option from below
+
+### A. Wrapper Plugin
+
+| Pros | Cons |
+| ------------------------------ | ---------------------------------------------- |
+| One placeholder | Extra container plugin |
+| Layout management consolidated | Build/Maintain wrapper |
+| | Must deprecate or migrate legacy `header-logo` |
+
+### B. Multiple Placeholders
+
+| Pros | Cons |
+| ---------------------------------- | ---------------------------------- |
+| Typical CMS usage | Several placeholders to configure |
+| | Layout management not consolidated |
+| Can keep using `header-logo` as is | |
+
+## CMS Editor UX
+
+Requirements:
+
+- Static placeholder always visible in Structure mode.
+- Edit plugin: auto-refreshes page preview.
+- Add/Delete plugin: auto-refreshes page preview.
+- Structure mode button toggles render of Structure sidebar.
+
+## Failed Ideas
+
+| Idea | Problem |
+| --- | --- |
+| `ContentRenderer` / template tag for the logo instead of `{% static_placeholder %}` at the logo | Structure and preview break; Picture edits need refresh (removed in [#1189](https://github.com/TACC/Core-CMS/pull/1189)) |
+| Published markup from code, placeholder somewhere else | Duplicate logo surfaces; draft image lags behind preview |
+| Child plugins under the logo, HTML rendered elsewhere | Structure/preview break |
+| `{% static_placeholder %}` not where the logo shows | Bad WYSIWYG; toolbar/Structure bugs |
+| Per-region render registry (not Option A or B) | Live site publish-only; bad edit UX |
+| Option A wrapper before more header plugins exist | Two plugins for one logo |
+| Plugin `cache = False` only | Does not fix placeholder vs programmatic render |
+| Edit-only placeholder at top of header | Structure JS errors; no real preview |
diff --git a/taccsite_cms/_settings/djangocms_plugins.py b/taccsite_cms/_settings/djangocms_plugins.py
index 87e378fd8..48f1b3380 100644
--- a/taccsite_cms/_settings/djangocms_plugins.py
+++ b/taccsite_cms/_settings/djangocms_plugins.py
@@ -47,6 +47,7 @@
]
DJANGOCMS_PICTURE_TEMPLATES = [
('no_link_to_ext_image', _('Do not link to external image')),
+ ('portal_logo', _('Portal logo')),
]
########################
diff --git a/taccsite_cms/settings/settings.py b/taccsite_cms/settings/settings.py
index cc1eed190..1e3633e39 100644
--- a/taccsite_cms/settings/settings.py
+++ b/taccsite_cms/settings/settings.py
@@ -494,6 +494,7 @@ def gettext(s): return s
'taccsite_card',
'djangocms_tacc_image_gallery',
'djangocms_tacc_system_monitor',
+ 'djangocms_tacc_header_logo',
# TACC CMS Plugins - DECPRECATED
'taccsite_cms.contrib.taccsite_blockquote',
@@ -567,6 +568,17 @@ def get_subdirs_as_module_names(path):
CMS_PERMISSION = True
CMS_PLACEHOLDER_CONF = {
+ None: {
+ 'excluded_plugins': ['TaccsiteHeaderLogoPlugin'],
+ },
+ 'header-logo': {
+ 'name': _('Header logo'),
+ 'plugins': ['TaccsiteHeaderLogoPlugin'],
+ 'excluded_plugins': [],
+ 'limits': {
+ 'global': 1,
+ },
+ },
'footer-content': {
'name': _('Footer content'),
},
diff --git a/taccsite_cms/templates/djangocms_picture/default/picture.html b/taccsite_cms/templates/djangocms_picture/default/picture.html
index 160e312ff..0ec26a477 100644
--- a/taccsite_cms/templates/djangocms_picture/default/picture.html
+++ b/taccsite_cms/templates/djangocms_picture/default/picture.html
@@ -59,6 +59,9 @@
{{ instance.attributes_str }}
{% endblock %}
{# /TACC #}
+ {# TACC (allow attributes to be force-added to
): #}
+ {% block picture_attributes_img %}{% if picture_img_class %}class="{{ picture_img_class }}"{% endif %}{% endblock %}
+ {# /TACC #}
>
{% endlocalize %}
diff --git a/taccsite_cms/templates/djangocms_picture/portal_logo/picture.html b/taccsite_cms/templates/djangocms_picture/portal_logo/picture.html
new file mode 100644
index 000000000..03e6af734
--- /dev/null
+++ b/taccsite_cms/templates/djangocms_picture/portal_logo/picture.html
@@ -0,0 +1,2 @@
+{% extends "djangocms_picture/default/picture.html" %}
+{% block picture_attributes_img %}class="portal-logo"{% endblock %}
diff --git a/taccsite_cms/templates/header.html b/taccsite_cms/templates/header.html
index 6eaa2ce2a..b6e3880b4 100644
--- a/taccsite_cms/templates/header.html
+++ b/taccsite_cms/templates/header.html
@@ -1,4 +1,5 @@
{# @var settings #}
+{% load cms_tags %}
{# WARNING: Some markup is duplicated in other repositories #}
{# SEE: https://confluence.tacc.utexas.edu/x/LoCnCQ #}
@@ -22,7 +23,9 @@
{% endif %}
">
- {% include "header_logo.html" %}
+ {% static_placeholder "header-logo" or %}
+ {% include "header_logo.html" %}
+ {% endstatic_placeholder %}