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:
2022-11-30 00:39:04 +01:00
committed by GitHub
parent 296cfb80a0
commit b2a15be12e
18 changed files with 103 additions and 61 deletions

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="no-NO">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="viewport" content="width=device-width,initial-scale=1" />

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

View File

@@ -1,4 +1,4 @@
import type IProduct from './IProduct'; import type { IProduct } from './IProduct';
import type { IOrder, IOrderSummary } from './IOrder'; import type { IOrder, IOrderSummary } from './IOrder';
export interface IProductResponse { export interface IProductResponse {
@@ -17,11 +17,11 @@ export interface IOrderSummaryResponse {
} }
export interface IProductResponse { export interface IProductResponse {
success: boolean success: boolean;
product: IProduct product: IProduct;
} }
export interface IProductsResponse { export interface IProductsResponse {
success: boolean success: boolean;
products: Array<IProduct> products: Array<IProduct>;
} }

View File

@@ -5,17 +5,21 @@ export interface IProduct {
description?: string; description?: string;
image: string; image: string;
primary_color?: string; primary_color?: string;
variation_count?: string;
sum_stock?: number;
updated?: Date; updated?: Date;
created?: Date; created?: Date;
variations?: IVariation[]; variations?: IVariation[];
} }
export interface IVariation { export interface IVariation {
sku_id: number; sku_id: number;
price: number; price: number;
size: string; size: string;
stock: number; stock: number;
default_price: boolean; default_price: boolean;
updated?: Date; updated?: Date;
created?: Date; created?: Date;
} }

View File

@@ -1,7 +1,7 @@
import type { IProduct } from '../interfaces/IProduct' import type { IProduct } from '../interfaces/IProduct';
function structureProduct(product: IProduct) { function structureProduct(product: IProduct) {
const output = product?.variations?.map(variation => { const output = product?.variations?.map((variation) => {
return { return {
'@context': 'https://schema.org/', '@context': 'https://schema.org/',
'@type': 'Product', '@type': 'Product',
@@ -23,16 +23,15 @@ function structureProduct(product: IProduct) {
itemCondition: 'https://schema.org/NewCondition', itemCondition: 'https://schema.org/NewCondition',
availability: 'https://schema.org/InStock' availability: 'https://schema.org/InStock'
} }
} };
}) });
return JSON.stringify(output); return JSON.stringify(output);
} }
export default function generateProductJsonLd(product: IProduct): HTMLElement { export default function generateProductJsonLd(product: IProduct): string {
const jsonldScript = document.createElement('script'); return `<script type="application/ld+json">
jsonldScript.setAttribute('type', 'application/ld+json'); ${structureProduct(product)}
jsonldScript.textContent = structureProduct(product); </script>
`;
return jsonldScript;
} }

View File

@@ -5,8 +5,8 @@
import Footer from './Footer.svelte'; import Footer from './Footer.svelte';
import CartModal from './CartModal.svelte'; import CartModal from './CartModal.svelte';
import { closeCart } from '$lib/cartStore'; import { closeCart } from '$lib/cartStore';
import requestSessionCookie from '$lib/utils/requestSessionCookie';
import { getCookie } from '$lib/utils/cookie'; import { getCookie } from '$lib/utils/cookie';
import requestSessionCookie from '$lib/utils/requestSessionCookie';
import { connectToCart, reconnectIfCartWSClosed } from '$lib/websocketCart'; import { connectToCart, reconnectIfCartWSClosed } from '$lib/websocketCart';
import './styles.css'; import './styles.css';

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import PageMeta from '$lib/components/PageMeta.svelte';
import FrontText from '$lib/components/FrontText.svelte'; import FrontText from '$lib/components/FrontText.svelte';
import FrontTextImage from '$lib/components/FrontTextImage.svelte'; import FrontTextImage from '$lib/components/FrontTextImage.svelte';
import FrontTextImageBubble from '$lib/components/FrontTextImageBubble.svelte'; import FrontTextImageBubble from '$lib/components/FrontTextImageBubble.svelte';
@@ -53,11 +54,7 @@
]; ];
</script> </script>
<svelte:head> <PageMeta title="Planetposen" description="Planetposen hjemmeside" />
<title>Home</title>
<meta name="description" content="Svelte demo app" />
</svelte:head>
<section class="frontpage"> <section class="frontpage">
<!-- {#each textImages as data} <!-- {#each textImages as data}
<TextImageParralax {data} /> <TextImageParralax {data} />

View File

@@ -132,13 +132,13 @@
} }
// li[aria-current='page']::after, li:hover::after { // li[aria-current='page']::after, li:hover::after {
// content: ''; // content: '';
// width: 90%; // width: 90%;
// height: 2px; // height: 2px;
// position: absolute; // position: absolute;
// bottom: 0; // bottom: 0;
// margin-left: 5%; // margin-left: 5%;
// background-color: var(--color-text); // background-color: var(--color-text);
// } // }
nav a { nav a {
@@ -156,6 +156,6 @@
} }
// a:hover { // a:hover {
// color: var(--color-theme-1); // color: var(--color-theme-1);
// } // }
</style> </style>

View File

@@ -1,14 +1,13 @@
<script lang="ts"> <script lang="ts">
import OrderSection from './OrderSection.svelte'; import OrderSection from './OrderSection.svelte';
import DeliverySection from './DeliverySection.svelte'; import DeliverySection from './DeliverySection.svelte';
import PageMeta from '$lib/components/PageMeta.svelte';
import CheckoutButton from '$lib/components/Button.svelte'; import CheckoutButton from '$lib/components/Button.svelte';
import StripeCard from '$lib/components/StripeCard.svelte'; import StripeCard from '$lib/components/StripeCard.svelte';
import ApplePayButton from '$lib/components/ApplePayButton.svelte'; import ApplePayButton from '$lib/components/ApplePayButton.svelte';
import VippsHurtigkasse from '$lib/components/VippsHurtigkasse.svelte'; import VippsHurtigkasse from '$lib/components/VippsHurtigkasse.svelte';
import { cart } from '$lib/cartStore'; import { cart } from '$lib/cartStore';
import type { IProduct } from '$lib/interfaces/IProduct';
function postOrder(event: any) { function postOrder(event: any) {
const formData = new FormData(event.target); const formData = new FormData(event.target);
@@ -35,6 +34,11 @@
} }
</script> </script>
<PageMeta
title="Kasse"
description="Kasse for bestilling og betaling av produkter i handlekurven"
/>
<h1>Checkout</h1> <h1>Checkout</h1>
<form class="checkout" on:submit|preventDefault="{postOrder}"> <form class="checkout" on:submit|preventDefault="{postOrder}">
<section id="delivery"> <section id="delivery">

View File

@@ -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> <article>
<h1>Cookies</h1> <h1>Cookies</h1>

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import Badge from '$lib/components/Badge.svelte'; 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'; import type { IOrder } from '$lib/interfaces/IOrder';
export let order: IOrder; export let order: IOrder;

View File

@@ -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> <article>
<h1>Personvernerklæring</h1> <h1>Personvernerklæring</h1>
<section> <section>

View File

@@ -1,12 +1,14 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
import ProductTile from './ProductTile.svelte'; import ProductTile from './ProductTile.svelte';
import PageMeta from '$lib/components/PageMeta.svelte';
import type { IProduct } from '$lib/interfaces/IProduct'; import type { IProduct } from '$lib/interfaces/IProduct';
export let data: PageData; export let data: PageData;
const products = data.products as Array<IProduct>; const products = data.products as Array<IProduct>;
</script> </script>
<PageMeta title="Nettbutikk" description="Planetposen nettbutikk" />
<div class="page"> <div class="page">
<h1>Nettbutikk</h1> <h1>Nettbutikk</h1>

View File

@@ -13,7 +13,7 @@
}`}" }`}"
> >
{#if !large} {#if !large}
<h3>{product?.name}</h3> <h3>{product?.name}</h3>
{/if} {/if}
<div class="{`image-frame ${large ? 'large' : null}`}"> <div class="{`image-frame ${large ? 'large' : null}`}">

View File

@@ -1,5 +1,6 @@
import { dev } from '$app/environment'; import { dev } from '$app/environment';
import { env } from '$env/dynamic/private'; import { env } from '$env/dynamic/private';
import generateProductJsonLd from '$lib/jsonld/product';
import type { IProductResponse } from '$lib/interfaces/ApiResponse'; import type { IProductResponse } from '$lib/interfaces/ApiResponse';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
@@ -12,6 +13,11 @@ export const load: PageServerLoad = async ({ fetch, params }) => {
} }
const res = await fetch(url); const res = await fetch(url);
const product: IProductResponse = await res.json(); const productResponse: IProductResponse = await res.json();
return product; const jsonld = generateProductJsonLd(productResponse?.product);
return {
product: productResponse?.product,
jsonld
};
}; };

View File

@@ -1,18 +1,15 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'
import type { PageData } from './$types';
import ProductTile from '../ProductTile.svelte'; import ProductTile from '../ProductTile.svelte';
import ProductVariationSelect from '$lib/components/ProductVariationSelect.svelte'; import ProductVariationSelect from '$lib/components/ProductVariationSelect.svelte';
import QuantitySelect from '$lib/components/QuantitySelect.svelte'; import QuantitySelect from '$lib/components/QuantitySelect.svelte';
import SizesSection from './SizesSection.svelte'; import SizesSection from './SizesSection.svelte';
import type { IProduct, IVariation } from '$lib/interfaces/IProduct';
import Button from '$lib/components/Button.svelte'; 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; export let data: PageData;
const product = data.product as IProduct; const product = data.product as IProduct;
import { addProductToCart } from '$lib/websocketCart'; import { addProductToCart } from '$lib/websocketCart';
console.log('shop product:', product);
function setSelectedVariation(event: CustomEvent) { function setSelectedVariation(event: CustomEvent) {
selectedVariation = event.detail; selectedVariation = event.detail;
@@ -30,13 +27,13 @@
} }
function defaultVariation() { 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 cooldownInputs = false;
let quantity = 1; let quantity = 1;
let selectedVariation: IVariation | undefined = defaultVariation() let selectedVariation: IVariation | undefined = defaultVariation();
$: addProductButtonText = cooldownInputs $: addProductButtonText = cooldownInputs
? `${quantity} produkt${quantity > 1 ? 'er' : ''} lagt til` ? `${quantity} produkt${quantity > 1 ? 'er' : ''} lagt til`
: `Legg til ${quantity} i handlekurven`; : `Legg til ${quantity} i handlekurven`;
@@ -71,9 +68,12 @@
</div> </div>
<SizesSection /> <SizesSection />
{#if data?.jsonld}
{@html data.jsonld}
{/if}
<style lang="scss" module="scoped"> <style lang="scss" module="scoped">
@import '../../../styles/media-queries.scss'; @import '../../../styles/media-queries.scss';
// @import "../styles/global.scss";
.product-container { .product-container {
display: grid; display: grid;

View File

@@ -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> <article>
<h1>Betingelser og vilkår</h1> <h1>Betingelser og vilkår</h1>
<section> <section>

View File

@@ -34,17 +34,17 @@
<td class="stock-column">{product?.sum_stock}</td> <td class="stock-column">{product?.sum_stock}</td>
<td class="date-column" <td class="date-column">
>{new Intl.DateTimeFormat('nb-NO', { dateStyle: 'short', timeStyle: 'short' }).format( {new Intl.DateTimeFormat('nb-NO', { dateStyle: 'short', timeStyle: 'short' }).format(
new Date(product.created || 0) new Date(product.created || 0)
)}</td )}
> </td>
<td class="date-column" <td class="date-column">
>{new Intl.DateTimeFormat('nb-NO', { dateStyle: 'short', timeStyle: 'short' }).format( {new Intl.DateTimeFormat('nb-NO', { dateStyle: 'short', timeStyle: 'short' }).format(
new Date(product.updated || 0) new Date(product.updated || 0)
)}</td )}
> </td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>