Tinkering with Docker
  • Home
  • About
Sign in Subscribe
By wody in ghost — 11 Sep 2025

Definitive Guide to Completely Removing the “Sign up” Text/Elements from the Ghost Blog Footer

Learn how to fully remove “Sign up,” “Subscribe,” and related membership UI from the Ghost footer—safely, without editing theme files—using Code Injection.

Definitive Guide to Completely Removing the “Sign up” Text/Elements from the Ghost Blog Footer

Audience: For Ghost(Pro) or self-hosted Ghost users who don’t use (or don’t want to expose) Membership/Portal.

1) Introduction

Ghost’s membership features—newsletters, members-only content, and paid subscriptions—are powerful. But many blogs don’t need any of that. In those cases, leftover Sign up buttons or Subscribe text can distract visitors and clutter your design.

Even if you set Membership to Nobody (disabled), some themes still leave a “Sign up” text stub in the footer (sometimes just plain text with no link). If you prefer a simple, distraction-free blog, that’s annoying.

This guide is for you.

Who this guide helps

  • You run Ghost(Pro) or self-hosted Ghost
  • You don’t use membership/subscription at all
  • You want to remove all remaining “Sign up / Subscribe / Membership” footer text cleanly
  • You want to fix it without editing theme files, using Code Injection only

Before we drop in the code, there’s one important prep step.


2) Prerequisite — Set Membership to “Nobody”

Before hiding UI with code, disable Membership in Ghost Admin. This keeps your site future-proof when themes or Ghost versions update; the code then acts as a belt-and-suspenders safeguard.

How to disable Membership:

  1. Log in to Ghost Admin
  2. Go to Settings → Membership
  3. Set Who can sign up? to Nobody
  4. Save

🔒 With this set, new sign-ups are blocked and Portal auto-disables.

👉 With that done, you’re ready to remove any leftover Sign up text and related footer elements using the code below.


3) At-a-Glance: What the Provided Code Does

You’ll paste this into Settings → Code Injection → Site Footer.
<style>
  /* 1) Hide subscribe/signup forms (broad theme coverage) */
  .subscribe-form,
  .post-subscribe,
  .gh-subscribe,
  .gh-subscribe-form,
  .subscription-form,
  .signup,
  .signup-box,
  .newsletter,
  .kg-signup-card,
  .kg-signup-card *,
  .gh-membership,
  .membership-cta { display:none !important; }

  /* 2) Hide Ghost Portal triggers/links */
  [data-portal],
  .gh-portal-trigger,
  a[href="#/portal/signup"],
  a[href*="#/portal/signup"],
  a[href*="/#/portal/signup"] { display:none !important; }

  /* 3) If 'Sign up' remains via aria-label (icon buttons, etc.) */
  a[aria-label*="Sign up" i],
  button[aria-label*="Sign up" i] { display:none !important; }

  /* 4) Common footer locations across themes */
  footer .subscribe,
  footer .signup,
  footer .nav-signup { display:none !important; }
</style>

<script>
  // 5) Final sweep: remove leftover 'Sign up' text nodes
  (function(){
    const killWords = ["sign up","signup","subscribe","구독","회원가입"];
    const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
    const targets = [];
    let n;
    while(n = walker.nextNode()){
      const t = n.nodeValue.trim().toLowerCase();
      if(!t) continue;
      if(killWords.some(w => t === w || t.includes(w))){
        // Hide an appropriate ancestor (link/button/span/div)
        let el = n.parentElement;
        for(let i=0;i<3 && el;i++){
          if(/^(a|button|div|span)$/i.test(el.tagName)){ break; }
          el = el.parentElement;
        }
        (el || n.parentElement).style.setProperty('display','none','important');
      }
    }
    // Handle delayed Portal insertions
    setTimeout(()=>document.querySelectorAll('[data-portal], .gh-portal-trigger').forEach(e=>e.style.setProperty('display','none','important')), 800);
  })();
</script>

How it works

  • CSS (1–4): Hides common subscribe/membership classes/attributes/links across many themes using display:none !important.
  • JS (5): Scans text nodes and hides nearby wrappers when it finds “sign up,” “signup,” “subscribe,” “구독,” or “회원가입.”
  • Delay handling: A setTimeout(800) catches late-injected Portal UI.

4) Where to Apply It in Ghost

  1. Ghost Admin → Settings → Code Injection
  2. Paste the <style> + <script> block into Site Footer
  3. Save, then hard-refresh your site (clear cache)
  4. Test on the home page, posts, and pages
  5. Also test in a private/incognito window to avoid admin-state cache effects
  6. If you use CDN or server caching (e.g., Cloudflare), purge once

5) Selector Notes & Compatibility Tips

  • .subscribe-form, .post-subscribe, .gh-subscribe*: Common wrappers/forms in official themes like Casper.
  • .kg-signup-card, .kg-signup-card *: Hides Koenig editor signup card blocks entirely (no leakage from children).
  • .gh-membership, .membership-cta: Generic membership CTAs.
  • [data-portal], .gh-portal-trigger: Core attributes/classes used by Ghost Portal.
  • a[href*="#/portal/signup"]: Catches Portal’s hash-based routes.
  • a[aria-label*="Sign up" i], button[aria-label*="Sign up" i]: Helps with icon-only buttons that rely on accessible labels.
  • footer .subscribe, footer .signup, footer .nav-signup: Footer-scoped selectors to reduce over-hiding elsewhere.

Pro tip: Prefer footer-scoped selectors where possible to avoid accidental removal (e.g., legitimate mentions in content).


6) Text-Node Scanning: Why and What to Watch For

How it works

  • Uses document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT) to traverse text nodes.
  • Lowercases and checks for sign up, signup, subscribe, 구독, 회원가입.
  • Hides an appropriate ancestor up to 3 levels to minimize overreach.

Benefits

  • Catches edge cases like icon buttons, small inline spans, or aria-label-only elements.
  • More resilient when theme class names change.

Cautions

  • Might hide legitimate phrases in post bodies (e.g., “subscriber guide”).
  • Mitigate by scoping to the footer or adding a whitelist (skip .post-content). See improved versions below.

7) Practical Hardening: Fewer False Positives & Stronger Against Dynamic UI

7-1. Safer Footer-Only Version

Use this if the problem is limited to the footer.

<script>
(function(){
  const killWords = ["sign up","signup","subscribe","구독","회원가입"];
  const root = document.querySelector('footer') || document.body; // prefer footer
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
  let n;
  while(n = walker.nextNode()){
    const t = n.nodeValue && n.nodeValue.trim().toLowerCase();
    if(!t) continue;
    if(killWords.some(w => t === w || t.includes(w))){
      let el = n.parentElement, steps = 0;
      while(el && steps < 3){
        if(/^(a|button|div|span)$/i.test(el.tagName)) break;
        el = el.parentElement; steps++;
      }
      (el || n.parentElement)?.style?.setProperty('display','none','important');
    }
  }
})();
</script>

7-2. MutationObserver Version for Aggressive Dynamic Insertion

Use this if Portal/scripts keep injecting elements after load.

<script>
(function(){
  const killWords = ["sign up","signup","subscribe","구독","회원가입"];
  const hide = (root) => {
    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
    let n;
    while(n = walker.nextNode()){
      const t = n.nodeValue && n.nodeValue.trim().toLowerCase();
      if(!t) continue;
      if(killWords.some(w => t.includes(w))){
        let el = n.parentElement, steps = 0;
        while(el && steps < 3){
          if(/^(a|button|div|span)$/i.test(el.tagName)) break;
          el = el.parentElement; steps++;
        }
        (el || n.parentElement)?.style?.setProperty('display','none','important');
      }
    }
    // Re-hide attribute-based Portal triggers
    root.querySelectorAll('[data-portal], .gh-portal-trigger, a[href*="#/portal/signup"], a[href*="/#/portal/signup"]').forEach(
      e => e.style.setProperty('display','none','important')
    );
  };

  const target = document.querySelector('footer') || document.body;
  hide(target);

  const mo = new MutationObserver(muts => muts.forEach(m => {
    m.addedNodes.forEach(node => {
      if(node.nodeType === 1) hide(node);
      else if(node.nodeType === 3){
        const wrap = document.createElement('span');
        node.parentNode && node.parentNode.insertBefore(wrap, node);
        wrap.appendChild(node);
        hide(wrap);
        wrap.replaceWith(...wrap.childNodes);
      }
    });
  }));
  mo.observe(target, { childList:true, subtree:true, characterData:true });
})();
</script>

Performance tip: Scoping the observer to footer keeps overhead low.


8) Accessibility (A11y) & SEO Considerations

  • A11y: Hiding elements with display:none removes them from screen readers too. That’s usually what you want for redundant CTAs—but avoid blanket selectors that might hide global nav or search.
  • Aria-label targeting: Good for icon buttons, but still prefer footer scoping to avoid touching non-footer UI.
  • SEO: Hiding irrelevant membership links typically has no downside. If you hide navigational links, you may change crawl paths. If you don’t use membership, that’s fine.

9) Code Injection vs. Theme-Level Edits

AspectCode InjectionTheme Customization (Handlebars/CSS)
Speed to applyVery fast (instant)Slower (repack/redeploy)
MaintenanceCentralized in Code InjectionMay conflict on theme updates
GranularityJS can target text nodesTemplate can remove rendering entirely
Recommended whenQuick, theme-agnostic blockingLong-term cleanliness at source

Long-term, editing the theme to conditionally omit membership blocks (e.g., removing {{#if @site.members_enabled}}) is the cleanest solution. If Ghost(Pro) theme management feels heavy, Code Injection is practical and safe.


10) Recommended Settings by Use Case

  • Never using membership
    Admin Membership = Nobody + footer-scoped CSS/JS + optional MutationObserver → near-perfect.
  • Running a manual external newsletter (e.g., Formspree/Mailchimp embed)
    Hide Ghost membership/Portal. Whitelist your custom block (e.g., add .custom-newsletter) so it remains visible.
  • Might use membership later
    Keep the injection code clearly commented and consider a simple toggle (e.g., window.HIDE_SIGNUP = true) so you can disable quickly later.

11) Safer Tuning Tips

  • Trim the word list: subscribe is common in content; consider removing it if you see false positives. Combine with footer scoping.
  • Tighten selectors: Use concrete containers + classes (e.g., footer .nav-signup) to reduce unintended matches.

Test checklist

  • Check home/post/page/tag/author templates
  • Check mobile and desktop breakpoints
  • Ensure floating/keyboard Portal triggers are removed
  • Test both logged-in and logged-out states

12) Troubleshooting FAQ

Q1. Buttons reappear occasionally.
A.
Late DOM insertions from Portal/theme scripts are likely. Use the MutationObserver version and purge caches (browser/CDN/Service Worker).

Q2. A paragraph with the word “subscribe” disappeared.
A.
The text scanner over-matched.

  • Use the footer-scoped version, or
  • Remove subscribe from killWords, or
  • Add a whitelist (e.g., skip .post-content).
if (n.parentElement && n.parentElement.closest('.post-content')) continue;

Q3. It doesn’t work on my theme (custom classes).
A.
Inspect with DevTools and add the theme’s specific classes/attributes to the CSS block (e.g., .footer-signup, .btn-portal, [data-action="signup"]).

Q4. How do I fully disable Portal?
A.
First disable it in Settings → Membership. If anything lingers, broaden selectors like [data-portal], .gh-portal-trigger, and a[href*="portal"]—but beware over-blocking.

Q5. Lighthouse shows minor a11y/SEO changes.
A.
Hidden DOM can slightly affect structure scores. If scores matter, remove the blocks at the theme template level instead of just hiding them.

Q6. How do I revert later?
A.
Remove or comment out the <style>/<script> blocks in Code Injection. Keep a before/after note for easy rollback.


13) Final Recommended Snippet (Scoped, Low False Positives, Dynamic-Ready)

<style>
  /* (A) Footer-scoped: common subscribe/signup UI */
  footer .subscribe,
  footer .signup,
  footer .nav-signup,
  footer .subscribe-form,
  footer .post-subscribe,
  footer .gh-subscribe,
  footer .gh-subscribe-form,
  footer .subscription-form,
  footer .signup-box,
  footer .newsletter,
  footer .kg-signup-card,
  footer .kg-signup-card *,
  footer .gh-membership,
  footer .membership-cta { display:none !important; }

  /* (B) Portal triggers/links (may appear outside footer) */
  [data-portal],
  .gh-portal-trigger,
  a[href="#/portal/signup"],
  a[href*="#/portal/signup"],
  a[href*="/#/portal/signup"] { display:none !important; }

  /* (C) A11y label-based icon buttons (footer only) */
  footer a[aria-label*="Sign up" i],
  footer button[aria-label*="Sign up" i] { display:none !important; }
</style>

<script>
(function(){
  // Footer-only text sweep to avoid false positives in content
  const killWords = ["sign up","signup","구독","회원가입"]; // 'subscribe' removed to reduce false positives
  const root = document.querySelector('footer') || document.body;

  const scan = (node) => {
    const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
    let n;
    while(n = walker.nextNode()){
      const t = n.nodeValue && n.nodeValue.trim().toLowerCase();
      if(!t) continue;
      if(killWords.some(w => t.includes(w))){
        // Whitelist example: skip post body
        if (n.parentElement && n.parentElement.closest('.post-content')) continue;

        let el = n.parentElement, steps = 0;
        while(el && steps < 3){
          if(/^(a|button|div|span)$/i.test(el.tagName)) break;
          el = el.parentElement; steps++;
        }
        (el || n.parentElement)?.style?.setProperty('display','none','important');
      }
    }
    // Re-hide Portal triggers/links
    document.querySelectorAll('[data-portal], .gh-portal-trigger, a[href*="#/portal/signup"], a[href*="/#/portal/signup"]').forEach(
      e => e.style.setProperty('display','none','important')
    );
  };

  scan(root);

  // Observe dynamic insertions
  const mo = new MutationObserver(muts => muts.forEach(m => {
    m.addedNodes.forEach(node => {
      if(node.nodeType === 1) scan(node);
      else if(node.nodeType === 3){
        const span = document.createElement('span');
        node.parentNode && node.parentNode.insertBefore(span, node);
        span.appendChild(node);
        scan(span);
        span.replaceWith(...span.childNodes);
      }
    });
  }));
  mo.observe(root, { childList:true, subtree:true, characterData:true });

  // Extra delayed pass for initial late loads
  setTimeout(() => scan(root), 800);
})();
</script>

14) Pre-/Post-Deploy Checklist

  • Settings → Membership: Confirm Nobody
  • Paste the final snippet into Code Injection → Site Footer
  • Verify on home/post/page/archive/search views
  • Check mobile & desktop
  • Test logged-in and logged-out
  • Purge CDN/browser cache; test again in incognito

15) Wrap-Up

This guide shows how to quickly and safely remove stray Sign up/Subscribe UI in Ghost using Code Injection only. The recipe: (1) disable Membership (Nobody), (2) use footer-scoped CSS/JS, and (3) optionally add a MutationObserver to catch dynamic inserts. Theme-level removal is the most elegant long-term fix, but for speed and reliability across theme updates, these snippets are a practical, production-ready solution.


Optional reference: Ghost’s official docs (membership, Portal, and theme structure) are helpful when customizing deeper: https://ghost.org/docs/

Previous

Running Guacamole on Synology NAS by Docker Installation

Next

Running Coral with Docker on Synology NAS

You might also like...

Running Docker on Windows WSL2/Ubuntu (Part 5) – Install Ghost with MySQL, Nginx, and phpMyAdmin
wsl

Running Docker on Windows WSL2/Ubuntu (Part 5) – Install Ghost with MySQL, Nginx, and phpMyAdmin

Learn how to deploy Ghost with MySQL 8, hardened Nginx reverse proxy, and phpMyAdmin on Docker inside WSL2/Ubuntu. Includes SEO-ready configuration, custom headers, SSL, and networking with Nginx Proxy Manager.
Read More
Moving from WordPress to Ghost
wordpress

Moving from WordPress to Ghost

I finally moved away from WordPress and started fresh with Ghost. Back
Read More
Add Syntax Highlighting to Ghost (with Prism.js + CDN)
ghost

Add Syntax Highlighting to Ghost (with Prism.js + CDN)

Most guides out there show you the “basic way” to add Prism.
Read More
[Docker] Ghost + MySQL8 + NginX + Imagor on Synology NAS
docker

[Docker] Ghost + MySQL8 + NginX + Imagor on Synology NAS

When most people think of blogging platforms, WordPress is the default. It’
Read More
Tinkering with Docker © 2025
  • Sign up
Powered by Ghost