From a410143ce74ab71552aabd71b1500b6cb4aa3913 Mon Sep 17 00:00:00 2001 From: Jeffrey Date: Thu, 19 Feb 2026 18:46:33 +0100 Subject: [PATCH] Feature: header footer update links --- public/robots.txt | 11 +++ public/sitemap.xml | 5 ++ src/Footer/Component.tsx | 21 +++++- src/Footer/config.ts | 47 +++++++++++++ src/Header/Component.client.tsx | 20 +++++- src/Header/Nav/index.tsx | 42 +++++++----- src/Header/config.ts | 114 +++++++++++++++++++++++++++++--- src/collections/Pages/index.ts | 34 ++++++++-- 8 files changed, 255 insertions(+), 39 deletions(-) create mode 100644 public/robots.txt create mode 100644 public/sitemap.xml diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..5b34ada --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,11 @@ +# * +User-agent: * +Disallow: /admin/* + +# Host +Host: http://localhost:3000 + +# Sitemaps +Sitemap: http://localhost:3000/sitemap.xml +Sitemap: http://localhost:3000/pages-sitemap.xml +Sitemap: http://localhost:3000/posts-sitemap.xml diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..69b81e0 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,5 @@ + + +http://localhost:3000/pages-sitemap.xml +http://localhost:3000/posts-sitemap.xml + \ No newline at end of file diff --git a/src/Footer/Component.tsx b/src/Footer/Component.tsx index d1342ba..87f4e86 100644 --- a/src/Footer/Component.tsx +++ b/src/Footer/Component.tsx @@ -1,16 +1,31 @@ import { getCachedGlobal } from '@/utilities/getGlobals' import Link from 'next/link' import React from 'react' -import type { Footer } from '@/payload-types' +import type { Footer, Page } from '@/payload-types' import { CMSLink } from '@/components/Link' import { Logo } from '@/components/Logo/Logo' +/** Resolves the logo href from the logoLink group field */ +function resolveLogoHref(logoLink: Footer['logoLink']): string { + if (!logoLink) return '/' + if (logoLink.type === 'reference') { + const ref = logoLink.reference + if (ref && typeof ref === 'object' && 'value' in ref) { + const page = ref.value + return typeof page === 'object' && page !== null ? `/${(page as Page).slug}` : '/' + } + return '/' + } + return logoLink.url || '/' +} + export async function Footer() { - const footerData = await getCachedGlobal("footer", 1)() as unknown as Footer + const footerData = await getCachedGlobal('footer', 1)() as unknown as Footer const columns = footerData?.columns || [] const navItems = footerData?.navItems || [] const hasColumns = columns.length > 0 + const logoHref = resolveLogoHref(footerData?.logoLink) const bottomLeft = (footerData?.bottomLeftText || '© {year} Fiber Direkt. Alla rättigheter förbehållna.').replace('{year}', new Date().getFullYear().toString()) const bottomRight = footerData?.bottomRightText || 'Svenskt datacenter · ISO 27001 · ISO 14001' @@ -21,7 +36,7 @@ export async function Footer() {
{/* Logo column */}
- +
diff --git a/src/Footer/config.ts b/src/Footer/config.ts index ccd65ac..41ecb43 100644 --- a/src/Footer/config.ts +++ b/src/Footer/config.ts @@ -8,6 +8,49 @@ export const Footer: GlobalConfig = { read: () => true, }, fields: [ + // ── Logo link ───────────────────────────────────────────────────────────── + { + name: 'logoLink', + type: 'group', + label: 'Logotyplänk', + admin: { + description: 'Vart man hamnar när man klickar på logotypen i footern. Standard är startsidan (/).', + }, + fields: [ + { + name: 'type', + type: 'radio', + label: 'Länktyp', + options: [ + { label: 'Intern sida', value: 'reference' }, + { label: 'Anpassad URL', value: 'custom' }, + ], + defaultValue: 'custom', + admin: { layout: 'horizontal' }, + }, + { + name: 'reference', + type: 'relationship', + label: 'Intern sida', + relationTo: ['pages'] as const, + admin: { + condition: (_, siblingData) => siblingData?.type === 'reference', + }, + }, + { + name: 'url', + type: 'text', + label: 'URL', + defaultValue: '/', + admin: { + condition: (_, siblingData) => siblingData?.type === 'custom', + description: 'T.ex. / eller /startsida', + }, + }, + ], + }, + + // ── Sitemap columns ─────────────────────────────────────────────────────── { name: 'columns', type: 'array', @@ -47,6 +90,8 @@ export const Footer: GlobalConfig = { }, ], }, + + // ── Simple nav (legacy) ─────────────────────────────────────────────────── { name: 'navItems', type: 'array', @@ -62,6 +107,8 @@ export const Footer: GlobalConfig = { ], maxRows: 6, }, + + // ── Bottom text ─────────────────────────────────────────────────────────── { name: 'bottomLeftText', type: 'text', diff --git a/src/Header/Component.client.tsx b/src/Header/Component.client.tsx index 248ea4d..9a12c92 100644 --- a/src/Header/Component.client.tsx +++ b/src/Header/Component.client.tsx @@ -4,7 +4,7 @@ import Link from 'next/link' import { usePathname } from 'next/navigation' import React, { useEffect, useState } from 'react' -import type { Header } from '@/payload-types' +import type { Header, Page } from '@/payload-types' import { Logo } from '@/components/Logo/Logo' import { HeaderNav } from './Nav' @@ -13,6 +13,20 @@ interface HeaderClientProps { data: Header } +/** Resolves the logo href from the logoLink group field */ +function resolveLogoHref(logoLink: Header['logoLink']): string { + if (!logoLink) return '/' + if (logoLink.type === 'reference') { + const ref = logoLink.reference + if (ref && typeof ref === 'object' && 'value' in ref) { + const page = ref.value + return typeof page === 'object' && page !== null ? `/${(page as Page).slug}` : '/' + } + return '/' + } + return logoLink.url || '/' +} + export const HeaderClient: React.FC = ({ data }) => { /* Storing the value in a useState to avoid hydration errors */ const [theme, setTheme] = useState(null) @@ -29,13 +43,15 @@ export const HeaderClient: React.FC = ({ data }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [headerTheme]) + const logoHref = resolveLogoHref(data?.logoLink) + return (
- + diff --git a/src/Header/Nav/index.tsx b/src/Header/Nav/index.tsx index c7bed09..c8d8202 100644 --- a/src/Header/Nav/index.tsx +++ b/src/Header/Nav/index.tsx @@ -1,22 +1,28 @@ 'use client' import React, { useState, useEffect, useRef } from 'react' -import type { Header as HeaderType } from '@/payload-types' +import type { Header as HeaderType, Page } from '@/payload-types' import Link from 'next/link' import { usePathname } from 'next/navigation' import { MenuIcon, XIcon, ChevronDownIcon, ChevronRightIcon } from 'lucide-react' -// ─── Types ─────────────────────────────────────────────────────────────────── -type NavChild = { - label: string - url: string - group?: string | null -} +// ─── Types ──────────────────────────────────────────────────────────────────── +type NavChild = NonNullable[number]['children']>[number]> +type NavItem = NonNullable[number] -type NavItem = { - label: string +// ─── Resolve href from reference or custom url ──────────────────────────────── +function resolveHref(item: { + type?: string | null url?: string | null - megaMenu?: boolean | null - children?: NavChild[] | null + reference?: { relationTo?: string; value?: number | Page | null } | null +}): string { + if (item.type === 'reference' && item.reference?.value) { + const page = item.reference.value + if (typeof page === 'object' && page !== null) { + return page.slug === 'home' || page.slug === 'startsida' ? '/' : `/${(page as Page).slug}` + } + return '#' + } + return item.url || '#' } // ─── Swedish Flag ───────────────────────────────────────────────────────────── @@ -81,14 +87,14 @@ const MegaMenuPanel: React.FC<{ return ( <> - {/* Blur backdrop — covers page below, click to close */} + {/* Blur backdrop */}
- {/* Panel — same white as header, no top border so they read as one */} + {/* Panel */}
(
  • = ({ data }) => { {item.children!.map((child, j) => ( setOpenDropdown(null)} className="block px-5 py-2.5 font-joey text-fd-navy hover:bg-fd-yellow/20 transition-colors text-sm" > @@ -250,7 +256,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => { ) : ( {item.label} @@ -326,7 +332,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => { {item.children!.map((child, j) => ( @@ -338,7 +344,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => { ) : ( diff --git a/src/Header/config.ts b/src/Header/config.ts index 0612e46..7b70546 100644 --- a/src/Header/config.ts +++ b/src/Header/config.ts @@ -7,6 +7,49 @@ export const Header: GlobalConfig = { read: () => true, }, fields: [ + // ── Logo link ───────────────────────────────────────────────────────────── + { + name: 'logoLink', + type: 'group', + label: 'Logotyplänk', + admin: { + description: 'Vart man hamnar när man klickar på logotypen. Standard är startsidan (/).', + }, + fields: [ + { + name: 'type', + type: 'radio', + label: 'Länktyp', + options: [ + { label: 'Intern sida', value: 'reference' }, + { label: 'Anpassad URL', value: 'custom' }, + ], + defaultValue: 'custom', + admin: { layout: 'horizontal' }, + }, + { + name: 'reference', + type: 'relationship', + label: 'Intern sida', + relationTo: ['pages'] as const, + admin: { + condition: (_, siblingData) => siblingData?.type === 'reference', + }, + }, + { + name: 'url', + type: 'text', + label: 'URL', + defaultValue: '/', + admin: { + condition: (_, siblingData) => siblingData?.type === 'custom', + description: 'T.ex. / eller /startsida', + }, + }, + ], + }, + + // ── Nav items ───────────────────────────────────────────────────────────── { name: 'navItems', type: 'array', @@ -22,13 +65,43 @@ export const Header: GlobalConfig = { name: 'label', type: 'text', required: true, - label: 'Label', + label: 'Etikett', + }, + // ── Link type toggle ──────────────────────────────────────────────── + { + name: 'type', + type: 'radio', + label: 'Länktyp', + options: [ + { label: 'Intern sida', value: 'reference' }, + { label: 'Anpassad URL', value: 'custom' }, + ], + defaultValue: 'custom', + admin: { + layout: 'horizontal', + description: + 'Lämna tomt / ignoreras om detta objekt har en undermeny.', + }, + }, + { + name: 'reference', + type: 'relationship', + label: 'Intern sida', + relationTo: ['pages'] as const, + admin: { + condition: (_, siblingData) => siblingData?.type === 'reference', + }, }, { name: 'url', type: 'text', - label: 'URL (leave empty if this item has a submenu)', + label: 'URL', + admin: { + condition: (_, siblingData) => siblingData?.type === 'custom', + description: 'T.ex. /bredband eller https://extern-sida.se', + }, }, + // ── Mega menu toggle ──────────────────────────────────────────────── { name: 'megaMenu', type: 'checkbox', @@ -36,17 +109,18 @@ export const Header: GlobalConfig = { defaultValue: false, admin: { description: - 'When checked, hovering/clicking this item opens a full-width mega menu instead of a small dropdown card.', + 'När ikryssad öppnar hover/klick ett fullbredds mega menu istället för en liten dropdown.', }, }, + // ── Children ──────────────────────────────────────────────────────── { name: 'children', type: 'array', - label: 'Submenu links', + label: 'Undermenylänkar', maxRows: 16, admin: { description: - 'Add submenu links. If these exist, the parent URL is ignored. Use "Group" to create columns in the mega menu.', + 'Lägg till undermenylänkar. Om dessa finns ignoreras förälderlänken. Använd "Grupp" för att skapa kolumner i mega menu.', initCollapsed: true, }, fields: [ @@ -54,21 +128,43 @@ export const Header: GlobalConfig = { name: 'label', type: 'text', required: true, - label: 'Label', + label: 'Etikett', + }, + { + name: 'type', + type: 'radio', + label: 'Länktyp', + options: [ + { label: 'Intern sida', value: 'reference' }, + { label: 'Anpassad URL', value: 'custom' }, + ], + defaultValue: 'custom', + admin: { layout: 'horizontal' }, + }, + { + name: 'reference', + type: 'relationship', + label: 'Intern sida', + relationTo: ['pages'] as const, + admin: { + condition: (_, siblingData) => siblingData?.type === 'reference', + }, }, { name: 'url', type: 'text', - required: true, label: 'URL', + admin: { + condition: (_, siblingData) => siblingData?.type === 'custom', + }, }, { name: 'group', type: 'text', - label: 'Group (optional)', + label: 'Grupp (valfri)', admin: { description: - 'Links with the same group name appear together in a separate column in the mega menu. Leave empty for the main column.', + 'Länkar med samma gruppnamn visas tillsammans i en separat kolumn i mega menu. Lämna tomt för huvudkolumnen.', }, }, ], diff --git a/src/collections/Pages/index.ts b/src/collections/Pages/index.ts index f881586..3826ea2 100644 --- a/src/collections/Pages/index.ts +++ b/src/collections/Pages/index.ts @@ -46,8 +46,8 @@ import { PreviewField, } from '@payloadcms/plugin-seo/fields' -// ── Slug generator — handles Swedish characters ──────────────────────────── -const generateSlug = (value: string): string => +// ── Slug generator — handles Swedish characters ──────────────────────────────── +const generateSlugFromTitle = (value: string): string => value .toLowerCase() .trim() @@ -131,7 +131,7 @@ export const Pages: CollectionConfig<'pages'> = { FDVideoBlock, FDCtaBannerBlock, FDTestimonialBlock, - FDTeamBlock + FDTeamBlock, ], required: true, admin: { @@ -173,7 +173,7 @@ export const Pages: CollectionConfig<'pages'> = { position: 'sidebar', }, }, - // ── Slug ────────────────────────────────────────────────────────────── + // ── Slug ───────────────────────────────────────────────────────────────── { name: 'slug', type: 'text', @@ -181,14 +181,34 @@ export const Pages: CollectionConfig<'pages'> = { index: true, admin: { position: 'sidebar', - description: 'Genereras automatiskt från titeln.', + description: + 'Sidans URL-slug, t.ex. "om-oss" → fiberdirekt.se/om-oss. Kan alltid redigeras manuellt. Bocka i "Generera slug" nedan för att skriva om automatiskt från titeln.', }, hooks: { beforeChange: [ - ({ data }) => generateSlug(data?.title || ''), + ({ data, siblingData, value }) => { + // Only auto-generate if the checkbox is explicitly checked + if (siblingData?.generateSlug) { + return generateSlugFromTitle(data?.title || siblingData?.title || '') + } + // Otherwise keep whatever value the editor typed (or the existing value) + return value + }, ], }, - }, // ← slug object closes here, then straight into ], + }, + // ── Auto-generate toggle ────────────────────────────────────────────────── + { + name: 'generateSlug', + type: 'checkbox', + label: 'Generera slug automatiskt från titeln', + defaultValue: true, + admin: { + position: 'sidebar', + description: + 'När ikryssad skrivs slugen om från titeln vid varje sparning. Avbocka för att låsa slugen och redigera den manuellt.', + }, + }, ], hooks: { afterChange: [revalidatePage],