mirror of
				https://github.com/KevinMidboe/planetposen-frontend.git
				synced 2025-10-29 13:10:12 +00:00 
			
		
		
		
	Feat: Refactor jsonld & method to update document title and description (#4)
* Generates JSON ld structured metadata from a product & appends to head * Updated IProduct & IVariation interface * Added IProductResponse & IProductsResponse interfaces * Fixed sitemap urls having to many protocols * Implemented jsonld for product w/ variations * Aligned Product responses between backend & frontend * PageMeta for updating head meta values: title & description Use on any page where we want to display a unique meta page title & description * Set document language to norwegian * Linting
This commit is contained in:
		| @@ -1,5 +1,5 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <html lang="no-NO"> | ||||
|   <head> | ||||
|     <meta charset="utf-8" /> | ||||
|     <meta name="viewport" content="width=device-width,initial-scale=1" /> | ||||
|   | ||||
							
								
								
									
										9
									
								
								src/lib/components/PageMeta.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/lib/components/PageMeta.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| <script lang="ts"> | ||||
|   export let title: string; | ||||
|   export let description: string; | ||||
| </script> | ||||
|  | ||||
| <svelte:head> | ||||
|   <title>{title}</title> | ||||
|   <meta name="description" content="{description}" /> | ||||
| </svelte:head> | ||||
| @@ -1,4 +1,4 @@ | ||||
| import type IProduct from './IProduct'; | ||||
| import type { IProduct } from './IProduct'; | ||||
| import type { IOrder, IOrderSummary } from './IOrder'; | ||||
|  | ||||
| export interface IProductResponse { | ||||
| @@ -17,11 +17,11 @@ export interface IOrderSummaryResponse { | ||||
| } | ||||
|  | ||||
| export interface IProductResponse { | ||||
|   success: boolean | ||||
|   product: IProduct | ||||
|   success: boolean; | ||||
|   product: IProduct; | ||||
| } | ||||
|  | ||||
| export interface IProductsResponse { | ||||
|   success: boolean | ||||
|   products: Array<IProduct> | ||||
| } | ||||
|   success: boolean; | ||||
|   products: Array<IProduct>; | ||||
| } | ||||
|   | ||||
| @@ -5,17 +5,21 @@ export interface IProduct { | ||||
|   description?: string; | ||||
|   image: string; | ||||
|   primary_color?: string; | ||||
|  | ||||
|   variation_count?: string; | ||||
|   sum_stock?: number; | ||||
|  | ||||
|   updated?: Date; | ||||
|   created?: Date; | ||||
|   variations?: IVariation[]; | ||||
| } | ||||
|  | ||||
| export interface IVariation { | ||||
|     sku_id:        number; | ||||
|     price:         number; | ||||
|     size:          string; | ||||
|     stock:         number; | ||||
|     default_price: boolean; | ||||
|     updated?:       Date; | ||||
|     created?:       Date; | ||||
|   sku_id: number; | ||||
|   price: number; | ||||
|   size: string; | ||||
|   stock: number; | ||||
|   default_price: boolean; | ||||
|   updated?: Date; | ||||
|   created?: Date; | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import type { IProduct } from '../interfaces/IProduct' | ||||
| import type { IProduct } from '../interfaces/IProduct'; | ||||
|  | ||||
| function structureProduct(product: IProduct) { | ||||
|   const output = product?.variations?.map(variation => { | ||||
|   const output = product?.variations?.map((variation) => { | ||||
|     return { | ||||
|       '@context': 'https://schema.org/', | ||||
|       '@type': 'Product', | ||||
| @@ -23,16 +23,15 @@ function structureProduct(product: IProduct) { | ||||
|         itemCondition: 'https://schema.org/NewCondition', | ||||
|         availability: 'https://schema.org/InStock' | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
|     }; | ||||
|   }); | ||||
|  | ||||
|   return JSON.stringify(output); | ||||
| } | ||||
|  | ||||
| export default function generateProductJsonLd(product: IProduct): HTMLElement { | ||||
|   const jsonldScript = document.createElement('script'); | ||||
|   jsonldScript.setAttribute('type', 'application/ld+json'); | ||||
|   jsonldScript.textContent = structureProduct(product); | ||||
|  | ||||
|   return jsonldScript; | ||||
| export default function generateProductJsonLd(product: IProduct): string { | ||||
|   return `<script type="application/ld+json"> | ||||
| ${structureProduct(product)} | ||||
| </script> | ||||
| `; | ||||
| } | ||||
|   | ||||
| @@ -5,8 +5,8 @@ | ||||
|   import Footer from './Footer.svelte'; | ||||
|   import CartModal from './CartModal.svelte'; | ||||
|   import { closeCart } from '$lib/cartStore'; | ||||
|   import requestSessionCookie from '$lib/utils/requestSessionCookie'; | ||||
|   import { getCookie } from '$lib/utils/cookie'; | ||||
|   import requestSessionCookie from '$lib/utils/requestSessionCookie'; | ||||
|   import { connectToCart, reconnectIfCartWSClosed } from '$lib/websocketCart'; | ||||
|   import './styles.css'; | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| <script lang="ts"> | ||||
|   import PageMeta from '$lib/components/PageMeta.svelte'; | ||||
|   import FrontText from '$lib/components/FrontText.svelte'; | ||||
|   import FrontTextImage from '$lib/components/FrontTextImage.svelte'; | ||||
|   import FrontTextImageBubble from '$lib/components/FrontTextImageBubble.svelte'; | ||||
| @@ -53,11 +54,7 @@ | ||||
|   ]; | ||||
| </script> | ||||
|  | ||||
| <svelte:head> | ||||
|   <title>Home</title> | ||||
|   <meta name="description" content="Svelte demo app" /> | ||||
| </svelte:head> | ||||
|  | ||||
| <PageMeta title="Planetposen" description="Planetposen hjemmeside" /> | ||||
| <section class="frontpage"> | ||||
|   <!-- {#each textImages as data} | ||||
|     <TextImageParralax {data} /> | ||||
|   | ||||
| @@ -132,13 +132,13 @@ | ||||
|   } | ||||
|  | ||||
|   // li[aria-current='page']::after, li:hover::after { | ||||
|   // 	content: ''; | ||||
|   // 	width: 90%; | ||||
|   // 	height: 2px; | ||||
|   // 	position: absolute; | ||||
|   // 	bottom: 0; | ||||
|   // 	margin-left: 5%; | ||||
|   // 	background-color: var(--color-text); | ||||
|   //  content: ''; | ||||
|   //  width: 90%; | ||||
|   //  height: 2px; | ||||
|   //  position: absolute; | ||||
|   //  bottom: 0; | ||||
|   //  margin-left: 5%; | ||||
|   //  background-color: var(--color-text); | ||||
|   // } | ||||
|  | ||||
|   nav a { | ||||
| @@ -156,6 +156,6 @@ | ||||
|   } | ||||
|  | ||||
|   // a:hover { | ||||
|   // 	color: var(--color-theme-1); | ||||
|   //  color: var(--color-theme-1); | ||||
|   // } | ||||
| </style> | ||||
|   | ||||
| @@ -1,14 +1,13 @@ | ||||
| <script lang="ts"> | ||||
|   import OrderSection from './OrderSection.svelte'; | ||||
|   import DeliverySection from './DeliverySection.svelte'; | ||||
|   import PageMeta from '$lib/components/PageMeta.svelte'; | ||||
|   import CheckoutButton from '$lib/components/Button.svelte'; | ||||
|   import StripeCard from '$lib/components/StripeCard.svelte'; | ||||
|   import ApplePayButton from '$lib/components/ApplePayButton.svelte'; | ||||
|   import VippsHurtigkasse from '$lib/components/VippsHurtigkasse.svelte'; | ||||
|   import { cart } from '$lib/cartStore'; | ||||
|  | ||||
|   import type { IProduct } from '$lib/interfaces/IProduct'; | ||||
|  | ||||
|   function postOrder(event: any) { | ||||
|     const formData = new FormData(event.target); | ||||
|  | ||||
| @@ -35,6 +34,11 @@ | ||||
|   } | ||||
| </script> | ||||
|  | ||||
| <PageMeta | ||||
|   title="Kasse" | ||||
|   description="Kasse for bestilling og betaling av produkter i handlekurven" | ||||
| /> | ||||
|  | ||||
| <h1>Checkout</h1> | ||||
| <form class="checkout" on:submit|preventDefault="{postOrder}"> | ||||
|   <section id="delivery"> | ||||
|   | ||||
| @@ -1,3 +1,8 @@ | ||||
| <script lang="ts"> | ||||
|   import PageMeta from '$lib/components/PageMeta.svelte'; | ||||
| </script> | ||||
|  | ||||
| <PageMeta title="Cookies" description="Beskrivelse av nettsidens bruk av cookies" /> | ||||
| <article> | ||||
|   <h1>Cookies</h1> | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <script lang="ts"> | ||||
|   import Badge from '$lib/components/Badge.svelte'; | ||||
|   import type BadgeType from '$lib/interfaces/BadgeType'; | ||||
|   // import type BadgeType from '$lib/interfaces/BadgeType'; | ||||
|   import type { IOrder } from '$lib/interfaces/IOrder'; | ||||
|  | ||||
|   export let order: IOrder; | ||||
|   | ||||
| @@ -1,3 +1,11 @@ | ||||
| <script lang="ts"> | ||||
|   import PageMeta from '$lib/components/PageMeta.svelte'; | ||||
| </script> | ||||
|  | ||||
| <PageMeta | ||||
|   title="Personvernerklæring" | ||||
|   description="Personvernerklæring for planetposen nettbutikk" | ||||
| /> | ||||
| <article> | ||||
|   <h1>Personvernerklæring</h1> | ||||
|   <section> | ||||
|   | ||||
| @@ -1,12 +1,14 @@ | ||||
| <script lang="ts"> | ||||
|   import type { PageData } from './$types'; | ||||
|   import ProductTile from './ProductTile.svelte'; | ||||
|   import PageMeta from '$lib/components/PageMeta.svelte'; | ||||
|   import type { IProduct } from '$lib/interfaces/IProduct'; | ||||
|  | ||||
|   export let data: PageData; | ||||
|   const products = data.products as Array<IProduct>; | ||||
| </script> | ||||
|  | ||||
| <PageMeta title="Nettbutikk" description="Planetposen nettbutikk" /> | ||||
| <div class="page"> | ||||
|   <h1>Nettbutikk</h1> | ||||
|  | ||||
|   | ||||
| @@ -13,7 +13,7 @@ | ||||
|     }`}" | ||||
|   > | ||||
|     {#if !large} | ||||
|     <h3>{product?.name}</h3> | ||||
|       <h3>{product?.name}</h3> | ||||
|     {/if} | ||||
|  | ||||
|     <div class="{`image-frame ${large ? 'large' : null}`}"> | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { dev } from '$app/environment'; | ||||
| import { env } from '$env/dynamic/private'; | ||||
| import generateProductJsonLd from '$lib/jsonld/product'; | ||||
| import type { IProductResponse } from '$lib/interfaces/ApiResponse'; | ||||
| import type { PageServerLoad } from './$types'; | ||||
|  | ||||
| @@ -12,6 +13,11 @@ export const load: PageServerLoad = async ({ fetch, params }) => { | ||||
|   } | ||||
|  | ||||
|   const res = await fetch(url); | ||||
|   const product: IProductResponse = await res.json(); | ||||
|   return product; | ||||
|   const productResponse: IProductResponse = await res.json(); | ||||
|   const jsonld = generateProductJsonLd(productResponse?.product); | ||||
|  | ||||
|   return { | ||||
|     product: productResponse?.product, | ||||
|     jsonld | ||||
|   }; | ||||
| }; | ||||
|   | ||||
| @@ -1,18 +1,15 @@ | ||||
| <script lang="ts"> | ||||
|   import { onMount } from 'svelte' | ||||
|   import type { PageData } from './$types'; | ||||
|   import ProductTile from '../ProductTile.svelte'; | ||||
|   import ProductVariationSelect from '$lib/components/ProductVariationSelect.svelte'; | ||||
|   import QuantitySelect from '$lib/components/QuantitySelect.svelte'; | ||||
|   import SizesSection from './SizesSection.svelte'; | ||||
|   import type { IProduct, IVariation } from '$lib/interfaces/IProduct'; | ||||
|   import Button from '$lib/components/Button.svelte'; | ||||
|   import generateProductJsonLd from '$lib/jsonld/product'; | ||||
|   import type { PageData } from './$types'; | ||||
|   import type { IProduct, IVariation } from '$lib/interfaces/IProduct'; | ||||
|  | ||||
|   export let data: PageData; | ||||
|   const product = data.product as IProduct; | ||||
|   import { addProductToCart } from '$lib/websocketCart'; | ||||
|   console.log('shop product:', product); | ||||
|  | ||||
|   function setSelectedVariation(event: CustomEvent) { | ||||
|     selectedVariation = event.detail; | ||||
| @@ -30,13 +27,13 @@ | ||||
|   } | ||||
|  | ||||
|   function defaultVariation() { | ||||
|     return product.variations?.find(variation => variation.default_price) | ||||
|     return product.variations?.find((variation) => variation.default_price); | ||||
|   } | ||||
|  | ||||
|   let jsonLd: HTMLElement; | ||||
|   let cooldownInputs = false; | ||||
|   let quantity = 1; | ||||
|   let selectedVariation: IVariation | undefined = defaultVariation() | ||||
|   let selectedVariation: IVariation | undefined = defaultVariation(); | ||||
|  | ||||
|   $: addProductButtonText = cooldownInputs | ||||
|     ? `${quantity} produkt${quantity > 1 ? 'er' : ''} lagt til` | ||||
|     : `Legg til ${quantity} i handlekurven`; | ||||
| @@ -71,9 +68,12 @@ | ||||
| </div> | ||||
| <SizesSection /> | ||||
|  | ||||
| {#if data?.jsonld} | ||||
|   {@html data.jsonld} | ||||
| {/if} | ||||
|  | ||||
| <style lang="scss" module="scoped"> | ||||
|   @import '../../../styles/media-queries.scss'; | ||||
|   // @import "../styles/global.scss"; | ||||
|  | ||||
|   .product-container { | ||||
|     display: grid; | ||||
|   | ||||
| @@ -1,3 +1,11 @@ | ||||
| <script lang="ts"> | ||||
|   import PageMeta from '$lib/components/PageMeta.svelte'; | ||||
| </script> | ||||
|  | ||||
| <PageMeta | ||||
|   title="Betingelser & vilkår" | ||||
|   description="Beskrivelse av alle betingelser og vilkår tilknyttet bruk av planetposen nettbutikk" | ||||
| /> | ||||
| <article> | ||||
|   <h1>Betingelser og vilkår</h1> | ||||
|   <section> | ||||
|   | ||||
| @@ -34,17 +34,17 @@ | ||||
|  | ||||
|         <td class="stock-column">{product?.sum_stock}</td> | ||||
|  | ||||
|         <td class="date-column" | ||||
|           >{new Intl.DateTimeFormat('nb-NO', { dateStyle: 'short', timeStyle: 'short' }).format( | ||||
|         <td class="date-column"> | ||||
|           {new Intl.DateTimeFormat('nb-NO', { dateStyle: 'short', timeStyle: 'short' }).format( | ||||
|             new Date(product.created || 0) | ||||
|           )}</td | ||||
|         > | ||||
|           )} | ||||
|         </td> | ||||
|  | ||||
|         <td class="date-column" | ||||
|           >{new Intl.DateTimeFormat('nb-NO', { dateStyle: 'short', timeStyle: 'short' }).format( | ||||
|         <td class="date-column"> | ||||
|           {new Intl.DateTimeFormat('nb-NO', { dateStyle: 'short', timeStyle: 'short' }).format( | ||||
|             new Date(product.updated || 0) | ||||
|           )}</td | ||||
|         > | ||||
|           )} | ||||
|         </td> | ||||
|       </tr> | ||||
|     {/each} | ||||
|   </tbody> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user