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