Ah ha good to know semiaddict. As you are the creator of this wonderful gem will you be adding the testing or waiting on the maintainers?
What’s the holdup on merging the PR? We use this in production across multiple websites. What can we do to get this moved into a new release?
Correct on the screenshot I grabbed that was an admin page. Here is a varnish hit.
To see if varnish is working you need to review the network tab, select the page and view the headers. Search for caching like attached.
@summer here is the complete varnish vcl we are using successfully.
vcl 4.0;
import std;
backend default {
.host = "127.0.0.1";
.port = "8080";
.connect_timeout = 600s;
.first_byte_timeout = 600s;
.between_bytes_timeout = 600s;
}
# Access control list for specific requests like PURGE.
# Here you need to put the IP address of your web server.
acl internal {
"127.0.0.1";
"191.101.233.147";
}
# Respond to incoming requests.
sub vcl_recv {
# Cache invalidation support:
#
# 1. ENTIRE SITE (200):
# - method: BAN
# - path: /site
# - header X-Varnish-Purge: site secret (MUST match X-Varnish-Secret value).
# 2. CACHE TAGS (200):
# - method: BAN
# - path: /tags
# - header X-Varnish-Purge: site secret (MUST match X-Varnish-Secret value).
# - header X-Tag: 32f5 de19 143b - hashed version of the tag to invalidate
# 3. DEFLATE CALL (200):
# - method: BAN
# - path: /deflate
# - header X-Varnish-Purge: site secret (MUST match X-Varnish-Secret value).
# 4. SINGLE URL + VARIANTS (200):
# - method: BAN
# - path: (the path to invalidate, e.g.: "path/a?p=1" or "path/*")
# - header Host: the hostname to clear the path for.
# - header X-Varnish-Purge: site secret (no verification)
if (req.method == "BAN") {
if (!req.http.X-Varnish-Purge) {
return (synth(405, "Permission denied."));
}
if (!client.ip ~ internal) {
return (synth(405, "Permission denied."));
}
set req.http.X-Varnish-Purge = std.tolower(req.http.X-Varnish-Purge);
if (req.url == "/site") {
ban("obj.http.X-Varnish-Secret == " + req.http.X-Varnish-Purge);
return (synth(200, "Site banned."));
}
else if ((req.url == "/tags") && req.http.X-Tag) {
set req.http.X-Tag = "(^|\s)" + regsuball(std.tolower(req.http.X-Tag), "\ ", "(\\s|$)|(^|\\s)") + "(\s|$)";
ban("obj.http.X-Varnish-Secret == " + req.http.X-Varnish-Purge + " && obj.http.X-Tag ~ " + req.http.X-Tag);
return (synth(200, "Tags banned."));
}
else if (req.url == "/deflate") {
ban("obj.http.ETag ~ .{8}" + req.http.X-Deflate-Tag + " && obj.http.X-Deflate-Key != " + req.http.X-Deflate-Key);
return (synth(200, "Deflate operation performed."));
}
else {
set req.url = std.tolower(req.url);
if (req.url ~ "\*") {
set req.url = regsuball(req.url, "\*", "\.*");
ban("obj.http.X-Varnish-Secret == " + req.http.X-Varnish-Purge + " && obj.http.X-Url ~ ^" + req.url + "$");
return (synth(200, "WILDCARD URL banned."));
}
else {
ban("obj.http.X-Varnish-Secret == " + req.http.X-Varnish-Purge + " && obj.http.X-Url ~ " + req.url);
return (synth(200, "URL banned."));
}
}
}
unset req.http.X-Real-Forwarded-For;
set req.http.X-Real-Forwarded-For = client.ip;
unset req.http.X-Varnish-Client-IP;
set req.http.X-Varnish-Client-IP = client.ip;
set req.url = std.querysort(req.url);
if (!req.method ~ "BAN|PURGE|GET|HEAD|PUT|POST|TRACE|OPTIONS|DELETE") {
return(synth(400, "Bad request"));
}
if (req.url ~ "^/(cron|install|update)\.php") {
return(synth(403, "Forbidden"));
}
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
if (req.http.Upgrade ~ "(?i)websocket") {
return (pipe);
}
if (req.url ~ "^/(cron|install|update)\.php") {
if (!client.ip ~ internal) {
return(synth(403, "Forbidden"));
}
return(pass);
}
if (req.url ~ "(?i)\.(twig|yml|module|info|inc|profile|engine|test|po|txt|theme|svn|git|tpl(\.php)?)(\?.*|)$" && !req.url ~ "(?i)robots\.txt") {
if (!client.ip ~ internal) {
return(synth(403, "Forbidden"));
}
}
# Pass Caching if it was requested from backend.
if (req.http.Cookie ~ "(^|;\s*)(S?SESS[a-zA-Z0-9]+|DRUPAL_UID)=\w+") {
set req.http.X-Pass-Varnish = "YES";
return (pass);
}
if (req.url ~ "\.(jpeg|jpg|png|gif|webp|svg|ico|swf|js|css|txt|eot|woff|woff2|ttf|htc)(\?.*|)$") {
unset req.http.Cookie;
return (hash);
}
if (req.url ~ "\.(webm|mp3|m4a|mp4|m4v|mov|mpeg|mpg|avi|divx|ogg|ogv|wma|pdf|tar|gz|gzip|bz2)(\?.*|)$") {
unset req.http.Cookie;
return(pipe);
}
if ((req.url ~ "/system/ajax/") && (! req.url ~ "/cached")) {
return(pass);
}
if (req.url ~ "/user" || req.url ~ "/admin") {
return(pass);
}
if (
req.url ~ "^/sites/.*/files/" || req.url ~ "^/sites/all/themes/" || req.url ~ "^/modules/.*\.(js|css)\?") {
unset req.http.Cookie;
}
set req.http.Surrogate-Capability = "abc=ESI/1.0";
return (hash);
}
sub vcl_hash {
/** Default hash */
hash_data(req.url);
hash_data(req.http.host);
/** Place ajax into separate bin. */
hash_data(req.http.X-Requested-With);
/** Add protocol if available. */
hash_data(req.http.X-Forwarded-Proto);
/** Process authenticated users */
if (req.http.Cookie ~ "^(|.*; ?)S?SESS([a-z0-9]{32}=[^;]+)(;.*|)$") {
/** Extract full session value */
set req.http.X-SESS = regsub(req.http.Cookie, "^(|.*; ?)S?SESS([a-z0-9]{32}=[^;]+)(;.*|)$", "\2");
# Get Cookie Bin. And Set new header for Vary caching.
if (req.http.Cookie ~ "^(|.*; ?)ADVBIN=([^;]+)(;.*|)$") {
set req.http.X-Bin = "role:" + regsub(req.http.Cookie, "^(|.*; ?)ADVBIN=([^;]+)(;.*|)$", "\2");
}
/** ESI_CACHEMODE_1 - SHARED */
if (req.url ~ "/adv_varnish/esi/" && req.url ~ "[\?&]cachemode=1(&|$)") {
set req.http.X-Bin = "role:anonymous";
}
/** ESI_CACHEMODE_2 - ROLE */
/** X-Bin role:... */
/** This is default behavior User role will be set as cookie bin at the beginning of this condition. */
/** ESI_CACHEMODE_3 - USER */
if (req.url ~ "/adv_varnish/esi/" && req.url ~ "[\?&]cachemode=3(&|$)") {
/** Set user session as bin */
set req.http.X-Bin = "user:" + req.http.X-SESS;
}
set req.http.X-URL = req.url;
}
else {
set req.http.X-Bin = "role:anonymous";
}
/** If Bin is set - add it to hash data for this page */
hash_data(req.http.X-Bin);
return (lookup);
}
# Instruct Varnish what to do in the case of certain backend responses (beresp).
sub vcl_backend_response {
# Set appropriate caching headers
if (beresp.http.X-Authenticated-User == "true") {
set beresp.ttl = 0s;
set beresp.uncacheable = true;
set beresp.http.Cache-Control = "no-cache, no-store, must-revalidate";
set beresp.http.Pragma = "no-cache";
set beresp.http.Expires = "0";
} else {
set beresp.http.Cache-Control = "public, max-age=120s";
set beresp.ttl = 2m;
}
/** Enable ESI if requested on this page */
if (beresp.http.X-DOESI) {
set beresp.do_esi = true;
/** Avoid cache onn Browser side */
unset beresp.http.ETag;
unset beresp.http.Last-Modified;
}
/** compression, vcl_miss/vcl_pass unset compression from the backend */
if (!beresp.http.Content-Encoding && (
beresp.http.content-type ~ "text" ||
beresp.http.content-type ~ "application/xml" ||
beresp.http.content-type ~ "application/xml\+rss" ||
beresp.http.content-type ~ "application/rss\+xml" ||
beresp.http.content-type ~ "application/xhtml+xml" ||
beresp.http.content-type ~ "application/x-javascript" ||
beresp.http.content-type ~ "application/javascript" ||
beresp.http.content-type ~ "application/json" ||
beresp.http.content-type ~ "font/truetype" ||
beresp.http.content-type ~ "application/x-font-ttf" ||
beresp.http.content-type ~ "application/x-font-opentype" ||
beresp.http.content-type ~ "font/opentype" ||
beresp.http.content-type ~ "application/vnd\.ms-fontobject" ||
beresp.http.content-type ~ "image/svg\+xml" ||
beresp.http.content-type ~ "image/x-icon"
)) {
set beresp.do_gzip = true;
}
# Set ban-lurker friendly custom headers.
set beresp.http.X-Url = bereq.url;
set beresp.http.X-Host = bereq.http.host;
# Cache 404s, 301s, at 500s with a short lifetime to protect the backend.
if (beresp.status == 404 || beresp.status == 301 || beresp.status == 500) {
set beresp.ttl = 10m;
}
# Don't allow static files to set cookies.
# (?i) denotes case insensitive in PCRE (perl compatible regular expressions).
# This list of extensions appears twice, once here and again in vcl_recv so
# make sure you edit both and keep them equal.
if (bereq.url ~ "(?i)\.(jpeg|jpg|png|gif|webp|svg|ico|swf|js|css|txt|eot|woff|woff2|ttf|htc|mp3|m4a|mp4|m4v|mov|mpeg|mpg|avi|divx|ogg|ogv|wma|pdf|tar|gz|gzip|bz2|asc|dat|doc|xls|ppt|tgz|csv)(\?.*|)$") {
unset beresp.http.set-cookie;
return(deliver);
}
# Allow items to remain in cache up to X hours past their cache expiration.
set beresp.grace = std.duration(beresp.http.X-Grace + "s", 0s);
# Use ttl from X-TTL header. If X-Adv-Varnish header exists (page created by Drupal) and
# missing X-Drupal-[Dynamic-]Cache headers, then the page should not be cached for some
# reason (Page-Cache-Kill-Switch, Vary per User or Session etc)
set beresp.ttl = std.duration(beresp.http.X-TTL + "s", 0s);
if (bereq.url !~ "/adv_varnish/esi/" &&
beresp.http.X-Adv-Varnish == "Cache-Enabled" &&
beresp.http.X-Drupal-Dynamic-Cache != "MISS" &&
beresp.http.X-Drupal-Dynamic-Cache != "HIT" &&
beresp.http.X-Drupal-Cache != "HIT" &&
beresp.http.X-Drupal-Cache != "MISS") {
set beresp.ttl = 0s;
}
if (beresp.http.Set-Cookie) {
set beresp.http.X-Cacheable = "NO:Cookie in the response";
set beresp.ttl = 0s;
}
elsif (beresp.ttl <= 0s) {
set beresp.http.X-Cacheable = "NO:Not Cacheable";
}
elsif (beresp.http.Cache-Control ~ "private" && !beresp.http.X-DOESI) {
set beresp.http.X-Cacheable = "NO:Cache-Control=private";
set beresp.uncacheable = true;
}
else {
set beresp.http.X-Cacheable = "YES";
}
if (beresp.ttl > 0s) {
unset beresp.http.Set-Cookie;
}
set beresp.http.X-Varnish-Secret = std.tolower(beresp.http.X-Varnish-Secret);
set beresp.http.X-TTL2 = beresp.ttl;
# Disable buffering only for BigPipe responses
if (beresp.http.Surrogate-Control ~ "BigPipe/1.0") {
set beresp.do_stream = true;
set beresp.ttl = 0s;
}
}
# Set a header to track a cache HITs and MISSes.
sub vcl_deliver {
# If the header doesn't already exist, set it.
#if (!req.http.X-Bin) {
#set resp.http.X-Bin = "role:anonymous";
#}
set resp.http.X-Bin = req.http.X-Bin;
if (obj.hits > 0) {
set resp.http.X-Varnish-Cache = "HIT";
set resp.http.X-Cache-TTL-Remaining = req.http.X-Cache-TTL-Remaining;
if (resp.http.Age) {
set resp.http.X-Cache-Age = resp.http.Age;
}
}
else {
set resp.http.X-Varnish-Cache = "MISS";
}
set resp.http.X-Cache-Hits = obj.hits;
# If it's a Drupal-Page with X-Bin vary, tell browsers to vary by Cookie.
if (resp.http.Vary ~ "X-Bin") {
set resp.http.Vary = resp.http.Vary + ", Cookie";
}
# Remove ban-lurker friendly custom headers when delivering to client.
if (!resp.http.X-Cache-Debug) {
unset resp.http.X-Url;
unset resp.http.X-Host;
unset resp.http.Purge-Cache-Tags;
unset resp.http.X-Drupal-Cache-Contexts;
unset resp.http.X-Drupal-Cache-Tags;
unset resp.http.X-Drupal-Dynamic-Cache;
unset resp.http.X-Bin;
unset resp.http.X-Tag;
unset resp.http.X-TTL2;
unset resp.http.X-Cache-TTL;
unset resp.http.X-Powered-By;
unset resp.http.Via;
unset resp.http.X-Generator;
unset resp.http.Connection;
unset resp.http.Server;
unset resp.http.X-DOESI;
unset resp.http.X-Varnish-Secret;
unset resp.http.X-Deflate-Key;
}
return (deliver);
}
# Right after an object has been found (hit) in the cache.
sub vcl_hit {
set req.http.X-Cache-TTL-Remaining = obj.ttl;
if (obj.ttl >= 0s) {
return (deliver);
}
if (std.healthy(req.backend_hint)) {
if (obj.ttl + 10s > 0s) {
return (deliver);
} else {
return(deliver);
}
} else {
if (obj.ttl + obj.grace > 0s) {
return (deliver);
} else {
return (deliver);
}
}
return (deliver);
}
# Right after an object was looked up and not found in cache.
sub vcl_miss {
return (fetch);
}
# Run after a pass in vcl_recv OR after a lookup that returned a hitpass.
sub vcl_pass {
# stub
}
sub vcl_pipe {
if (req.http.upgrade) {
set bereq.http.upgrade = req.http.upgrade;
}
set bereq.http.connection = "close";
}
sub vcl_synth {
if (resp.status == 400) {
set resp.status = 400;
set resp.http.Content-Type = "text/html; charset=utf-8";
synthetic ({"
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>400 Bad request</title>
</head>
<body>
<h1>Error 400 Bad request</h1>
<p>Bad request</p>
</body>
</html>
"});
return(deliver);
}
if (resp.status == 401) {
set resp.status = 401;
set resp.http.Content-Type = "text/html; charset=utf-8";
set resp.http.WWW-Authenticate = "Basic realm=Authentication required. Please login";
synthetic ({"
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/1999/REC-html401-19991224/loose.dtd">
<HTML>
<HEAD>
<TITLE>Error</TITLE>
<META HTTP-EQUIV='Content-Type' CONTENT='text/html;'>
</HEAD>
<BODY><H1>401 Unauthorized.</H1></BODY>
</HTML>
"});
return(deliver);
}
if (resp.status == 403) {
set resp.status = 403;
set resp.http.Content-Type = "text/html; charset=utf-8";
synthetic ({"
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/1999/REC-html401-19991224/loose.dtd">
<HTML>
<HEAD>
<TITLE>403 Forbidden</TITLE>
<META HTTP-EQUIV='Content-Type' CONTENT='text/html;'>
</HEAD>
<BODY><H1>Forbidden</H1></BODY>
<p>You don't have permissions to access "} + req.url + {" on this server</p>
</HTML>
"});
return(deliver);
}
}
sub vcl_fini {
return (ok);
}
Rockstar thank you so much for pushing this fix.
Yes this suggestions resolves the issue as well
/**
* Implements hook_library_info_build().
*/
function turnstile_library_info_build() {
$config = \Drupal::config('turnstile.settings');
$turnstile_src = $config->get('turnstile_src');
$libraries = [];
$libraries['turnstile.remote'] = [
'js' => [
$turnstile_src => [
'type' => 'external',
'attributes' => [
'defer' => TRUE,
'async' => TRUE,
],
],
],
];
return $libraries;
}
@greatmatter you are referring to the services file right?
parameters:
session.storage.options:
cookie_samesite: Lax
We already have this set on our sites, so we can rule that one out.
I can confirm that updating the widget does resolve the issue
/**
* Build the Turnstile captcha form.
*
* @return mixed
* The return value.
*/
public function getWidget($validation_function) {
// Add Turnstile script with async and defer attributes.
$widget['form']['#attached']['html_head'][] = [
[
'#tag' => 'script',
'#attributes' => [
'src' => 'https://challenges.cloudflare.com/turnstile/v0/api.js',
'async' => 'async',
'defer' => 'defer',
],
],
'turnstile_api',
];
// Captcha requires TRUE to be returned in solution.
$widget['solution'] = TRUE;
$widget['captcha_validate'] = $validation_function;
$widget['form']['captcha_response'] = [
'#type' => 'hidden',
'#value' => self::EMPTY_CAPTCHA_RESPONSE,
];
// Allows the CAPTCHA to be displayed on cached pages.
$widget['cacheable'] = TRUE;
// Ensure the widget is properly rendered.
$widget['form']['turnstile_widget'] = [
'#markup' => '<div' . $this->getAttributesString() . '></div>',
];
return $widget;
}
Okay well all our sites are behind Cloudflare and as soon as we disable RocketLoader it works. But we have always used this service. Since the change this is now conflicting it seems.
Only 1, nothing fancy going on here. These are just basic webforms and GIn Login pages.
I wish I could have good news and it be that simple but it is not.
This seems to work:
/**
* @file
* JavaScript behaviors for Turnstile.
*/
(function ($, Drupal, once) {
'use strict';
/**
* Ensures Turnstile renders correctly on page load & AJAX events.
*/
Drupal.behaviors.turnstileAutoRender = {
attach: function (context) {
once('turnstile-init', '.cf-turnstile', context).forEach((el) => {
turnstile.render(el);
});
}
};
/**
* Re-renders Turnstile when an AJAX request updates the form.
*/
Drupal.AjaxCommands.prototype.turnstileRender = function () {
$('.cf-turnstile').each(function(){
turnstile.render(this);
});
};
})(jQuery, Drupal, once);
No luck on my end We have cleared caches Drupal, Varnish, Cloudflare, etc. The only fix is this:
turnstile.render('.cf-turnstile', { sitekey: "#################" });
With the current version or revert to 1.1.13.
@greatmatter thanks so much for being so responsive. I just deployed with your new hotfix and sadly I get the same results, fails to load/initialize.
So it seems this actually fixes the issue:
1️⃣ Run This in Browser Console to Force a Reset
document.querySelectorAll('.cf-turnstile').forEach(el => {
turnstile.remove(el);
});
turnstile.render('.cf-turnstile', { sitekey: "######" });
2. Modify the Form Rendering Code to Prevent Duplicate Initialization
To avoid this problem, modify your Drupal form rendering:
$form['turnstile_widget'] = [
'#markup' => '<div id="turnstile-container" class="cf-turnstile" data-sitekey="' . turnstile_site_key() . '"></div>',
];
$form['#attached']['drupalSettings']['turnstile'] = [
'callback' => 'turnstileSuccess',
'errorCallback' => 'turnstileError',
];
$form['#attached']['library'][] = 'turnstile/turnstile';
Then add this script in turnstile.ajax.js to remove any existing Turnstile instances before rendering a new one:
document.addEventListener("DOMContentLoaded", function () {
const turnstileContainer = document.querySelector('.cf-turnstile');
if (turnstileContainer) {
turnstile.remove(turnstileContainer);
turnstile.render(turnstileContainer, {
sitekey: drupalSettings.turnstile.sitekey
});
}
});
I am debugging at the moment to try and be useful. There are no console errors. However here are some observations:
Locally it does load fine.
On production it does not.
Markup wise I can see
<div class="cf-turnstile" data-sitekey="################" data-theme="light" data-size="normal" data-language="auto" data-retry="auto" interval="8000" data-appearance="always"></div>
but on version 1.1.13 the markup is:
<div class="cf-turnstile" data-sitekey="################" data-theme="light" data-size="normal" data-language="auto" data-retry="auto" interval="8000" data-appearance="always"><div><input type="hidden" name="cf-turnstile-response" id="cf-chl-widget-b5dkd_response" value="TOKEN"></div></div>
glynster → created an issue.
@useernamee great news. This will be very helpful. My only suggesting is the API route /api/lupus/site-info changes to /api/site-info seems more appropriate to me. Great job!
@fago I have fixed the phpcs issues and added some tests. Let me know.
@useernamee this is what we are currently doing lupus_ce_renderer_response_data for various things thanks for the tip!
Let me know if there is anything else I can do to help here.
Confirmed that, once the merge is applied and the submodule is installed, layouts with paragraphs clone perfectly? RTBC to have this committed.
@fago, this makes total sense and the entire reason behind Nuxt layers.
How do you propose I can help here?
Resolved now and closing.
@fago sorry for the confusion. Added a new subscriber, tested and works! MR is ready.
Great job this resolves the issue for us! +1 RTBC
@fago I have gone ahead and created a merge request:
https://www.drupal.org/project/lupus_ce_renderer/issues/3506828
✨
Add basic user session info to ce-api
Active
glynster → created an issue.
Yup no worries! I will review the cache and do a pull request as suggested in the correct place.
@fargo just adding a tested patch here to make sure I have done this correctly?
Great I do remember seeing something from you about this. Cool. That will be nice as I just merged the account links to the admin menu so you have the complete package. Once that is rolled out it will be solid.
I agree with the layer side of things. ATM I am working on a webform comp and field render. I prefer to keep all the API as json and allow the frontend to render as needed. Perhaps the first step is the Nuxt Layer for admin we were chatting about? Get feedback and testing and that would be one way I can help?
Of course I agree with you 100%!!
Sounds like a perfect idea to me, and as you say it can be added to depending on your needs. Totally agree on the lookup once and cached on Nuxt end. We can achieve this via useState
glynster → created an issue.
glynster → created an issue.
glynster → created an issue.
glynster → created an issue. See original summary → .
@fago, it is a Nuxt layer and intended as a solid base. The issue is the customizations (Paragraphs, Layouts). How about I setup a branch that is more streamlined and more basic like what you have with your other 2 options? I have also utilized the app.config for theming overrides. Then you can see how to integrate this? The idea is to add the git via the package and then extend from within the Nuxt config. Very little friction. Let me know your thoughts.
@anybody I can confirm that #51 🐛 Update module can get 'stuck' with 'no releases available' Needs work works well and resolves the issue across all our sites running Drupal 10.3+. This bug has plagued us for the past six months. After installing and deploying the patch, the status page now functions correctly again. For us, this issue only occurred in the production environment.
Confirmed so much better!
@fago, here is what I have generated for a Nuxt layer:
https://github.com/StirStudios/stir_nuxt_base
At the moment, it is suited to our project needs, though it may include more than necessary. Here’s what it does currently:
- Wraps Lupus CE and other checks into a composable.
- Checks for user roles and session, returning an admin editing menu. I didn’t see the need to include the full Drupal admin menu—just the editing tabs.
- Adds a Vite hook so Nuxt dev tools work locally with DDEV.
- Nuxt 4 compatibility.
- Smooth scroll.
- Page transition.
- Turnstile for forms (currently webform, with CSRF token fetching).
- Robots.
- Sitemap (we generated an API on the Drupal end).
- Nuxt UI (as the base for ease of theming).
- Heavily integrated with Paragraphs, as you’ll see.
- Paragraph entity edit links
This is my first time working with layers, and it made sense to start with Nuxt 4 compatibility. The webform is quite flexible, allowing for many options, and basic validation is handled via Nuxt UI and Yup. I’ve also implemented basic theming for user login forms, though there’s less flexibility since we don’t have the same control over theming as we do with webforms.
If you apply this patch it solves the issue:
https://git.drupalcode.org/project/drupal/-/merge_requests/1657/diffs
@fago, here is what I have generated for a Nuxt layer:
https://github.com/StirStudios/stir_nuxt_base
At the moment, it is suited to our project needs, though it may include more than necessary. Here’s what it does currently:
- Wraps Lupus CE and other checks into a composable.
- Checks for user roles and session, returning an admin editing menu. I didn’t see the need to include the full Drupal admin menu—just the editing tabs.
- Adds a Vite hook so Nuxt dev tools work locally with DDEV.
- Nuxt 4 compatibility.
- Smooth scroll.
- Page transition.
- Turnstile for forms (currently webform, with CSRF token fetching).
- Robots.
- Sitemap (we generated an API on the Drupal end).
- Nuxt UI (as the base for ease of theming).
- Heavily integrated with Paragraphs, as you’ll see.
This is my first time working with layers, and it made sense to start with Nuxt 4 compatibility. The webform is quite flexible, allowing for many options, and basic validation is handled via Nuxt UI and Yup. I’ve also implemented basic theming for user login forms, though there’s less flexibility since we don’t have the same control over theming as we do with webforms.
I can get this response if I try user/logout (if I am logged in). What we currently have setup is a absolute url such as https://test-drupal.ddev.site/user/logout. This redirects the user to Drupal backend for confirmation. That works without issue using the Drupal process.
@fago, is there a bare minimum you’d like to see here? For example, here are a few suggestions:
- Base theme with Nuxt UI (Tailwind).
- Admin menu.
- Sticky main navigation.
- Footer.
- Light/dark mode toggle.
The components might be tricky since they depend on the Drupal setup. For instance, we use Paragraph Layouts.
I’d love to help—just need a solid plan to get started.
I can't confirm this, but I suspect it might be a configuration issue.
Have you set up your services correctly?
parameters:
session.storage.options:
cookie_domain: '.ddev.site'
cookie_domain_bc_mode: true
This ensures the cookie is shared across all subdomains. Make sure both the front-end and back-end are using the same domain, differentiated only by a subdomain.
Great, confirmed!
That said the latest release does not apply any more.
Fixed the problem for us.
Uninstall and reinstall:
drush pm-uninstall -y update && drush pm-enable -y update
Then clear the fetch task:
drush php:eval "\Drupal::keyValue('update_fetch_task')->deleteAll();"
Now that you explain it, it does make this pointless.
glynster → created an issue.
Here is the patch
glynster → created an issue.
I really like this idea! Starting with a basic Nuxt version sounds great, and allowing features to be configurable within nuxt.config adds flexibility for those who want to enable them. Nuxt UI/Tailwind is a solid choice—it stays within the Nuxt ecosystem and provides a strong foundation for development. But as you mentioned, it’s important for devs to pick the tools that best suit their project.
What are your thoughts on collaboration? I assume we could handle most of this on GitHub?
The idea is to add an extra layer of security to the Lupus API by requiring an authentication key (API key) to be included in the headers of all requests made to the Drupal backend. This key could be configured either in the environment file (env) or directly through the Lupus config form. This would help restrict access to the API, ensuring that only authorized clients can make requests.
Here’s a more detailed explanation:
Authentication Key: The proposal is to have a configurable API key that must be included in the request headers. This key could be set in the CMS environment file or through the Lupus config form, providing flexibility depending on the setup. This method is similar to approaches used in other systems, like the Rest API module, which allows protection of REST API endpoints.
Security Benefits: Implementing this would add a significant security layer to the API, helping to prevent unauthorized access. This feature would be particularly useful in cases where the API might otherwise be publicly accessible.
Implementation Flexibility: The use of a header-based approach is both simple and effective, allowing for easy integration into existing workflows. It’s a familiar pattern for many developers, which should make adoption straightforward.
While the rest_api_access_token module is just an example of a contrib module that offers protection for REST API endpoints, the concept here is to integrate a similar mechanism directly into Lupus, providing a built-in option for those who want this additional security feature.
I hope this provides a clearer picture of the proposal. Let me know if you have any questions or if further discussion is needed.
We use this in a custom theme without disabling bigpipe
use Drupal\Core\Render\Element\StatusMessages;
function yourtheme_preprocess_block__system_messages_block(&$variables) {
$variables['content'] = StatusMessages::renderMessages();
$variables['#cache']['max-age'] = 0;
}
Resolved issues for us.
I like the idea of some granular permissions are exposed options you can add to the API.
The simple solution of adding user info to the renderer made many things possible. Just on that level it helps with so much flexibility.
These guys have done some interesting stuff:
https://www.drupal.org/project/nuxtify →
Mainly in regards to the API points for user info, sessions. Otherwise their approach is completely different.
Again always happy to help where I can!
In our example, we used the Drupal Tabs already integrated within Nuxt. For most cases with our development, this setup meets the client’s needs, with the exception of providing a way to return to the backend and log out.
Here is the code we generated:
<script setup lang="ts">
import type { TabsProps } from '~/types'
const props = defineProps<TabsProps>()
const config = useRuntimeConfig()
const siteApi = config.public.api
// Function to format local task links
const getLocalTaskLinks = () => {
return props.tabs.primary.map((tab) => ({
label: tab.label,
to: tab.label === 'View' ? tab.url : `${siteApi}${tab.url}`,
icon: filterIconByLabel(tab.label),
}))
}
// Custom function to filter icons based on tab labels
const filterIconByLabel = (label: string) => {
switch (label) {
case 'View':
return 'i-heroicons-eye'
case 'Edit':
return 'i-heroicons-pencil'
case 'Delete':
return 'i-heroicons-trash'
case 'Revisions':
return 'i-heroicons-document-duplicate'
case 'Export':
return 'i-heroicons-arrow-up-tray'
case 'API':
return 'i-heroicons-code-bracket'
default:
return null
}
}
let links = []
if (props.tabs.primary && props.tabs.primary.length > 0) {
links = [
[
{
label: 'Drupal CMS',
icon: 'i-heroicons-home',
to: `${siteApi}/admin/content`,
},
],
[...getLocalTaskLinks()],
[
{
label: 'Log out',
icon: 'i-heroicons-arrow-left-start-on-rectangle',
to: `${siteApi}/user/logout`,
},
],
]
} else {
links = [
[
{
label: 'Drupal CMS',
icon: 'i-heroicons-home',
to: `${siteApi}/admin/content`,
},
],
[
{
label: 'Log out',
icon: 'i-heroicons-arrow-left-start-on-rectangle',
to: `${siteApi}/user/logout`,
},
],
]
}
</script>
<template>
<div
class="admin-links md:px-auto sticky top-0 z-20 w-full bg-zinc-200 bg-opacity-70 px-4 px-8 text-black shadow shadow-gray-300 backdrop-blur-md dark:bg-gray-800 dark:text-white dark:shadow-gray-700"
>
<UHorizontalNavigation
:links="links"
:ui="{
base: 'text-xs',
}"
/>
</div>
</template>
<style lang="css">
.admin-links {
.truncate {
@apply hidden md:block;
}
}
</style>
There are a few key points to note:
- The Drupal Tabs were relative, so we had to force the API environment to recognize the root.
- We added some extra links, as having access to the API is incredibly helpful for debugging and checking things. This has been invaluable.
- We included a link back to Drupal—specifically, for us, this is at admin/content.
- We are also using Nuxt UI, as we've migrated away from Bootstrap in favor of Tailwind.
- On the Drupal side, we added additional routing for the API call:
custom_builder.routing.yml
custom.ce_api:
path: '/ce-api/node/{node}'
defaults:
_controller: '\Drupal\custom\Controller\LocalApiController::nodeExtraSettings'
_title: 'API'
requirements:
_permission: 'access content'
options:
parameters:
node:
type: 'entity:node'
I believe incorporating this link into the default Lupus setup will be a game-changer for developers.
Yes, your thoughts align perfectly with what we had in mind. That’s why we decided to revert to a simpler approach. This highlights the beauty of Lupus, where we consistently rely on Drupal to excel at what it does best. Now that you’ve created separate tasks, I’ll respond to those accordingly.
This gives you and idea on larger device and smaller.
@fargo we have played a lot with the setup and ended up reverting to the default you have setup. I think it is the right approach as it is exactly what Drupal does.
So in nuxt for login we just redirect them to Drupal backend and log them in. That sets the session and then when they start editing or viewing content they have access to the admin menu in the front end.
We just extended the Lupus renderer to include some simple user info and this gave us the ability to add admin capability like this:
function custom_lupus_ce_renderer_response_alter(array &$data, BubbleableMetadata $bubbleable_metadata, Request $request) {
// Get the currently logged-in user.
$user = \Drupal::currentUser();
// Create an array containing the current user's name, uid, and roles.
$current_user = [
'uid' => $user->id(),
'name' => $user->getDisplayName(),
'roles' => $user->getRoles(),
];
// Add the current user array to the data.
$data['current_user'] = $current_user;
// Fetch the site configuration.
$site_config = \Drupal::config('system.site');
$site_info = [
'name' => $site_config->get('name'),
'slogan' => $site_config->get('slogan'),
'mail' => $site_config->get('mail'),
];
// Add the site info to the data.
$data['site_info'] = $site_info;
// Remove 'meta' attribute from 'content' if it exists.
if (isset($data['content']['meta'])) {
unset($data['content']['meta']);
}
}
I would love to share what we did on the Nuxt end for admin features as I think it has been super helpful. Bottom line this is all possible due to the session.
I love what you guys are doing and opening up the flexibility of Nuxt as the frontend has been awesome.
Yup great job on the caching tweak. I feel like this could be added to the sub theme as a tiny default tweak?
glynster → made their first commit to this issue’s fork.
glynster → made their first commit to this issue’s fork.
Thanks for the patch @bradhawkins. As this was a breaking change it was better to add a schema instead. Please update to the latest version and enjoy.
glynster → made their first commit to this issue’s fork.
glynster → made their first commit to this issue’s fork.
Closed outdated.
Thanks for all the work here @avpaderno.
glynster → made their first commit to this issue’s fork.
Thanks @R0djer adding this to the next release.
Best solution is to add to your theme preprocess andikanio 🐛 10.3 upgrade now missing status-message theme sugestions Active
glynster → created an issue.