initial commit: LM USA Payload CMS

This commit is contained in:
Jeffrey 2026-03-10 09:54:58 +01:00
parent fb8b75ecaa
commit 36531b71ca
243 changed files with 28919 additions and 183774 deletions

View File

@ -0,0 +1,21 @@
-- cleanup-orphaned-types.sql
-- Run this against your database BEFORE regenerating the migration.
-- Safe to run — all statements use IF EXISTS.
-- Orphaned enum types from failed migration attempts
DROP TYPE IF EXISTS "public"."enum_pages_blocks_lm_product_detail_section_background" CASCADE;
DROP TYPE IF EXISTS "public"."enum__pages_v_blocks_lm_product_detail_section_background" CASCADE;
DROP TYPE IF EXISTS "public"."enum_pages_blocks_lm_service_cards_cards_icon_slug" CASCADE;
DROP TYPE IF EXISTS "public"."enum__pages_v_blocks_lm_service_cards_cards_icon_slug" CASCADE;
-- Orphaned tables (if partially created)
DROP TABLE IF EXISTS "public"."_pages_v_blocks_lm_product_detail_locales" CASCADE;
DROP TABLE IF EXISTS "public"."_pages_v_blocks_lm_product_detail" CASCADE;
DROP TABLE IF EXISTS "public"."pages_blocks_lm_product_detail_locales" CASCADE;
DROP TABLE IF EXISTS "public"."pages_blocks_lm_product_detail" CASCADE;
-- Orphaned columns added to service_cards (will be re-added by fresh migration)
ALTER TABLE IF EXISTS "public"."pages_blocks_lm_service_cards_cards" DROP COLUMN IF EXISTS "icon_slug";
ALTER TABLE IF EXISTS "public"."pages_blocks_lm_service_cards_cards_locales" DROP COLUMN IF EXISTS "eyebrow";
ALTER TABLE IF EXISTS "public"."_pages_v_blocks_lm_service_cards_cards" DROP COLUMN IF EXISTS "icon_slug";
ALTER TABLE IF EXISTS "public"."_pages_v_blocks_lm_service_cards_cards_locales" DROP COLUMN IF EXISTS "eyebrow";

79
debug-seed.mjs Normal file
View File

@ -0,0 +1,79 @@
// Minimal test — one block at a time to find what's breaking
const BASE_URL = process.env.PAYLOAD_API_URL || 'http://localhost:3000'
const API_KEY = process.env.PAYLOAD_API_KEY
const headers = {
'Content-Type': 'application/json',
Authorization: `users API-Key ${API_KEY}`,
}
async function tryPage(title, slug, layout) {
const res = await fetch(`${BASE_URL}/api/pages?locale=sv`, {
method: 'POST',
headers,
body: JSON.stringify({ title, slug, layout, _status: 'draft' }),
})
const json = await res.json()
if (!res.ok) {
console.log(`❌ "${title}": ${JSON.stringify(json, null, 2)}`)
} else {
console.log(`✅ "${title}" id:${json.doc?.id}`)
}
}
// Test 1: totally empty page
await tryPage('Test Empty', 'test-empty', [])
// Test 2: single hero block
await tryPage('Test Hero', 'test-hero', [{
blockType: 'lmHero',
heading: 'Test',
theme: 'dark',
textColor: 'auto',
}])
// Test 3: statistics block
await tryPage('Test Stats', 'test-stats', [{
blockType: 'lmStatistics',
stats: [{ number: '99%', label: 'Uptime' }],
sectionBackground: 'dark',
numberColor: 'gradient',
}])
// Test 4: cardGrid with dark cardStyle
await tryPage('Test CardGrid', 'test-cardgrid', [{
blockType: 'lmCardGrid',
layout: '1-1-1',
cardStyle: 'dark',
sectionBackground: 'dark',
cards: [{ displayMode: 'centeredHeading', heading: 'Card 1' }],
}])
// Test 5: ctaSideImage
await tryPage('Test CtaSide', 'test-ctaside', [{
blockType: 'lmCtaSideImage',
heading: 'Test',
body: 'Body text',
theme: 'dark',
}])
// Test 6: uspChecklist
await tryPage('Test USP', 'test-usp', [{
blockType: 'lmUspChecklist',
heading: 'Why us',
items: [{ text: 'Item one' }],
checkColor: 'yellow',
sectionBackground: 'dark',
textColor: 'white',
}])
// Test 7: ctaBanner
await tryPage('Test Banner', 'test-banner', [{
blockType: 'lmCtaBanner',
heading: 'Get started',
ctaText: 'Go',
ctaLink: '/contact',
sectionBackground: 'dark',
alignment: 'center',
size: 'large',
}])

82
debug-seed2.mjs Normal file
View File

@ -0,0 +1,82 @@
const BASE_URL = process.env.PAYLOAD_API_URL || 'http://localhost:3000'
const API_KEY = process.env.PAYLOAD_API_KEY
const headers = { 'Content-Type': 'application/json', Authorization: `users API-Key ${API_KEY}` }
let i = 0
async function tryPage(label, layout) {
i++
const res = await fetch(`${BASE_URL}/api/pages?locale=sv`, {
method: 'POST', headers,
body: JSON.stringify({ title: `Debug ${i}`, slug: `debug-${i}`, layout, _status: 'draft' }),
})
const json = await res.json()
if (!res.ok) console.log(`${label}:\n${JSON.stringify(json, null, 2)}`)
else console.log(`${label}`)
}
// --- CtaBanner variants ---
await tryPage('Banner: minimal', [{
blockType: 'lmCtaBanner',
heading: 'Test',
sectionBackground: 'dark',
alignment: 'center',
size: 'large',
}])
await tryPage('Banner: with cta fields', [{
blockType: 'lmCtaBanner',
heading: 'Test',
ctaText: 'Go',
ctaLink: '/contact',
sectionBackground: 'dark',
alignment: 'center',
size: 'large',
}])
// --- CardGrid variants ---
await tryPage('CardGrid: minimal 1 card no contentLines', [{
blockType: 'lmCardGrid',
layout: '1-1-1',
cardStyle: 'dark',
sectionBackground: 'dark',
cards: [{ displayMode: 'centeredHeading', heading: 'Card 1' }],
}])
await tryPage('CardGrid: content mode with contentLines', [{
blockType: 'lmCardGrid',
layout: '1-1-1',
cardStyle: 'dark',
sectionBackground: 'dark',
cards: [{
displayMode: 'content',
heading: 'Card 1',
contentLines: [{ text: 'Line one', style: 'normal' }],
}],
}])
// --- USP Checklist ---
await tryPage('USP: textColor white', [{
blockType: 'lmUspChecklist',
heading: 'Why us',
items: [{ text: 'Item one' }],
checkColor: 'yellow',
sectionBackground: 'dark',
textColor: 'white',
}])
await tryPage('USP: no textColor', [{
blockType: 'lmUspChecklist',
heading: 'Why us',
items: [{ text: 'Item one' }],
checkColor: 'yellow',
sectionBackground: 'dark',
}])
await tryPage('USP: textColor navy', [{
blockType: 'lmUspChecklist',
heading: 'Why us',
items: [{ text: 'Item one' }],
checkColor: 'yellow',
sectionBackground: 'dark',
textColor: 'navy',
}])

87
fix-migration.mjs Normal file
View File

@ -0,0 +1,87 @@
/**
* fix-migration.mjs (v2)
*
* Run from your project root:
* node fix-migration.mjs
*
* What it does:
* 1. Deletes the broken migration file (the newest one)
* 2. Removes it from migrations/index.ts
* 3. Writes a cleanup-orphaned-types.sql file to your project root
* 4. Tells you exactly what to run next
*/
import { readFileSync, writeFileSync, unlinkSync, readdirSync, statSync } from 'fs'
import { join } from 'path'
const MIGRATIONS_DIR = './src/migrations'
// ── Find the newest migration .ts file ───────────────────────────────────────
const tsFiles = readdirSync(MIGRATIONS_DIR)
.filter(f => f.endsWith('.ts') && f !== 'index.ts')
.map(f => ({ name: f, mtime: statSync(join(MIGRATIONS_DIR, f)).mtime }))
.sort((a, b) => b.mtime - a.mtime)
if (tsFiles.length === 0) {
console.error('❌ No migration files found in', MIGRATIONS_DIR)
process.exit(1)
}
const migrationName = tsFiles[0].name.replace('.ts', '')
const newestTs = join(MIGRATIONS_DIR, tsFiles[0].name)
const newestJson = newestTs.replace('.ts', '.json')
console.log(`\n🗑️ Deleting broken migration: ${tsFiles[0].name}`)
try { unlinkSync(newestTs); console.log(` ✅ deleted .ts`) } catch(e) { console.log(` ⚠️ .ts: ${e.message}`) }
try { unlinkSync(newestJson); console.log(` ✅ deleted .json`) } catch(e) { console.log(` (no .json found)`) }
// ── Remove from migrations/index.ts ──────────────────────────────────────────
const indexPath = join(MIGRATIONS_DIR, 'index.ts')
let indexSrc = readFileSync(indexPath, 'utf8')
// Remove any line that references this migration name
const safeRef = migrationName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
indexSrc = indexSrc.replace(new RegExp(`^.*${safeRef}.*\\n`, 'gm'), '')
writeFileSync(indexPath, indexSrc, 'utf8')
console.log(` ✅ removed from migrations/index.ts`)
// ── Write cleanup SQL ─────────────────────────────────────────────────────────
const cleanupSql = `-- cleanup-orphaned-types.sql
-- Run this against your database BEFORE regenerating the migration.
-- Safe to run all statements use IF EXISTS.
-- Orphaned enum types from failed migration attempts
DROP TYPE IF EXISTS "public"."enum_pages_blocks_lm_product_detail_section_background" CASCADE;
DROP TYPE IF EXISTS "public"."enum__pages_v_blocks_lm_product_detail_section_background" CASCADE;
DROP TYPE IF EXISTS "public"."enum_pages_blocks_lm_service_cards_cards_icon_slug" CASCADE;
DROP TYPE IF EXISTS "public"."enum__pages_v_blocks_lm_service_cards_cards_icon_slug" CASCADE;
-- Orphaned tables (if partially created)
DROP TABLE IF EXISTS "public"."_pages_v_blocks_lm_product_detail_locales" CASCADE;
DROP TABLE IF EXISTS "public"."_pages_v_blocks_lm_product_detail" CASCADE;
DROP TABLE IF EXISTS "public"."pages_blocks_lm_product_detail_locales" CASCADE;
DROP TABLE IF EXISTS "public"."pages_blocks_lm_product_detail" CASCADE;
-- Orphaned columns added to service_cards (will be re-added by fresh migration)
ALTER TABLE IF EXISTS "public"."pages_blocks_lm_service_cards_cards" DROP COLUMN IF EXISTS "icon_slug";
ALTER TABLE IF EXISTS "public"."pages_blocks_lm_service_cards_cards_locales" DROP COLUMN IF EXISTS "eyebrow";
ALTER TABLE IF EXISTS "public"."_pages_v_blocks_lm_service_cards_cards" DROP COLUMN IF EXISTS "icon_slug";
ALTER TABLE IF EXISTS "public"."_pages_v_blocks_lm_service_cards_cards_locales" DROP COLUMN IF EXISTS "eyebrow";
`
writeFileSync('./cleanup-orphaned-types.sql', cleanupSql, 'utf8')
console.log(`\n📄 Generated: cleanup-orphaned-types.sql`)
console.log(`
${'─'.repeat(60)}
👉 Run these 3 commands in order:
1. psql $DATABASE_URI -f cleanup-orphaned-types.sql
(or open cleanup-orphaned-types.sql in TablePlus /
Postico / any DB GUI and run it as a query)
2. pnpm payload migrate:create
3. pnpm payload migrate
${'─'.repeat(60)}
`)

View File

@ -85,6 +85,8 @@ const nextConfig = {
protocol: url.protocol.replace(':', ''),
}
}),
{ hostname: 'img.youtube.com', protocol: 'https' },
{ hostname: 'i.vimeocdn.com', protocol: 'https' },
],
},
webpack: (webpackConfig) => {

42
nuclear-cleanup.sql Normal file
View File

@ -0,0 +1,42 @@
-- nuclear-cleanup.sql
-- Step 1: show what enum types currently exist related to our blocks
SELECT typname FROM pg_type
WHERE typname LIKE '%lm_service_cards%'
OR typname LIKE '%lm_product_detail%'
ORDER BY typname;
-- Step 2: Drop everything with DO block so we can be dynamic
DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN
SELECT typname FROM pg_type
WHERE typname LIKE '%lm_service_cards%'
OR typname LIKE '%lm_product_detail%'
LOOP
EXECUTE 'DROP TYPE IF EXISTS public.' || quote_ident(r.typname) || ' CASCADE';
RAISE NOTICE 'Dropped type: %', r.typname;
END LOOP;
END $$;
-- Step 3: Drop tables
DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN
SELECT tablename FROM pg_tables
WHERE schemaname = 'public'
AND (tablename LIKE '%lm_service_cards%'
OR tablename LIKE '%lm_product_detail%')
LOOP
EXECUTE 'DROP TABLE IF EXISTS public.' || quote_ident(r.tablename) || ' CASCADE';
RAISE NOTICE 'Dropped table: %', r.tablename;
END LOOP;
END $$;
-- Step 4: Confirm nothing left
SELECT typname FROM pg_type
WHERE typname LIKE '%lm_service_cards%'
OR typname LIKE '%lm_product_detail%';

879
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "fdweb2",
"name": "layer-mesh-usa",
"version": "1.0.0",
"description": "Website template for Payload",
"description": "Layer & Mesh USA",
"license": "MIT",
"type": "module",
"scripts": {
@ -22,18 +22,19 @@
"test:int": "cross-env NODE_OPTIONS=--no-deprecation vitest run --config ./vitest.config.mts"
},
"dependencies": {
"@payloadcms/admin-bar": "^3.76.1",
"@payloadcms/db-postgres": "^3.76.1",
"@payloadcms/email-nodemailer": "^3.76.1",
"@payloadcms/live-preview-react": "3.76.1",
"@payloadcms/next": "^3.76.1",
"@payloadcms/plugin-form-builder": "^3.76.1",
"@payloadcms/plugin-nested-docs": "^3.76.1",
"@payloadcms/plugin-redirects": "^3.76.1",
"@payloadcms/plugin-search": "^3.76.1",
"@payloadcms/plugin-seo": "^3.76.1",
"@payloadcms/richtext-lexical": "^3.76.1",
"@payloadcms/ui": "^3.76.1",
"@payloadcms/admin-bar": "^3.79.0",
"@payloadcms/db-postgres": "^3.79.0",
"@payloadcms/email-nodemailer": "^3.79.0",
"@payloadcms/graphql": "^3.79.0",
"@payloadcms/live-preview-react": "^3.79.0",
"@payloadcms/next": "^3.79.0",
"@payloadcms/plugin-form-builder": "^3.79.0",
"@payloadcms/plugin-nested-docs": "^3.79.0",
"@payloadcms/plugin-redirects": "^3.79.0",
"@payloadcms/plugin-search": "^3.79.0",
"@payloadcms/plugin-seo": "^3.79.0",
"@payloadcms/richtext-lexical": "^3.79.0",
"@payloadcms/ui": "^3.79.0",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
@ -49,12 +50,13 @@
"next": "^16.1.6",
"next-sitemap": "^4.2.3",
"nodemailer": "^8.0.1",
"payload": "^3.76.1",
"payload": "^3.79.0",
"prism-react-renderer": "^2.3.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-hook-form": "7.71.1",
"sharp": "0.34.2",
"stripe": "^20.4.0",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

463
seed-lm-pages.mjs Normal file
View File

@ -0,0 +1,463 @@
/**
* Layer & Mesh Page Seed Script (FIXED)
*
* FIX: All 'navy' select values replaced with 'dark'.
* The LM block configs don't include 'navy' as a valid option.
* If 'dark' doesn't work either, run discover-valid-values.mjs
* to find the correct values for your block select fields.
*
* Seeds 3 pages:
* 1. Homepage (slug: home, replaces root page)
* 2. SD-WAN (slug: sd-wan)
* 3. Contact (slug: contact)
*
* Usage:
* PAYLOAD_API_URL=http://localhost:3000 PAYLOAD_API_KEY=your_key node seed-lm-pages.mjs
*
* Notes:
* - Pages are created as DRAFTS publish manually in admin
* - Images are intentionally omitted attach manually post-seed
* - Run once; re-running will create duplicates (check admin first)
*/
const BASE_URL = process.env.PAYLOAD_API_URL || 'http://localhost:3000'
const API_KEY = process.env.PAYLOAD_API_KEY
if (!API_KEY) {
console.error('❌ Set PAYLOAD_API_KEY environment variable first.')
process.exit(1)
}
const headers = {
'Content-Type': 'application/json',
Authorization: `users API-Key ${API_KEY}`,
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
/** Wrap a plain string into Payload Lexical richText JSON */
function richText(text) {
return {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
children: [
{
type: 'paragraph',
format: '',
indent: 0,
version: 1,
children: [{ type: 'text', text, format: 0, style: '', mode: 'normal', version: 1, detail: 0 }],
textFormat: 0,
textStyle: '',
},
],
direction: 'ltr',
},
}
}
async function createPage(data) {
const res = await fetch(`${BASE_URL}/api/pages?locale=sv`, {
method: 'POST',
headers,
body: JSON.stringify({ ...data, _status: 'draft' }),
})
const json = await res.json()
if (!res.ok) {
console.error(`❌ Failed to create "${data.title}":`, JSON.stringify(json, null, 2))
return null
}
console.log(`✅ Created: "${data.title}" (id: ${json.doc?.id}, slug: ${json.doc?.slug})`)
return json.doc
}
// ─── Page Definitions ─────────────────────────────────────────────────────────
// ── 1. HOMEPAGE ──────────────────────────────────────────────────────────────
const homepage = {
title: 'Hem',
slug: 'home',
layout: [
// Hero
{
blockType: 'lmHero',
heading: 'The Network Infrastructure Layer for Modern Business',
subheading: 'SD-WAN · Connectivity · Cloud Networking',
body: 'Layer & Mesh delivers enterprise-grade SD-WAN and connectivity solutions built for US businesses that demand performance, security, and control.',
ctaText: 'Get Started',
ctaLink: '/contact',
secondaryCtaText: 'View Solutions',
secondaryCtaLink: '/sd-wan',
theme: 'dark',
textColor: 'auto',
},
// Stats
{
blockType: 'lmStatistics',
stats: [
{ number: '99.99%', label: 'Uptime SLA' },
{ number: '<10ms', label: 'Latency' },
{ number: '24/7', label: 'NOC Support' },
{ number: '40+', label: 'PoP Locations' },
],
sectionBackground: 'dark',
numberColor: 'gradient',
},
// What we do — two-column CTA
{
blockType: 'lmCtaSideImage',
heading: 'Built for Distributed Enterprises',
body: 'Whether you\'re connecting branch offices, securing remote workers, or migrating to cloud-first infrastructure — Layer & Mesh provides the network fabric that makes it possible. Our SD-WAN platform gives you application-aware routing, zero-trust security, and full visibility from a single dashboard.',
ctaText: 'Explore SD-WAN',
ctaLink: '/sd-wan',
imagePosition: 'right',
theme: 'dark',
},
// Core services
{
blockType: 'lmServicesGrid',
heading: 'Our Solutions',
columns: '3',
services: [
{
title: 'SD-WAN',
description: 'Intelligent, application-aware wide-area networking that replaces legacy MPLS with cloud-ready overlay infrastructure.',
link: '/sd-wan',
},
{
title: 'Secure Access',
description: 'Zero-trust network access (ZTNA) and integrated firewall-as-a-service to protect every user and device, everywhere.',
link: '/contact',
},
{
title: 'Cloud Connect',
description: 'Direct, low-latency connections to AWS, Azure, and GCP — bypassing the public internet for mission-critical workloads.',
link: '/contact',
},
{
title: 'Managed WAN',
description: 'Fully managed wide-area network service with proactive monitoring, 24/7 NOC, and guaranteed SLAs.',
link: '/contact',
},
{
title: 'Connectivity',
description: 'Fiber, broadband, and LTE failover circuits — Layer & Mesh sources and manages the underlay so you don\'t have to.',
link: '/contact',
},
{
title: 'Network Analytics',
description: 'Real-time visibility, flow analysis, and AI-driven anomaly detection across your entire WAN fabric.',
link: '/contact',
},
],
},
// USP checklist
{
blockType: 'lmUspChecklist',
heading: 'Why Layer & Mesh',
items: [
{ text: 'Carrier-agnostic — works with any ISP or circuit type' },
{ text: 'Single-pane-of-glass management across all locations' },
{ text: 'Built-in security: NGFW, IDS/IPS, ZTNA in one platform' },
{ text: 'Rapid deployment — new sites live in hours, not weeks' },
{ text: 'US-based support team with dedicated account managers' },
{ text: 'Flexible commercial models: OPEX or CAPEX' },
],
checkColor: 'yellow',
sectionBackground: 'dark',
textColor: 'white',
},
// CTA banner
{
blockType: 'lmCtaBanner',
heading: 'Ready to modernize your WAN?',
subheading: 'Talk to a network architect today — no sales pressure, just expertise.',
ctaText: 'Schedule a Call',
ctaLink: '/contact',
secondaryCtaText: 'Learn About SD-WAN',
secondaryCtaLink: '/sd-wan',
sectionBackground: 'dark',
alignment: 'center',
size: 'large',
},
],
}
// ── 2. SD-WAN SERVICE PAGE ────────────────────────────────────────────────────
const sdwanPage = {
title: 'SD-WAN',
slug: 'sd-wan',
layout: [
// Hero
{
blockType: 'lmAlternateHero',
heading: 'SD-WAN That Scales With Your Business',
description: 'Replace legacy MPLS with a cloud-native, application-aware network overlay. Layer & Mesh SD-WAN gives IT teams full control and visibility across every location — from headquarters to the smallest branch.',
primaryCtaText: 'Get a Demo',
primaryCtaLink: '/contact',
secondaryCtaText: 'Download Datasheet',
secondaryCtaLink: '/contact',
sectionBackground: 'dark',
},
// What is SD-WAN
{
blockType: 'lmCtaSideImage',
heading: 'What is SD-WAN?',
body: 'Software-Defined Wide Area Networking (SD-WAN) separates the network control plane from the data plane, giving you centralized management over a distributed WAN. Unlike traditional MPLS, SD-WAN is transport-agnostic — it can run over fiber, broadband, LTE, or any combination — and routes traffic intelligently based on real-time application performance metrics.',
ctaText: 'Talk to an Expert',
ctaLink: '/contact',
imagePosition: 'left',
theme: 'dark',
},
// Key capabilities
{
blockType: 'lmCardGrid',
layout: '1-1-1',
cardStyle: 'dark',
sectionBackground: 'dark',
cards: [
{
displayMode: 'centeredHeading',
heading: 'Application-Aware Routing',
},
{
displayMode: 'centeredHeading',
heading: 'Zero-Touch Provisioning',
},
{
displayMode: 'centeredHeading',
heading: 'Integrated Security Stack',
},
{
displayMode: 'centeredHeading',
heading: 'Cloud On-Ramp',
},
{
displayMode: 'centeredHeading',
heading: 'WAN Optimization',
},
{
displayMode: 'centeredHeading',
heading: 'Real-Time Analytics',
},
],
},
// Tech properties — spec callout
{
blockType: 'lmTechProperties',
properties: [
{ category: 'Deployment Model', value: 'Cloud-Managed' },
{ category: 'Failover Time', value: '<500ms' },
{ category: 'Encryption', value: 'AES-256' },
{ category: 'Uptime SLA', value: '99.99%' },
],
sectionBackground: 'dark',
categoryColor: 'white',
valueColor: 'yellow',
},
// USP table
{
blockType: 'lmUspTable',
heading: 'SD-WAN vs. Legacy MPLS',
rows: [
{
title: 'Cost',
description: richText('SD-WAN runs over affordable broadband and LTE circuits, eliminating expensive dedicated MPLS lines. Most customers see 4070% WAN cost reduction.'),
},
{
title: 'Deployment Speed',
description: richText('Zero-touch provisioning gets new sites online in hours. MPLS circuits can take 90+ days to provision.'),
},
{
title: 'Cloud Performance',
description: richText('Direct cloud breakout routes traffic to AWS, Azure, and GCP without backhauling through the datacenter — dramatically improving SaaS application performance.'),
},
{
title: 'Visibility',
description: richText('Per-application, per-user, per-site analytics in a single dashboard. Legacy WAN provides minimal insight into traffic quality.'),
},
{
title: 'Resilience',
description: richText('Automatic failover across multiple transport paths (fiber + LTE) in under 500ms. MPLS has no built-in redundancy.'),
},
],
checkColor: 'yellow',
sectionBackground: 'dark',
textColor: 'white',
},
// Wide card — VF Standard CPE callout
{
blockType: 'lmWideCard',
heading: 'VF Standard CPE',
body: 'Our flagship SD-WAN customer premises equipment. Pre-configured, zero-touch deployable, and managed 24/7 by Layer & Mesh NOC engineers. Ships preconfigured for your network — plug it in and it comes online automatically.',
ctaText: 'Order Now',
ctaLink: '/contact',
cardBackground: 'dark',
buttonColor: 'yellow',
sectionBackground: 'dark',
},
// FAQ
{
blockType: 'lmFaq',
heading: 'SD-WAN FAQs',
items: [
{
question: 'How long does SD-WAN deployment take?',
answer: richText('New sites can be deployed in as little as 4 hours using zero-touch provisioning. The device ships pre-configured — your team just plugs it in and connects the WAN circuits. Edge cases like custom firewall policies or complex BGP configurations may take longer.'),
},
{
question: 'Can SD-WAN replace our existing MPLS?',
answer: richText('Yes. Most customers migrate in phases — we run SD-WAN alongside existing MPLS during transition, then decommission MPLS once performance is validated. Layer & Mesh manages the full migration process.'),
},
{
question: 'What happens if my internet connection goes down?',
answer: richText('Layer & Mesh SD-WAN supports active-active dual-WAN configurations. If your primary circuit fails, traffic automatically shifts to the secondary path (e.g., LTE failover) in under 500 milliseconds — most applications won\'t even notice.'),
},
{
question: 'Does SD-WAN include security features?',
answer: richText('Yes. Our platform includes an integrated next-generation firewall (NGFW), IDS/IPS, DNS filtering, and zero-trust network access (ZTNA). Security policies are managed centrally from the same dashboard as your WAN.'),
},
{
question: 'What is the contract term?',
answer: richText('We offer 12, 24, and 36-month terms. Longer terms come with pricing discounts. Month-to-month is available for qualified customers. Contact us to discuss what works for your situation.'),
},
],
theme: 'gray',
},
// CTA
{
blockType: 'lmCtaBanner',
heading: 'See SD-WAN in action',
subheading: 'Book a 30-minute technical demo with one of our network architects.',
ctaText: 'Book a Demo',
ctaLink: '/contact',
sectionBackground: 'yellow',
alignment: 'center',
size: 'medium',
},
],
}
// ── 3. CONTACT PAGE ───────────────────────────────────────────────────────────
const contactPage = {
title: 'Contact',
slug: 'contact',
layout: [
// Header
{
blockType: 'lmAlternateHero',
heading: 'Get in Touch',
description: 'Whether you\'re evaluating SD-WAN, need a quote, or want to talk architecture — our team is ready. No sales script, just honest conversation.',
sectionBackground: 'dark',
},
// Two-column: contact methods + form placeholder
{
blockType: 'lmContact',
heading: 'Reach Us Directly',
contactMethods: [
{
label: 'Sales & New Business',
link: 'mailto:sales@layermesh.com',
},
{
label: 'Technical Support',
link: 'mailto:support@layermesh.com',
},
{
label: 'General Inquiries',
link: 'mailto:hello@layermesh.com',
},
{
label: 'Call Us',
link: 'tel:+18005550100',
},
],
},
// Text block — response time expectations
{
blockType: 'lmText',
heading: 'What to Expect',
body: richText('We respond to all sales inquiries within one business day. Support tickets are triaged within 2 hours for customers on a managed plan. For urgent network issues, call our 24/7 NOC directly.'),
alignment: 'center',
textColor: 'white',
sectionBackground: 'dark',
maxWidth: 'medium',
},
// Stats — trust signals
{
blockType: 'lmStatistics',
stats: [
{ number: '< 1hr', label: 'Avg. First Response' },
{ number: '24/7', label: 'NOC Coverage' },
{ number: '98%', label: 'Customer Satisfaction' },
],
sectionBackground: 'dark',
numberColor: 'gradient',
},
// FAQ — pre-sales questions
{
blockType: 'lmFaq',
heading: 'Common Questions Before You Reach Out',
items: [
{
question: 'Do you work with small businesses?',
answer: richText('Our solutions are best suited for organizations with 3 or more locations, or distributed remote workforces. Single-site businesses may be better served by a local MSP.'),
},
{
question: 'What information should I have ready for a demo?',
answer: richText('It helps to know: number of locations, rough headcount per site, current WAN technology (MPLS, broadband, etc.), and your top 23 pain points. That\'s it — we\'ll do the rest.'),
},
{
question: 'Do you offer a free trial?',
answer: richText('We offer a 30-day proof-of-concept (PoC) for qualified enterprise customers. Contact us to discuss eligibility and scope.'),
},
{
question: 'Where are you based?',
answer: richText('Layer & Mesh is headquartered in the United States with PoP infrastructure across 40+ locations nationwide.'),
},
],
theme: 'dark',
},
],
}
// ─── Run ──────────────────────────────────────────────────────────────────────
async function run() {
console.log(`\n🚀 Seeding Layer & Mesh pages → ${BASE_URL}\n`)
await createPage(homepage)
await createPage(sdwanPage)
await createPage(contactPage)
console.log('\n✨ Done. Pages created as drafts — review and publish in /admin.')
console.log(' Remember to attach images to blocks that need them.')
}
run().catch(console.error)

496
seed-lm-products.mjs Normal file
View File

@ -0,0 +1,496 @@
/**
* Layer & Mesh Product Page Seed Script (FIXED)
*
* FIX: All 'navy' select values replaced with 'dark'.
* The LM block configs don't include 'navy' as a valid option.
* If 'dark' doesn't work either, run discover-valid-values.mjs
* to find the correct values for your block select fields.
*
* Seeds 2 pages:
* 1. Virtual Fiber (slug: virtual-fiber)
* 2. Virtual Datacenter (slug: virtual-datacenter)
*
* Usage:
* PAYLOAD_API_URL=http://localhost:3000 PAYLOAD_API_KEY=your_key node seed-lm-products.mjs
*
* Pages are created as DRAFTS. Publish manually in /admin after reviewing.
* Images: attach manually post-seed slots are noted inline as comments.
*/
const BASE_URL = process.env.PAYLOAD_API_URL || 'http://localhost:3000'
const API_KEY = process.env.PAYLOAD_API_KEY
if (!API_KEY) {
console.error('❌ Set PAYLOAD_API_KEY environment variable first.')
process.exit(1)
}
const headers = {
'Content-Type': 'application/json',
Authorization: `users API-Key ${API_KEY}`,
}
function richText(text) {
return {
root: {
type: 'root', format: '', indent: 0, version: 1,
direction: 'ltr',
children: [{
type: 'paragraph', format: '', indent: 0, version: 1,
textFormat: 0, textStyle: '',
children: [{ type: 'text', text, format: 0, style: '', mode: 'normal', version: 1, detail: 0 }],
}],
},
}
}
async function createPage(data) {
const res = await fetch(`${BASE_URL}/api/pages?locale=sv`, {
method: 'POST',
headers,
body: JSON.stringify({ ...data, _status: 'draft' }),
})
const json = await res.json()
if (!res.ok) {
console.error(`❌ Failed to create "${data.title}":`, JSON.stringify(json?.errors ?? json, null, 2))
return null
}
console.log(`✅ Created: "${data.title}" — id: ${json.doc?.id} slug: /${json.doc?.slug}`)
return json.doc
}
// ─────────────────────────────────────────────────────────────────────────────
// PAGE 1: VIRTUAL FIBER
// ─────────────────────────────────────────────────────────────────────────────
// Positioning: High-performance private WAN connectivity — dedicated bandwidth
// over the Layer & Mesh mesh network. No public internet. Runs between offices,
// datacenters, and cloud endpoints. Key angle: Swedish companies with US
// operations who need a private, GDPR-safe path between continents.
const virtualFiberPage = {
title: 'Virtual Fiber',
slug: 'virtual-fiber',
layout: [
// ── Hero ──
// IMAGE SLOT: Abstract fiber/light-paths visual, dark background
{
blockType: 'lmAlternateHero',
heading: 'Private Connectivity at Fiber Speed',
description: 'Virtual Fiber gives your business a dedicated, private bandwidth path — no shared internet, no traffic contention, no surprises. Whether you\'re connecting offices across the US, linking a Stockholm HQ to a New York branch, or building a direct cloud on-ramp, Virtual Fiber delivers predictable, low-latency performance at scale.',
primaryCtaText: 'Request a Quote',
primaryCtaLink: '/contact',
secondaryCtaText: 'See Technical Specs',
secondaryCtaLink: '#specs',
sectionBackground: 'dark',
},
// ── What is Virtual Fiber ──
// IMAGE SLOT: Clean network diagram — two office nodes connected by a glowing path
{
blockType: 'lmCtaSideImage',
heading: 'Dedicated bandwidth. Not shared. Not "up to."',
body: 'Traditional internet connections share capacity with thousands of other businesses. Virtual Fiber is different — it provisions a dedicated logical circuit across the Layer & Mesh mesh network, delivering your committed bandwidth 100% of the time. Think of it as a private lane on the network highway: your traffic never competes, never slows, and never takes a detour through the public internet.',
ctaText: 'Talk to an Engineer',
ctaLink: '/contact',
imagePosition: 'right',
theme: 'dark',
},
// ── Tech specs callout ──
{
blockType: 'lmTechProperties',
anchorId: 'specs',
properties: [
{ category: 'Bandwidth Options', value: '100M 100G' },
{ category: 'Latency', value: '<1ms metro' },
{ category: 'Availability SLA', value: '99.99%' },
{ category: 'Encryption', value: 'MACsec / AES-256' },
],
sectionBackground: 'dark',
categoryColor: 'white',
valueColor: 'yellow',
},
// ── Use cases — 6 tile grid ──
{
blockType: 'lmCardGrid',
layout: '1-1-1',
cardStyle: 'gray',
sectionBackground: 'dark',
cards: [
{
displayMode: 'content',
heading: 'Multi-Site Office Connectivity',
contentLines: [
{ text: 'Connect branch offices, HQ, and remote sites over a private backbone — no VPN patchwork, no latency variance.', style: 'normal' },
],
},
{
displayMode: 'content',
heading: 'Datacenter Interconnect',
contentLines: [
{ text: 'High-throughput, sub-millisecond links between colocation facilities, private DCs, and cloud edge nodes.', style: 'normal' },
],
},
{
displayMode: 'content',
heading: 'Cloud Direct Connect',
contentLines: [
{ text: 'Skip the public internet entirely for AWS, Azure, and GCP — dedicated on-ramp with guaranteed bandwidth.', style: 'normal' },
],
},
{
displayMode: 'content',
heading: 'Backup & Replication Links',
contentLines: [
{ text: 'Predictable throughput for continuous data replication between primary and DR sites — no surprise throttling.', style: 'normal' },
],
},
{
displayMode: 'content',
heading: 'SD-WAN Underlay',
contentLines: [
{ text: 'Use Virtual Fiber as a premium transport path in your SD-WAN policy — route latency-sensitive apps over dedicated circuits automatically.', style: 'normal' },
],
},
{
displayMode: 'content',
heading: 'Transatlantic Private WAN',
contentLines: [
{ text: 'Connect your US operations directly to your Swedish or European infrastructure — private, encrypted, and compliant by design.', style: 'normal' },
],
},
],
},
// ── Swedish companies in the US — the differentiator angle ──
{
blockType: 'lmWideCard',
heading: 'Swedish operations in the US? This was built for you.',
body: 'Hundreds of Swedish companies have a footprint in the United States — from single-employee outposts to full subsidiary operations. They share a common problem: how do you maintain a fast, secure, and GDPR-compliant connection between your US team and your Swedish infrastructure?\n\nVirtual Fiber provides a private transatlantic path that keeps your data off the public internet, your compliance team satisfied, and your Stockholm engineers actually reachable from New York. No more slow VPNs. No more wondering which jurisdiction your data crossed.',
ctaText: 'Talk to Our Team',
ctaLink: '/contact',
cardBackground: 'dark',
buttonColor: 'yellow',
sectionBackground: 'dark',
},
// ── USP checklist ──
{
blockType: 'lmUspChecklist',
heading: 'Why Virtual Fiber Over Standard Internet',
items: [
{ text: 'Committed bandwidth — your full allocation, 24/7, not a "best effort" estimate' },
{ text: 'No public internet exposure — traffic never leaves the Layer & Mesh network fabric' },
{ text: 'MACsec / AES-256 encryption in transit — required for GDPR, HIPAA, and SOC 2 workloads' },
{ text: 'Sub-1ms metro latency — critical for real-time applications, VoIP, and database replication' },
{ text: '99.99% SLA backed by diverse physical paths — not a marketing claim' },
{ text: 'Symmetric upload and download — no asymmetric throttling on backup or replication jobs' },
{ text: 'Scales from 100 Mbps to 100 Gbps — no hardware swaps, provisioned in software' },
],
checkColor: 'yellow',
sectionBackground: 'dark',
textColor: 'white',
},
// ── FAQ ──
{
blockType: 'lmFaq',
heading: 'Virtual Fiber — Common Questions',
items: [
{
question: 'How is Virtual Fiber different from a regular internet connection?',
answer: richText('A standard internet connection is shared — you\'re competing for bandwidth with every other customer on that circuit. Virtual Fiber provisions a dedicated logical path across the Layer & Mesh mesh network. Your committed bandwidth is always available, latency is consistent, and your traffic doesn\'t touch the public internet.'),
},
{
question: 'Can Virtual Fiber connect to locations outside the Layer & Mesh network?',
answer: richText('Yes. We extend Virtual Fiber circuits to customer premises via our physical PoP infrastructure and carrier peering agreements. For transatlantic connections, we partner with tier-1 submarine cable operators to extend the private path between the US and Europe.'),
},
{
question: 'What bandwidth tiers are available?',
answer: richText('Virtual Fiber is available from 100 Mbps through 100 Gbps. Bandwidth is provisioned in software, so upgrades take hours not weeks. We offer burstable options if you need guaranteed baseline with occasional headroom.'),
},
{
question: 'Is Virtual Fiber GDPR compliant?',
answer: richText('Yes. Because traffic never traverses the public internet and the Layer & Mesh network operates under documented data handling policies, Virtual Fiber is a suitable transport layer for GDPR-regulated data flows — including personal data transferred between the EU and the US under the EU-US Data Privacy Framework.'),
},
{
question: 'How long does provisioning take?',
answer: richText('Metro connections within our existing PoP footprint typically provision in 25 business days. Long-haul and transatlantic circuits depend on last-mile access and typically take 1020 business days. We provide a firm delivery date at time of order.'),
},
{
question: 'What happens if the circuit goes down?',
answer: richText('Virtual Fiber runs over physically diverse paths in the Layer & Mesh mesh. In the event of a path failure, traffic reroutes automatically — most customers see sub-second failover. Our 24/7 NOC monitors every circuit and proactively alerts you before most issues affect service.'),
},
],
theme: 'gray',
},
// ── CTA ──
{
blockType: 'lmCtaBanner',
heading: 'Build your private network backbone.',
subheading: 'Tell us your locations and bandwidth needs — we\'ll design a circuit and quote within 24 hours.',
ctaText: 'Request a Quote',
ctaLink: '/contact',
sectionBackground: 'dark',
alignment: 'center',
size: 'large',
},
],
}
// ─────────────────────────────────────────────────────────────────────────────
// PAGE 2: VIRTUAL DATACENTER
// ─────────────────────────────────────────────────────────────────────────────
// Based on the LM "Delad vDC" Swedish reference, rewritten for the US market.
// Key angles:
// — Shared cloud compute without hyperscaler lock-in
// — Swedish sovereignty: GDPR-compliant by architecture, not just policy
// — Sustainability: shared resources = real carbon reduction
// — Swedish companies in the US: keep data in Sweden, access it from anywhere
// — No big upfront hardware spend; pay for what you use
const virtualDatacenterPage = {
title: 'Virtual Datacenter',
slug: 'virtual-datacenter',
layout: [
// ── Hero ──
// IMAGE SLOT: Abstract datacenter / cluster visual — cool dark tones
{
blockType: 'lmHero',
heading: 'Your Infrastructure. Without the Infrastructure.',
subheading: 'Cloud compute that\'s actually yours — sovereign, scalable, and carbon-efficient.',
body: 'The Layer & Mesh Virtual Datacenter gives your business dedicated access to enterprise-grade compute, storage, and networking — without buying, racking, or managing a single server. Built on our mesh network across geographically separated facilities, and designed from day one for businesses that care where their data lives.',
ctaText: 'Start a Conversation',
ctaLink: '/contact',
secondaryCtaText: 'See Technical Specs',
secondaryCtaLink: '#vdc-specs',
theme: 'dark',
textColor: 'auto',
},
// ── Stats ──
{
blockType: 'lmStatistics',
stats: [
{ number: '3', label: 'Geographically Separated Facilities' },
{ number: '99.99%', label: 'Availability SLA' },
{ number: '0', label: 'Hardware Purchases Required' },
{ number: '<30min', label: 'New VM Provisioning Time' },
],
sectionBackground: 'dark',
numberColor: 'gradient',
},
// ── What is it ──
// IMAGE SLOT: Clean diagram showing shared resource pools → isolated VMs
{
blockType: 'lmCtaSideImage',
heading: 'Shared resources. Completely isolated data.',
body: 'A Virtual Datacenter gives your business direct access to large pools of CPU, RAM, SSD, and disk capacity — managed through a clean web dashboard. The resources are shared across the cluster for efficiency, but your data and workloads are fully isolated from every other tenant. Think of it as having your own datacenter floor, without owning the building.',
ctaText: 'See How It Works',
ctaLink: '/contact',
imagePosition: 'right',
theme: 'dark',
},
// ── Three pillars: Climate / Scalable / Simple ──
{
blockType: 'lmCardGrid',
layout: '1-1-1',
cardStyle: 'dark',
sectionBackground: 'dark',
cards: [
{
displayMode: 'content',
heading: 'Climate-Smart by Design',
contentLines: [
{ text: 'Physical servers sit idle 6080% of the time. Shared virtualization eliminates that waste — the same compute serves many workloads, dramatically reducing the hardware footprint and energy consumption per unit of work.', style: 'normal' },
{ text: 'Layer & Mesh facilities run on renewable energy and recover waste heat. You use only what you need, when you need it.', style: 'normal' },
],
},
{
displayMode: 'content',
heading: 'Scales in Minutes, Not Months',
contentLines: [
{ text: 'Ordering, shipping, racking, and configuring physical hardware takes weeks or months. In the Virtual Datacenter, scaling CPU, RAM, or storage is a dashboard operation — done in minutes.', style: 'normal' },
{ text: 'Whether you\'re launching a new project, handling a seasonal spike, or downsizing after a migration — you match resources to actual demand, not anticipated demand.', style: 'normal' },
],
},
{
displayMode: 'content',
heading: 'Managed by Us. Run by You.',
contentLines: [
{ text: 'The cluster is maintained and monitored by Layer & Mesh engineers around the clock. Firmware updates, hardware failures, capacity management — handled. You log into the dashboard and focus on running your applications.', style: 'normal' },
{ text: 'A file server or development environment can be provisioned and online within 30 minutes — accessible from your office LAN or any secure connection.', style: 'normal' },
],
},
],
},
// ── Tech specs ──
{
blockType: 'lmTechProperties',
anchorId: 'vdc-specs',
properties: [
{ category: 'vCPU', value: 'Up to 64 cores' },
{ category: 'RAM', value: 'Up to 512 GB' },
{ category: 'SSD Storage', value: 'Up to 20 TB' },
{ category: 'Network', value: '125 Gbps internal' },
],
sectionBackground: 'dark',
categoryColor: 'white',
valueColor: 'yellow',
},
// ── Swedish sovereignty angle — key differentiator for US market ──
{
blockType: 'lmWideCard',
heading: 'Swedish company. US operations. Where does your data live?',
body: 'If your business is Swedish-owned, Swedish-operated, or subject to GDPR — this question matters enormously. Storing customer or employee data in US hyperscaler infrastructure creates real compliance exposure under both GDPR and the EU-US Data Privacy Framework.\n\nThe Layer & Mesh Virtual Datacenter keeps your data in Swedish-jurisdiction facilities, with Swedish ownership and governance, while giving your US-based employees fast, secure access to every resource they need. No compliance grey areas. No data sovereignty tradeoffs. No surprise subpoenas to cloud providers you\'ve never heard of.',
ctaText: 'Discuss Your Compliance Needs',
ctaLink: '/contact',
cardBackground: 'dark',
buttonColor: 'yellow',
sectionBackground: 'dark',
},
// ── Use cases — who is this for ──
{
blockType: 'lmServicesGrid',
heading: 'Common Use Cases',
columns: '3',
services: [
{
title: 'Replace On-Premises Servers',
description: 'Stop buying, racking, and maintaining physical servers. Migrate workloads to virtual machines — same performance, a fraction of the overhead.',
},
{
title: 'Development & Test Environments',
description: 'Spin up isolated dev or staging environments in minutes. Tear them down when the sprint ends. Pay only for the hours used.',
},
{
title: 'Business Applications',
description: 'Host ERP systems, file servers, print servers, databases, and internal tools — accessible from any office location over your LAN or VPN.',
},
{
title: 'Cross-Atlantic Collaboration',
description: 'US teams access the same applications and file systems as Stockholm colleagues — fast, secure, and without data leaving Swedish jurisdiction.',
},
{
title: 'Disaster Recovery',
description: 'Replicate on-premises workloads to the Virtual Datacenter for fast failover — activate your DR environment in minutes, not days.',
},
{
title: 'Regulated Data Processing',
description: 'Process GDPR-regulated, HIPAA-adjacent, or sensitive customer data in a facility with documented jurisdiction, ownership, and security certifications.',
},
],
},
// ── Add-on services ──
{
blockType: 'lmUspTable',
heading: 'Optional Add-Ons',
rows: [
{
title: 'Firewall as a Service (FWaaS)',
description: richText('Centralized, virtualized firewall managed by Layer & Mesh. Define rules that apply across one or multiple VLANs — protects your Virtual Datacenter environment without requiring you to maintain firewall hardware or configurations yourself.'),
},
{
title: 'Private VLAN',
description: richText('Extend your office LAN directly into the Virtual Datacenter — Virtual Machines appear on the same network as your physical workstations. Ideal for seamless file sharing, printing, and application access without any VPN friction for end users.'),
},
{
title: 'Colocation',
description: richText('When a pure virtualization environment doesn\'t fit your requirements, combine Virtual Datacenter with physical colocation. Run specialized hardware alongside virtual workloads, connected at wire speed within the same facility.'),
},
{
title: 'Managed Backup',
description: richText('Automated daily backups of Virtual Machine snapshots, stored in a geographically separate facility within the Layer & Mesh mesh. Configurable retention periods and one-click restore via the management portal.'),
},
{
title: 'Virtual Fiber Integration',
description: richText('Connect your US office directly to the Virtual Datacenter via a Layer & Mesh Virtual Fiber circuit — private, encrypted, and off the public internet. Eliminates VPN complexity and delivers datacenter-class connectivity to any office.'),
},
],
checkColor: 'yellow',
sectionBackground: 'dark',
textColor: 'white',
},
// ── FAQ ──
{
blockType: 'lmFaq',
heading: 'Virtual Datacenter — Common Questions',
items: [
{
question: 'How is this different from AWS, Azure, or Google Cloud?',
answer: richText('The major hyperscalers are headquartered in the United States and operate under US jurisdiction — including the CLOUD Act, which allows US authorities to compel data access regardless of where servers are physically located. Layer & Mesh is a Swedish company, operating Swedish-jurisdiction facilities, with no US parent entity. For businesses subject to GDPR or with data sovereignty requirements, this distinction is significant. We\'re also considerably simpler — no 200-item service catalog, no unexpected egress fees, no certification maze.'),
},
{
question: 'Is my data isolated from other customers?',
answer: richText('Yes. Resources are shared at the hardware level for efficiency, but every tenant\'s data, network traffic, and Virtual Machines are completely isolated. Your VMs cannot communicate with or access other customers\' environments. This is standard hypervisor-level isolation, audited and certified.'),
},
{
question: 'What operating systems can I run?',
answer: richText('Any x86-64 operating system: Linux (all major distributions), Windows Server, FreeBSD, and others. We provide standard OS templates for fast provisioning, or you can upload your own ISO. Windows Server licensing can be included in your subscription or brought under your own agreement.'),
},
{
question: 'Can my US-based employees access systems in the Virtual Datacenter?',
answer: richText('Yes — through several methods. You can connect via standard VPN, through a VLAN extension that makes VMs appear on your local network, or (for the best performance) over a dedicated Virtual Fiber private circuit between your US office and the Layer & Mesh facility. Access is fast, secure, and fully auditable.'),
},
{
question: 'What happens if a physical host fails?',
answer: richText('The Layer & Mesh Virtual Datacenter uses clustered infrastructure across geographically separate facilities. If a physical host fails, affected VMs restart automatically on another host in the cluster — typically within 6090 seconds. Storage is replicated across the mesh, so no data is lost in a hardware failure.'),
},
{
question: 'How is pricing structured?',
answer: richText('Virtual Datacenter is priced on the resources you allocate — vCPU, RAM, SSD, and HDD — on a monthly subscription. There are no per-request charges, no egress fees, and no surprise costs. You can adjust your resource allocation up or down each billing cycle.'),
},
{
question: 'What certifications does the facility hold?',
answer: richText('Layer & Mesh facilities are ISO 27001 certified (information security), ISO 14001 certified (environmental management), and comply with SSF physical security standards. All facilities and services are operated in compliance with GDPR and NIS2.'),
},
],
theme: 'dark',
},
// ── CTA ──
{
blockType: 'lmCtaBanner',
heading: 'Move your infrastructure. Keep your data where you trust it.',
subheading: 'Talk to a Layer & Mesh architect about migrating your on-premises workloads — or just getting started with a single VM.',
ctaText: 'Schedule a Call',
ctaLink: '/contact',
secondaryCtaText: 'Compare Add-Ons',
secondaryCtaLink: '#vdc-specs',
sectionBackground: 'dark',
alignment: 'center',
size: 'large',
},
],
}
// ─── Run ──────────────────────────────────────────────────────────────────────
async function run() {
console.log(`\n🚀 Seeding Layer & Mesh product pages → ${BASE_URL}\n`)
await createPage(virtualFiberPage)
await createPage(virtualDatacenterPage)
console.log('\n✨ Done. Pages created as drafts.')
console.log('\n Image slots to fill manually:')
console.log(' Virtual Fiber hero — abstract fiber/light-paths visual, dark bg')
console.log(' Virtual Fiber CTA — network diagram: two nodes connected by glowing path')
console.log(' vDC hero — datacenter/cluster visual, cool dark tones')
console.log(' vDC explainer — diagram: shared resource pools → isolated VMs')
}
run().catch(console.error)

419
seed-lm-usa.mjs Normal file
View File

@ -0,0 +1,419 @@
/**
* Layer & Mesh USA Consolidated Seed
* Pages: Home · Virtual Fiber · Managed WAN · Virtual Server · Contact
* Usage: PAYLOAD_API_KEY=your_key node seed-lm-usa.mjs
* Delete old Home (id:2) and Contact (id:3) in /admin before re-running
*/
const BASE_URL = process.env.PAYLOAD_API_URL || 'http://localhost:3000'
const API_KEY = process.env.PAYLOAD_API_KEY
if (!API_KEY) { console.error('❌ Set PAYLOAD_API_KEY first.'); process.exit(1) }
const headers = { 'Content-Type': 'application/json', Authorization: `users API-Key ${API_KEY}` }
function richText(...paragraphs) {
return { root: { type: 'root', format: '', indent: 0, version: 1, direction: 'ltr',
children: paragraphs.map(text => ({
type: 'paragraph', format: '', indent: 0, version: 1, textFormat: 0, textStyle: '',
children: [{ type: 'text', text, format: 0, style: '', mode: 'normal', version: 1, detail: 0 }],
})),
}}
}
function richTextSpec(items) {
return { root: { type: 'root', format: '', indent: 0, version: 1, direction: 'ltr',
children: items.map(item => item.type === 'heading'
? { type: 'heading', tag: 'h4', format: '', indent: 0, version: 1, direction: 'ltr',
children: [{ type: 'text', text: item.text, format: 0, style: '', mode: 'normal', version: 1, detail: 0 }] }
: { type: 'paragraph', format: '', indent: 0, version: 1, textFormat: 0, textStyle: '',
children: [{ type: 'text', text: item.text, format: 0, style: '', mode: 'normal', version: 1, detail: 0 }] }
),
}}
}
async function createPage(data) {
const res = await fetch(`${BASE_URL}/api/pages?locale=en`, {
method: 'POST', headers,
body: JSON.stringify({ ...data, _status: 'draft' }),
})
const json = await res.json()
if (!res.ok) { console.error(`❌ Failed: "${data.title}"\n`, JSON.stringify(json?.errors ?? json, null, 2)); return null }
console.log(`✅ "${data.title}" → /${json.doc?.slug} (id: ${json.doc?.id})`)
return json.doc
}
// ── 1. HOME ───────────────────────────────────────────────────────────────────
await createPage({
title: 'Home', slug: 'home',
layout: [
{ blockType: 'lmHero',
heading: 'Managed IT Infrastructure for Business',
subheading: 'Virtual Fiber · Managed WAN · Virtual Servers',
body: 'Layer & Mesh delivers the network and compute infrastructure underneath your business — designed, deployed, and managed by engineers who pick up the phone.',
ctaText: 'Talk to an Engineer', ctaLink: '/contact',
secondaryCtaText: 'See Services', secondaryCtaLink: '#services',
theme: 'dark' },
{ blockType: 'lmStatistics',
stats: [{ number: '99.99%', label: 'Uptime SLA' }, { number: '24/7', label: 'NOC Coverage' }, { number: '<500ms', label: 'WAN Failover' }, { number: '40+', label: 'US PoP Locations' }],
sectionBackground: 'dark', numberColor: 'gradient' },
{ blockType: 'lmServiceCards',
anchorId: 'services',
heading: 'Three Services. One Accountable Partner.',
subheading: 'Private connectivity, managed networking, and dedicated compute — from a team that manages all three together.',
columns: '3', cardSurface: 'outlined-teal', sectionBackground: 'dark',
cards: [
{ eyebrow: 'Connectivity', title: 'Virtual Fiber',
description: 'A managed SD-WAN appliance ships to your door. Plug it in — it connects to the Layer & Mesh private mesh automatically. Managed remotely. Monitored 24/7. Works anywhere.',
readMoreText: 'Explore Virtual Fiber', readMoreLink: '/virtual-fiber', iconSlug: 'connectivity' },
{ eyebrow: 'Networking', title: 'Managed WAN',
description: 'We design, deploy, and operate your wide-area network — from branch offices to cloud. Application-aware routing, zero-trust security, full visibility from one dashboard.',
readMoreText: 'Explore Managed WAN', readMoreLink: '/managed-wan', iconSlug: 'connectivity-grid' },
{ eyebrow: 'Compute', title: 'Virtual Server',
description: 'Dedicated vCPU, RAM, and NVMe SSD from our redundant Stockholm cluster. Provisioned in minutes, replicated across geographically separate facilities. Genuine enterprise availability.',
readMoreText: 'Explore Virtual Server', readMoreLink: '/virtual-server', iconSlug: 'virtual-server' },
] },
{ blockType: 'lmUspChecklist',
heading: 'Why Layer & Mesh',
items: [
{ text: 'One vendor across connectivity, networking, and compute — one call when something goes wrong' },
{ text: 'All three services share a private mesh — your servers are reachable from your offices natively' },
{ text: 'Swedish ownership and governance — meaningful for GDPR data flows between the US and EU' },
{ text: 'Transparent monthly pricing — no egress fees, no surprise costs' },
{ text: 'US-based support with named account managers, not anonymous ticketing queues' },
{ text: 'Scales in minutes — add bandwidth, compute, or new sites from a dashboard' },
],
checkColor: 'yellow', sectionBackground: 'dark', textColor: 'white' },
{ blockType: 'lmCtaBanner',
heading: "Let's talk about your infrastructure.",
subheading: "No sales script. Just an honest conversation about what you're running and whether we can improve it.",
ctaText: 'Schedule a Call', ctaLink: '/contact',
secondaryCtaText: 'See Virtual Fiber', secondaryCtaLink: '/virtual-fiber',
sectionBackground: 'dark', alignment: 'center', size: 'large' },
]
})
// ── 2. VIRTUAL FIBER ──────────────────────────────────────────────────────────
// Real product: VF Standard — 1U rack unit, Layer 2 encrypted, plug and play
// $90/month. 2 WAN + 2 LAN RJ-45 1GbE, active cooling, VLAN/QinQ/Full MTU
await createPage({
title: 'Virtual Fiber', slug: 'virtual-fiber',
layout: [
// IMAGE SLOT: upload virtual-fiber-unit.svg to Media in /admin,
// then swap lmAlternateHero for lmHeaderTextImageBlock with that image attached
{ blockType: 'lmAlternateHero',
heading: 'VF Standard — Virtual Fiber Unit',
description: "A rack-mounted Layer 2 networking appliance that ships pre-configured to your door. Plug it into your internet connection and it connects automatically to the Layer & Mesh private mesh. No configuration. No public IP needed. Encrypted Layer 2 transport — from anywhere.",
primaryCtaText: 'Order — $90/mo', primaryCtaLink: '/contact',
secondaryCtaText: 'Tech Specs', secondaryCtaLink: '#specs',
sectionBackground: 'dark' },
// Tutus-style product detail: big description left / specs right
{ blockType: 'lmProductDetail',
eyebrow: 'Platform / Network / Virtual Fiber',
productName: 'VF Standard',
description: "Virtual Fiber is not a VPN. It creates an encrypted, transparent Layer 2 network — built on top of any available internet connection — that behaves exactly like a private fiber link between your locations.\n\nThe VF Standard unit ships pre-configured and ready. Connect it to your existing broadband, fiber, or 4G connection and it joins the Layer & Mesh mesh automatically. Your devices get full Layer 2 transport to every other point on the network — with VLAN, QinQ, and Full MTU support — without a single line of configuration.\n\nNo fixed IP address needed. No IT team required on site. Works as a permanent connection or deployed temporarily. Contract terms start at one day.",
ctaText: 'Order Now — $90/month', ctaLink: '/contact',
secondaryCtaText: 'Download Datasheet', secondaryCtaLink: '/contact',
sectionBackground: 'dark',
body: richTextSpec([
{ type: 'heading', text: '$90 / month' },
{ type: 'paragraph', text: 'Shipping additional. No setup fee. Cancel anytime.' },
{ type: 'heading', text: 'Performance' },
{ type: 'paragraph', text: 'Throughput: Up to 1,000 Mbit/s' },
{ type: 'paragraph', text: 'Transport: Encrypted Layer 2 over any internet' },
{ type: 'paragraph', text: 'Redundancy: Automatic failover via 4G or secondary ISP' },
{ type: 'heading', text: 'Hardware' },
{ type: 'paragraph', text: 'Form factor: 1U rack-mounted' },
{ type: 'paragraph', text: 'Cooling: Active (fan)' },
{ type: 'paragraph', text: 'WAN: 2 × 1GbE RJ-45' },
{ type: 'paragraph', text: 'LAN: 2 × 1GbE RJ-45' },
{ type: 'heading', text: 'Features' },
{ type: 'paragraph', text: 'Plug and play — pre-configured at dispatch' },
{ type: 'paragraph', text: 'VLAN · QinQ · Full MTU' },
{ type: 'paragraph', text: 'No public IP address required' },
{ type: 'paragraph', text: 'World-class Layer 2 encryption' },
]) },
{ blockType: 'lmTechProperties',
anchorId: 'specs',
properties: [
{ category: 'Speed', value: 'Up to 1,000 Mbit/s' },
{ category: 'Form Factor', value: '1U Rack-Mounted' },
{ category: 'Redundancy', value: '4G / Dual-WAN Failover' },
{ category: 'Price', value: '$90 / month' },
],
sectionBackground: 'dark', categoryColor: 'white', valueColor: 'yellow' },
{ blockType: 'lmServiceCards',
heading: 'What Virtual Fiber Makes Possible',
columns: '3', cardSurface: 'outlined-teal', sectionBackground: 'dark',
cards: [
{ eyebrow: 'Reach',
title: 'Access Resources From Anywhere',
description: "Your team in New York reaches your Stockholm servers the same way they reach the printer down the hall — over a private Layer 2 network, without a VPN client or public IP.",
iconSlug: 'connectivity' },
{ eyebrow: 'Simplicity',
title: 'No Configuration Required',
description: "Units arrive ready. Connect to any internet access — fiber, broadband, 4G — and the device joins the mesh automatically. Dual transport paths for redundancy are supported out of the box.",
iconSlug: 'connectivity-grid' },
{ eyebrow: 'Security',
title: 'World-Class Layer 2 Encryption',
description: "All traffic is encrypted using state-of-the-art technology. Secure channels are established over public networks without any configuration — a clear replacement for traditional VPN.",
iconSlug: 'virtual-server' },
] },
{ blockType: 'lmUspChecklist',
heading: 'VF Standard at a Glance',
items: [
{ text: "1U rack-mounted unit — ships pre-configured, online in minutes" },
{ text: "Up to 1,000 Mbit/s Layer 2 encrypted transport — not a VPN, a genuine private network" },
{ text: "2 WAN + 2 LAN ports (RJ-45, 1GbE) — active/active or failover over 4G or secondary ISP" },
{ text: "Full VLAN, QinQ, and MTU support — works with all Layer & Mesh services natively" },
{ text: "No public IP address needed — works over any available internet connection" },
{ text: "$90/month subscription — no long-term commitment required, shipping additional" },
],
checkColor: 'yellow', sectionBackground: 'dark', textColor: 'white' },
{ blockType: 'lmFaq',
heading: 'Virtual Fiber Questions',
items: [
{ question: "What's the difference between Virtual Fiber and a VPN?",
answer: richText("A VPN is a Layer 3 tunnel — it routes IP packets between networks but doesn't behave like a direct connection. Virtual Fiber creates a transparent Layer 2 network, meaning devices on opposite ends of the connection behave as if they're on the same local network switch. This enables VLAN transport, full MTU, QinQ, and eliminates the latency and compatibility issues common with VPN-based solutions.") },
{ question: 'What internet connection does the VF Standard need?',
answer: richText("Any available internet connection works — fiber, broadband, cable, DSL, 4G LTE, or fixed wireless. No fixed or public IP address is required. For redundancy, the unit supports dual transport paths, for example a fixed fiber connection combined with a 4G SIM as automatic failover.") },
{ question: 'Does it need any configuration on site?',
answer: richText("No. Units are fully configured before dispatch. Connect the WAN port to your internet access and the LAN ports to your network — the unit joins the Layer & Mesh mesh automatically. There is nothing to configure.") },
{ question: 'Can I connect multiple offices?',
answer: richText("Yes. Each location gets a VF Standard unit. All units connect into the same Layer & Mesh private mesh, so your sites communicate over encrypted Layer 2 paths automatically. For more complex multi-site network policy and management, that's our Managed WAN service.") },
{ question: 'What are the contract terms?',
answer: richText("Virtual Fiber subscriptions start at one day — there is no forced long-term commitment. Most customers run on a monthly rolling subscription at $90/month plus shipping for the unit. Contact us if you need multi-site pricing or volume terms.") },
],
theme: 'dark' },
{ blockType: 'lmCtaBanner',
heading: 'Private network. Anywhere. $90/month.',
subheading: 'Ships pre-configured. No IT required on site. Cancel anytime.',
ctaText: 'Order Now', ctaLink: '/contact',
secondaryCtaText: 'Talk to an Engineer', secondaryCtaLink: '/contact',
sectionBackground: 'yellow', alignment: 'center', size: 'medium' },
]
})
// ── 3. MANAGED WAN ────────────────────────────────────────────────────────────
await createPage({
title: 'Managed WAN', slug: 'managed-wan',
layout: [
{ blockType: 'lmAlternateHero',
heading: 'Your Network. Fully Managed.',
description: "Layer & Mesh designs, deploys, and operates your wide-area network — from the SD-WAN edge at each site to the private backbone connecting them. One dashboard. One SLA. One team to call.",
primaryCtaText: 'Get a Network Review', primaryCtaLink: '/contact',
secondaryCtaText: 'Tech Specs', secondaryCtaLink: '#specs',
sectionBackground: 'dark' },
{ blockType: 'lmProductDetail',
eyebrow: 'Managed WAN',
productName: 'Your Network, Our Responsibility',
description: "Most businesses are running a patchwork of ISP circuits, aging routers, and VPNs bolted together over the years. Layer & Mesh replaces that with a coherent managed network fabric — SD-WAN on top of the best available underlay at each site, centrally managed, 24/7 monitored.\n\nWhether you're connecting branch offices, securing remote workers, or extending your network to cover a team in another country — we design the architecture, deploy the hardware, manage the carriers, and keep everything running.",
ctaText: 'Request a Network Design', ctaLink: '/contact',
sectionBackground: 'dark',
body: richTextSpec([
{ type: 'heading', text: "What's Managed" },
{ type: 'paragraph', text: 'SD-WAN edge hardware at every location' },
{ type: 'paragraph', text: 'Underlay circuits — sourced and managed by LM' },
{ type: 'paragraph', text: 'Carrier relationships — one bill, one contact' },
{ type: 'paragraph', text: 'Firewall, IDS/IPS, ZTNA — built into platform' },
{ type: 'heading', text: 'Performance' },
{ type: 'paragraph', text: 'WAN failover: <500ms automatic path switching' },
{ type: 'paragraph', text: 'Uptime SLA: 99.99%' },
{ type: 'paragraph', text: 'New site provisioning: under 24 hours' },
{ type: 'heading', text: 'Visibility' },
{ type: 'paragraph', text: 'Single dashboard: all sites, all circuits' },
{ type: 'paragraph', text: 'Per-application, per-site traffic analytics' },
{ type: 'paragraph', text: '24/7 NOC monitoring with proactive alerting' },
]) },
{ blockType: 'lmTechProperties',
anchorId: 'specs',
properties: [
{ category: 'Deployment', value: 'Zero-Touch Provisioning' },
{ category: 'Failover', value: '<500ms automatic' },
{ category: 'Uptime SLA', value: '99.99%' },
{ category: 'Security', value: 'NGFW · IDS/IPS · ZTNA' },
],
sectionBackground: 'dark', categoryColor: 'white', valueColor: 'yellow' },
{ blockType: 'lmUspTable',
heading: "What's Included",
rows: [
{ title: 'SD-WAN Edge Hardware',
description: richText("Pre-configured CPE ships to each site. Plug it in and it comes online automatically — no on-site IT required. Firmware, configuration, and hardware replacements managed throughout the contract.") },
{ title: 'Underlay Sourcing',
description: richText("We source the best available circuits at each location — fiber, broadband, LTE — and manage the carrier relationships. One bill and one point of contact regardless of how many ISPs are in the mix.") },
{ title: 'Integrated Security',
description: richText("Next-generation firewall, IDS/IPS, DNS filtering, and zero-trust network access built into the platform — not sold separately. Security policies managed centrally from the same dashboard as your routing.") },
{ title: '24/7 NOC Monitoring',
description: richText("Our network operations center watches every circuit, device, and tunnel around the clock. Most issues are resolved before they affect your users. Your named account manager handles anything that can't wait.") },
],
checkColor: 'yellow', sectionBackground: 'dark', textColor: 'white' },
{ blockType: 'lmFaq',
heading: 'Common Questions',
items: [
{ question: 'How long to connect a new site?',
answer: richText("Standard sites using zero-touch provisioning are live within 48 hours of hardware delivery. Sites with complex firewall requirements may take 12 days. We provide a site-specific timeline at kickoff.") },
{ question: "Can you connect our team when they're working in another country?",
answer: richText("Yes. Remote users connect via our zero-trust network access client — available on any device, any OS. Traffic routes over the Layer & Mesh mesh to your corporate resources with the same security policies that apply in your offices. Country doesn't matter.") },
{ question: 'What about regulated data?',
answer: richText("The platform includes AES-256 encryption in transit, ZTNA for every user, and an integrated NGFW with IDS/IPS. All traffic stays within the Layer & Mesh private mesh. This architecture meets transport requirements for HIPAA, GDPR, and SOC 2 workloads.") },
{ question: 'Can you work with our existing circuits?',
answer: richText("Yes. Our SD-WAN platform is transport-agnostic. We can onboard your existing broadband, MPLS, fiber, or LTE links alongside any new circuits we source for you.") },
],
theme: 'dark' },
{ blockType: 'lmCtaBanner',
heading: "Let's design your network.",
subheading: "Tell us your locations and what's not working. We'll come back with a design and a number.",
ctaText: 'Start the Conversation', ctaLink: '/contact',
sectionBackground: 'yellow', alignment: 'center', size: 'medium' },
]
})
// ── 4. VIRTUAL SERVER ─────────────────────────────────────────────────────────
await createPage({
title: 'Virtual Server', slug: 'virtual-server',
layout: [
{ blockType: 'lmAlternateHero',
heading: 'Dedicated Server Performance. Zero Hardware.',
description: "Layer & Mesh Virtual Servers give you dedicated vCPU, RAM, and NVMe SSD from our redundant Stockholm cluster — provisioned in under 30 minutes, replicated across geographically separate facilities, and managed by engineers who keep it running.",
primaryCtaText: 'Configure a Server', primaryCtaLink: '/contact',
secondaryCtaText: 'See Specifications', secondaryCtaLink: '#specs',
sectionBackground: 'dark' },
{ blockType: 'lmProductDetail',
eyebrow: 'Virtual Server',
productName: 'Enterprise Compute Without the Hardware',
description: "When cloud economics don't add up — or when you need to know exactly where your data lives — Layer & Mesh Virtual Servers give you the control of owned infrastructure without the cost and complexity of running a datacenter.\n\nResources are dedicated to your environment. Your VMs run on high-availability clusters across geographically separate Layer & Mesh facilities in Stockholm — if a host fails, your VMs restart on another node within 6090 seconds. Storage is replicated. No data loss on hardware failure.\n\nAnd because it's part of the same Layer & Mesh mesh as Virtual Fiber and Managed WAN, your servers are privately reachable from any of your offices — no VPN client required.",
ctaText: 'Configure a Server', ctaLink: '/contact',
sectionBackground: 'dark',
body: richTextSpec([
{ type: 'heading', text: 'Compute' },
{ type: 'paragraph', text: 'vCPU: Up to 64 dedicated cores' },
{ type: 'paragraph', text: 'RAM: Up to 512 GB DDR5' },
{ type: 'paragraph', text: 'NVMe SSD: Up to 20 TB' },
{ type: 'paragraph', text: 'Network: 125 Gbps internal' },
{ type: 'heading', text: 'Availability' },
{ type: 'paragraph', text: 'Cluster HA: VM restart on host failure in <90s' },
{ type: 'paragraph', text: 'Storage: Replicated across geographically separate sites' },
{ type: 'paragraph', text: 'Uptime SLA: 99.99%' },
{ type: 'heading', text: 'Access' },
{ type: 'paragraph', text: 'OS: Linux (all distros), Windows Server, FreeBSD' },
{ type: 'paragraph', text: 'Provisioning: Under 30 minutes' },
{ type: 'paragraph', text: 'Access: Web portal, VNC console, or private LAN via Virtual Fiber' },
]) },
{ blockType: 'lmTechProperties',
anchorId: 'specs',
properties: [
{ category: 'vCPU', value: 'Up to 64 cores' },
{ category: 'RAM', value: 'Up to 512 GB' },
{ category: 'Provisioning', value: 'Under 30 minutes' },
{ category: 'Uptime SLA', value: '99.99%' },
],
sectionBackground: 'dark', categoryColor: 'white', valueColor: 'yellow' },
{ blockType: 'lmServiceCards',
heading: 'Common Use Cases',
columns: '3', cardSurface: 'outlined-teal', sectionBackground: 'dark',
cards: [
{ eyebrow: 'Migration', title: 'Replace On-Prem Servers',
description: "Migrate your physical servers to virtual machines — same performance, no hardware management overhead, genuine HA across separate facilities.",
iconSlug: 'virtual-server' },
{ eyebrow: 'Development', title: 'Dev & Test Environments',
description: "Spin up isolated environments in minutes. Tear them down when the sprint ends. Pay for hours used — no idle hardware sitting in a rack.",
iconSlug: 'storage' },
{ eyebrow: 'Compliance', title: 'Regulated Data',
description: "Swedish-jurisdiction infrastructure with ISO 27001, SOC 2, and GDPR compliance — for businesses that need to know exactly where their data lives.",
iconSlug: 'backup' },
] },
{ blockType: 'lmUspChecklist',
heading: 'Why Not AWS or Azure',
items: [
{ text: "Swedish jurisdiction — no US CLOUD Act exposure, no data sovereignty tradeoffs" },
{ text: "No egress fees — transfer data freely between your VMs, offices, and LM services" },
{ text: "Dedicated resources — no shared CPU contention or noisy neighbor effects" },
{ text: "Natively connected to Virtual Fiber and Managed WAN — one private mesh, three services" },
{ text: "Transparent monthly pricing — no per-request charges, no surprise costs" },
{ text: "ISO 27001 · SOC 2 · GDPR · NIS2 compliant — certification docs available on request" },
],
checkColor: 'yellow', sectionBackground: 'dark', textColor: 'white' },
{ blockType: 'lmFaq',
heading: 'Virtual Server Questions',
items: [
{ question: 'How is this different from AWS or Azure?',
answer: richText("The major hyperscalers are US-headquartered and operate under the CLOUD Act — US authorities can compel data access regardless of where servers are located. Layer & Mesh is a Swedish company operating Swedish-jurisdiction facilities. For businesses subject to GDPR or EU data residency requirements, this is a meaningful distinction. We're also simpler: no egress fees and a team that answers the phone.") },
{ question: 'What if the physical host my VM runs on fails?',
answer: richText("The cluster detects the failure and restarts your VMs on another healthy host — typically within 6090 seconds. Storage is replicated across the cluster, so no data is lost. For most workloads, this is completely transparent.") },
{ question: 'Can my US team access servers in Stockholm?',
answer: richText("Yes. If you have a Virtual Fiber unit at your US office, your servers appear on the same private network as your local devices — no VPN client needed. You can also access servers via the web portal console or a standard VPN connection.") },
{ question: 'How is pricing structured?',
answer: richText("Monthly subscription based on allocated vCPU, RAM, and storage. No per-request charges, no egress fees. Adjust your allocation up or down each billing cycle.") },
],
theme: 'dark' },
{ blockType: 'lmCtaBanner',
heading: 'Dedicated compute. Stockholm reliability.',
subheading: "Tell us what you're running and we'll configure a VM environment to match.",
ctaText: 'Configure a Server', ctaLink: '/contact',
sectionBackground: 'yellow', alignment: 'center', size: 'medium' },
]
})
// ── 5. CONTACT ────────────────────────────────────────────────────────────────
await createPage({
title: 'Contact', slug: 'contact',
layout: [
{ blockType: 'lmAlternateHero',
heading: "Let's Talk Infrastructure.",
description: "No sales script. No commitment on the first call. Just an honest conversation about what you're running and whether we can make it better.",
sectionBackground: 'dark' },
{ blockType: 'lmContact',
heading: 'Reach Us Directly',
contactMethods: [
{ label: 'Sales — sales@layermesh.com', link: 'mailto:sales@layermesh.com' },
{ label: 'Support — support@layermesh.com', link: 'mailto:support@layermesh.com' },
{ label: 'General — hello@layermesh.com', link: 'mailto:hello@layermesh.com' },
{ label: '24/7 NOC — +1 800 555 0100', link: 'tel:+18005550100' },
] },
{ blockType: 'lmStatistics',
stats: [{ number: '< 4hr', label: 'Sales Response' }, { number: '< 2hr', label: 'Support Triage' }, { number: '24/7', label: 'NOC Coverage' }, { number: '98%', label: 'CSAT Score' }],
sectionBackground: 'dark', numberColor: 'gradient' },
{ blockType: 'lmFaq',
heading: 'Before You Reach Out',
items: [
{ question: 'What size of business do you work with?',
answer: richText("We work best with businesses that have multiple locations, distributed teams, or significant compute needs. If you're not sure whether we're the right fit, reach out — we'll be honest about it.") },
{ question: 'What should I prepare for a first call?',
answer: richText("Not much. Number of locations, what you're currently running, and your top one or two pain points. That's enough. We'll ask the right questions from there.") },
{ question: "We're a Swedish company with a US presence — is this common for you?",
answer: richText("Very common. A significant portion of our US customers are Swedish-owned businesses with a US footprint. The compliance, data sovereignty, and connectivity challenges are well-understood on our end. We have a playbook.") },
{ question: 'Do you offer a proof of concept?',
answer: richText("Yes. For qualified customers we offer a 30-day PoC — typically a single Virtual Fiber unit or a trial VM — at no charge. Ask us about eligibility on the first call.") },
],
theme: 'dark' },
{ blockType: 'lmCtaBanner',
heading: 'One conversation. No commitment.',
subheading: "The worst outcome is 30 minutes and a mutual decision it's not the right fit.",
ctaText: 'Book a Call', ctaLink: 'mailto:sales@layermesh.com',
sectionBackground: 'yellow', alignment: 'center', size: 'medium' },
]
})
console.log('\n✨ Done — 5 pages seeded as drafts.')
console.log('\n📋 Next steps:')
console.log(' 1. Delete old Home (id:2) and Contact (id:3) in /admin if they exist')
console.log(' 2. Upload virtual-fiber-unit.svg to Media in /admin')
console.log(' 3. On the Virtual Fiber page, swap lmAlternateHero for lmHeaderTextImageBlock once image is attached')
console.log(' 4. Publish pages when ready\n')

419
seed-lm-usa2.mjs Normal file
View File

@ -0,0 +1,419 @@
/**
* Layer & Mesh USA Consolidated Seed
* Pages: Home · Virtual Fiber · Managed WAN · Virtual Server · Contact
* Usage: PAYLOAD_API_KEY=your_key node seed-lm-usa.mjs
* Delete old Home (id:2) and Contact (id:3) in /admin before re-running
*/
const BASE_URL = process.env.PAYLOAD_API_URL || 'http://localhost:3000'
const API_KEY = process.env.PAYLOAD_API_KEY
if (!API_KEY) { console.error('❌ Set PAYLOAD_API_KEY first.'); process.exit(1) }
const headers = { 'Content-Type': 'application/json', Authorization: `users API-Key ${API_KEY}` }
function richText(...paragraphs) {
return { root: { type: 'root', format: '', indent: 0, version: 1, direction: 'ltr',
children: paragraphs.map(text => ({
type: 'paragraph', format: '', indent: 0, version: 1, textFormat: 0, textStyle: '',
children: [{ type: 'text', text, format: 0, style: '', mode: 'normal', version: 1, detail: 0 }],
})),
}}
}
function richTextSpec(items) {
// HeadingNode not registered in lmProductDetail richText config — use bold paragraphs instead
return { root: { type: 'root', format: '', indent: 0, version: 1, direction: 'ltr',
children: items.map(item => {
const bold = item.type === 'heading' ? 1 : 0
return { type: 'paragraph', format: '', indent: 0, version: 1, textFormat: bold, textStyle: '',
children: [{ type: 'text', text: item.text, format: bold, style: '', mode: 'normal', version: 1, detail: 0 }] }
}),
}}
}
async function createPage(data) {
const res = await fetch(`${BASE_URL}/api/pages?locale=en`, {
method: 'POST', headers,
body: JSON.stringify({ ...data, _status: 'draft' }),
})
const json = await res.json()
if (!res.ok) { console.error(`❌ Failed: "${data.title}"\n`, JSON.stringify(json?.errors ?? json, null, 2)); return null }
console.log(`✅ "${data.title}" → /${json.doc?.slug} (id: ${json.doc?.id})`)
return json.doc
}
// ── 1. HOME ───────────────────────────────────────────────────────────────────
await createPage({
title: 'Home', slug: 'home',
layout: [
{ blockType: 'lmHero',
heading: 'Managed IT Infrastructure for Business',
subheading: 'Virtual Fiber · Managed WAN · Virtual Servers',
body: 'Layer & Mesh delivers the network and compute infrastructure underneath your business — designed, deployed, and managed by engineers who pick up the phone.',
ctaText: 'Talk to an Engineer', ctaLink: '/contact',
secondaryCtaText: 'See Services', secondaryCtaLink: '#services',
theme: 'dark' },
{ blockType: 'lmStatistics',
stats: [{ number: '99.99%', label: 'Uptime SLA' }, { number: '24/7', label: 'NOC Coverage' }, { number: '<500ms', label: 'WAN Failover' }, { number: '40+', label: 'US PoP Locations' }],
sectionBackground: 'dark', numberColor: 'gradient' },
{ blockType: 'lmServiceCards',
anchorId: 'services',
heading: 'Three Services. One Accountable Partner.',
subheading: 'Private connectivity, managed networking, and dedicated compute — from a team that manages all three together.',
columns: '3', cardSurface: 'outlined-teal', sectionBackground: 'dark',
cards: [
{ eyebrow: 'Connectivity', title: 'Virtual Fiber',
description: 'A managed SD-WAN appliance ships to your door. Plug it in — it connects to the Layer & Mesh private mesh automatically. Managed remotely. Monitored 24/7. Works anywhere.',
readMoreText: 'Explore Virtual Fiber', readMoreLink: '/virtual-fiber', iconSlug: 'connectivity' },
{ eyebrow: 'Networking', title: 'Managed WAN',
description: 'We design, deploy, and operate your wide-area network — from branch offices to cloud. Application-aware routing, zero-trust security, full visibility from one dashboard.',
readMoreText: 'Explore Managed WAN', readMoreLink: '/managed-wan', iconSlug: 'connectivity-grid' },
{ eyebrow: 'Compute', title: 'Virtual Server',
description: 'Dedicated vCPU, RAM, and NVMe SSD from our redundant Stockholm cluster. Provisioned in minutes, replicated across geographically separate facilities. Genuine enterprise availability.',
readMoreText: 'Explore Virtual Server', readMoreLink: '/virtual-server', iconSlug: 'virtual-server' },
] },
{ blockType: 'lmUspChecklist',
heading: 'Why Layer & Mesh',
items: [
{ text: 'One vendor across connectivity, networking, and compute — one call when something goes wrong' },
{ text: 'All three services share a private mesh — your servers are reachable from your offices natively' },
{ text: 'Swedish ownership and governance — meaningful for GDPR data flows between the US and EU' },
{ text: 'Transparent monthly pricing — no egress fees, no surprise costs' },
{ text: 'US-based support with named account managers, not anonymous ticketing queues' },
{ text: 'Scales in minutes — add bandwidth, compute, or new sites from a dashboard' },
],
checkColor: 'yellow', sectionBackground: 'dark', textColor: 'white' },
{ blockType: 'lmCtaBanner',
heading: "Let's talk about your infrastructure.",
subheading: "No sales script. Just an honest conversation about what you're running and whether we can improve it.",
ctaText: 'Schedule a Call', ctaLink: '/contact',
secondaryCtaText: 'See Virtual Fiber', secondaryCtaLink: '/virtual-fiber',
sectionBackground: 'dark', alignment: 'center', size: 'large' },
]
})
// ── 2. VIRTUAL FIBER ──────────────────────────────────────────────────────────
// Real product: VF Standard — 1U rack unit, Layer 2 encrypted, plug and play
// $90/month. 2 WAN + 2 LAN RJ-45 1GbE, active cooling, VLAN/QinQ/Full MTU
await createPage({
title: 'Virtual Fiber', slug: 'virtual-fiber',
layout: [
// IMAGE SLOT: upload virtual-fiber-unit.svg to Media in /admin,
// then swap lmAlternateHero for lmHeaderTextImageBlock with that image attached
{ blockType: 'lmAlternateHero',
heading: 'VF Standard — Virtual Fiber Unit',
description: "A rack-mounted Layer 2 networking appliance that ships pre-configured to your door. Plug it into your internet connection and it connects automatically to the Layer & Mesh private mesh. No configuration. No public IP needed. Encrypted Layer 2 transport — from anywhere.",
primaryCtaText: 'Order — $90/mo', primaryCtaLink: '/contact',
secondaryCtaText: 'Tech Specs', secondaryCtaLink: '#specs',
sectionBackground: 'dark' },
// Tutus-style product detail: big description left / specs right
{ blockType: 'lmProductDetail',
eyebrow: 'Platform / Network / Virtual Fiber',
productName: 'VF Standard',
description: "Virtual Fiber is not a VPN. It creates an encrypted, transparent Layer 2 network — built on top of any available internet connection — that behaves exactly like a private fiber link between your locations.\n\nThe VF Standard unit ships pre-configured and ready. Connect it to your existing broadband, fiber, or 4G connection and it joins the Layer & Mesh mesh automatically. Your devices get full Layer 2 transport to every other point on the network — with VLAN, QinQ, and Full MTU support — without a single line of configuration.\n\nNo fixed IP address needed. No IT team required on site. Works as a permanent connection or deployed temporarily. Contract terms start at one day.",
ctaText: 'Order Now — $90/month', ctaLink: '/contact',
secondaryCtaText: 'Download Datasheet', secondaryCtaLink: '/contact',
sectionBackground: 'dark',
body: richTextSpec([
{ type: 'heading', text: '$90 / month' },
{ type: 'paragraph', text: 'Shipping additional. No setup fee. Cancel anytime.' },
{ type: 'heading', text: 'Performance' },
{ type: 'paragraph', text: 'Throughput: Up to 1,000 Mbit/s' },
{ type: 'paragraph', text: 'Transport: Encrypted Layer 2 over any internet' },
{ type: 'paragraph', text: 'Redundancy: Automatic failover via 4G or secondary ISP' },
{ type: 'heading', text: 'Hardware' },
{ type: 'paragraph', text: 'Form factor: 1U rack-mounted' },
{ type: 'paragraph', text: 'Cooling: Active (fan)' },
{ type: 'paragraph', text: 'WAN: 2 × 1GbE RJ-45' },
{ type: 'paragraph', text: 'LAN: 2 × 1GbE RJ-45' },
{ type: 'heading', text: 'Features' },
{ type: 'paragraph', text: 'Plug and play — pre-configured at dispatch' },
{ type: 'paragraph', text: 'VLAN · QinQ · Full MTU' },
{ type: 'paragraph', text: 'No public IP address required' },
{ type: 'paragraph', text: 'World-class Layer 2 encryption' },
]) },
{ blockType: 'lmTechProperties',
anchorId: 'specs',
properties: [
{ category: 'Speed', value: 'Up to 1,000 Mbit/s' },
{ category: 'Form Factor', value: '1U Rack-Mounted' },
{ category: 'Redundancy', value: '4G / Dual-WAN Failover' },
{ category: 'Price', value: '$90 / month' },
],
sectionBackground: 'dark', categoryColor: 'white', valueColor: 'yellow' },
{ blockType: 'lmServiceCards',
heading: 'What Virtual Fiber Makes Possible',
columns: '3', cardSurface: 'outlined-teal', sectionBackground: 'dark',
cards: [
{ eyebrow: 'Reach',
title: 'Access Resources From Anywhere',
description: "Your team in New York reaches your Stockholm servers the same way they reach the printer down the hall — over a private Layer 2 network, without a VPN client or public IP.",
iconSlug: 'connectivity' },
{ eyebrow: 'Simplicity',
title: 'No Configuration Required',
description: "Units arrive ready. Connect to any internet access — fiber, broadband, 4G — and the device joins the mesh automatically. Dual transport paths for redundancy are supported out of the box.",
iconSlug: 'connectivity-grid' },
{ eyebrow: 'Security',
title: 'World-Class Layer 2 Encryption',
description: "All traffic is encrypted using state-of-the-art technology. Secure channels are established over public networks without any configuration — a clear replacement for traditional VPN.",
iconSlug: 'virtual-server' },
] },
{ blockType: 'lmUspChecklist',
heading: 'VF Standard at a Glance',
items: [
{ text: "1U rack-mounted unit — ships pre-configured, online in minutes" },
{ text: "Up to 1,000 Mbit/s Layer 2 encrypted transport — not a VPN, a genuine private network" },
{ text: "2 WAN + 2 LAN ports (RJ-45, 1GbE) — active/active or failover over 4G or secondary ISP" },
{ text: "Full VLAN, QinQ, and MTU support — works with all Layer & Mesh services natively" },
{ text: "No public IP address needed — works over any available internet connection" },
{ text: "$90/month subscription — no long-term commitment required, shipping additional" },
],
checkColor: 'yellow', sectionBackground: 'dark', textColor: 'white' },
{ blockType: 'lmFaq',
heading: 'Virtual Fiber Questions',
items: [
{ question: "What's the difference between Virtual Fiber and a VPN?",
answer: richText("A VPN is a Layer 3 tunnel — it routes IP packets between networks but doesn't behave like a direct connection. Virtual Fiber creates a transparent Layer 2 network, meaning devices on opposite ends of the connection behave as if they're on the same local network switch. This enables VLAN transport, full MTU, QinQ, and eliminates the latency and compatibility issues common with VPN-based solutions.") },
{ question: 'What internet connection does the VF Standard need?',
answer: richText("Any available internet connection works — fiber, broadband, cable, DSL, 4G LTE, or fixed wireless. No fixed or public IP address is required. For redundancy, the unit supports dual transport paths, for example a fixed fiber connection combined with a 4G SIM as automatic failover.") },
{ question: 'Does it need any configuration on site?',
answer: richText("No. Units are fully configured before dispatch. Connect the WAN port to your internet access and the LAN ports to your network — the unit joins the Layer & Mesh mesh automatically. There is nothing to configure.") },
{ question: 'Can I connect multiple offices?',
answer: richText("Yes. Each location gets a VF Standard unit. All units connect into the same Layer & Mesh private mesh, so your sites communicate over encrypted Layer 2 paths automatically. For more complex multi-site network policy and management, that's our Managed WAN service.") },
{ question: 'What are the contract terms?',
answer: richText("Virtual Fiber subscriptions start at one day — there is no forced long-term commitment. Most customers run on a monthly rolling subscription at $90/month plus shipping for the unit. Contact us if you need multi-site pricing or volume terms.") },
],
theme: 'dark' },
{ blockType: 'lmCtaBanner',
heading: 'Private network. Anywhere. $90/month.',
subheading: 'Ships pre-configured. No IT required on site. Cancel anytime.',
ctaText: 'Order Now', ctaLink: '/contact',
secondaryCtaText: 'Talk to an Engineer', secondaryCtaLink: '/contact',
sectionBackground: 'yellow', alignment: 'center', size: 'medium' },
]
})
// ── 3. MANAGED WAN ────────────────────────────────────────────────────────────
await createPage({
title: 'Managed WAN', slug: 'managed-wan',
layout: [
{ blockType: 'lmAlternateHero',
heading: 'Your Network. Fully Managed.',
description: "Layer & Mesh designs, deploys, and operates your wide-area network — from the SD-WAN edge at each site to the private backbone connecting them. One dashboard. One SLA. One team to call.",
primaryCtaText: 'Get a Network Review', primaryCtaLink: '/contact',
secondaryCtaText: 'Tech Specs', secondaryCtaLink: '#specs',
sectionBackground: 'dark' },
{ blockType: 'lmProductDetail',
eyebrow: 'Managed WAN',
productName: 'Your Network, Our Responsibility',
description: "Most businesses are running a patchwork of ISP circuits, aging routers, and VPNs bolted together over the years. Layer & Mesh replaces that with a coherent managed network fabric — SD-WAN on top of the best available underlay at each site, centrally managed, 24/7 monitored.\n\nWhether you're connecting branch offices, securing remote workers, or extending your network to cover a team in another country — we design the architecture, deploy the hardware, manage the carriers, and keep everything running.",
ctaText: 'Request a Network Design', ctaLink: '/contact',
sectionBackground: 'dark',
body: richTextSpec([
{ type: 'heading', text: "What's Managed" },
{ type: 'paragraph', text: 'SD-WAN edge hardware at every location' },
{ type: 'paragraph', text: 'Underlay circuits — sourced and managed by LM' },
{ type: 'paragraph', text: 'Carrier relationships — one bill, one contact' },
{ type: 'paragraph', text: 'Firewall, IDS/IPS, ZTNA — built into platform' },
{ type: 'heading', text: 'Performance' },
{ type: 'paragraph', text: 'WAN failover: <500ms automatic path switching' },
{ type: 'paragraph', text: 'Uptime SLA: 99.99%' },
{ type: 'paragraph', text: 'New site provisioning: under 24 hours' },
{ type: 'heading', text: 'Visibility' },
{ type: 'paragraph', text: 'Single dashboard: all sites, all circuits' },
{ type: 'paragraph', text: 'Per-application, per-site traffic analytics' },
{ type: 'paragraph', text: '24/7 NOC monitoring with proactive alerting' },
]) },
{ blockType: 'lmTechProperties',
anchorId: 'specs',
properties: [
{ category: 'Deployment', value: 'Zero-Touch Provisioning' },
{ category: 'Failover', value: '<500ms automatic' },
{ category: 'Uptime SLA', value: '99.99%' },
{ category: 'Security', value: 'NGFW · IDS/IPS · ZTNA' },
],
sectionBackground: 'dark', categoryColor: 'white', valueColor: 'yellow' },
{ blockType: 'lmUspTable',
heading: "What's Included",
rows: [
{ title: 'SD-WAN Edge Hardware',
description: richText("Pre-configured CPE ships to each site. Plug it in and it comes online automatically — no on-site IT required. Firmware, configuration, and hardware replacements managed throughout the contract.") },
{ title: 'Underlay Sourcing',
description: richText("We source the best available circuits at each location — fiber, broadband, LTE — and manage the carrier relationships. One bill and one point of contact regardless of how many ISPs are in the mix.") },
{ title: 'Integrated Security',
description: richText("Next-generation firewall, IDS/IPS, DNS filtering, and zero-trust network access built into the platform — not sold separately. Security policies managed centrally from the same dashboard as your routing.") },
{ title: '24/7 NOC Monitoring',
description: richText("Our network operations center watches every circuit, device, and tunnel around the clock. Most issues are resolved before they affect your users. Your named account manager handles anything that can't wait.") },
],
checkColor: 'yellow', sectionBackground: 'dark', textColor: 'white' },
{ blockType: 'lmFaq',
heading: 'Common Questions',
items: [
{ question: 'How long to connect a new site?',
answer: richText("Standard sites using zero-touch provisioning are live within 48 hours of hardware delivery. Sites with complex firewall requirements may take 12 days. We provide a site-specific timeline at kickoff.") },
{ question: "Can you connect our team when they're working in another country?",
answer: richText("Yes. Remote users connect via our zero-trust network access client — available on any device, any OS. Traffic routes over the Layer & Mesh mesh to your corporate resources with the same security policies that apply in your offices. Country doesn't matter.") },
{ question: 'What about regulated data?',
answer: richText("The platform includes AES-256 encryption in transit, ZTNA for every user, and an integrated NGFW with IDS/IPS. All traffic stays within the Layer & Mesh private mesh. This architecture meets transport requirements for HIPAA, GDPR, and SOC 2 workloads.") },
{ question: 'Can you work with our existing circuits?',
answer: richText("Yes. Our SD-WAN platform is transport-agnostic. We can onboard your existing broadband, MPLS, fiber, or LTE links alongside any new circuits we source for you.") },
],
theme: 'dark' },
{ blockType: 'lmCtaBanner',
heading: "Let's design your network.",
subheading: "Tell us your locations and what's not working. We'll come back with a design and a number.",
ctaText: 'Start the Conversation', ctaLink: '/contact',
sectionBackground: 'yellow', alignment: 'center', size: 'medium' },
]
})
// ── 4. VIRTUAL SERVER ─────────────────────────────────────────────────────────
await createPage({
title: 'Virtual Server', slug: 'virtual-server',
layout: [
{ blockType: 'lmAlternateHero',
heading: 'Dedicated Server Performance. Zero Hardware.',
description: "Layer & Mesh Virtual Servers give you dedicated vCPU, RAM, and NVMe SSD from our redundant Stockholm cluster — provisioned in under 30 minutes, replicated across geographically separate facilities, and managed by engineers who keep it running.",
primaryCtaText: 'Configure a Server', primaryCtaLink: '/contact',
secondaryCtaText: 'See Specifications', secondaryCtaLink: '#specs',
sectionBackground: 'dark' },
{ blockType: 'lmProductDetail',
eyebrow: 'Virtual Server',
productName: 'Enterprise Compute Without the Hardware',
description: "When cloud economics don't add up — or when you need to know exactly where your data lives — Layer & Mesh Virtual Servers give you the control of owned infrastructure without the cost and complexity of running a datacenter.\n\nResources are dedicated to your environment. Your VMs run on high-availability clusters across geographically separate Layer & Mesh facilities in Stockholm — if a host fails, your VMs restart on another node within 6090 seconds. Storage is replicated. No data loss on hardware failure.\n\nAnd because it's part of the same Layer & Mesh mesh as Virtual Fiber and Managed WAN, your servers are privately reachable from any of your offices — no VPN client required.",
ctaText: 'Configure a Server', ctaLink: '/contact',
sectionBackground: 'dark',
body: richTextSpec([
{ type: 'heading', text: 'Compute' },
{ type: 'paragraph', text: 'vCPU: Up to 64 dedicated cores' },
{ type: 'paragraph', text: 'RAM: Up to 512 GB DDR5' },
{ type: 'paragraph', text: 'NVMe SSD: Up to 20 TB' },
{ type: 'paragraph', text: 'Network: 125 Gbps internal' },
{ type: 'heading', text: 'Availability' },
{ type: 'paragraph', text: 'Cluster HA: VM restart on host failure in <90s' },
{ type: 'paragraph', text: 'Storage: Replicated across geographically separate sites' },
{ type: 'paragraph', text: 'Uptime SLA: 99.99%' },
{ type: 'heading', text: 'Access' },
{ type: 'paragraph', text: 'OS: Linux (all distros), Windows Server, FreeBSD' },
{ type: 'paragraph', text: 'Provisioning: Under 30 minutes' },
{ type: 'paragraph', text: 'Access: Web portal, VNC console, or private LAN via Virtual Fiber' },
]) },
{ blockType: 'lmTechProperties',
anchorId: 'specs',
properties: [
{ category: 'vCPU', value: 'Up to 64 cores' },
{ category: 'RAM', value: 'Up to 512 GB' },
{ category: 'Provisioning', value: 'Under 30 minutes' },
{ category: 'Uptime SLA', value: '99.99%' },
],
sectionBackground: 'dark', categoryColor: 'white', valueColor: 'yellow' },
{ blockType: 'lmServiceCards',
heading: 'Common Use Cases',
columns: '3', cardSurface: 'outlined-teal', sectionBackground: 'dark',
cards: [
{ eyebrow: 'Migration', title: 'Replace On-Prem Servers',
description: "Migrate your physical servers to virtual machines — same performance, no hardware management overhead, genuine HA across separate facilities.",
iconSlug: 'virtual-server' },
{ eyebrow: 'Development', title: 'Dev & Test Environments',
description: "Spin up isolated environments in minutes. Tear them down when the sprint ends. Pay for hours used — no idle hardware sitting in a rack.",
iconSlug: 'storage' },
{ eyebrow: 'Compliance', title: 'Regulated Data',
description: "Swedish-jurisdiction infrastructure with ISO 27001, SOC 2, and GDPR compliance — for businesses that need to know exactly where their data lives.",
iconSlug: 'backup' },
] },
{ blockType: 'lmUspChecklist',
heading: 'Why Not AWS or Azure',
items: [
{ text: "Swedish jurisdiction — no US CLOUD Act exposure, no data sovereignty tradeoffs" },
{ text: "No egress fees — transfer data freely between your VMs, offices, and LM services" },
{ text: "Dedicated resources — no shared CPU contention or noisy neighbor effects" },
{ text: "Natively connected to Virtual Fiber and Managed WAN — one private mesh, three services" },
{ text: "Transparent monthly pricing — no per-request charges, no surprise costs" },
{ text: "ISO 27001 · SOC 2 · GDPR · NIS2 compliant — certification docs available on request" },
],
checkColor: 'yellow', sectionBackground: 'dark', textColor: 'white' },
{ blockType: 'lmFaq',
heading: 'Virtual Server Questions',
items: [
{ question: 'How is this different from AWS or Azure?',
answer: richText("The major hyperscalers are US-headquartered and operate under the CLOUD Act — US authorities can compel data access regardless of where servers are located. Layer & Mesh is a Swedish company operating Swedish-jurisdiction facilities. For businesses subject to GDPR or EU data residency requirements, this is a meaningful distinction. We're also simpler: no egress fees and a team that answers the phone.") },
{ question: 'What if the physical host my VM runs on fails?',
answer: richText("The cluster detects the failure and restarts your VMs on another healthy host — typically within 6090 seconds. Storage is replicated across the cluster, so no data is lost. For most workloads, this is completely transparent.") },
{ question: 'Can my US team access servers in Stockholm?',
answer: richText("Yes. If you have a Virtual Fiber unit at your US office, your servers appear on the same private network as your local devices — no VPN client needed. You can also access servers via the web portal console or a standard VPN connection.") },
{ question: 'How is pricing structured?',
answer: richText("Monthly subscription based on allocated vCPU, RAM, and storage. No per-request charges, no egress fees. Adjust your allocation up or down each billing cycle.") },
],
theme: 'dark' },
{ blockType: 'lmCtaBanner',
heading: 'Dedicated compute. Stockholm reliability.',
subheading: "Tell us what you're running and we'll configure a VM environment to match.",
ctaText: 'Configure a Server', ctaLink: '/contact',
sectionBackground: 'yellow', alignment: 'center', size: 'medium' },
]
})
// ── 5. CONTACT ────────────────────────────────────────────────────────────────
await createPage({
title: 'Contact', slug: 'contact',
layout: [
{ blockType: 'lmAlternateHero',
heading: "Let's Talk Infrastructure.",
description: "No sales script. No commitment on the first call. Just an honest conversation about what you're running and whether we can make it better.",
sectionBackground: 'dark' },
{ blockType: 'lmContact',
heading: 'Reach Us Directly',
contactMethods: [
{ label: 'Sales — sales@layermesh.com', link: 'mailto:sales@layermesh.com' },
{ label: 'Support — support@layermesh.com', link: 'mailto:support@layermesh.com' },
{ label: 'General — hello@layermesh.com', link: 'mailto:hello@layermesh.com' },
{ label: '24/7 NOC — +1 800 555 0100', link: 'tel:+18005550100' },
] },
{ blockType: 'lmStatistics',
stats: [{ number: '< 4hr', label: 'Sales Response' }, { number: '< 2hr', label: 'Support Triage' }, { number: '24/7', label: 'NOC Coverage' }, { number: '98%', label: 'CSAT Score' }],
sectionBackground: 'dark', numberColor: 'gradient' },
{ blockType: 'lmFaq',
heading: 'Before You Reach Out',
items: [
{ question: 'What size of business do you work with?',
answer: richText("We work best with businesses that have multiple locations, distributed teams, or significant compute needs. If you're not sure whether we're the right fit, reach out — we'll be honest about it.") },
{ question: 'What should I prepare for a first call?',
answer: richText("Not much. Number of locations, what you're currently running, and your top one or two pain points. That's enough. We'll ask the right questions from there.") },
{ question: "We're a Swedish company with a US presence — is this common for you?",
answer: richText("Very common. A significant portion of our US customers are Swedish-owned businesses with a US footprint. The compliance, data sovereignty, and connectivity challenges are well-understood on our end. We have a playbook.") },
{ question: 'Do you offer a proof of concept?',
answer: richText("Yes. For qualified customers we offer a 30-day PoC — typically a single Virtual Fiber unit or a trial VM — at no charge. Ask us about eligibility on the first call.") },
],
theme: 'dark' },
{ blockType: 'lmCtaBanner',
heading: 'One conversation. No commitment.',
subheading: "The worst outcome is 30 minutes and a mutual decision it's not the right fit.",
ctaText: 'Book a Call', ctaLink: 'mailto:sales@layermesh.com',
sectionBackground: 'yellow', alignment: 'center', size: 'medium' },
]
})
console.log('\n✨ Done — 5 pages seeded as drafts.')
console.log('\n📋 Next steps:')
console.log(' 1. Delete old Home (id:2) and Contact (id:3) in /admin if they exist')
console.log(' 2. Upload virtual-fiber-unit.svg to Media in /admin')
console.log(' 3. On the Virtual Fiber page, swap lmAlternateHero for lmHeaderTextImageBlock once image is attached')
console.log(' 4. Publish pages when ready\n')

500
seed.mjs Normal file
View File

@ -0,0 +1,500 @@
#!/usr/bin/env node
/*
* LM USA Seed Script
*
* Usage:
* PAYLOAD_URL=http://localhost:3000 PAYLOAD_API_KEY=your-key node seed.mjs
*
* Creates:
* - Contact form
* - Home page /
* - Virtuell Fiber /virtuell-fiber
* - VPS Server /vps
* - Contact /contact
* - Header navigation
* - Footer
*/
const PAYLOAD_URL = process.env.PAYLOAD_URL || 'http://localhost:3000'
const API_KEY = process.env.PAYLOAD_API_KEY
if (!API_KEY) {
console.error('❌ Missing PAYLOAD_API_KEY environment variable')
process.exit(1)
}
const headers = {
'Content-Type': 'application/json',
Authorization: `users API-Key ${API_KEY}`,
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
function richText(text) {
const paragraphs = text.split('\n').filter(Boolean)
return {
root: {
type: 'root',
children: paragraphs.map((p) => ({
type: 'paragraph',
children: [
{ type: 'text', text: p, format: 0, detail: 0, mode: 'normal', style: '', version: 1 },
],
direction: 'ltr',
format: '',
indent: 0,
version: 1,
})),
direction: 'ltr',
format: '',
indent: 0,
version: 1,
},
}
}
async function apiPost(collection, data) {
const res = await fetch(`${PAYLOAD_URL}/api/${collection}`, {
method: 'POST',
headers,
body: JSON.stringify(data),
})
if (!res.ok) {
const err = await res.text()
throw new Error(`POST /api/${collection} failed (${res.status}): ${err}`)
}
return res.json()
}
async function apiGet(collection, query = '') {
const res = await fetch(`${PAYLOAD_URL}/api/${collection}${query}`, { headers })
if (!res.ok) {
const err = await res.text()
throw new Error(`GET /api/${collection} failed (${res.status}): ${err}`)
}
return res.json()
}
async function apiUpdateGlobal(slug, data) {
const res = await fetch(`${PAYLOAD_URL}/api/globals/${slug}`, {
method: 'POST',
headers,
body: JSON.stringify(data),
})
if (!res.ok) {
const err = await res.text()
throw new Error(`POST /api/globals/${slug} failed (${res.status}): ${err}`)
}
return res.json()
}
async function createPageIfNotExists(slug, data) {
const existing = await apiGet('pages', `?where[slug][equals]=${slug}&limit=1`)
if (existing.docs?.length > 0) {
console.log(` ⏭ Page "${slug}" already exists — skipping`)
return existing.docs[0]
}
const result = await apiPost('pages', data)
return result.doc
}
// ─── Main ─────────────────────────────────────────────────────────────────────
async function seed() {
console.log(`🌱 Seeding LM USA database at ${PAYLOAD_URL}...`)
// ─── 1. Contact Form ───────────────────────────────────────────────────────
console.log(' → Creating contact form...')
let contactForm
const existingForms = await apiGet('forms', '?where[title][equals]=Contact Form&limit=1')
if (existingForms.docs?.length > 0) {
console.log(' ⏭ Contact form already exists — skipping')
contactForm = existingForms.docs[0]
} else {
const result = await apiPost('forms', {
title: 'Contact Form',
fields: [
{ name: 'name', label: 'Name', blockType: 'text', required: true, width: 50 },
{ name: 'company', label: 'Company', blockType: 'text', required: false, width: 50 },
{ name: 'email', label: 'Email', blockType: 'email', required: true, width: 50 },
{ name: 'phone', label: 'Phone', blockType: 'text', required: false, width: 50 },
{ name: 'message', label: 'Message', blockType: 'textarea', required: true },
],
submitButtonLabel: 'Send Message',
confirmationType: 'message',
confirmationMessage: richText(
"Thanks for reaching out. We'll get back to you within 24 hours.",
),
})
contactForm = result.doc
}
// ─── 2. Home Page ──────────────────────────────────────────────────────────
console.log(' → Creating Home page...')
await createPageIfNotExists('index', {
title: 'Home',
slug: 'index',
_status: 'published',
publishedAt: new Date().toISOString(),
layout: [
// Hero
// theme: 'light' | 'dark'
// textColor: 'auto' | 'white' | 'dark'
{
blockType: 'lmHero',
heading: 'Infrastructure Without Borders',
subheading: 'Layer & Mesh USA',
body: 'Edge compute, virtual fiber, and managed servers — built for businesses that need sovereign, reliable infrastructure. Configure your setup and get started in minutes.',
ctaText: 'Explore Products',
ctaLink: '#products',
secondaryCtaText: 'Contact Us',
secondaryCtaLink: '/contact',
theme: 'dark',
textColor: 'auto',
},
// Link Cards
// cardStyle: 'outlined' | 'dark' | 'gray' | 'yellow'
// sectionBackground: 'white' | 'gray' | 'dark'
// title is textarea (supports \n)
{
blockType: 'lmLinkCards',
heading: 'Choose Your Infrastructure',
description: 'Two paths to managed IT — hardware at your location or virtual in our cloud.',
columns: '2',
cardStyle: 'outlined',
sectionBackground: 'dark',
anchorId: 'products',
cards: [
{
title: 'Virtuell Fiber\nEdge compute hardware deployed at your location. Plug in, connect to the mesh — instant private infrastructure.',
linkLabel: 'Explore VF Enhet',
linkUrl: '/virtuell-fiber',
},
{
title: 'VPS Servers\nCloud-hosted virtual servers on Layer & Mesh infrastructure. Full root access, instant provisioning, sovereign data.',
linkLabel: 'Configure a VPS',
linkUrl: '/vps',
},
],
},
// Statistics
// numberColor: 'gradient' | 'yellow' | 'mint' | 'dark' | 'white'
// sectionBackground: 'white' | 'dark' | 'gray'
{
blockType: 'lmStatistics',
heading: 'Built for Reliability',
sectionBackground: 'dark',
numberColor: 'gradient',
stats: [
{ number: '99.99%', label: 'Uptime SLA' },
{ number: '<5ms', label: 'Edge Latency' },
{ number: '24/7', label: 'Support' },
{ number: '100%', label: 'Swedish Sovereign' },
],
},
// Swedish CTA
// sectionBackground: 'dark' | 'elevated' | 'gray'
{
blockType: 'lmSwedishCta',
headingText: 'Söker du Fiber Direkt i Sverige?',
subText: 'Visit our Swedish site for local fiber and hosting services.',
ctaLabel: 'fiberdirekt.se',
ctaUrl: 'https://fiberdirekt.se',
sectionBackground: 'dark',
},
// Bottom CTA
// sectionBackground: 'yellow' | 'dark' | 'gray' | 'white'
// alignment: 'center' | 'left'
// size: 'small' | 'medium' | 'large'
{
blockType: 'lmCtaBanner',
heading: 'Ready to get started?',
subheading: 'Configure your infrastructure or talk to our team — no commitment required.',
ctaText: 'Contact Us',
ctaLink: '/contact',
secondaryCtaText: 'View VPS Pricing',
secondaryCtaLink: '/vps',
sectionBackground: 'yellow',
alignment: 'center',
size: 'medium',
},
],
})
// ─── 3. Virtuell Fiber Page ────────────────────────────────────────────────
console.log(' → Creating Virtuell Fiber page...')
await createPageIfNotExists('virtuell-fiber', {
title: 'Virtuell Fiber',
slug: 'virtuell-fiber',
_status: 'published',
publishedAt: new Date().toISOString(),
layout: [
// Product Detail
// sectionBackground: 'white' | 'dark'
// cartProductType: 'hardware' | 'vps' | 'service'
{
blockType: 'lmProductDetail',
eyebrow: 'Hardware',
productName: 'VF Enhet',
description: 'Purpose-built edge compute hardware for virtuell fiber deployment. Plug the VF Enhet into your location, connect it to the Layer & Mesh network, and get instant private infrastructure — no data center required. Each unit runs a full mesh node with encrypted backhaul, local compute, and automatic failover.',
ctaText: 'Add to Cart',
ctaLink: '/contact',
secondaryCtaText: 'Contact Sales',
secondaryCtaLink: '/contact',
enableCart: true,
cartProductName: 'VF Enhet',
cartPrice: 149,
cartProductType: 'hardware',
sectionBackground: 'dark',
},
// Tech Properties — maxRows: 4!
// categoryColor: 'white' | 'dark'
// valueColor: 'yellow' | 'white' | 'dark'
// sectionBackground: 'dark' | 'white' | 'gray' | 'yellow'
{
blockType: 'lmTechProperties',
sectionBackground: 'dark',
categoryColor: 'white',
valueColor: 'yellow',
properties: [
{ category: 'Processor', value: 'ARM Cortex-A76 Quad-Core' },
{ category: 'Memory', value: '8 GB DDR4' },
{ category: 'Storage', value: '256 GB NVMe SSD' },
{ category: 'Network', value: '2.5 GbE + Wi-Fi 6' },
],
},
// Second Tech Properties block for remaining specs
{
blockType: 'lmTechProperties',
sectionBackground: 'dark',
categoryColor: 'white',
valueColor: 'white',
properties: [
{ category: 'Power', value: '12W idle / 25W peak' },
{ category: 'Dimensions', value: '145 × 100 × 40 mm' },
{ category: 'Connectivity', value: 'Encrypted mesh backhaul' },
{ category: 'Management', value: 'Remote dashboard included' },
],
},
// FAQ — answer is richText
// theme: 'gray' | 'light' | 'dark'
{
blockType: 'lmFaq',
heading: 'Frequently Asked Questions',
theme: 'dark',
items: [
{
question: 'What is the VF Enhet?',
answer: richText(
'A compact hardware node that connects your location directly to the Layer & Mesh network. It provides edge compute, encrypted connectivity, and mesh routing — all in a box smaller than a paperback book.',
),
},
{
question: 'Do I need special wiring or a server room?',
answer: richText(
'No. The VF Enhet plugs into any standard ethernet connection and power outlet. It runs silently at 12W idle — about the same as a phone charger.',
),
},
{
question: 'How is this different from a VPS?',
answer: richText(
'A VPS runs in our cloud. The VF Enhet runs at your location. Both connect to the same Layer & Mesh network, but the Enhet gives you physical edge compute with lower latency.',
),
},
{
question: 'Can I cancel?',
answer: richText(
'Yes. Month-to-month with no long-term commitment. Give us 30 days notice and we will arrange return of the hardware.',
),
},
],
},
// CTA
{
blockType: 'lmCtaBanner',
heading: 'Need a custom deployment?',
subheading: 'Talk to our team about multi-unit rollouts, custom configurations, or enterprise pricing.',
ctaText: 'Contact Us',
ctaLink: '/contact',
sectionBackground: 'yellow',
alignment: 'left',
size: 'small',
},
],
})
// ─── 4. VPS Server Page ────────────────────────────────────────────────────
console.log(' → Creating VPS page...')
await createPageIfNotExists('vps', {
title: 'VPS Servers',
slug: 'vps',
_status: 'published',
publishedAt: new Date().toISOString(),
layout: [
// Alternate Hero
// sectionBackground: 'white' | 'dark' | 'gray'
{
blockType: 'lmAlternateHero',
heading: 'Virtual Private Servers',
description: 'Cloud-hosted servers on Layer & Mesh infrastructure. Full root access, instant provisioning, and Swedish-sovereign data. Configure your resources below — pricing updates in real-time.',
primaryCtaText: 'Configure Below',
primaryCtaLink: '#calculator',
secondaryCtaText: 'Contact Sales',
secondaryCtaLink: '/contact',
sectionBackground: 'dark',
},
// VPS Calculator
// sectionBackground: 'dark' | 'gray' | 'teal'
// currency: 'usd' | 'sek' | 'eur'
{
blockType: 'lmVpsCalculator',
heading: 'Estimate your cost',
orderCtaText: 'Add to Cart',
contactCtaText: 'Questions?',
contactCtaLink: '/contact',
currency: 'usd',
sectionBackground: 'dark',
pricingCpuPerCore: 12,
pricingRamPerGb: 5,
pricingSsdPerGb: 0.08,
pricingHddPerGb: 0.02,
pricingWindowsLicense: 25,
anchorId: 'calculator',
},
// USP Table — description is richText
// checkColor: 'dark' | 'yellow' | 'gray'
// textColor: 'dark' | 'white'
// sectionBackground: 'white' | 'gray' | 'dark'
{
blockType: 'lmUspTable',
heading: "What's Included",
sectionBackground: 'dark',
checkColor: 'dark',
textColor: 'white',
rows: [
{
title: 'Full Root Access',
description: richText('SSH, VNC, or console — full control of your server.'),
},
{
title: 'Unmetered Bandwidth',
description: richText('No traffic caps or overage fees.'),
},
{
title: 'NVMe Storage',
description: richText('Enterprise-grade SSDs for maximum I/O.'),
},
{
title: 'Instant Provisioning',
description: richText('Your server is ready in under 60 seconds.'),
},
{
title: '99.99% Uptime SLA',
description: richText('Guaranteed availability with automatic failover.'),
},
{
title: 'Swedish Sovereignty',
description: richText('Data stored and processed in Sweden.'),
},
],
},
// CTA
{
blockType: 'lmCtaBanner',
heading: 'Need dedicated hardware?',
subheading: 'Check out the VF Enhet — edge compute deployed at your location.',
ctaText: 'Learn About VF Enhet',
ctaLink: '/virtuell-fiber',
secondaryCtaText: 'Contact Sales',
secondaryCtaLink: '/contact',
sectionBackground: 'yellow',
alignment: 'left',
size: 'small',
},
],
})
// ─── 5. Contact Page ───────────────────────────────────────────────────────
console.log(' → Creating Contact page...')
await createPageIfNotExists('contact', {
title: 'Contact',
slug: 'contact',
_status: 'published',
publishedAt: new Date().toISOString(),
layout: [
// Contact Form
// sectionBackground: 'white' | 'gray' | 'dark' | 'navyGradient'
// layout: 'standard' | 'withImage' | 'card'
{
blockType: 'lmContactForm',
heading: 'Get in Touch',
description: 'Tell us about your infrastructure needs — our team will get back to you within 24 hours.',
form: contactForm.id,
submitText: 'Send Message',
sectionBackground: 'dark',
layout: 'card',
},
// FAQ
{
blockType: 'lmFaq',
heading: 'Common Questions',
theme: 'dark',
items: [
{
question: 'Where are you located?',
answer: richText(
'Layer & Mesh operates infrastructure in Sweden with support available globally. Our US operations serve North American customers.',
),
},
{
question: 'How quickly can I get started?',
answer: richText(
'VPS servers are provisioned instantly. VF Enhet hardware typically ships within 57 business days.',
),
},
{
question: 'Do you offer enterprise pricing?',
answer: richText(
'Yes. Contact us for volume pricing, custom SLAs, and dedicated account management.',
),
},
],
},
],
})
// ─── 6. Header Navigation ─────────────────────────────────────────────────
// Header navItems are flat: { label, type, url } directly on each item
console.log(' → Setting up header...')
await apiUpdateGlobal('header', {
logoLink: { type: 'custom', url: '/' },
navItems: [
{ label: 'Virtuell Fiber', type: 'custom', url: '/virtuell-fiber' },
{ label: 'VPS Servers', type: 'custom', url: '/vps' },
{ label: 'Contact', type: 'custom', url: '/contact' },
],
})
// ─── 7. Footer ────────────────────────────────────────────────────────────
// Footer navItems use link() field: { link: { type, label, url } }
console.log(' → Setting up footer...')
await apiUpdateGlobal('footer', {
logoLink: { type: 'custom', url: '/' },
navItems: [
{ link: { type: 'custom', label: 'Virtuell Fiber', url: '/virtuell-fiber' } },
{ link: { type: 'custom', label: 'VPS Servers', url: '/vps' } },
{ link: { type: 'custom', label: 'Contact', url: '/contact' } },
{ link: { type: 'custom', label: 'Fiber Direkt (Sweden)', url: 'https://fiberdirekt.se', newTab: true } },
],
})
console.log('')
console.log('✅ Seed complete!')
console.log(' Pages: /, /virtuell-fiber, /vps, /contact')
}
seed().catch((err) => {
console.error('❌ Seed failed:', err.message || err)
process.exit(1)
})

View File

@ -0,0 +1,717 @@
// seed-block-kitchen-sink.mjs
// Fiber Direkt — Block Kitchen Sink
// One instance of every block that works without images/file uploads.
//
// Skipped (require mandatory file uploads or form relationships):
// fdHeaderTextImage — image required
// fdVideoHero — videoMp4 required
// fdLocationsGrid — card images required
// fdIconBar — icon items required
// fdPartnersLogos — logo items required
// fdContactForm — form relationship required
//
// Run: PAYLOAD_API_URL=http://localhost:3000 PAYLOAD_API_KEY=xxx node seed-block-kitchen-sink.mjs
const PAYLOAD_API_URL = process.env.PAYLOAD_API_URL || 'http://localhost:3000'
const PAYLOAD_API_KEY = process.env.PAYLOAD_API_KEY
// Required for fdFaq answers, fdUspTable descriptions, fdText body
function richText(text) {
return {
root: {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{ type: 'text', text, format: 0, detail: 0, mode: 'normal', style: '', version: 1 },
],
direction: 'ltr',
format: '',
indent: 0,
version: 1,
textFormat: 0,
},
],
direction: 'ltr',
format: '',
indent: 0,
version: 1,
},
}
}
async function createPage(slug, title, blocks) {
const res = await fetch(`${PAYLOAD_API_URL}/api/pages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `users API-Key ${PAYLOAD_API_KEY}`,
},
body: JSON.stringify({ title, slug, layout: blocks, _status: 'draft' }),
})
const data = await res.json()
console.log(`${res.ok ? '✓' : '✗'} ${title} (/${slug}): ${res.status}`)
if (!res.ok) console.error(JSON.stringify(data.errors || data, null, 2))
return data
}
async function seed() {
if (!PAYLOAD_API_KEY) {
console.error('Error: PAYLOAD_API_KEY environment variable is required.')
process.exit(1)
}
console.log(`Seeding to: ${PAYLOAD_API_URL}`)
console.log('─'.repeat(60))
await createPage('block-kitchen-sink', 'Block Kitchen Sink', [
// ─── 1. fdHero ────────────────────────────────────────────────
{
blockType: 'fdHero',
heading: 'Sveriges bästa IT-infrastruktur för företag',
subheading: 'Fiber, Backup, Colocation och Cloud',
body: 'Dedikerat fiber, backup, colocation och virtuella servrar — allt från en leverantör med svenskt huvudmannaskap.',
ctaText: 'Kom igång',
ctaLink: '/kontakt',
secondaryCtaText: 'Läs mer',
secondaryCtaLink: '#tjanster',
theme: 'dark',
anchorId: 'block-fdhero',
},
// ─── 2. fdAlternateHero ────────────────────────────────────────
{
blockType: 'fdAlternateHero',
heading: 'Er data under svensk lag. Punkt.',
description: 'Vi erbjuder dedikerat fiber, backup, colocation och cloud — allt från svenska datacenter, under svensk lag, med svensk support.',
primaryCtaText: 'Kontakta oss',
primaryCtaLink: '/kontakt',
secondaryCtaText: 'Se tjänster',
secondaryCtaLink: '#tjanster',
sectionBackground: 'white',
anchorId: 'block-fdalternatehero',
},
// ─── 3. fdStatistics ──────────────────────────────────────────
{
blockType: 'fdStatistics',
heading: 'Fiber Direkt i siffror',
sectionBackground: 'navy',
numberColor: 'yellow',
anchorId: 'block-fdstatistics',
stats: [
{ number: '99,99 %', label: 'garanterad drifttid' },
{ number: '400 Gbit/s', label: 'maxkapacitet dedikerat fiber' },
{ number: '24/7', label: 'svensk support' },
{ number: '100 %', label: 'svensk infrastruktur' },
],
},
// ─── 4. fdUspChecklist ─────────────────────────────────────────
{
blockType: 'fdUspChecklist',
heading: 'Varför välja Fiber Direkt?',
sectionBackground: 'white',
imagePosition: 'right',
checkColor: 'navy',
textColor: 'navy',
anchorId: 'block-fduspchecklist',
items: [
{ text: 'Dedikerat fiber — symmetrisk kapacitet, inte delad' },
{ text: 'Skyddsklass 3 — högsta civila säkerhetsklassning' },
{ text: 'ISO 27001 — certifierad informationssäkerhet' },
{ text: 'Ingen CLOUD Act-exponering — all data stannar i Sverige' },
{ text: 'En leverantör, en faktura — fiber, backup, cloud och colocation' },
{ text: 'Svensk support dygnet runt — inga chatbotar, inga callcenters' },
],
},
// ─── 5. fdUspTable ────────────────────────────────────────────
{
blockType: 'fdUspTable',
heading: 'Tekniska fördelar',
sectionBackground: 'gray',
checkColor: 'navy',
textColor: 'navy',
anchorId: 'block-fdusptable',
rows: [
{
title: 'Skyddsklass 3',
description: richText('Högsta civila säkerhetsklassning i Sverige. Biometrisk åtkomstkontroll och kameraövervakning dygnet runt.'),
},
{
title: 'N+1 redundans',
description: richText('Redundant kyla och kraft i varje rack. Driftsäkert även vid komponentfel.'),
},
{
title: 'Symmetrisk fiber',
description: richText('Samma hastighet upp som ned — dedikerad kapacitet, inte delad med andra kunder.'),
},
{
title: 'ISO 27001 & ISO 14001',
description: richText('Certifierade för informationssäkerhet och miljöledning. NIS2-redo.'),
},
],
},
// ─── 6. fdCardGrid ────────────────────────────────────────────
{
blockType: 'fdCardGrid',
sectionBackground: 'white',
layout: '1-1-1',
cardStyle: 'outlined',
anchorId: 'block-fdcardgrid',
cards: [
{
displayMode: 'content',
heading: 'Fiber Direkt',
contentLines: [
{ text: 'Dedikerat fiber med symmetrisk kapacitet — 10 Gbit/s till 400 Gbit/s.', style: 'normal' },
{ text: 'Direkt anslutning till svenskt nät utan Big Tech-mellanhänder.', style: 'normal' },
],
},
{
displayMode: 'content',
heading: 'Server Direkt',
contentLines: [
{ text: 'Virtuella servrar på svensk infrastruktur. Proxmox-baserad plattform.', style: 'normal' },
{ text: 'Ny server på under 5 minuter. Betala för det ni använder.', style: 'normal' },
],
},
{
displayMode: 'content',
heading: 'Datacenter Direkt',
contentLines: [
{ text: 'Colocation i Skyddsklass 3 — Stockholm Nord, 1,6 MW kapacitet.', style: 'normal' },
{ text: 'Upp till 88 kW per rack. AI/HPC-redo.', style: 'normal' },
],
},
],
},
// ─── 7. fdPricingCard ─────────────────────────────────────────
{
blockType: 'fdPricingCard',
sectionTitle: 'Välj din tjänstenivå',
sectionBackground: 'gray',
cardStyle: 'navy',
buttonColor: 'yellow',
titleColor: 'navy',
anchorId: 'block-fdpricingcard',
cards: [
{
title: 'Fiber Basic',
subtitle: 'från 1 195 kr/mån',
description: 'Dedikerat fiber för mindre kontor och startups.',
ctaText: 'Få offert',
ctaLink: '/kontakt',
bulletPoints: [
{ text: '10 Gbit/s symmetrisk' },
{ text: 'SLA med ekonomisk garanti' },
{ text: 'Svensk support 24/7' },
],
},
{
title: 'Fiber Platina',
subtitle: 'från 7 995 kr/mån',
description: 'Hög kapacitet för krävande verksamheter och datacenter.',
ctaText: 'Få offert',
ctaLink: '/kontakt',
bulletPoints: [
{ text: '100 Gbit/s symmetrisk' },
{ text: 'BGP-stöd och VLAN-taggning' },
{ text: 'Redundanta uplinks' },
{ text: 'Prioriterad support' },
],
},
],
},
// ─── 8. fdCtaBanner ───────────────────────────────────────────
{
blockType: 'fdCtaBanner',
heading: 'Redo att ta kontrollen över er IT?',
subheading: 'Boka ett samtal med vårt team. Vi hjälper er hitta rätt upplägg — utan förpliktelser.',
ctaText: 'Kontakta oss',
ctaLink: '/kontakt',
secondaryCtaText: 'Prata med en expert',
secondaryCtaLink: '/kontakt',
sectionBackground: 'yellow',
alignment: 'center',
size: 'large',
anchorId: 'block-fdctabanner',
},
// ─── 9. fdCtaSideImage ────────────────────────────────────────
{
blockType: 'fdCtaSideImage',
heading: 'Flytta in. Skala upp. Sov gott.',
body: 'Vårt team hanterar hela flytten — från planering till driftsättning. Ni behöver inte lyfta ett finger. Och när allt är igång finns vi kvar som er enda IT-kontakt.',
ctaText: 'Boka ett möte',
ctaLink: '/kontakt',
imagePosition: 'right',
theme: 'dark',
anchorId: 'block-fdctasideimage',
},
// ─── 10. fdFaq ────────────────────────────────────────────────
{
blockType: 'fdFaq',
heading: 'Vanliga frågor',
theme: 'light',
anchorId: 'block-fdfaq',
items: [
{
question: 'Lyder min data under CLOUD Act om ni har servrar i EU?',
answer: richText('Nej — inte med Fiber Direkt. Vi är ett svenskägt bolag och all infrastruktur finns i Sverige. Ingen data passerar utländsk juridisdiktion, och US Cloud Act gäller inte oss.'),
},
{
question: 'Vad ingår i er SLA?',
answer: richText('Vår SLA garanterar 99,99 % drifttid med ekonomisk kompensation vid avvikelse. Exakt villkor beror på tjänst — kontakta oss för aktuellt avtal.'),
},
{
question: 'Kan vi skala upp kapaciteten utan att byta leverantör?',
answer: richText('Ja. Fibern skalas upp utan att byta hårdvara. Virtuella servrar justeras på minuter. Colocation-kapacitet utökas i samma rack eller med nytt rack.'),
},
],
},
// ─── 11. fdTestimonial ────────────────────────────────────────
{
blockType: 'fdTestimonial',
heading: 'Vad våra kunder säger',
sectionBackground: 'gray',
layout: 'featured',
anchorId: 'block-fdtestimonial',
testimonials: [
{
quote: 'Vi hade all data i AWS Frankfurt utan egentlig kontroll. Flytten till Fiber Direkt tog tre veckor — och nu sover vi bättre.',
authorName: '[REPRESENTANT — kräver verklig kundgodkännande]',
authorRole: 'IT-chef',
authorCompany: '[Företagsnamn]',
},
],
},
// ─── 12. fdServicesGrid ───────────────────────────────────────
{
blockType: 'fdServicesGrid',
heading: 'Alla tjänster',
columns: '4',
anchorId: 'block-fdservicesgrid',
services: [
{ title: 'Fiber', description: 'Dedikerat fiber 10400 Gbit/s. Symmetrisk kapacitet.', link: '/fiber' },
{ title: 'Colocation', description: 'Rack i Skyddsklass 3. Upp till 88 kW per rack.', link: '/colocation' },
{ title: 'Virtuellt datacenter', description: 'VPS på Proxmox-plattform. Ny server på 5 minuter.', link: '/virtuellt-datacenter' },
{ title: 'Backup', description: 'Automatisk backup till svenska datacenter. 30 dagars retention.', link: '/backup' },
],
},
// ─── 13. fdWideCard ───────────────────────────────────────────
{
blockType: 'fdWideCard',
heading: 'Klimatsmart sedan dag ett',
body: 'Överskottsvärmen från våra datacenter matas in i Stockholms fjärrvärmenät. HVO100 förnybart diesel i reservkraftverken. ISO 14001-certifierade — 20+ år före EU Green Deal.',
ctaText: 'Läs om vårt hållbarhetsarbete',
ctaLink: '/hallbarhet',
cardBackground: 'navy',
buttonColor: 'yellow',
sectionBackground: 'white',
anchorId: 'block-fdwidecard',
},
// ─── 14. fdTechProperties ─────────────────────────────────────
{
blockType: 'fdTechProperties',
sectionBackground: 'navy',
categoryColor: 'white',
valueColor: 'yellow',
anchorId: 'block-fdtechproperties',
properties: [
{ category: 'Skyddsklass', value: '3' },
{ category: 'Effekt per rack', value: '88 kW' },
{ category: 'Total kapacitet', value: '1,6 MW' },
{ category: 'Certifieringar', value: 'ISO 27001 · ISO 14001 · SSF 200' },
],
},
// ─── 15. fdFeatureAnnouncement ────────────────────────────────
{
blockType: 'fdFeatureAnnouncement',
heading: 'Nyhet: Office Komplett — allt kontoret behöver på en faktura',
body: 'Dedikerat fiber, managed WiFi, nätverksswitch och 1 TB svensk molnlagring i ett paket för 2 995 kr/mån.',
ctaText: 'Läs mer',
ctaLink: '/office-komplett',
theme: 'gray',
anchorId: 'block-fdfeatureannouncement',
},
// ─── 16. fdText ───────────────────────────────────────────────
{
blockType: 'fdText',
heading: 'Om Fiber Direkt',
subheading: 'Svenskägt. Driftsäkert. Ansvarsfullt.',
body: richText('Fiber Direkt är ett stockholmsbaserat IT-infrastrukturbolag med eget nät, egna datacenter och egen support. Vi äger vår infrastruktur — vi återsäljer ingenting. Sedan starten har vi levererat dedikerad fiber, colocation och cloud till svenska företag som kräver kontroll, prestanda och datasuveränitet.'),
alignment: 'left',
textColor: 'navy',
sectionBackground: 'white',
maxWidth: 'medium',
anchorId: 'block-fdtext',
},
// ─── 17. fdDataTable ──────────────────────────────────────────
{
blockType: 'fdDataTable',
heading: 'Fibernivåer — översikt',
description: 'Alla priser exkl. moms. Exakt pris beror på adress och avtalstid.',
dataSource: 'manual',
sectionBackground: 'gray',
headerStyle: 'navy',
stripeRows: true,
bordered: false,
firstColumnBold: true,
anchorId: 'block-fddatatable',
headers: [
{ text: 'Nivå' },
{ text: 'Hastighet' },
{ text: 'Pris från' },
],
rows: [
{ cells: 'Basic, 10 Gbit/s, 1 195 kr/mån' },
{ cells: 'Standard, 40 Gbit/s, Offert' },
{ cells: 'Premium, 100 Gbit/s, Offert' },
{ cells: 'Platina, 400 Gbit/s, 7 995 kr/mån' },
],
},
// ─── 18. fdTeam ───────────────────────────────────────────────
{
blockType: 'fdTeam',
heading: 'Möt vårt team',
subheading: 'Svenska experter. Svenska svar.',
columns: '3',
cardStyle: 'navy',
sectionBackground: 'white',
anchorId: 'block-fdteam',
members: [
{
name: 'Anna Lindqvist',
role: 'Säljansvarig',
bio: 'Anna hjälper er hitta rätt lösning för ert kontor eller datacenter.',
email: 'anna@fiberdirekt.se',
},
{
name: 'Marcus Bergström',
role: 'Nätverksingenjör',
bio: 'Marcus ansvarar för fibernätet och teknisk driftsättning.',
},
{
name: 'Sara Holm',
role: 'Kundsupport',
bio: 'Sara är er kontakt för support och felanmälan — dygnet runt.',
email: 'support@fiberdirekt.se',
},
],
},
// ─── 19. fdNewsletter ────────────────────────────────────────
{
blockType: 'fdNewsletter',
heading: 'Håll er uppdaterade',
description: 'Prenumerera på nyheter om fiber, cloud och IT-infrastruktur i Sverige.',
submitEndpoint: '/api/newsletter',
buttonText: 'Prenumerera',
successMessage: 'Tack! Ni är nu prenumeranter.',
consentText: 'Jag godkänner att mina uppgifter används enligt integritetspolicyn.',
privacyPolicyLink: '/integritetspolicy',
collectName: false,
collectCompany: false,
layout: 'inline',
sectionBackground: 'navy',
textColor: 'auto',
},
// ─── 20. fdTags ──────────────────────────────────────────────
{
blockType: 'fdTags',
heading: 'Tjänster och teknologier',
tagStyle: 'navy',
tagSize: 'large',
alignment: 'left',
sectionBackground: 'white',
tags: [
{ text: 'Fiber' },
{ text: 'Colocation' },
{ text: 'Virtuellt datacenter' },
{ text: 'Backup' },
{ text: 'Layer 2' },
{ text: 'ISO 27001' },
{ text: 'Skyddsklass 3' },
{ text: 'NIS2' },
{ text: 'GDPR' },
],
},
// ─── 21. fdServiceChooser ─────────────────────────────────────
{
blockType: 'fdServiceChooser',
heading: 'Välj er bransch',
description: 'Vi har erfarenhet av IT-infrastruktur för ett brett spektrum av branscher.',
sectionBackground: 'gray',
anchorId: 'block-fdservicechooser',
categories: [
{
label: 'Kontor & Företag',
intro: 'Turnkey IT för kontor — fiber, WiFi, switch och molnlagring på en faktura.',
services: [
{ title: 'Office Komplett', description: 'Fiber + WiFi + switch + 1 TB cloud. 2 995 kr/mån.', ctaText: 'Läs mer', ctaLink: '/office-komplett' },
{ title: 'Dedikerat fiber', description: 'Symmetrisk kapacitet direkt till ert kontor.', ctaText: 'Se priser', ctaLink: '/fiber' },
],
},
{
label: 'Datacenter & Hosting',
intro: 'Hög densitet, hög säkerhet — colocation i Stockholm Nord.',
services: [
{ title: 'Colocation', description: 'Från halvskåp till full cage. Upp till 88 kW per rack.', ctaText: 'Se alternativ', ctaLink: '/colocation' },
{ title: 'Virtuellt datacenter', description: 'VPS på Proxmox. Ny server på 5 minuter.', ctaText: 'Beräkna kostnad', ctaLink: '/virtuellt-datacenter' },
],
},
],
},
// ─── 22. fdSpecCards ──────────────────────────────────────────
{
blockType: 'fdSpecCards',
heading: 'Colocation — specifikationer',
description: 'Stockholm Nord datacenter. Industrins starkaste civila säkerhetsklassning.',
ctaText: 'Beräkna din kostnad',
ctaLink: '/kontakt',
layout: 'sideBySide',
cardStyle: 'outlined',
sectionBackground: 'navy',
anchorId: 'block-fdspeccards',
cards: [
{ title: 'Halvskåp', description: '18U, 24/7-bevakat.', specLabel: 'Pris från', specValue: '3 495 kr/mån' },
{ title: 'Helskåp', description: '40U, N+1 redundans, KVM ingår.', specLabel: 'Pris från', specValue: '5 995 kr/mån' },
{ title: 'Bur', description: 'Dedikerat burutrymme för större installationer.', specLabel: 'Pris', specValue: 'Offert' },
],
},
// ─── 23. fdLinkCards ──────────────────────────────────────────
{
blockType: 'fdLinkCards',
heading: 'Utforska våra tjänster',
description: 'Allt ni behöver — från en leverantör.',
columns: '3',
cardStyle: 'outlined',
sectionBackground: 'navy',
anchorId: 'block-fdlinkcards',
cards: [
{ title: 'Dedikerat fiber\n10400 Gbit/s', linkLabel: 'Se fibertjänster', linkUrl: '/fiber' },
{ title: 'Virtuella servrar\nProxmox-plattform', linkLabel: 'Utforska VPS', linkUrl: '/virtuellt-datacenter' },
{ title: 'Colocation\nSkyddsklass 3', linkLabel: 'Mer om datacenter', linkUrl: '/colocation' },
],
},
// ─── 24. fdQuiz ───────────────────────────────────────────────
{
blockType: 'fdQuiz',
heading: 'Vilken tjänst passar er?',
description: 'Svara på tre frågor — vi rekommenderar rätt produkt.',
triggerLabel: 'Starta quiz',
sectionBackground: 'gray',
anchorId: 'block-fdquiz',
outcomes: [
{
key: 'fiber',
title: 'Dedikerat fiber',
description: 'Er verksamhet kräver pålitlig, symmetrisk uppkoppling. Fiber Direkt levererar från 10 Gbit/s.',
ctaText: 'Se fiberalternativ',
ctaLink: '/fiber',
},
{
key: 'vdc',
title: 'Virtuellt datacenter',
description: 'Ni vill ersätta fysiska servrar med flexibla virtuella maskiner på svensk mark.',
ctaText: 'Utforska VPS',
ctaLink: '/virtuellt-datacenter',
},
{
key: 'colo',
title: 'Colocation',
description: 'Ni har egen hårdvara och behöver säkert utrymme med hög driftsäkerhet.',
ctaText: 'Se colocation',
ctaLink: '/colocation',
},
],
questions: [
{
question: 'Vad är er viktigaste utmaning just nu?',
options: [
{ label: 'Dålig eller instabil uppkoppling', outcomeKeys: 'fiber' },
{ label: 'Kostsam och svårskalad serverhårdvara', outcomeKeys: 'vdc' },
{ label: 'Serverrummet räcker inte till', outcomeKeys: 'colo' },
],
},
{
question: 'Hur hanterar ni er infrastruktur idag?',
options: [
{ label: 'Delad bredbandsuppkoppling', outcomeKeys: 'fiber' },
{ label: 'Egna fysiska servrar på kontoret', outcomeKeys: 'vdc,colo' },
{ label: 'Blandat moln och egna servrar', outcomeKeys: 'vdc,colo' },
],
},
{
question: 'Hur viktig är datasuveränitet för er?',
options: [
{ label: 'Kritisk — all data måste vara i Sverige', outcomeKeys: 'fiber,vdc,colo' },
{ label: 'Viktig men inte absolut krav', outcomeKeys: 'vdc,fiber' },
{ label: 'Vi fokuserar mer på pris och prestanda', outcomeKeys: 'vdc' },
],
},
],
},
// ─── 25. fdVideo (YouTube — no upload needed) ─────────────────
{
blockType: 'fdVideo',
heading: 'Fiber Direkt — så fungerar det',
description: 'En kort genomgång av hur vi levererar fiber, colocation och cloud.',
videoSource: 'youtube',
youtubeUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
aspectRatio: '16/9',
maxWidth: 'default',
sectionBackground: 'white',
textColor: 'auto',
anchorId: 'block-fdvideo',
},
// ─── 26. fdVpsCalculator ──────────────────────────────────────
{
blockType: 'fdVpsCalculator',
heading: 'Beräkna kostnaden för din VPS',
description: 'Välj resurser och se priset direkt.',
orderCtaText: 'Beställ',
orderCtaLink: '/kontakt?subject=vps-bestallning',
contactCtaText: 'Frågor? Kontakta oss',
contactCtaLink: '/kontakt',
sectionBackground: 'gray',
pricingCpuPerCore: 120,
pricingRamPerGb: 100,
pricingSsdPerGb: 4,
pricingHddPerGb: 1,
pricingWindowsLicense: 250,
showAdminFee: false,
anchorId: 'block-fdvpscalculator',
},
// ─── 27. fdServiceCalc ────────────────────────────────────────
{
blockType: 'fdServiceCalc',
heading: 'Beräkna er colocation-kostnad',
description: 'Välj rack-storlek och tilläggstjänster.',
summaryHeading: 'Kostnadsöversikt',
totalLabel: 'Totalt per månad',
totalSuffix: 'exkl. moms',
orderCtaText: 'Beställ',
orderCtaLink: '/kontakt',
contactCtaText: 'Frågor? Kontakta oss',
contactCtaLink: '/kontakt',
sectionBackground: 'white',
anchorId: 'block-fdservicecalc',
optionGroups: [
{
groupLabel: 'Rackstorlek',
options: [
{ label: 'Halvskåp (18U)', price: 3495 },
{ label: 'Helskåp (40U)', price: 5995 },
],
},
],
resources: [
{
label: 'Extra bandbredd',
unit: 'Gbit/s',
pricePerUnit: 500,
defaultValue: 0,
min: 0,
max: 10,
step: 1,
summaryTemplate: 'Extra bandbredd ({value} {unit})',
},
],
addons: [
{ label: 'Remote hands (10 tim/mån)', price: 1500, description: 'Fysisk assistans i datacenter på begäran.' },
{ label: 'KVM-åtkomst', price: 0, description: 'Ingår kostnadsfritt i helskåp.' },
],
fixedFees: [],
},
// ─── 28. fdCodeEmbed ──────────────────────────────────────────
{
blockType: 'fdCodeEmbed',
heading: 'Inbäddat formulär',
description: 'Exempel på fdCodeEmbed med iframe.',
embedType: 'iframe',
iframeSrc: 'https://example.com/embed-placeholder',
iframeTitle: 'Inbäddat formulär',
iframeHeight: '500px',
maxWidth: 'narrow',
sectionBackground: 'gray',
textColor: 'navy',
embedBackground: 'card',
anchorId: 'block-fdcodeembed',
},
// ─── 29. fdContact ────────────────────────────────────────────
{
blockType: 'fdContact',
heading: 'Kontakta oss',
anchorId: 'block-fdcontact',
contactMethods: [
{ label: 'Telefon: 08-123 456 78', link: 'tel:+4681234567' },
{ label: 'E-post: info@fiberdirekt.se', link: 'mailto:info@fiberdirekt.se' },
{ label: 'Adress: Stockholm Nord, Sverige', link: 'https://maps.google.com' },
],
},
// ─── 30. fdSpacer ─────────────────────────────────────────────
{
blockType: 'fdSpacer',
height: 'lg',
sectionBackground: 'white',
},
// ─── 31. fdCtaBanner (closing) ────────────────────────────────
{
blockType: 'fdCtaBanner',
heading: 'Redo att komma igång?',
subheading: 'Kontakta oss idag och få en offert anpassad för er verksamhet.',
ctaText: 'Kontakta oss',
ctaLink: '/kontakt',
sectionBackground: 'navy',
alignment: 'center',
size: 'large',
anchorId: 'block-closing-cta',
},
])
console.log('─'.repeat(60))
console.log('Done. 30 blocks seeded (+ closing CTA = 31 block instances).')
console.log()
console.log('Skipped blocks (require mandatory file uploads):')
console.log(' fdHeaderTextImage — image required')
console.log(' fdVideoHero — videoMp4 required')
console.log(' fdLocationsGrid — card images required')
console.log(' fdIconBar — icon items required')
console.log(' fdPartnersLogos — logo items required')
console.log(' fdContactForm — form relationship required')
}
seed().catch((err) => {
console.error('Seed failed:', err)
process.exit(1)
})

528
seedv2.mjs Normal file
View File

@ -0,0 +1,528 @@
#!/usr/bin/env node
/*
* LM USA Seed Script
*
* Usage:
* PAYLOAD_URL=http://localhost:3000 PAYLOAD_API_KEY=your-key node seed.mjs
*
* Creates:
* - Contact form
* - Home page /
* - Virtuell Fiber /virtuell-fiber
* - VPS Server /vps
* - Contact /contact
* - Header navigation
* - Footer
*/
const PAYLOAD_URL = process.env.PAYLOAD_URL || 'http://localhost:3000'
const API_KEY = process.env.PAYLOAD_API_KEY
if (!API_KEY) {
console.error('❌ Missing PAYLOAD_API_KEY environment variable')
process.exit(1)
}
const headers = {
'Content-Type': 'application/json',
Authorization: `users API-Key ${API_KEY}`,
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
function richText(text) {
const paragraphs = text.split('\n').filter(Boolean)
return {
root: {
type: 'root',
children: paragraphs.map((p) => ({
type: 'paragraph',
children: [
{ type: 'text', text: p, format: 0, detail: 0, mode: 'normal', style: '', version: 1 },
],
direction: 'ltr',
format: '',
indent: 0,
version: 1,
})),
direction: 'ltr',
format: '',
indent: 0,
version: 1,
},
}
}
async function apiPost(collection, data) {
const res = await fetch(`${PAYLOAD_URL}/api/${collection}`, {
method: 'POST',
headers,
body: JSON.stringify(data),
})
if (!res.ok) {
const err = await res.text()
throw new Error(`POST /api/${collection} failed (${res.status}): ${err}`)
}
return res.json()
}
async function apiGet(collection, query = '') {
const res = await fetch(`${PAYLOAD_URL}/api/${collection}${query}`, { headers })
if (!res.ok) {
const err = await res.text()
throw new Error(`GET /api/${collection} failed (${res.status}): ${err}`)
}
return res.json()
}
async function apiUpdateGlobal(slug, data) {
const res = await fetch(`${PAYLOAD_URL}/api/globals/${slug}`, {
method: 'POST',
headers,
body: JSON.stringify(data),
})
if (!res.ok) {
const err = await res.text()
throw new Error(`POST /api/globals/${slug} failed (${res.status}): ${err}`)
}
return res.json()
}
async function createPageIfNotExists(slug, data) {
const existing = await apiGet('pages', `?where[slug][equals]=${slug}&limit=1`)
if (existing.docs?.length > 0) {
console.log(` ⏭ Page "${slug}" already exists — skipping`)
return existing.docs[0]
}
const result = await apiPost('pages', data)
return result.doc
}
// ─── Main ─────────────────────────────────────────────────────────────────────
async function seed() {
console.log(`🌱 Seeding LM USA database at ${PAYLOAD_URL}...`)
// ─── 1. Contact Form ───────────────────────────────────────────────────────
console.log(' → Creating contact form...')
let contactForm
const existingForms = await apiGet('forms', '?where[title][equals]=Contact Form&limit=1')
if (existingForms.docs?.length > 0) {
console.log(' ⏭ Contact form already exists — skipping')
contactForm = existingForms.docs[0]
} else {
const result = await apiPost('forms', {
title: 'Contact Form',
fields: [
{ name: 'name', label: 'Name', blockType: 'text', required: true, width: 50 },
{ name: 'company', label: 'Company', blockType: 'text', required: false, width: 50 },
{ name: 'email', label: 'Email', blockType: 'email', required: true, width: 50 },
{ name: 'phone', label: 'Phone', blockType: 'text', required: false, width: 50 },
{ name: 'message', label: 'Message', blockType: 'textarea', required: true },
],
submitButtonLabel: 'Send Message',
confirmationType: 'message',
confirmationMessage: richText(
"Thanks for reaching out. We'll get back to you within 24 hours.",
),
})
contactForm = result.doc
}
// ─── 2. Home Page ──────────────────────────────────────────────────────────
console.log(' → Creating Home page...')
await createPageIfNotExists('index', {
title: 'Home',
slug: 'index',
_status: 'published',
publishedAt: new Date().toISOString(),
layout: [
// Hero
// theme: 'light' | 'dark'
// textColor: 'auto' | 'white' | 'dark'
{
blockType: 'lmHero',
heading: 'Infrastructure Without Borders',
subheading: 'Layer & Mesh USA',
body: 'Edge compute, virtual fiber, and managed servers — built for businesses that need sovereign, reliable infrastructure. Configure your setup and get started in minutes.',
ctaText: 'Explore Products',
ctaLink: '#products',
secondaryCtaText: 'Contact Us',
secondaryCtaLink: '/contact',
theme: 'dark',
textColor: 'auto',
},
// Link Cards
// cardStyle: 'outlined' | 'dark' | 'gray' | 'yellow'
// sectionBackground: 'white' | 'gray' | 'dark'
// title is textarea (supports \n)
{
blockType: 'lmLinkCards',
heading: 'Choose Your Infrastructure',
description: 'Two paths to managed IT — hardware at your location or virtual in our cloud.',
columns: '2',
cardStyle: 'outlined',
sectionBackground: 'dark',
anchorId: 'products',
cards: [
{
title: 'Virtuell Fiber\nEdge compute hardware deployed at your location. Plug in, connect to the mesh — instant private infrastructure.',
linkLabel: 'Explore VF Enhet',
linkUrl: '/virtuell-fiber',
},
{
title: 'VPS Servers\nCloud-hosted virtual servers on Layer & Mesh infrastructure. Full root access, instant provisioning, sovereign data.',
linkLabel: 'Configure a VPS',
linkUrl: '/vps',
},
],
},
// Statistics
// numberColor: 'gradient' | 'yellow' | 'mint' | 'dark' | 'white'
// sectionBackground: 'white' | 'dark' | 'gray'
{
blockType: 'lmStatistics',
heading: 'Built for Reliability',
sectionBackground: 'dark',
numberColor: 'gradient',
stats: [
{ number: '99.99%', label: 'Uptime SLA' },
{ number: '<5ms', label: 'Edge Latency' },
{ number: '24/7', label: 'Support' },
{ number: '100%', label: 'Swedish Sovereign' },
],
},
// Swedish CTA
// sectionBackground: 'dark' | 'elevated' | 'gray'
{
blockType: 'lmSwedishCta',
headingText: 'Söker du Fiber Direkt i Sverige?',
subText: 'Visit our Swedish site for local fiber and hosting services.',
ctaLabel: 'fiberdirekt.se',
ctaUrl: 'https://fiberdirekt.se',
sectionBackground: 'dark',
},
// Bottom CTA
// sectionBackground: 'yellow' | 'dark' | 'gray' | 'white'
// alignment: 'center' | 'left'
// size: 'small' | 'medium' | 'large'
{
blockType: 'lmCtaBanner',
heading: 'Ready to get started?',
subheading: 'Configure your infrastructure or talk to our team — no commitment required.',
ctaText: 'Contact Us',
ctaLink: '/contact',
secondaryCtaText: 'View VPS Pricing',
secondaryCtaLink: '/vps',
sectionBackground: 'yellow',
alignment: 'center',
size: 'medium',
},
],
})
// ─── 3. Virtuell Fiber Page ────────────────────────────────────────────────
console.log(' → Creating Virtuell Fiber page...')
await createPageIfNotExists('virtuell-fiber', {
title: 'Virtuell Fiber',
slug: 'virtuell-fiber',
_status: 'published',
publishedAt: new Date().toISOString(),
layout: [
// Product Detail — eyebrow as breadcrumb, specs in richText body (right col)
// sectionBackground: 'white' | 'dark'
// cartProductType: 'hardware' | 'vps' | 'service'
{
blockType: 'lmProductDetail',
eyebrow: 'Platform / Network / Virtual Fiber',
productName: 'VF Standard',
description: 'Virtual Fiber is not a VPN. It creates an encrypted, transparent Layer 2 network, built on top of any available internet connection, that behaves exactly like a private fiber link between your locations. The VF Standard unit ships pre-configured and ready. Connect it to your existing broadband, fiber, or 4G connection and it joins the Layer & Mesh mesh automatically. Your devices get full Layer 2 transport to every other point on the network — with VLAN, QinQ, and Full MTU support — without a single line of configuration. No fixed IP address needed. No IT team required on site. Works as a permanent connection or deployed temporarily. Contract terms start at one day.',
body: richText('$90 / month\nShipping additional. No setup fee. Cancel anytime.\nPerformance\nThroughput: Up to 1,000 Mbit/s\nTransport: Encrypted Layer 2 over any internet\nRedundancy: Automatic failover via 4G or secondary ISP\nHardware\nForm factor: 1U rack-mounted\nCooling: Active (fan)\nWAN: 2 × 1GbE RJ-45\nLAN: 2 × 1GbE RJ-45\nFeatures\nPlug and play — pre-configured at dispatch\nVLAN · QinQ · Full MTU\nNo public IP address required\nWorld-class Layer 2 encryption'),
ctaText: 'Order Now — $90/month',
ctaLink: '/contact',
secondaryCtaText: 'Download Datasheet',
secondaryCtaLink: '#',
enableCart: true,
cartProductName: 'VF Standard',
cartPrice: 90,
cartProductType: 'hardware',
sectionBackground: 'dark',
},
// Service Cards — "What Virtual Fiber Makes Possible"
// cardSurface: 'outlined-teal' | 'dark' | 'elevated' | 'teal' | 'light' | 'cyan'
// sectionBackground: 'dark' | 'gray' | 'white' | 'teal'
// iconSlug: 'backup' | 'colocation' | 'disaster-recovery' | 'connectivity' | 'connectivity-grid' | 'storage' | 'virtual-server'
{
blockType: 'lmServiceCards',
heading: 'What Virtual Fiber Makes Possible',
columns: '3',
cardSurface: 'outlined-teal',
sectionBackground: 'dark',
cards: [
{
eyebrow: 'Reach',
title: 'Access Resources From Anywhere',
description: 'Your team in New York reaches your Stockholm servers the same way they reach the printer down the hall — over a private Layer 2 network, without a VPN client or public IP.',
iconSlug: 'connectivity',
},
{
eyebrow: 'Simplicity',
title: 'No Configuration Required',
description: 'Units arrive ready. Connect to any internet access — fiber, broadband, 4G — and the device joins the mesh automatically. Dual transport paths for redundancy are supported out of the box.',
iconSlug: 'connectivity-grid',
},
{
eyebrow: 'Security',
title: 'World-Class Layer 2 Encryption',
description: 'All traffic is encrypted using state-of-the-art technology. Secure channels are established over public networks without any configuration — a clear replacement for traditional VPN.',
iconSlug: 'storage',
},
],
},
// USP Checklist — "VF Standard at a Glance"
// checkColor: 'dark' | 'yellow' | 'gray'
// sectionBackground: 'white' | 'gray' | 'dark'
// textColor: 'dark' | 'white'
{
blockType: 'lmUspChecklist',
heading: 'VF Standard at a Glance',
sectionBackground: 'dark',
textColor: 'white',
checkColor: 'dark',
items: [
{ text: 'Micro PC portable unit, ships pre-configured, online in minutes' },
{ text: 'Up to 1,000 Mbit/s Layer 2 encrypted transport, not a VPN, a dedicated private network' },
{ text: '2 WAN + 2 LAN ports (RJ-45, 1GbE), or failover over 4G/5G or secondary ISP' },
{ text: 'Full VLAN, QinQ, and MTU support, works with all Layer & Mesh services natively' },
{ text: 'No public IP address needed, works over any available internet connection' },
{ text: '$90/month subscription, no long-term commitment required, shipping additional' },
],
},
// FAQ — matching VF-specific questions from screenshot
// theme: 'gray' | 'light' | 'dark'
{
blockType: 'lmFaq',
heading: 'Virtual Fiber Questions',
theme: 'dark',
items: [
{
question: "What's the difference between Virtual Fiber and a VPN?",
answer: richText(
'Virtual Fiber creates a true Layer 2 network — your devices see each other as if they were on the same physical switch. A VPN tunnels Layer 3 traffic and requires client software, configuration, and a public IP. Virtual Fiber needs none of that.',
),
},
{
question: 'What internet connection does the VF Standard need?',
answer: richText(
'Any internet connection works — broadband, fiber, 4G/5G, or even satellite. The VF Standard automatically establishes encrypted transport over whatever is available. For redundancy, connect a second WAN port to a backup ISP or 4G modem.',
),
},
{
question: 'Does it need any configuration on site?',
answer: richText(
'No. Units are fully configured before dispatch. Connect the WAN port to your internet access and the LAN ports to your network — the unit joins the Layer & Mesh mesh automatically. There is nothing to configure.',
),
},
{
question: 'Can I connect multiple offices?',
answer: richText(
'Yes. Each office gets a VF Standard unit. All units on your account see each other over a single flat Layer 2 network. Add or remove locations at any time — no reconfiguration needed.',
),
},
{
question: 'What are the contract terms?',
answer: richText(
'Month-to-month with no long-term commitment. Contract terms start at one day. Give us 30 days notice and we will arrange return of the hardware. Shipping is additional.',
),
},
],
},
// CTA
{
blockType: 'lmCtaBanner',
heading: 'Need a custom deployment?',
subheading: 'Talk to our team about multi-unit rollouts, custom configurations, or enterprise pricing.',
ctaText: 'Contact Us',
ctaLink: '/contact',
sectionBackground: 'yellow',
alignment: 'left',
size: 'small',
},
],
})
// ─── 4. VPS Server Page ────────────────────────────────────────────────────
console.log(' → Creating VPS page...')
await createPageIfNotExists('vps', {
title: 'VPS Servers',
slug: 'vps',
_status: 'published',
publishedAt: new Date().toISOString(),
layout: [
// Alternate Hero
// sectionBackground: 'white' | 'dark' | 'gray'
{
blockType: 'lmAlternateHero',
heading: 'Virtual Private Servers',
description: 'Cloud-hosted servers on Layer & Mesh infrastructure. Full root access, instant provisioning, and Swedish-sovereign data. Configure your resources below — pricing updates in real-time.',
primaryCtaText: 'Configure Below',
primaryCtaLink: '#calculator',
secondaryCtaText: 'Contact Sales',
secondaryCtaLink: '/contact',
sectionBackground: 'dark',
},
// VPS Calculator
// sectionBackground: 'dark' | 'gray' | 'teal'
// currency: 'usd' | 'sek' | 'eur'
{
blockType: 'lmVpsCalculator',
heading: 'Estimate your cost',
orderCtaText: 'Add to Cart',
contactCtaText: 'Questions?',
contactCtaLink: '/contact',
currency: 'usd',
sectionBackground: 'dark',
pricingCpuPerCore: 12,
pricingRamPerGb: 5,
pricingSsdPerGb: 0.08,
pricingHddPerGb: 0.02,
pricingWindowsLicense: 25,
anchorId: 'calculator',
},
// USP Table — description is richText
// checkColor: 'dark' | 'yellow' | 'gray'
// textColor: 'dark' | 'white'
// sectionBackground: 'white' | 'gray' | 'dark'
{
blockType: 'lmUspTable',
heading: "What's Included",
sectionBackground: 'dark',
checkColor: 'dark',
textColor: 'white',
rows: [
{
title: 'Full Root Access',
description: richText('SSH, VNC, or console — full control of your server.'),
},
{
title: 'Unmetered Bandwidth',
description: richText('No traffic caps or overage fees.'),
},
{
title: 'NVMe Storage',
description: richText('Enterprise-grade SSDs for maximum I/O.'),
},
{
title: 'Instant Provisioning',
description: richText('Your server is ready in under 60 seconds.'),
},
{
title: '99.99% Uptime SLA',
description: richText('Guaranteed availability with automatic failover.'),
},
{
title: 'Swedish Sovereignty',
description: richText('Data stored and processed in Sweden.'),
},
],
},
// CTA
{
blockType: 'lmCtaBanner',
heading: 'Need dedicated hardware?',
subheading: 'Check out the VF Enhet — edge compute deployed at your location.',
ctaText: 'Learn About VF Enhet',
ctaLink: '/virtuell-fiber',
secondaryCtaText: 'Contact Sales',
secondaryCtaLink: '/contact',
sectionBackground: 'yellow',
alignment: 'left',
size: 'small',
},
],
})
// ─── 5. Contact Page ───────────────────────────────────────────────────────
console.log(' → Creating Contact page...')
await createPageIfNotExists('contact', {
title: 'Contact',
slug: 'contact',
_status: 'published',
publishedAt: new Date().toISOString(),
layout: [
// Contact Form
// sectionBackground: 'white' | 'gray' | 'dark' | 'navyGradient'
// layout: 'standard' | 'withImage' | 'card'
{
blockType: 'lmContactForm',
heading: 'Get in Touch',
description: 'Tell us about your infrastructure needs — our team will get back to you within 24 hours.',
form: contactForm.id,
submitText: 'Send Message',
sectionBackground: 'dark',
layout: 'card',
},
// FAQ
{
blockType: 'lmFaq',
heading: 'Common Questions',
theme: 'dark',
items: [
{
question: 'Where are you located?',
answer: richText(
'Layer & Mesh operates infrastructure in Sweden with support available globally. Our US operations serve North American customers.',
),
},
{
question: 'How quickly can I get started?',
answer: richText(
'VPS servers are provisioned instantly. VF Enhet hardware typically ships within 57 business days.',
),
},
{
question: 'Do you offer enterprise pricing?',
answer: richText(
'Yes. Contact us for volume pricing, custom SLAs, and dedicated account management.',
),
},
],
},
],
})
// ─── 6. Header Navigation ─────────────────────────────────────────────────
// Header navItems are flat: { label, type, url } directly on each item
console.log(' → Setting up header...')
await apiUpdateGlobal('header', {
logoLink: { type: 'custom', url: '/' },
navItems: [
{ label: 'Virtuell Fiber', type: 'custom', url: '/virtuell-fiber' },
{ label: 'VPS Servers', type: 'custom', url: '/vps' },
{ label: 'Contact', type: 'custom', url: '/contact' },
],
})
// ─── 7. Footer ────────────────────────────────────────────────────────────
// Footer navItems use link() field: { link: { type, label, url } }
console.log(' → Setting up footer...')
await apiUpdateGlobal('footer', {
logoLink: { type: 'custom', url: '/' },
navItems: [
{ link: { type: 'custom', label: 'Virtuell Fiber', url: '/virtuell-fiber' } },
{ link: { type: 'custom', label: 'VPS Servers', url: '/vps' } },
{ link: { type: 'custom', label: 'Contact', url: '/contact' } },
{ link: { type: 'custom', label: 'Fiber Direkt (Sweden)', url: 'https://fiberdirekt.se', newTab: true } },
],
})
console.log('')
console.log('✅ Seed complete!')
console.log(' Pages: /, /virtuell-fiber, /vps, /contact')
}
seed().catch((err) => {
console.error('❌ Seed failed:', err.message || err)
process.exit(1)
})

554
seedv3.mjs Normal file
View File

@ -0,0 +1,554 @@
#!/usr/bin/env node
/*
* LM USA Seed Script
*
* Usage:
* PAYLOAD_URL=http://localhost:3000 PAYLOAD_API_KEY=your-key node seed.mjs
*
* Creates:
* - Contact form
* - Home page /
* - Virtuell Fiber /virtuell-fiber
* - VPS Server /vps
* - Contact /contact
* - Header navigation
* - Footer
*/
const PAYLOAD_URL = process.env.PAYLOAD_URL || 'http://localhost:3000'
const API_KEY = process.env.PAYLOAD_API_KEY
if (!API_KEY) {
console.error('❌ Missing PAYLOAD_API_KEY environment variable')
process.exit(1)
}
const headers = {
'Content-Type': 'application/json',
Authorization: `users API-Key ${API_KEY}`,
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
function richText(text) {
const paragraphs = text.split('\n').filter(Boolean)
return {
root: {
type: 'root',
children: paragraphs.map((p) => ({
type: 'paragraph',
children: [
{ type: 'text', text: p, format: 0, detail: 0, mode: 'normal', style: '', version: 1 },
],
direction: 'ltr',
format: '',
indent: 0,
version: 1,
})),
direction: 'ltr',
format: '',
indent: 0,
version: 1,
},
}
}
async function apiPost(collection, data) {
const res = await fetch(`${PAYLOAD_URL}/api/${collection}`, {
method: 'POST',
headers,
body: JSON.stringify(data),
})
if (!res.ok) {
const err = await res.text()
throw new Error(`POST /api/${collection} failed (${res.status}): ${err}`)
}
return res.json()
}
async function apiGet(collection, query = '') {
const res = await fetch(`${PAYLOAD_URL}/api/${collection}${query}`, { headers })
if (!res.ok) {
const err = await res.text()
throw new Error(`GET /api/${collection} failed (${res.status}): ${err}`)
}
return res.json()
}
async function apiUpdateGlobal(slug, data) {
const res = await fetch(`${PAYLOAD_URL}/api/globals/${slug}`, {
method: 'POST',
headers,
body: JSON.stringify(data),
})
if (!res.ok) {
const err = await res.text()
throw new Error(`POST /api/globals/${slug} failed (${res.status}): ${err}`)
}
return res.json()
}
async function createPageIfNotExists(slug, data) {
const existing = await apiGet('pages', `?where[slug][equals]=${slug}&limit=1`)
if (existing.docs?.length > 0) {
console.log(` ⏭ Page "${slug}" already exists — skipping`)
return existing.docs[0]
}
const result = await apiPost('pages', data)
return result.doc
}
// ─── Main ─────────────────────────────────────────────────────────────────────
async function seed() {
console.log(`🌱 Seeding LM USA database at ${PAYLOAD_URL}...`)
// ─── 1. Contact Form ───────────────────────────────────────────────────────
console.log(' → Creating contact form...')
let contactForm
const existingForms = await apiGet('forms', '?where[title][equals]=Contact Form&limit=1')
if (existingForms.docs?.length > 0) {
console.log(' ⏭ Contact form already exists — skipping')
contactForm = existingForms.docs[0]
} else {
const result = await apiPost('forms', {
title: 'Contact Form',
fields: [
{ name: 'name', label: 'Name', blockType: 'text', required: true, width: 50 },
{ name: 'company', label: 'Company', blockType: 'text', required: false, width: 50 },
{ name: 'email', label: 'Email', blockType: 'email', required: true, width: 50 },
{ name: 'phone', label: 'Phone', blockType: 'text', required: false, width: 50 },
{ name: 'message', label: 'Message', blockType: 'textarea', required: true },
],
submitButtonLabel: 'Send Message',
confirmationType: 'message',
confirmationMessage: richText(
"Thanks for reaching out. We'll get back to you within 24 hours.",
),
})
contactForm = result.doc
}
// ─── 1b. Cart Order Form ──────────────────────────────────────────────────
console.log(' → Creating cart order form...')
const existingCartForms = await apiGet('forms', '?where[title][equals]=Cart Order&limit=1')
if (existingCartForms.docs?.length > 0) {
console.log(' ⏭ Cart order form already exists — skipping')
} else {
await apiPost('forms', {
title: 'Cart Order',
fields: [
{ name: 'company', label: 'Company', blockType: 'text', required: true, width: 50 },
{ name: 'name', label: 'Contact Name', blockType: 'text', required: true, width: 50 },
{ name: 'email', label: 'Email', blockType: 'email', required: true, width: 50 },
{ name: 'phone', label: 'Phone', blockType: 'text', required: false, width: 50 },
{ name: 'message', label: 'Notes', blockType: 'textarea', required: false },
{ name: 'items', label: 'Cart Items (JSON)', blockType: 'text', required: false },
{ name: 'totalMonthly', label: 'Monthly Total', blockType: 'text', required: false },
{ name: 'currency', label: 'Currency', blockType: 'text', required: false },
],
submitButtonLabel: 'Submit Order',
confirmationType: 'message',
confirmationMessage: richText(
"We've received your order and will contact you within 24 hours to confirm.",
),
})
}
// ─── 2. Home Page ──────────────────────────────────────────────────────────
console.log(' → Creating Home page...')
await createPageIfNotExists('index', {
title: 'Home',
slug: 'index',
_status: 'published',
publishedAt: new Date().toISOString(),
layout: [
// Hero
// theme: 'light' | 'dark'
// textColor: 'auto' | 'white' | 'dark'
{
blockType: 'lmHero',
heading: 'Infrastructure Without Borders',
subheading: 'Layer & Mesh USA',
body: 'Edge compute, virtual fiber, and managed servers — built for businesses that need sovereign, reliable infrastructure. Configure your setup and get started in minutes.',
ctaText: 'Explore Products',
ctaLink: '#products',
secondaryCtaText: 'Contact Us',
secondaryCtaLink: '/contact',
theme: 'dark',
textColor: 'auto',
},
// Link Cards
// cardStyle: 'outlined' | 'dark' | 'gray' | 'yellow'
// sectionBackground: 'white' | 'gray' | 'dark'
// title is textarea (supports \n)
{
blockType: 'lmLinkCards',
heading: 'Choose Your Infrastructure',
description: 'Two paths to managed IT — hardware at your location or virtual in our cloud.',
columns: '2',
cardStyle: 'outlined',
sectionBackground: 'dark',
anchorId: 'products',
cards: [
{
title: 'Virtuell Fiber\nEdge compute hardware deployed at your location. Plug in, connect to the mesh — instant private infrastructure.',
linkLabel: 'Explore VF Enhet',
linkUrl: '/virtuell-fiber',
},
{
title: 'VPS Servers\nCloud-hosted virtual servers on Layer & Mesh infrastructure. Full root access, instant provisioning, sovereign data.',
linkLabel: 'Configure a VPS',
linkUrl: '/vps',
},
],
},
// Statistics
// numberColor: 'gradient' | 'yellow' | 'mint' | 'dark' | 'white'
// sectionBackground: 'white' | 'dark' | 'gray'
{
blockType: 'lmStatistics',
heading: 'Built for Reliability',
sectionBackground: 'dark',
numberColor: 'gradient',
stats: [
{ number: '99.99%', label: 'Uptime SLA' },
{ number: '<5ms', label: 'Edge Latency' },
{ number: '24/7', label: 'Support' },
{ number: '100%', label: 'Swedish Sovereign' },
],
},
// Swedish CTA
// sectionBackground: 'dark' | 'elevated' | 'gray'
{
blockType: 'lmSwedishCta',
headingText: 'Söker du Fiber Direkt i Sverige?',
subText: 'Visit our Swedish site for local fiber and hosting services.',
ctaLabel: 'fiberdirekt.se',
ctaUrl: 'https://fiberdirekt.se',
sectionBackground: 'dark',
},
// Bottom CTA
// sectionBackground: 'yellow' | 'dark' | 'gray' | 'white'
// alignment: 'center' | 'left'
// size: 'small' | 'medium' | 'large'
{
blockType: 'lmCtaBanner',
heading: 'Ready to get started?',
subheading: 'Configure your infrastructure or talk to our team — no commitment required.',
ctaText: 'Contact Us',
ctaLink: '/contact',
secondaryCtaText: 'View VPS Pricing',
secondaryCtaLink: '/vps',
sectionBackground: 'yellow',
alignment: 'center',
size: 'medium',
},
],
})
// ─── 3. Virtuell Fiber Page ────────────────────────────────────────────────
console.log(' → Creating Virtuell Fiber page...')
await createPageIfNotExists('virtuell-fiber', {
title: 'Virtuell Fiber',
slug: 'virtuell-fiber',
_status: 'published',
publishedAt: new Date().toISOString(),
layout: [
// Product Detail — eyebrow as breadcrumb, specs in richText body (right col)
// sectionBackground: 'white' | 'dark'
// cartProductType: 'hardware' | 'vps' | 'service'
{
blockType: 'lmProductDetail',
eyebrow: 'Platform / Network / Virtual Fiber',
productName: 'VF Standard',
description: 'Virtual Fiber is not a VPN. It creates an encrypted, transparent Layer 2 network, built on top of any available internet connection, that behaves exactly like a private fiber link between your locations. The VF Standard unit ships pre-configured and ready. Connect it to your existing broadband, fiber, or 4G connection and it joins the Layer & Mesh mesh automatically. Your devices get full Layer 2 transport to every other point on the network — with VLAN, QinQ, and Full MTU support — without a single line of configuration. No fixed IP address needed. No IT team required on site. Works as a permanent connection or deployed temporarily. Contract terms start at one day.',
body: richText('$90 / month\nShipping additional. No setup fee. Cancel anytime.\nPerformance\nThroughput: Up to 1,000 Mbit/s\nTransport: Encrypted Layer 2 over any internet\nRedundancy: Automatic failover via 4G or secondary ISP\nHardware\nForm factor: 1U rack-mounted\nCooling: Active (fan)\nWAN: 2 × 1GbE RJ-45\nLAN: 2 × 1GbE RJ-45\nFeatures\nPlug and play — pre-configured at dispatch\nVLAN · QinQ · Full MTU\nNo public IP address required\nWorld-class Layer 2 encryption'),
ctaText: 'Order Now — $90/month',
ctaLink: '/contact',
secondaryCtaText: 'Download Datasheet',
secondaryCtaLink: '#',
enableCart: true,
cartProductName: 'VF Standard',
cartPrice: 90,
cartProductType: 'hardware',
sectionBackground: 'dark',
},
// Service Cards — "What Virtual Fiber Makes Possible"
// cardSurface: 'outlined-teal' | 'dark' | 'elevated' | 'teal' | 'light' | 'cyan'
// sectionBackground: 'dark' | 'gray' | 'white' | 'teal'
// iconSlug: 'backup' | 'colocation' | 'disaster-recovery' | 'connectivity' | 'connectivity-grid' | 'storage' | 'virtual-server'
{
blockType: 'lmServiceCards',
heading: 'What Virtual Fiber Makes Possible',
columns: '3',
cardSurface: 'outlined-teal',
sectionBackground: 'dark',
cards: [
{
eyebrow: 'Reach',
title: 'Access Resources From Anywhere',
description: 'Your team in New York reaches your Stockholm servers the same way they reach the printer down the hall — over a private Layer 2 network, without a VPN client or public IP.',
iconSlug: 'connectivity',
},
{
eyebrow: 'Simplicity',
title: 'No Configuration Required',
description: 'Units arrive ready. Connect to any internet access — fiber, broadband, 4G — and the device joins the mesh automatically. Dual transport paths for redundancy are supported out of the box.',
iconSlug: 'connectivity-grid',
},
{
eyebrow: 'Security',
title: 'World-Class Layer 2 Encryption',
description: 'All traffic is encrypted using state-of-the-art technology. Secure channels are established over public networks without any configuration — a clear replacement for traditional VPN.',
iconSlug: 'storage',
},
],
},
// USP Checklist — "VF Standard at a Glance"
// checkColor: 'dark' | 'yellow' | 'gray'
// sectionBackground: 'white' | 'gray' | 'dark'
// textColor: 'dark' | 'white'
{
blockType: 'lmUspChecklist',
heading: 'VF Standard at a Glance',
sectionBackground: 'dark',
textColor: 'white',
checkColor: 'dark',
items: [
{ text: 'Micro PC portable unit, ships pre-configured, online in minutes' },
{ text: 'Up to 1,000 Mbit/s Layer 2 encrypted transport, not a VPN, a dedicated private network' },
{ text: '2 WAN + 2 LAN ports (RJ-45, 1GbE), or failover over 4G/5G or secondary ISP' },
{ text: 'Full VLAN, QinQ, and MTU support, works with all Layer & Mesh services natively' },
{ text: 'No public IP address needed, works over any available internet connection' },
{ text: '$90/month subscription, no long-term commitment required, shipping additional' },
],
},
// FAQ — matching VF-specific questions from screenshot
// theme: 'gray' | 'light' | 'dark'
{
blockType: 'lmFaq',
heading: 'Virtual Fiber Questions',
theme: 'dark',
items: [
{
question: "What's the difference between Virtual Fiber and a VPN?",
answer: richText(
'Virtual Fiber creates a true Layer 2 network — your devices see each other as if they were on the same physical switch. A VPN tunnels Layer 3 traffic and requires client software, configuration, and a public IP. Virtual Fiber needs none of that.',
),
},
{
question: 'What internet connection does the VF Standard need?',
answer: richText(
'Any internet connection works — broadband, fiber, 4G/5G, or even satellite. The VF Standard automatically establishes encrypted transport over whatever is available. For redundancy, connect a second WAN port to a backup ISP or 4G modem.',
),
},
{
question: 'Does it need any configuration on site?',
answer: richText(
'No. Units are fully configured before dispatch. Connect the WAN port to your internet access and the LAN ports to your network — the unit joins the Layer & Mesh mesh automatically. There is nothing to configure.',
),
},
{
question: 'Can I connect multiple offices?',
answer: richText(
'Yes. Each office gets a VF Standard unit. All units on your account see each other over a single flat Layer 2 network. Add or remove locations at any time — no reconfiguration needed.',
),
},
{
question: 'What are the contract terms?',
answer: richText(
'Month-to-month with no long-term commitment. Contract terms start at one day. Give us 30 days notice and we will arrange return of the hardware. Shipping is additional.',
),
},
],
},
// CTA
{
blockType: 'lmCtaBanner',
heading: 'Need a custom deployment?',
subheading: 'Talk to our team about multi-unit rollouts, custom configurations, or enterprise pricing.',
ctaText: 'Contact Us',
ctaLink: '/contact',
sectionBackground: 'yellow',
alignment: 'left',
size: 'small',
},
],
})
// ─── 4. VPS Server Page ────────────────────────────────────────────────────
console.log(' → Creating VPS page...')
await createPageIfNotExists('vps', {
title: 'VPS Servers',
slug: 'vps',
_status: 'published',
publishedAt: new Date().toISOString(),
layout: [
// Alternate Hero
// sectionBackground: 'white' | 'dark' | 'gray'
{
blockType: 'lmAlternateHero',
heading: 'Virtual Private Servers',
description: 'Cloud-hosted servers on Layer & Mesh infrastructure. Full root access, instant provisioning, and Swedish-sovereign data. Configure your resources below — pricing updates in real-time.',
primaryCtaText: 'Configure Below',
primaryCtaLink: '#calculator',
secondaryCtaText: 'Contact Sales',
secondaryCtaLink: '/contact',
sectionBackground: 'dark',
},
// VPS Calculator
// sectionBackground: 'dark' | 'gray' | 'teal'
// currency: 'usd' | 'sek' | 'eur'
{
blockType: 'lmVpsCalculator',
heading: 'Estimate your cost',
orderCtaText: 'Add to Cart',
contactCtaText: 'Questions?',
contactCtaLink: '/contact',
currency: 'usd',
sectionBackground: 'dark',
pricingCpuPerCore: 12,
pricingRamPerGb: 5,
pricingSsdPerGb: 0.08,
pricingHddPerGb: 0.02,
pricingWindowsLicense: 25,
anchorId: 'calculator',
},
// USP Table — description is richText
// checkColor: 'dark' | 'yellow' | 'gray'
// textColor: 'dark' | 'white'
// sectionBackground: 'white' | 'gray' | 'dark'
{
blockType: 'lmUspTable',
heading: "What's Included",
sectionBackground: 'dark',
checkColor: 'dark',
textColor: 'white',
rows: [
{
title: 'Full Root Access',
description: richText('SSH, VNC, or console — full control of your server.'),
},
{
title: 'Unmetered Bandwidth',
description: richText('No traffic caps or overage fees.'),
},
{
title: 'NVMe Storage',
description: richText('Enterprise-grade SSDs for maximum I/O.'),
},
{
title: 'Instant Provisioning',
description: richText('Your server is ready in under 60 seconds.'),
},
{
title: '99.99% Uptime SLA',
description: richText('Guaranteed availability with automatic failover.'),
},
{
title: 'Swedish Sovereignty',
description: richText('Data stored and processed in Sweden.'),
},
],
},
// CTA
{
blockType: 'lmCtaBanner',
heading: 'Need dedicated hardware?',
subheading: 'Check out the VF Enhet — edge compute deployed at your location.',
ctaText: 'Learn About VF Enhet',
ctaLink: '/virtuell-fiber',
secondaryCtaText: 'Contact Sales',
secondaryCtaLink: '/contact',
sectionBackground: 'yellow',
alignment: 'left',
size: 'small',
},
],
})
// ─── 5. Contact Page ───────────────────────────────────────────────────────
console.log(' → Creating Contact page...')
await createPageIfNotExists('contact', {
title: 'Contact',
slug: 'contact',
_status: 'published',
publishedAt: new Date().toISOString(),
layout: [
// Contact Form
// sectionBackground: 'white' | 'gray' | 'dark' | 'navyGradient'
// layout: 'standard' | 'withImage' | 'card'
{
blockType: 'lmContactForm',
heading: 'Get in Touch',
description: 'Tell us about your infrastructure needs — our team will get back to you within 24 hours.',
form: contactForm.id,
submitText: 'Send Message',
sectionBackground: 'dark',
layout: 'card',
},
// FAQ
{
blockType: 'lmFaq',
heading: 'Common Questions',
theme: 'dark',
items: [
{
question: 'Where are you located?',
answer: richText(
'Layer & Mesh operates infrastructure in Sweden with support available globally. Our US operations serve North American customers.',
),
},
{
question: 'How quickly can I get started?',
answer: richText(
'VPS servers are provisioned instantly. VF Enhet hardware typically ships within 57 business days.',
),
},
{
question: 'Do you offer enterprise pricing?',
answer: richText(
'Yes. Contact us for volume pricing, custom SLAs, and dedicated account management.',
),
},
],
},
],
})
// ─── 6. Header Navigation ─────────────────────────────────────────────────
// Header navItems are flat: { label, type, url } directly on each item
console.log(' → Setting up header...')
await apiUpdateGlobal('header', {
logoLink: { type: 'custom', url: '/' },
navItems: [
{ label: 'Virtuell Fiber', type: 'custom', url: '/virtuell-fiber' },
{ label: 'VPS Servers', type: 'custom', url: '/vps' },
{ label: 'Contact', type: 'custom', url: '/contact' },
],
})
// ─── 7. Footer ────────────────────────────────────────────────────────────
// Footer navItems use link() field: { link: { type, label, url } }
console.log(' → Setting up footer...')
await apiUpdateGlobal('footer', {
logoLink: { type: 'custom', url: '/' },
navItems: [
{ link: { type: 'custom', label: 'Virtuell Fiber', url: '/virtuell-fiber' } },
{ link: { type: 'custom', label: 'VPS Servers', url: '/vps' } },
{ link: { type: 'custom', label: 'Contact', url: '/contact' } },
{ link: { type: 'custom', label: 'Fiber Direkt (Sweden)', url: 'https://fiberdirekt.se', newTab: true } },
],
})
console.log('')
console.log('✅ Seed complete!')
console.log(' Pages: /, /virtuell-fiber, /vps, /contact')
}
seed().catch((err) => {
console.error('❌ Seed failed:', err.message || err)
process.exit(1)
})

BIN
src.zip Normal file

Binary file not shown.

View File

@ -4,7 +4,7 @@ import React from 'react'
import type { Footer, Media, Page } from '@/payload-types'
import { CMSLink } from '@/components/Link'
import { Logo } from '@/components/Logo/Logo'
import { FDImage } from '@/components/FDImage'
import { LMImage } from '@/components/LMImage'
import { SocialIconsRow } from '@/components/SocialIcons'
import type { SocialLinkData } from '@/components/SocialIcons'
@ -49,7 +49,7 @@ export async function Footer() {
const logoHref = resolveLogoHref(footerData?.logoLink)
const bottomLeft = (
footerData?.bottomLeftText || '© {year} Fiber Direkt. Alla rättigheter förbehållna.'
footerData?.bottomLeftText || '© {year} Layer & Mesh. All rights reserved.'
).replace('{year}', new Date().getFullYear().toString())
return (
@ -68,7 +68,7 @@ export async function Footer() {
const media = mark.image as Media | null | undefined
if (!media) return null
const imgEl = (
<FDImage
<LMImage
media={media}
size="thumbnail"
className="h-16 w-auto object-contain"
@ -131,7 +131,7 @@ export async function Footer() {
const media = mark.image as Media | null | undefined
if (!media) return null
const imgEl = (
<FDImage
<LMImage
media={media}
size="thumbnail"
className="h-14 w-auto object-contain"

View File

@ -241,7 +241,7 @@ export const Footer: GlobalConfig = {
name: 'bottomLeftText',
type: 'text',
label: 'Copyrighttext (nedre vänster)',
defaultValue: '© {year} Fiber Direkt. Alla rättigheter förbehållna.',
defaultValue: '© {year} Layer & Mesh. Alla rättigheter förbehållna.',
admin: {
description: 'Använd {year} för aktuellt årtal',
},

View File

@ -56,7 +56,7 @@ export const HeaderClient: React.FC<HeaderClientProps> = ({ data, socialLinks =
return (
<header
className="relative z-20 w-full bg-white dark:bg-fd-navy border-b border-transparent dark:border-white/10"
className="relative z-20 w-full bg-lm-dark border-b border-white/8"
{...(theme ? { 'data-theme': theme } : {})}
>
{/*
@ -67,7 +67,7 @@ export const HeaderClient: React.FC<HeaderClientProps> = ({ data, socialLinks =
*/}
<div className="w-full px-6 md:px-8 lg:px-12 py-5 flex justify-between items-center">
<Link href={logoHref}>
<Logo loading="eager" priority="high" variant={isDark ? 'white' : 'blue'} className="max-w-[80px] md:max-w-[100px]" />
<Logo loading="eager" priority="high" variant="white" className="max-w-[300px] md:max-w-[440px] lg:max-w-[560px]" />
</Link>
<HeaderNav data={data} socialLinks={socialLinks} />
</div>

View File

@ -25,14 +25,6 @@ function resolveHref(item: {
return item.url || '#'
}
const SwedishFlag = () => (
<svg width="28" height="20" viewBox="0 0 28 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" className="rounded-[3px]">
<rect width="28" height="20" fill="#006AA7" />
<rect x="8" width="4" height="20" fill="#FECC02" />
<rect y="8" width="28" height="4" fill="#FECC02" />
</svg>
)
function groupChildren(children: NavChild[]): { heading: string | null; links: NavChild[] }[] {
const main: NavChild[] = []
const groups: Record<string, NavChild[]> = {}
@ -59,43 +51,25 @@ function groupChildren(children: NavChild[]): { heading: string | null; links: N
function useFocusTrap(containerRef: React.RefObject<HTMLElement | null>, active: boolean) {
useEffect(() => {
if (!active || !containerRef.current) return
const container = containerRef.current
const focusableSelector =
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
const getFocusableElements = () =>
Array.from(container.querySelectorAll<HTMLElement>(focusableSelector))
const elements = getFocusableElements()
if (elements.length > 0) {
setTimeout(() => elements[0]?.focus(), 100)
}
const handleKeyDown = (e: KeyboardEvent) => {
const sel = 'a[href], button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
const getEls = () => Array.from(container.querySelectorAll<HTMLElement>(sel))
const els = getEls()
if (els.length > 0) setTimeout(() => els[0]?.focus(), 100)
const handleKey = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return
const focusable = getFocusableElements()
if (focusable.length === 0) return
const focusable = getEls()
if (!focusable.length) return
const first = focusable[0]
const last = focusable[focusable.length - 1]
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault()
last.focus()
}
if (document.activeElement === first) { e.preventDefault(); last.focus() }
} else {
if (document.activeElement === last) {
e.preventDefault()
first.focus()
}
if (document.activeElement === last) { e.preventDefault(); first.focus() }
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
document.addEventListener('keydown', handleKey)
return () => document.removeEventListener('keydown', handleKey)
}, [active, containerRef])
}
@ -105,7 +79,6 @@ function isActiveSection(item: NavItem, pathname: string | null): boolean {
if (href === '/') return pathname === '/'
return pathname?.startsWith(href) || false
}
return item.children.some((child) => {
const childHref = resolveHref(child)
if (childHref === '/' || childHref === '#') return false
@ -119,13 +92,12 @@ const MegaMenuPanel: React.FC<{
panelRef: React.RefObject<HTMLDivElement | null>
onClose: () => void
}> = ({ item, headerRef, panelRef, onClose }) => {
const [top, setTop] = useState(72)
const [top, setTop] = useState(64)
useEffect(() => {
const update = () => {
if (headerRef.current) {
const rect = headerRef.current.getBoundingClientRect()
setTop(rect.bottom)
setTop(headerRef.current.getBoundingClientRect().bottom)
}
}
update()
@ -141,35 +113,33 @@ const MegaMenuPanel: React.FC<{
return (
<>
{/* Blur backdrop */}
{/* Backdrop */}
<div
className="fixed inset-0 z-30"
style={{ top, backdropFilter: 'blur(6px)', WebkitBackdropFilter: 'blur(6px)', backgroundColor: 'rgba(14, 35, 56, 0.08)' }}
style={{ top, backdropFilter: 'blur(4px)', backgroundColor: 'rgba(0,0,0,0.5)' }}
onClick={onClose}
/>
{/* Panel */}
{/* Panel — dark, sharp corners */}
<div
ref={panelRef}
className="fixed left-0 right-0 z-40 bg-white dark:bg-fd-navy border-b border-fd-navy/10 dark:border-white/10"
className="fixed left-0 right-0 z-40 bg-lm-dark border-b border-white/10"
style={{ top }}
>
<div className="max-w-[1200px] mx-auto px-6 md:px-8 py-10 md:py-14">
<div className="grid grid-cols-1 md:grid-cols-[200px_1fr] gap-8 md:gap-12">
{/* Left: category title */}
<div className="max-w-[1200px] mx-auto px-6 md:px-8 py-8 md:py-12">
<div className="grid grid-cols-1 md:grid-cols-[180px_1fr] gap-8 md:gap-12">
<div>
<h2 className="font-joey-heavy text-fd-h1 text-fd-navy dark:text-fd-yellow leading-tight">
<h2 className="text-white font-bold" style={{ fontSize: '1.375rem' }}>
{item.label}
</h2>
</div>
{/* Right: columns */}
<div className={`grid gap-6 ${columns.length === 1 ? 'grid-cols-1' : columns.length === 2 ? 'grid-cols-2' : 'grid-cols-3'}`}>
<div className={`grid gap-6 ${
columns.length === 1 ? 'grid-cols-1' :
columns.length === 2 ? 'grid-cols-2' : 'grid-cols-3'
}`}>
{columns.map((col, ci) => (
<div key={ci}>
{col.heading && (
<p className="font-joey-medium text-fd-small text-fd-navy/40 dark:text-white/40 uppercase tracking-widest mb-2">
<p className="text-white/40 text-xs font-semibold uppercase tracking-widest mb-3">
{col.heading}
</p>
)}
@ -179,10 +149,10 @@ const MegaMenuPanel: React.FC<{
<Link
href={resolveHref(link)}
onClick={onClose}
className={`block py-1 transition-colors duration-150 ${
className={`block py-1.5 transition-colors duration-150 ${
col.heading
? 'font-joey-medium text-lg text-fd-navy/80 dark:text-white/75 hover:text-fd-navy dark:hover:text-fd-yellow'
: 'font-joey-bold text-fd-h3 text-fd-navy dark:text-white hover:text-fd-yellow dark:hover:text-fd-yellow'
? 'text-white/70 hover:text-lm-teal text-base'
: 'text-white font-semibold text-lg hover:text-lm-teal'
}`}
>
{link.label}
@ -193,7 +163,6 @@ const MegaMenuPanel: React.FC<{
</div>
))}
</div>
</div>
</div>
</div>
@ -218,9 +187,7 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
useFocusTrap(mobileMenuRef, mobileOpen)
useEffect(() => {
if (navRef.current) {
headerRef.current = navRef.current.closest('header')
}
if (navRef.current) headerRef.current = navRef.current.closest('header')
}, [])
useEffect(() => {
@ -239,9 +206,7 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
if (
navRef.current && !navRef.current.contains(e.target as Node) &&
megaMenuRef.current && !megaMenuRef.current.contains(e.target as Node)
) {
setOpenDropdown(null)
}
) setOpenDropdown(null)
}
document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
@ -249,26 +214,19 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setOpenDropdown(null)
setMobileOpen(false)
}
if (e.key === 'Escape') { setOpenDropdown(null); setMobileOpen(false) }
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [])
const closeMobile = () => {
setMobileOpen(false)
setMobileOpenSection(null)
}
const activeItem = navItems.find((item) => item.label === openDropdown)
const closeMobile = () => { setMobileOpen(false); setMobileOpenSection(null) }
const activeItem = navItems.find(item => item.label === openDropdown)
return (
<>
{/* ── Desktop nav — shows at lg (1024px+) ── */}
<nav ref={navRef} className="hidden lg:flex gap-6 items-center">
{/* ── Desktop nav ── */}
<nav ref={navRef} className="hidden lg:flex gap-7 items-center">
{navItems.map((item, i) => {
const hasChildren = item.children && item.children.length > 0
const isOpen = openDropdown === item.label
@ -279,29 +237,22 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
<div key={i} className="relative">
<button
onClick={() => setOpenDropdown(isOpen ? null : item.label ?? null)}
className={`flex items-center gap-1 font-joey-bold transition-colors text-base ${
isOpen
? 'text-fd-yellow'
: isActive
? 'text-fd-yellow'
: 'text-fd-navy dark:text-white hover:text-fd-yellow dark:hover:text-fd-yellow'
className={`flex items-center gap-1 font-medium text-sm transition-colors ${
isOpen || isActive ? 'text-lm-teal' : 'text-white hover:text-lm-teal'
}`}
aria-haspopup="true"
aria-expanded={isOpen}
>
{item.label}
<ChevronDownIcon
className={`w-4 h-4 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
className={`w-3.5 h-3.5 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
/>
{isActive && !isOpen && (
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-fd-yellow" />
)}
</button>
{/* Small card dropdown */}
{/* Small dropdown — dark, sharp */}
{!isMega && (
<div
className={`absolute top-full left-0 mt-3 min-w-[220px] bg-white dark:bg-fd-navy rounded-[16px] shadow-lg border border-fd-navy/10 dark:border-white/10 py-2 z-50 transition-all duration-200 origin-top ${
className={`absolute top-full left-0 mt-2 min-w-[200px] bg-lm-dark-3 border border-white/10 py-1 z-50 transition-all duration-150 origin-top shadow-xl ${
isOpen
? 'opacity-100 scale-y-100 pointer-events-auto'
: 'opacity-0 scale-y-95 pointer-events-none'
@ -312,7 +263,7 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
key={j}
href={resolveHref(child)}
onClick={() => setOpenDropdown(null)}
className="block px-5 py-2.5 font-joey text-fd-navy dark:text-white hover:bg-fd-yellow/20 dark:hover:bg-white/10 transition-colors text-sm"
className="block px-4 py-2.5 text-white/80 hover:text-white hover:bg-white/5 transition-colors text-sm"
>
{child.label}
</Link>
@ -324,21 +275,17 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
<Link
key={i}
href={resolveHref(item)}
className={`relative font-joey-bold transition-colors text-base ${
isActive
? 'text-fd-yellow'
: 'text-fd-navy dark:text-white hover:text-fd-yellow dark:hover:text-fd-yellow'
className={`relative font-medium text-sm transition-colors ${
isActive ? 'text-lm-teal' : 'text-white hover:text-lm-teal'
}`}
>
{item.label}
{isActive && (
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-fd-yellow" />
<span className="absolute -bottom-1 left-0 right-0 h-px bg-lm-teal" />
)}
</Link>
)
})}
<SwedishFlag />
</nav>
{/* ── Mega menu panel ── */}
@ -351,74 +298,70 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
/>
)}
{/* ── Hamburger — shows below lg (mobile + tablet) ── */}
{/* ── Hamburger ── */}
<button
className="lg:hidden text-fd-navy dark:text-white p-2"
className="lg:hidden text-white p-2 hover:text-lm-teal transition-colors"
onClick={() => setMobileOpen(!mobileOpen)}
aria-label={mobileOpen ? 'Stäng meny' : 'Öppna meny'}
>
{mobileOpen ? <XIcon className="w-6 h-6" /> : <MenuIcon className="w-6 h-6" />}
</button>
{/* ── Tablet backdrop — blur overlay left of panel on md+ ── */}
{/* ── Mobile backdrop ── */}
<div
className={`fixed inset-0 z-40 lg:hidden transition-opacity duration-300 ${
mobileOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'
}`}
style={{ backdropFilter: 'blur(4px)', WebkitBackdropFilter: 'blur(4px)', backgroundColor: 'rgba(14, 35, 56, 0.45)' }}
style={{ backdropFilter: 'blur(4px)', backgroundColor: 'rgba(0,0,0,0.6)' }}
onClick={closeMobile}
aria-hidden="true"
/>
{/* Mobile/tablet slide-in panel
Mobile (<768px): full-screen overlay
Tablet (768-1023px): right-side panel, 420px wide, with backdrop above
*/}
{/* ── Mobile slide-in — dark ── */}
<div
ref={mobileMenuRef}
className={`fixed inset-y-0 right-0 z-50 flex flex-col bg-fd-navy lg:hidden
w-full md:w-[420px]
className={`fixed inset-y-0 right-0 z-50 flex flex-col bg-lm-dark lg:hidden
w-full md:w-[400px]
transition-transform duration-300
${mobileOpen ? 'translate-x-0 md:shadow-[-24px_0_60px_rgba(0,0,0,0.25)]' : 'translate-x-full'}
${mobileOpen ? 'translate-x-0 shadow-[-24px_0_60px_rgba(0,0,0,0.4)]' : 'translate-x-full'}
`}
role="dialog"
aria-modal="true"
aria-label="Navigeringsmeny"
>
{/* Header bar */}
<div className="flex items-center justify-between px-6 md:px-8 py-5 border-b border-white/10">
<Link href="/" onClick={closeMobile}>
<span className="font-joey-heavy text-fd-yellow text-xl">Fiber Direkt</span>
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
<Link href="/" onClick={closeMobile} className="text-white font-bold text-lg">
Layer &amp; Mesh
</Link>
<button
onClick={closeMobile}
className="text-white p-2 hover:text-fd-yellow transition-colors"
className="text-white/60 p-2 hover:text-white transition-colors"
aria-label="Stäng meny"
>
<XIcon className="w-6 h-6" />
<XIcon className="w-5 h-5" />
</button>
</div>
{/* Nav links */}
<nav className="flex-1 overflow-y-auto px-6 md:px-8 py-6 flex flex-col">
{/* Links */}
<nav className="flex-1 overflow-y-auto px-6 py-4 flex flex-col">
{navItems.map((item, i) => {
const hasChildren = item.children && item.children.length > 0
const isExpanded = mobileOpenSection === item.label
return hasChildren ? (
<div key={i} className="border-b border-white/10">
<div key={i} className="border-b border-white/8">
<button
onClick={() => setMobileOpenSection(isExpanded ? null : item.label ?? null)}
className="w-full flex justify-between items-center py-4 md:py-5 text-white font-joey-medium text-xl md:text-2xl hover:text-fd-yellow transition-colors"
className="w-full flex justify-between items-center py-3.5 text-white font-medium text-base hover:text-lm-teal transition-colors"
>
{item.label}
<ChevronRightIcon
className={`w-5 h-5 transition-transform duration-200 ${isExpanded ? 'rotate-90' : ''}`}
className={`w-4 h-4 transition-transform duration-200 ${isExpanded ? 'rotate-90' : ''}`}
/>
</button>
<div
className={`overflow-hidden transition-all duration-300 ${
isExpanded ? 'max-h-[500px] pb-3' : 'max-h-0'
isExpanded ? 'max-h-[400px] pb-2' : 'max-h-0'
}`}
>
{item.children!.map((child, j) => (
@ -426,7 +369,7 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
key={j}
href={resolveHref(child)}
onClick={closeMobile}
className="block pl-4 py-3 md:py-3.5 text-fd-yellow font-joey text-lg md:text-xl border-b border-white/5 hover:text-white transition-colors"
className="block pl-4 py-2.5 text-lm-teal text-sm border-b border-white/5 hover:text-white transition-colors"
>
{child.label}
</Link>
@ -438,7 +381,7 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
key={i}
href={resolveHref(item)}
onClick={closeMobile}
className="py-4 md:py-5 text-white font-joey-medium text-xl md:text-2xl border-b border-white/10 hover:text-fd-yellow transition-colors block"
className="py-3.5 text-white font-medium text-base border-b border-white/8 hover:text-lm-teal transition-colors block"
>
{item.label}
</Link>
@ -446,14 +389,14 @@ export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkDat
})}
</nav>
{/* Footer: Swedish flag left, social icons right */}
<div className="px-6 md:px-8 py-5 border-t border-white/10 flex items-center justify-between">
<SwedishFlag />
{/* Footer */}
<div className="px-6 py-4 border-t border-white/10 flex items-center justify-between">
<span className="text-white/40 text-xs tracking-widest uppercase">Layer &amp; Mesh</span>
{socialLinks.length > 0 && (
<SocialIconsRow
links={socialLinks}
linkClassName="text-white/50 hover:text-white"
iconSize="w-6 h-6"
linkClassName="text-white/40 hover:text-white"
iconSize="w-5 h-5"
/>
)}
</div>

View File

@ -4,7 +4,6 @@ import configPromise from '@payload-config'
import { getPayload, type RequiredDataFromCollectionSlug } from 'payload'
import { draftMode } from 'next/headers'
import React, { cache } from 'react'
import { homeStatic } from '@/endpoints/seed/home-static'
import { RenderBlocks } from '@/blocks/RenderBlocks'
import type { Page } from '@/payload-types'
import { generateMeta } from '@/utilities/generateMeta'
@ -12,6 +11,7 @@ import PageClient from './page.client'
import { LivePreviewListener } from '@/components/LivePreviewListener'
export const revalidate = 60
export const dynamicParams = true
import { notFound } from 'next/navigation'
type Args = {
@ -35,7 +35,7 @@ export default async function Page({ params: paramsPromise }: Args) {
// Remove this code once your website is seeded
if (!page && slug === 'home') {
page = homeStatic
return notFound()
}
if (!page) {

View File

@ -1,87 +0,0 @@
import type { Metadata } from 'next'
import { PayloadRedirects } from '@/components/PayloadRedirects'
import configPromise from '@payload-config'
import { getPayload, type RequiredDataFromCollectionSlug } from 'payload'
import { draftMode } from 'next/headers'
import React, { cache } from 'react'
import { RenderBlocks } from '@/blocks/RenderBlocks'
import type { Page } from '@/payload-types'
import { generateMeta } from '@/utilities/generateMeta'
import PageClient from '../../[slug]/page.client'
import { LivePreviewListener } from '@/components/LivePreviewListener'
export const revalidate = 60
export const dynamicParams = true
type Args = {
params: Promise<{
slug?: string
}>
}
export default async function PageEN({ params: paramsPromise }: Args) {
const { isEnabled: draft } = await draftMode()
const { slug = 'home' } = await paramsPromise
const decodedSlug = decodeURIComponent(slug)
const url = '/en/' + decodedSlug
const page = await queryPageBySlugEN({ slug: decodedSlug })
if (!page) {
return <PayloadRedirects url={url} />
}
const { layout } = page as Page
return (
<article>
<PageClient />
<PayloadRedirects disableNotFound url={url} />
{draft && <LivePreviewListener />}
<RenderBlocks blocks={layout} />
</article>
)
}
export async function generateMetadata({ params: paramsPromise }: Args): Promise<Metadata> {
const { slug = 'home' } = await paramsPromise
const decodedSlug = decodeURIComponent(slug)
const page = await queryPageBySlugEN({ slug: decodedSlug })
return generateMeta({ doc: page })
}
export async function generateStaticParams() {
const payload = await getPayload({ config: configPromise })
const pages = await payload.find({
collection: 'pages',
draft: false,
limit: 1000,
pagination: false,
overrideAccess: false,
select: { slug: true },
locale: 'en',
})
return pages.docs?.map(({ slug }) => ({ slug })) ?? []
}
const queryPageBySlugEN = cache(async ({ slug }: { slug: string }) => {
const { isEnabled: draft } = await draftMode()
const payload = await getPayload({ config: configPromise })
const result = await payload.find({
collection: 'pages',
draft,
limit: 1,
pagination: false,
overrideAccess: draft,
locale: 'en', // ← only difference from the sv route
fallbackLocale: 'sv', // ← if en field is empty, fall back to Swedish
where: {
slug: {
equals: slug,
},
},
})
return result.docs?.[0] || null
})

View File

@ -1,5 +0,0 @@
import { redirect } from 'next/navigation'
export default function EnHomePage() {
redirect('/en/home')
}

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@ import { mergeOpenGraph } from '@/utilities/mergeOpenGraph'
import { draftMode } from 'next/headers'
import { getCachedGlobal } from '@/utilities/getGlobals'
import { CookieConsent } from '@/components/CookieConsent'
import { CartDrawer, CartButton } from '@/components/CartDrawer'
import { MatomoScript } from '@/components/MatomoScript'
import { HeadInjection, FooterInjection } from '@/components/HeadInjection'
import { PopupAnnouncementComponent } from '@/globals/PopupAnnouncement/Component'
@ -44,7 +45,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
return (
<html
lang="sv"
lang="en"
suppressHydrationWarning
style={{ colorScheme: 'light dark' }}
>
@ -72,6 +73,8 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<Header />
{children}
<Footer />
<CartDrawer />
<CartButton />
<BackToTop />
<PopupAnnouncementComponent {...popupData} />
{cookieEnabled && (
@ -97,7 +100,7 @@ export const metadata: Metadata = {
openGraph: mergeOpenGraph(),
twitter: {
card: 'summary_large_image',
creator: '@fiberdirekt',
creator: '@layermesh',
},
}

View File

@ -1,31 +1,3 @@
import { createLocalReq, getPayload } from 'payload'
import { seed } from '@/endpoints/seed'
import config from '@payload-config'
import { headers } from 'next/headers'
export const maxDuration = 60 // This function can run for a maximum of 60 seconds
export async function POST(): Promise<Response> {
const payload = await getPayload({ config })
const requestHeaders = await headers()
// Authenticate by passing request headers
const { user } = await payload.auth({ headers: requestHeaders })
if (!user) {
return new Response('Action forbidden.', { status: 403 })
}
try {
// Create a Payload request object to pass to the Local API for transactions
// At this point you should pass in a user, locale, and any other context you need for the Local API
const payloadReq = await createLocalReq({ user }, payload)
await seed({ payload, req: payloadReq })
return Response.json({ success: true })
} catch (e) {
payload.logger.error({ err: e, message: 'Error seeding data' })
return new Response('Error seeding data.', { status: 500 })
}
return new Response('Seed endpoint not configured.', { status: 404 })
}

View File

@ -1,9 +1,9 @@
import React from 'react'
import type { Metadata } from 'next'
import { FDButton } from '@/components/FDButton'
import { LMButton } from '@/components/LMButton'
export const metadata: Metadata = {
title: '404 Sidan hittades inte | Fiber Direkt',
title: '404 Page not found | Layer & Mesh',
description: 'Sidan du letar efter verkar ha kopplats bort.',
}
@ -42,9 +42,9 @@ export default function NotFound() {
{/* CTA */}
<div className="mt-2">
<FDButton href="/" variant="primary" onDark={false}>
<LMButton href="/" variant="primary" onDark={false}>
Tillbaka till startsidan
</FDButton>
</LMButton>
</div>
</div>

View File

@ -3,8 +3,8 @@ import { notFound } from 'next/navigation'
import { getPayload } from 'payload'
import config from '@payload-config'
import type { Post, Media, Category } from '@/payload-types'
import { FDImage } from '@/components/FDImage'
import { FDButton } from '@/components/FDButton'
import { LMImage } from '@/components/LMImage'
import { LMButton } from '@/components/LMButton'
import { generateMeta } from '@/utilities/generateMeta'
import { formatDateTime } from '@/utilities/formatDateTime'
import { getCachedGlobal } from '@/utilities/getGlobals'
@ -88,7 +88,7 @@ export default async function PostPage({ params }: Args) {
{heroImage?.url && (
<figure className="mb-6">
<div className="rounded-[20px] overflow-hidden">
<FDImage
<LMImage
media={heroImage}
size="large"
className="w-full h-auto object-cover"
@ -126,12 +126,12 @@ export default async function PostPage({ params }: Args) {
)}
{cta?.enabled && cta.text && (
<FDButton
<LMButton
href={cta.link || '/kontakt'}
variant={cta.variant === 'outline' ? 'outline' : 'primary'}
>
{cta.text}
</FDButton>
</LMButton>
)}
</div>
@ -181,7 +181,7 @@ export default async function PostPage({ params }: Args) {
<div className="relative aspect-[4/3] rounded-[16px] overflow-hidden bg-fd-navy/5 dark:bg-white/5">
{relHero?.url ? (
<FDImage
<LMImage
media={relHero}
size="medium"
fill

View File

@ -3,7 +3,7 @@ import configPromise from '@payload-config'
import { getPayload } from 'payload'
import React from 'react'
import type { Post, Media, Category } from '@/payload-types'
import { FDImage } from '@/components/FDImage'
import { LMImage } from '@/components/LMImage'
import { formatDateTime } from '@/utilities/formatDateTime'
import { Pagination } from '@/components/Pagination'
import { getCachedGlobal } from '@/utilities/getGlobals'
@ -63,7 +63,7 @@ export default async function Page() {
},
})
const heading = settings?.listingHeading || 'Nyheter'
const heading = settings?.listingHeading || 'News'
const description = settings?.listingDescription || null
const bgKey = settings?.listingBackground || 'white'
const theme = listingBgMap[bgKey] || listingBgMap.white
@ -126,7 +126,7 @@ export default async function Page() {
<div className="relative aspect-[4/3] rounded-[20px] overflow-hidden bg-fd-navy/5 dark:bg-white/5">
{heroImage?.url ? (
<FDImage
<LMImage
media={heroImage}
size="medium"
fill
@ -160,9 +160,9 @@ export default async function Page() {
export async function generateMetadata(): Promise<Metadata> {
const settings = (await getCachedGlobal('post-settings' as any, 1)()) as any
const title = settings?.metaTitle || `${settings?.listingHeading || 'Nyheter'} | Fiber Direkt`
const title = settings?.metaTitle || `${settings?.listingHeading || 'News'} | Layer & Mesh`
const description =
settings?.metaDescription || settings?.listingDescription || 'Senaste nytt från Fiber Direkt'
settings?.metaDescription || settings?.listingDescription || 'Senaste nytt från Layer & Mesh'
return { title, description }
}

View File

@ -4,7 +4,7 @@ import { getPayload } from 'payload'
import React from 'react'
import { notFound } from 'next/navigation'
import type { Post, Media, Category } from '@/payload-types'
import { FDImage } from '@/components/FDImage'
import { LMImage } from '@/components/LMImage'
import { formatDateTime } from '@/utilities/formatDateTime'
import { Pagination } from '@/components/Pagination'
import { getCachedGlobal } from '@/utilities/getGlobals'
@ -76,7 +76,7 @@ export default async function Page({ params: paramsPromise }: Args) {
if (sanitizedPageNumber > posts.totalPages) notFound()
const heading = settings?.listingHeading || 'Nyheter'
const heading = settings?.listingHeading || 'News'
const description = settings?.listingDescription || null
const bgKey = settings?.listingBackground || 'white'
const theme = listingBgMap[bgKey] || listingBgMap.white
@ -139,7 +139,7 @@ export default async function Page({ params: paramsPromise }: Args) {
<div className="relative aspect-[4/3] rounded-[20px] overflow-hidden bg-fd-navy/5 dark:bg-white/5">
{heroImage?.url ? (
<FDImage
<LMImage
media={heroImage}
size="medium"
fill
@ -175,7 +175,7 @@ export async function generateMetadata({ params: paramsPromise }: Args): Promise
const settings = (await getCachedGlobal('post-settings' as any, 1)()) as any
const title = settings?.metaTitle
|| `${settings?.listingHeading || 'Nyheter'} — Sida ${pageNumber} | Fiber Direkt`
|| `${settings?.listingHeading || 'News'} — Sida ${pageNumber} | Layer & Mesh`
return { title }
}

View File

@ -3,7 +3,7 @@ import configPromise from '@payload-config'
import { getPayload } from 'payload'
import React from 'react'
import type { Post, Media } from '@/payload-types'
import { FDImage } from '@/components/FDImage'
import { LMImage } from '@/components/LMImage'
import { formatDateTime } from '@/utilities/formatDateTime'
import { Pagination } from '@/components/Pagination'
@ -32,7 +32,7 @@ export default async function Page({ params }: { params: Promise<{ pageNumber: s
<div className="min-h-screen bg-white">
<div className="max-w-[1200px] mx-auto px-6 md:px-8 pt-12 md:pt-16 pb-16 md:pb-24">
<h1 className="font-joey-heavy text-4xl md:text-5xl lg:text-[3.25rem] text-fd-navy mb-10 md:mb-14">
Nyheter!
News!
</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-12">
{posts.docs.map((post) => {
@ -50,7 +50,7 @@ export default async function Page({ params }: { params: Promise<{ pageNumber: s
)}
<div className="relative aspect-[4/3] rounded-[20px] overflow-hidden bg-fd-navy/5">
{heroImage?.url ? (
<FDImage
<LMImage
media={heroImage}
size="medium"
fill
@ -80,6 +80,6 @@ export default async function Page({ params }: { params: Promise<{ pageNumber: s
export function generateMetadata(): Metadata {
return {
title: 'Nyheter | Fiber Direkt',
title: 'News | Layer & Mesh',
}
}

View File

@ -0,0 +1,77 @@
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
/**
* POST /api/checkout
*
* Creates a Stripe Checkout Session and returns the redirect URL.
*
* Body: { items: Array<{ stripePriceId: string; quantity: number; name: string }> }
*
* Usage from client:
* const { url } = await fetch('/api/checkout', {
* method: 'POST',
* headers: { 'Content-Type': 'application/json' },
* body: JSON.stringify({ items }),
* }).then(r => r.json())
* router.push(url)
*/
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2026-02-25.clover',
})
export async function POST(req: Request) {
try {
const { items } = await req.json()
if (!items || items.length === 0) {
return NextResponse.json({ error: 'No items in cart' }, { status: 400 })
}
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'payment',
currency: 'usd',
// Collect shipping address (US only)
shipping_address_collection: {
allowed_countries: ['US'],
},
// Shipping rate options — configure these in Stripe Dashboard
// shipping_options: [
// { shipping_rate: 'shr_standard' },
// { shipping_rate: 'shr_expedited' },
// ],
line_items: items.map((item: {
stripePriceId: string
quantity: number
}) => ({
price: item.stripePriceId,
quantity: item.quantity,
})),
// Collect customer email for order record
customer_email: undefined, // Or pass if known
// Redirect URLs
success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/shop/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/shop?cancelled=true`,
// Store metadata for webhook
metadata: {
source: 'layer-mesh-usa-shop',
},
})
return NextResponse.json({ url: session.url })
} catch (error) {
console.error('[Checkout] Error creating session:', error)
return NextResponse.json(
{ error: 'Failed to create checkout session' },
{ status: 500 },
)
}
}

View File

@ -0,0 +1,151 @@
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
import { headers } from 'next/headers'
/**
* POST /api/stripe-webhook
*
* Handles Stripe webhook events. Currently processes:
* - checkout.session.completed creates Order in Payload
* - payment_intent.payment_failed logs / alerts
*
* Setup:
* 1. Add webhook endpoint in Stripe Dashboard pointing to:
* https://your-domain.com/api/stripe-webhook
* 2. Select events: checkout.session.completed, payment_intent.payment_failed
* 3. Copy signing secret to STRIPE_WEBHOOK_SECRET env var
*/
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2026-02-25.clover',
})
export async function POST(req: Request) {
const body = await req.text()
const headersList = await headers()
const signature = headersList.get('stripe-signature')
if (!signature) {
return NextResponse.json({ error: 'No signature' }, { status: 400 })
}
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!,
)
} catch (err) {
console.error('[Webhook] Signature verification failed:', err)
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
}
// ─── Handle Events ──────────────────────────────────────────────────────────
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session
try {
await createOrderFromSession(session, event)
} catch (err) {
console.error('[Webhook] Failed to create order:', err)
// Return 200 so Stripe doesn't retry — log and investigate manually
}
break
}
case 'payment_intent.payment_failed': {
const paymentIntent = event.data.object as Stripe.PaymentIntent
console.warn('[Webhook] Payment failed:', paymentIntent.id)
// TODO: Send customer notification email
break
}
default:
// Ignore other events
break
}
return NextResponse.json({ received: true })
}
// ─── Helper: Create Order in Payload ─────────────────────────────────────────
async function createOrderFromSession(
session: Stripe.Checkout.Session,
event: Stripe.Event,
) {
// Fetch full session with line items
const fullSession = await stripe.checkout.sessions.retrieve(session.id, {
expand: ['line_items', 'line_items.data.price.product'],
})
const lineItems = fullSession.line_items?.data ?? []
const shippingAddress = session.customer_details?.address
// Generate human-readable order number from session suffix
const sessionSuffix = session.id.split('_').pop()?.slice(-8).toUpperCase()
const orderNumber = `LM-${sessionSuffix}`
// Map Stripe line items to Payload order items
const items = lineItems.map((item) => {
const product = item.price?.product as Stripe.Product | undefined
return {
productName: product?.name ?? item.description ?? 'Unknown',
quantity: item.quantity ?? 1,
unitPriceUsd: (item.amount_total ?? 0) / 100 / (item.quantity ?? 1),
stripePriceId: item.price?.id,
}
})
const orderData = {
orderNumber,
stripeSessionId: session.id,
stripePaymentIntentId:
typeof session.payment_intent === 'string'
? session.payment_intent
: session.payment_intent?.id,
email: session.customer_details?.email ?? session.customer_email,
customerName: session.customer_details?.name,
status: 'paid',
items,
subtotalUsd: (session.amount_subtotal ?? 0) / 100,
shippingUsd: (session.shipping_cost?.amount_total ?? 0) / 100,
totalUsd: (session.amount_total ?? 0) / 100,
shippingAddress: shippingAddress
? {
name: session.customer_details?.name,
line1: shippingAddress.line1,
line2: shippingAddress.line2,
city: shippingAddress.city,
state: shippingAddress.state,
zip: shippingAddress.postal_code,
country: shippingAddress.country ?? 'US',
}
: undefined,
stripeEventData: event,
}
// POST to Payload REST API
const payloadUrl = `${process.env.NEXT_PUBLIC_SITE_URL}/api/orders`
const response = await fetch(payloadUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `users API-Key ${process.env.PAYLOAD_API_KEY}`,
},
body: JSON.stringify(orderData),
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Payload order creation failed: ${response.status} ${errorText}`)
}
const created = await response.json()
console.log(`[Webhook] Order created: ${orderNumber} (${created.doc?.id})`)
return created
}

Some files were not shown because too many files have changed in this diff Show More