๐Ÿ“ฆ directus / docs

๐Ÿ“„ create-a-cms-using-directus-and-sveltekit.md ยท 968 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968---
id: 11a1c86f-36bf-4dd5-8bce-7eed75451514
slug: create-a-cms-using-directus-and-sveltekit
title: Create a CMS using Directus and SvelteKit
technologies:
  - sveltekit
authors:
  - name: Temitope Oyedelde
    title: Guest Author
description: Learn how to create a CMS using Directus and SvelteKit.
---

Directus provides a headless CMS, which when combined with SvelteKit will streamline content management. This post covers how to connect them to create a flexible, modern content management system.

## Before You Start

You will need:
- A new Directus project with admin access.

## Set Up Your Directus Project

You'll need to configure CORS for this project. Update your `docker-compose.yml` file as follows:

```bash
CORS_ENABLED: "true" 
CORS_ORIGIN: "http://localhost:5173" 
CORS_CREDENTIALS: "true" 
```

Configure Directus with the necessary collections and permissions.

### Apply the CMS Template

First, generate a static token for the admin user by going to the Users Directory. Choose the `Administrative User` or any user of your choice, scroll down to the Token field, and generate a static token. Copy the token and save it. Do not forget to save the user, or you will encounter an "Invalid token" error in the next step.

Next, use the [Directus Template CLI](https://github.com/directus-labs/directus-template-cli) to apply the CMS template for your project. 

Open your terminal, run the following command, and follow the prompts: 

```bash
npx directus-template-cli@latest apply
```

Choose Community templates, and select the CMS template. Fill in your Directus URL, and select Directus Access Token as the authentication method, filling in the token created earlier.

The Directus CLI command will make the required changes to Directus to add the CMS template.

Next, go back  your Directus dashboard, navigate to `Public policy` and ensure `read` permissions are enabled for most of the collections.

![image showing the necessary permission enabled](/img/sveltekit-permissions.png)

## Set Up Your SvelteKit Project

### Initialize Your Project

To start building, you need to install SvelteKit and Directus sdk. Run this command to install SvelteKit:

```bash
npx sv create dynamic_cms
```
When prompted, select SvelteKit minimal as the template. Do not add type checking, as this tutorial is implemented in JavaScript. Your output should look like this:

```bash
 Welcome to the Svelte CLI! (v0.6.16)
โ”‚
โ—‡  Which template would you like?
โ”‚  SvelteKit minimal
โ”‚
โ—‡  Add type checking with Typescript?
โ”‚  No
โ”‚
โ—†  Project created
โ”‚
โ—‡  What would you like to add to your project? (use arrow keys / space bar)
โ”‚  none
โ”‚
โ—‡  Which package manager do you want to install dependencies with?
โ”‚  npm
โ”‚
โ—†  Successfully installed dependencies
โ”‚
โ—‡  Project next steps โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚                                                                          โ”‚
โ”‚  1: cd dynamic-cms                                                       โ”‚
โ”‚  2: git init && git add -A && git commit -m "Initial commit" (optional)  โ”‚
โ”‚  3: npm run dev -- --open
```

Afterward, `cd` into your project directory and install the Directus SDK by running this command:

```bash
npm install @directus/sdk
```

### Configure the Directus SDK

To set up your Directus client with the authentication composable, create a file called `directus.js` inside the `./src/lib` directory. Add the following code:

```javascript
// src/lib/directus.js
import { createDirectus, rest, readItems } from '@directus/sdk';

const directus = createDirectus('http://localhost:8055').with(rest());

export const getDirectusClient = () => {
  return directus;
};
export { readItems };
```

## Create the Home Page

In this section, you will implement the according parts of the home page, which is already added by the starter template.

From the Directus dashboard sidebar menu, navigate to **Content > Website > Pages**. Under Pages, click on the Home option. It should contain the Hero, Rich Text, Gallery, Pricing, and Form blocks, as shown in the screenshot below. 
![Image showing the home containing the neccessary fields](/img/sveltekit-home.png)

The header and footer can be accessed as shown in the GIF below:

![image showing the navigation colection](/img/sveltekit-nav.gif)

### Hero Section
To create the `Hero` component, navigate to `./src/lib` directory and create a subdirectory named `components`. Inside it, create a file called `HeroSection.svelte` and add the following code:

```javascript
<!--  src/lib/componentd/HeroSection.svelte -->
<script>
 export let data = {};
 let buttonGroup = [];
 if (data && data.button_group) {
      try {
        if (typeof data.button_group === 'object') {
          buttonGroup = Array.isArray(data.button_group) ? data.button_group : [data.button_group];
 }
 } catch (error) {
 console.error('Error processing button_group:', error);
        buttonGroup = [];
 }
    }
</script>
  
  <section class="hero">
    {#if data.image}
      <img 
        src={`http://localhost:8055/assets/${data.image}`} 
        alt="Hero image" 
        class="hero-image"
      />
    {/if}
    
    {#if data.tagline}
      <p class="tagline">{data.tagline}</p>
    {/if}
    
    {#if data.headline}
      <h1 class="headline">{data.headline}</h1>
    {/if}
    
    {#if data.description}
      <div class="description">{data.description}</div>
    {/if}
    
    {#if buttonGroup && buttonGroup.length > 0}
      <div class="button-group">
        {#each buttonGroup as button}
          <a href={button.url || '#'} class="button">
            {button.label || 'Learn More'}
          </a>
        {/each}
      </div>
    {/if}
  </section>
```
The code above renders a dynamic Hero section using data from Directus. The data displayed are
the hero image, tagline, headline, description, and a set of buttons, if they exist.

### Rich Text Section
Inside the `./src/lib/components` directory, create a file called `RichTextSection.svelte` and add the following code:

```javascript
<!--  src/lib/components/RichTextSection.svelte -->
<script>
 export let data = {};
  </script>
  
  <section class="rich-text">
    {#if data.tagline}
      <p class="tagline">{data.tagline}</p>
    {/if}
    
    {#if data.headline}
      <h2 class="headline">{data.headline}</h2>
    {/if}
    
    {#if data.content}
      <div class="content">
        {@html data.content}
      </div>
    {/if}

  </section>
```

The code above renders a Rich Text section based on the data it receives. It conditionally displays the tagline, headline, and rich text content if they exist, with the content being injected as raw HTML to preserve formatting from the CMS.

### Gallery Section
Inside the `./src/lib/components` directory, create a file called `GallerySection.svelte` and add the following code:

```javascript
<script>
 export let data = {};
 import { onMount } from 'svelte';
    
 let galleryItems = [];
 let loading = true;
 let error = null;
    
 onMount(async () => {
        try {
            loading = true;
            if (!data || !data.items || !Array.isArray(data.items)) {
                galleryItems = [];
                return;
 }
            
            galleryItems = data.items.map(item => {
                return {
                    id: item.id,
                    image: item.directus_file, 
                    title: item.title || item.directus_file?.title || 'Gallery Item', 
                    description: item.description || 'Gallery item description',
                    sort: item.sort || 0
 };
 });
            
 galleryItems.sort((a, b) => a.sort - b.sort);
            
 } catch (err) {
            error = 'Failed to load gallery items';
 } finally {
            loading = false;
 }
    });
</script>
<section class="gallery">
    {#if data.tagline}
        <p class="tagline">{data.tagline}</p>
    {/if}
    {#if data.headline}
        <h2 class="headline">{data.headline}</h2>
    {/if}
    {#if loading}
        <div class="loading">Loading gallery items...</div>
    {:else if error}
        <div class="error">{error}</div>
    {:else if galleryItems && galleryItems.length > 0}
        <div class="gallery-grid">
            {#each galleryItems as item}
                <div class="gallery-item">
                    {#if item.image}
                        <img 
                            src={`http://localhost:8055/assets/${item.image.id}`}
                            alt={item.title || item.image.title || 'Gallery image'} 
                            class="gallery-image"
                            loading="lazy"
                        />
                    {:else}
                        <div class="placeholder-image">No Image Available</div>
                    {/if}
                    {#if item.title}
                        <h3 class="item-title">{item.title}</h3>
                    {/if}
                    {#if item.description}
                        <p class="item-description">{item.description}</p>
                    {/if}
                </div>
            {/each}
        </div>
    {:else}
        <p class="no-items">No gallery items available.</p>
    {/if}
</section>
```

The code above loads and displays a Gallery section from the data provided. It fetches and sorts gallery items on mount, handles loading and error states, and conditionally renders each item with its image, title, and description. If no items are available, a fallback message is displayed to maintain the layout integrity.

### Pricing Section
Inside the `./src/lib/components` directory, create a file called `PricingSection.svelte` and add the following code:

```javascript
<!--  Updated src/lib/components/PricingSection.svelte -->
<script>
 export let data = {};
 import { onMount } from 'svelte';
  
 let pricingCards = [];
 let loading = true;
 let error = null;
  
 onMount(async () => {
    try {
      loading = true;
      if (data && data.pricing_cards && Array.isArray(data.pricing_cards)) {
        pricingCards = data.pricing_cards.map(card => {
          let features = [];
          if (card.features) {
            try {
              features = typeof card.features === 'string' 
                ? JSON.parse(card.features) 
                : (Array.isArray(card.features) ? card.features : []);
 } catch (err) {
              console.warn('Error parsing features:', err);
 }
 }
          
          return {
            id: card.id,
            title: card.title || 'Plan',
            price: card.price || '0',
            badge: card.badge || '',
            description: card.description || '',
            features: features,
            button: card.button || 'Get Started',
            button_url: card.button_url || '#',
            is_highlighted: card.is_highlighted || false,
            sort: card.sort || 0
 };
 });
        pricingCards.sort((a, b) => a.sort - b.sort);
 }
 } catch (err) {
      error = 'Failed to load pricing plans';
 } finally {
      loading = false;
 }
  });
</script>

<section class="pricing">
  
  {#if data.tagline}
    <p class="tagline">{data.tagline}</p>
  {/if}
  
  {#if data.headline}
    <h2 class="headline">{data.headline}</h2>
  {/if}
  
  {#if loading}
    <div class="loading">Loading pricing plans...</div>
  {:else if error}
    <div class="error">{error}</div>
  {:else if pricingCards && pricingCards.length > 0}
    <div class="pricing-grid">
      {#each pricingCards as card}
        <div class="pricing-card {card.is_highlighted ? 'featured' : ''}">
          {#if card.badge}
            <span class="badge">{card.badge}</span>
          {/if}
          
          {#if card.title}
            <h3 class="card-name">{card.title}</h3>
          {/if}
          
          {#if card.price}
            <div class="price">
              <span class="currency">$</span>
              <span class="amount">{card.price}</span>
              {#if card.interval}
                <span class="interval">/{card.interval}</span>
              {:else}
                <span class="interval">/month</span>
              {/if}
            </div>
          {/if}
          
          {#if card.description}
            <p class="card-description">{card.description}</p>
          {/if}
          
          {#if card.features && card.features.length > 0}
            <ul class="features">
              {#each card.features as feature}
                <li class="feature">{feature}</li>
              {/each}
            </ul>
          {/if}
          
          {#if card.button}
            <a href={card.button_url || '#'} class="card-button">
 Get Started
            </a>
          {/if}
        </div>
      {/each}
    </div>
  {:else}
    <p class="no-plans">No pricing plans available. Data received: {JSON.stringify(data)}</p>
  {/if}
</section>
```

The code above, builds a dynamic Pricing section by loading pricing cards from the provided data. It processes each card, safely parses feature lists, sorts the cards, and handles loading and error states. 

### Form Section
Inside the `./src/lib/components` directory, create a file called `FormSection.svelte` and add the following code:

```javascript

<!--  src/lib/components/FormSection.svelte -->
<script>
 export let data = {};
    
 let formData = {
        name: '',
        email: '',
        message: ''
    };
 let submitting = false;
 let submitted = false;
 let error = null;
    

</script>

<section class="form-section">
    {#if data.tagline}
        <p class="tagline">{data.tagline}</p>
    {/if}
    
    {#if data.headline}
        <h2 class="headline">{data.headline}</h2>
    {/if}
    
    {#if data.description}
        <div class="description">{data.description}</div>
    {/if}
    
    {#if submitted}
        <div class="success-message">
            <h3>Thank you!</h3>
            <p>Your submission has been received.</p>
            <button 
                class="reset-button"
                on:click={() => submitted = false}
            >
 Submit another response
            </button>
        </div>
    {:else}
        <form class="contact-form">
            <div class="form-group">
                <label for="name">Name</label>
                <input 
                    type="text" 
                    id="name" 
                    bind:value={formData.name} 
                    required
                    placeholder="Your name"
                />
            </div>
            
            <div class="form-group">
                <label for="email">Email</label>
                <input 
                    type="email" 
                    id="email" 
                    bind:value={formData.email} 
                    required
                    placeholder="Your email address"
                />
            </div>
            
            <div class="form-group">
                <label for="message">Message</label>
                <textarea 
                    id="message" 
                    bind:value={formData.message} 
                    rows="5"
                    placeholder="Your message"
                ></textarea>
            </div>
            
            {#if error}
                <div class="error-message">{error}</div>
            {/if}
            
            <button 
                type="submit" 
                class="submit-button"
                disabled={submitting}
            >
                {submitting ? 'Submitting...' : (data.button_text || 'Submit')}
            </button>
        </form>
    {/if}
</section>
```

The code above, builds a CMS-driven contact form that handles user input for name, email, and message.

### SEO
Inside the `./src/lib/components` directory, create a file called `SEO.svelte` and add the following code:
```html
<!-- src/lib/components/SEO.svelte -->
<script>
    export let data = {
      title: 'Default Page Title',
      meta_description: 'Default page description',
      no_index: false,
      no_follow: false
    };
    
    const pageTitle = data?.title || 'Default Title';
    
    const metaDescription = data?.meta_description || 'Default description';
    
    let robotsContent = [];
    if (data?.no_index) robotsContent.push('noindex');
    if (data?.no_follow) robotsContent.push('nofollow');
    const robots = robotsContent.length > 0 ? robotsContent.join(', ') : 'index, follow';
  </script>
  
  <svelte:head>
    <title>{pageTitle}</title>
    <meta name="description" content={metaDescription} />
    <meta name="robots" content={robots} />
    
    <meta property="og:type" content="website" />
    <meta property="og:title" content={pageTitle} />
    <meta property="og:description" content={metaDescription} />
    
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:title" content={pageTitle} />
    <meta name="twitter:description" content={metaDescription} />
  </svelte:head>
```

This will be used to manage the SEO metadata. 

### Header Section
Inside the `./src/lib/components` directory, create a file called `Header.svelte` and add the following code:

```javascript
<!-- src/lib/components/Header.svelte -->
<script>
 export let items = [];
    
 const processNavItems = (items) => {
      return items.map(item => {
        let url = '#';
        if (item.url) {
          url = item.url;
 } else if (item.page) {
          url = `/pages/${item.page}`;
 } else if (item.post) {
          url = `/blog/${item.post}`;
 }
        
        return {
 ...item,
          url,
          children: item.children ? processNavItems(item.children) : []
 };
 });
    };
    
 $: processedItems = processNavItems(items);
</script>

<header class="header">
  <div class="header-container">
    <div class="logo">
      <a href="/">
        <span class="logo-text">Directus</span>
      </a>
    </div>
  
    
    <nav class="desktop-nav">
      <ul class="nav-menu">
        {#each processedItems as item}
          <li class="nav-item">
            <a href={item.url} class="nav-link">{item.title}</a>
            {#if item.children && item.children.length > 0}
              <ul class="dropdown-menu">
                {#each item.children as child}
                  <li class="dropdown-item">
                    <a href={child.url} class="dropdown-link">{child.title}</a>
                  </li>
                {/each}
              </ul>
            {/if}
          </li>
        {/each}
      </ul>
    </nav>
  </div>
</header>
```

### Footer Section
Inside the `./src/lib/components` directory, create a file named `Footer.svelte` and add the following code:

```javascript
<!-- src/lib/components/Footer.svelte -->
<script>
 export let items = [];
 const processNavItems = (items) => {
      return items.map(item => {
        let url = '#';
        if (item.url) {
          url = item.url;
 } else if (item.page) {
          url = `/pages/${item.page}`;
 } else if (item.post) {
          url = `/blog/${item.post}`;
 }
        
        return {
 ...item,
          url,
          children: item.children ? processNavItems(item.children) : []
 };
 });
    };
    
 $: processedItems = processNavItems(items);
  </script>
  
  <footer class="footer">
    <div class="footer-container">
      <div class="footer-content">
        
        <nav class="footer-navigation">
          <ul class="footer-menu">
            {#each processedItems as item}
              <li class="footer-menu-item">
                <a href={item.url} class="footer-link">{item.title}</a>
                {#if item.children && item.children.length > 0}
                  <ul class="footer-submenu">
                    {#each item.children as child}
                      <li class="footer-submenu-item">
                        <a href={child.url} class="footer-link">{child.title}</a>
                      </li>
                    {/each}
                  </ul>
                {/if}
              </li>
            {/each}
          </ul>
        </nav>
      </div>
      
    </div>
  </footer>
```

## Create the Application Layout
To create the application base layout, navigate to `./src/routes` directory, create a new file name `+layout.js` and add the code below:

```javascript
// In src/routes/+layout.js
import { getDirectusClient, readItems } from '$lib/directus';
const directus = getDirectusClient();

async function fetchFooterNavigation() {
  try {
    const response = await directus.request(
      readItems('navigation_items', {
        filter: {
          navigation: {
            id: 'footer'
          }
        },
        fields: [
          'id',
          'title',
          'url',
          'page',
          'post',
          'parent',
          'children'
        ],
        sort: ['sort']
      })
    );
    return response && response.length > 0 ? response : [];
  } catch (error) {
    console.error('Error fetching footer navigation:', error);
    return [];
  }
}

async function fetchMainNavigation() {
  try {
    const response = await directus.request(
      readItems('navigation_items', {
        filter: {
          navigation: {
            id: 'main'
          }
        },
        fields: [
          'id',
          'title',
          'url',
          'page',
          'post',
          'parent',
          'children'
        ],
        sort: ['sort']
      })
    );
    return response && response.length > 0 ? response : [];
  } catch (error) {
    console.error('Error fetching main navigation:', error);
    return [];
  }
}

async function fetchSiteSEO() {
  try {
    const response = await directus.request(
      readItems('pages', {
        limit: 1
      })
    );
    
    return response && response.length > 0 ? response[0] : null;
  } catch (error) {
    console.error('Error fetching site settings:', error);
    return null;
  }
}

export async function load() {
  const [footerNavigation, mainNavigation, siteSettings] = await Promise.all([
    fetchFooterNavigation(),
    fetchMainNavigation(),
    fetchSiteSEO()
  ]);
  
  return {
    footerNavigation,
    mainNavigation,
    siteSettings
  };
}
```
The code above sets up the Directus client and defines async functions to fetch footer links, main navigation links, and site SEO settings. Each function queries specific collections in Directus with filters and field selections. The `load` function then runs all three fetches in parallel and returns the combined data.

Next, create another file named `+layout.svelte` inside the `./src/routes` directory and add the following code.

```html
<!-- src/routes/+layout.svelte -->
<script>
  import Header from '$lib/components/Header.svelte';
  import Footer from '$lib/components/Footer.svelte';
  import SEO from '$lib/components/SEO.svelte';
  
  export let data;
</script>

<SEO data={data.siteSettings?.seo} />

<div class="app">
  <Header items={data.mainNavigation} />
  <slot />
  <Footer items={data.footerNavigation} />
</div>

<style>
  .app {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
  }
  :global(body) {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 
      Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
    line-height: 1.5;
  }
</style>

```

This is just to set up the basic structure of the app by rendering a dynamic `Header`, `Footer`, and `SEO` based on the navigation data fetched from Directus. 

### Load the Application Components
Replace the code in `+page.js` inside the `./src/routes` directory and add the following code.

```javascript
// In src/routes/+page.js
import { getDirectusClient, readItems } from '$lib/directus';
const directus = getDirectusClient();
async function fetchHeroContent() {
  try {
    const response = await directus.request(
      readItems('block_hero', {
        limit: 1,
        sort: ['-id']
 })
 );
    return response && response.length > 0 ? response[0] : null;
 } catch (error) {
    console.error('Error fetching hero content:', error);
    return null;
 }
}

async function fetchRichTextContent() {
  try {
    const response = await directus.request(
      readItems('block_richtext', {
        limit: 1,
        sort: ['-id'] 
 })
 );
    return response && response.length > 0 ? response[0] : null;
 } catch (error) {
    console.error('Error fetching rich text content:', error);
    return null;
 }
}

async function fetchGalleryContent() {
  try {
    const response = await directus.request(
      readItems('block_gallery', {
        limit: 1,
        sort: ['-id'], 
        fields: [
          '*',
          'items.*', 
          'items.directus_file.*' 
 ]
 })
 );
    return response && response.length > 0 ? response[0] : null;
 } catch (error) {
    console.error('Error fetching gallery content:', error);
    return null;
 }
}

async function fetchPricingContent() {
  try {
    const pricingBlock = await directus.request(
      readItems('block_pricing', {
        limit: 1,
        sort: ['-id']
 })
 );
    
    const pricingCards = await directus.request(
      readItems('block_pricing_cards', {
        sort: ['sort']
 })
 );
    
    const result = pricingBlock && pricingBlock.length > 0 
      ? { ...pricingBlock[0], pricing_cards: pricingCards } 
      : { pricing_cards: pricingCards };
    
    return result;
 } catch (error) {
    console.error('Error fetching pricing content:', error);
    return null;
 }
}

async function fetchFormContent() {
  try {
    const response = await directus.request(
      readItems('block_form', {
        limit: 1,
        sort: ['-id']
 })
 );
    console.log('form :', response);
    return response && response.length > 0 ? response[0] : null;
 } catch (error) {
    console.error('Error fetching form content:', error);
    return null;
 }
}

export async function load() {
  const [heroContent, richTextContent, galleryContent, pricingContent, formContent] = await Promise.all([
    fetchHeroContent(),
    fetchRichTextContent(),
    fetchGalleryContent(),
    fetchPricingContent(),
    fetchFormContent()
 ]);
  
  return {
    hero: heroContent,
    richText: richTextContent,
    gallery: galleryContent,
    pricing: pricingContent,
    form: formContent
 };
}
```
In the code above, multiple async functions fetch different content blocks from Directus. Each block pulls the latest entry based on ID sorting. The `load` function runs all the fetches in parallel and returns the results, making the page content fully dynamic and driven by the CMS.

Next, create a `+page.svelte` file in the `./src/routes` directory and add the following code:

```javascript
<!-- src/routes/+page.svelte -->
<script>
 import HeroSection from '$lib/components/HeroSection.svelte';
 import RichTextSection from '$lib/components/RichTextSection.svelte';
 import GallerySection from '$lib/components/GallerySection.svelte';
 import PricingSection from '$lib/components/PricingSection.svelte';
 import FormSection from '$lib/components/FormSection.svelte';
  
 export let data;
</script>

<main>
  {#if data.hero}
    <HeroSection data={data.hero} />
  {/if}
  
  {#if data.richText}
    <RichTextSection data={data.richText} />
  {/if}
  
  {#if data.gallery}
    <GallerySection data={data.gallery} />
  {/if}
  
  {#if data.pricing}
    <PricingSection data={data.pricing} />
  {/if}
  
  {#if data.form}
    <FormSection data={data.form} />
  {/if}
</main>
```

The code above, the page component imports and renders each section, `Hero`, `Rich Text`, `Gallery`, `Pricing`, and `Form`, based on the dynamic data passed from Directus.

## Test the Application
To test the application, run this command:

```bash
npm run dev
```

Afterward, open **http://localhost:5173/** in your browser. You should see your cms displayed:

![image showing the CMS in full display](/img/sveltekit-demo.gif)

Feel free to expand on this by styling the application!

## Conclusion

In this tutorial, you built a fully functional CMS by setting up a structured content management with Directus, pulled dynamic content into your SvelteKit project, and created flexible, reusable components to render that content cleanly.

 As your project grows, you can easily expand it by adding new collections, layouts, or even complex features without being locked into a rigid structure.