Skip to content
Grav 2.0 is officially stable. Read the announcement →
Support

clean way to show some content only if someone is logged in?

twig

Started by Anna 4 weeks ago · 9 replies · 433 views
4 weeks ago

I understand that twig in content is a lot more restrictive now, for security reasons. So I’m currently doing this in a custom twig template with {% if not grav.user.authenticated %}, but I feel content really should go in the pages and not in a twig template. So what is the clean way to do this now?

Edit: I wrapped the content bits in section tags (named “authenticated” and “unauthenticated” respectively) and plugged those into my custom template. That is so far an acceptable solution, though I would still love to hear if there’s a better way.

Adding to that, how do I show the login form for unauthenticated users? If I put {% include 'partials/login-form.html.twig' } %} in my template, nothing shows up. If I do {% include 'partials/login-form.html.twig' with { show_login_form: true } %}, the remember me checkbox and the buttons turn up, but the username and password fields are still missing. Not sure how I should be doing this…

last edited 07/20/26 by Anna
4 weeks ago

I am making a tutorial site that is only available to logged in users. So for most of the pages I did it as you suggested, but for the home page I would like to have a little bit of text for people who are not yet logged in and a login form. And once they’re logged in, there should be a different text.

Like I said, I solved the problem with the different bits of text quite neatly, but I can’t add the login form directly, only a link to it. And that’s an extra click that should be avoided.

…Hmm, but that’s an idea: I could add my „show if home page“ logic to the login form template… that might be a neat way to do it! Will report back later 😃

4 weeks ago

Sadly, this does not work easily, because page.home is always false (since it’s the login page). How could I check whether the login form came from/redirects to the home page? I can’t find that information in the page object… out of ideas for now.

3 weeks ago

Anna, when you include {% include 'partials/login-form.html.twig' with { show_login_form: true } %} in a custom Twig template, you must provide form structure in that page's .md frontmatter (even if a custom route is configured in the plugin settings). Simply copy the default login form into your page header.

❤️ 1
1 reply
last edited 07/24/26 by Vadym
3 weeks ago Anna Oh, I managed to miss those! Very nice, thank you. I’m still left with the problem of show…

My 2 cents for fully private websites. In my scenario, I needed to lock down the entire site. I used a simple redirect check inside base.html.twig and a custom login.html.twig template.

Note: If a login page exists (setting config.plugins.login.route), it must contain a form, otherwise the default form provided by the plugin will be loaded.

TWIG
{# templates/partials/base.html.twig #}
{% set login_routes = [
    config.plugins.login.route,
    config.plugins.login.route_forgot,
    config.plugins.login.route_reset,
    config.plugins.login.route_register,
    ]
%}
{% set has_access = grav.user.authenticated and grav.user.authorized %}
{%- if page.route not in login_routes and not has_access -%}
    {{ redirect_me(url(login_routes|first,true), 302) }}
{%- endif -%}

And 😂 just tested fetching default login form dynamically using Fetch API, example below seems to work as expected. So it can be used inside a modal/dialog or directly included in your homepage template.

JS
{# templates/partials/async-login-box.html.twig #}
{% script at 'bottom' %}
document.addEventListener('DOMContentLoaded', () => {
  const container = document.getElementById('js-login-container');
  const target = document.getElementById('js-login-target');
  // Guard clause: exit if user is already authenticated or container is missing
  if (!container || !target) return;
  const loginUrl = container.dataset.loginUrl || '/login';
  const fetchLoginForm = async () => {
    try {
      const response = await fetch(loginUrl, {
        headers: {
          'X-Requested-With': 'XMLHttpRequest'
        }
      });
      if (!response.ok) {
        throw new Error(`HTTP Error ${response.status}`);
      }
      const htmlText = await response.text();
      const parser = new DOMParser();
      const doc = parser.parseFromString(htmlText, 'text/html');
      // Extract the form element from the fetched login page
      const loginForm = doc.querySelector('form[name="login"]') || doc.querySelector('.login-form');
      if (loginForm) {
        // Clear skeleton placeholder and mount the form
        target.innerHTML = '';
        target.appendChild(loginForm);
      } else {
        target.innerHTML = '<p class="error">Form not found.</p>';
      }
    } catch (error) {
      console.error('[Grav Async Login] Background fetch error:', error);
      target.innerHTML = '<p class="error">Failed to load login form.</p>';
    }
  };
  // Use requestIdleCallback to avoid blocking the main thread during initial page render
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => fetchLoginForm(), { timeout: 2000 });
  } else {
    // Fallback for browsers lacking requestIdleCallback support
    setTimeout(fetchLoginForm, 200);
  }
});
{% endscript %}

{% if config.plugins.login.enabled %}
  <div id="js-login-container" class="login-box-wrapper" data-login-url="{{ url(config.plugins.login.route) }}">
    {% if grav.user.authenticated %}
      {# 1. Authenticated user view #}
      <div class="user-card">
        <p>You are logged in as <strong>{{ grav.user.fullname ?: grav.user.username }}</strong></p>
        {# Secure Grav logout button with CSRF nonce generation #}
        <a href="{{ uri.addNonce(url(config.plugins.login.route ~ '/task:login.logout'), 'logout-form', 'logout-nonce') }}" 
           class="button btn-logout">
          {{ 'PLUGIN_LOGIN.BTN_LOGOUT'|t }}
        </a>
      </div>
    {% else %}
      {# 2. Guest view — background fetch container with skeleton loader #}
      <div id="js-login-target" class="login-target-area">
        {# Skeleton placeholder while background fetch completes #}
        <div class="login-skeleton">
          <p class="text-muted">Loading login form...</p>
        </div>
      </div>
    {% endif %}
  </div>
{% endif %}
❤️ 1
2 weeks ago Vadym Anna, when you include {% include 'partials/login-form.html.twig' with { show_login_form…

You know, that seemed such a delightfully simple solution, I implemented it straight away! And the login form appeared – but I just realised the logout link does not work/do anything, for reasons I just don’t want to figure out right now.

In a similar vein, the new [authenticated] shortcodes do not quite work for me, specifically the [guest] shortcode. But I really don’t have the nerve for debugging today, so this is to say thank you for helping me out here, and I will get back to it soonish!

2 weeks ago

If you're using the first approach (adding the form to the page frontmatter), and your homepage is not same as config.plugins.login.route, copy the login-form.html.twig partial to your theme and change logout href (at L14) to

TWIG
{{ uri.addNonce(base_url_relative ~ config.plugins.login.route|defined('/login') ~ '/task' ~ config.system.param_sep ~ 'login.logout', 'logout-form', 'logout-nonce')|e }}

In the example using fetch API (which works without this extra step), it's probably also better to change logout href (but in my test it worked without language code in URLs)

Suggested topics

Topic Participants Replies Views Activity
Support · by Anna, 1 week ago
4 327 9 hours ago
Support · by gtx, 2 weeks ago
4 331 1 week ago
Support · by Paul Hodges, 3 weeks ago
13 375 2 weeks ago
Support · by TomW, 3 weeks ago
4 273 3 weeks ago
Support · by Anna, 3 weeks ago
4 269 3 weeks ago