Motivation

Make it easy to switch back and forth between frontend and backend.

Ideas

* Provide a very basic custom elements that triggers loading it + have dedicated endpoints which allow re-using Drupal markup and JS.
* We should not get into the business of rewriting links, with existing redirects and possible some additional client-side redirects we should be able to simply redirect to the right admin-backend transparently.

Comments

fago created an issue.

glynster’s picture

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 for the Drupal Tabs:

<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:

  1. The Drupal Tabs were relative, so we had to force the API environment to recognize the root.
  2. We added some extra links, as having access to the API is incredibly helpful for debugging and checking things. This has been invaluable.
  3. We included a link back to Drupal—specifically, for us, this is at admin/content. and a logout.
  4. We are also using Nuxt UI, as we've migrated away from Bootstrap in favor of Tailwind.
  5. 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.

Simple Lupus renderer added for the user info check:

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']);
  }
}

We also added a composable for the Lupus API:

export async function useDrupalApi() {
  const { fetchPage, renderCustomElements, fetchMenu } = useDrupalCe()
  const route = useRoute()

  const page = await fetchPage(route.path, { query: route.query })

  const isAdministrator = computed(() => {
    return page.value?.current_user?.roles.includes('administrator') || false
  })

  const routeSlug = route.params.slug?.[0] || 'front'
  const isFront = routeSlug === 'front'
  const classes = computed(() => {
    return routeSlug + (isAdministrator.value ? ' logged-in' : '')
  })

  // Flag to indicate data readiness
  const dataReady = ref(false)
  dataReady.value = true

  return {
    isAdministrator,
    renderCustomElements,
    fetchMenu,
    classes,
    isFront,
    page,
    dataReady,
  }
}

To log in we push them via Nuxt to the Drupal backend:

export default defineNuxtConfig({
  routeRules: {
    '/user/login': {
      redirect: `${process.env.SITE_API}/user/login`,
    },
  },
}

Let me know if I can help any further.

fago’s picture

Thanks for sharing, this seems awesome!

I wonder how we can facilitate creating, sharing and re-using solutions like this.

If we could manage to work-in necessary backend solutions to be available by default or configurable, we could also package the frontend parts up as sort of theme/component you add. So we could have a couple of add-ons like this available for people to install and use depending on preference. Like a collection of themes you use for starting.

What about providing this as a nuxt-layer? What do you think? Would you be interested in re-shaping it to make it re-usable for multiple sites?

I don't think we should depend on nuxt-ui and tailwind by default, they are great, but we should enable people to make their choices. So things like that should better be optional add-ons, which we then can opt to use for new sites, demos etc.

glynster’s picture

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?

fago’s picture

> What are your thoughts on collaboration? I assume we could handle most of this on GitHub?

Yes, agreed. Since Drupal Gitlab makes all code gplv2+ it's not a good choice for frontend stuff atm, anyway Github seems natural for that, so I think it's fine to go with it atm! We could make some github topic like lupus-decoupled-nuxt-layer and link them from the project page / website?

glynster’s picture

@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:

  1. Wraps Lupus CE and other checks into a composable.
  2. 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.
  3. Adds a Vite hook so Nuxt dev tools work locally with DDEV.
  4. Nuxt 4 compatibility.
  5. Smooth scroll.
  6. Page transition.
  7. Turnstile for forms (currently webform, with CSRF token fetching).
  8. Robots.
  9. Sitemap (we generated an API on the Drupal end).
  10. Nuxt UI (as the base for ease of theming).
  11. 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.