Designbest Core

Da Webmobili Wiki.

Database

Popolamento Region/Province/City

Attualmente abbiamo importato i dati sporchi così come sono con la seguente query

INSERT INTO DesignbestCore.dbo.Region
SELECT Region.ID,
	CASE
		WHEN Region.CountryID = 3 THEN 1
		WHEN Region.CountryID = 2 THEN 3
		WHEN Region.CountryID = 4 THEN 4
		WHEN Region.CountryID = 1000 THEN 2
	END AS LanguageID,
	Region.Coordinates,
	Region.LocalizerCode,
	RegionCulture.Region AS [Name],
	LOWER(RegionCulture.SEORegion) AS SeoName
FROM Region
	INNER JOIN RegionCulture ON Region.ID = RegionCulture.RegionID


INSERT INTO DesignbestCore.dbo.Province
SELECT Province.ID,
	CASE
		WHEN Province.CountryID = 3 THEN 1
		WHEN Province.CountryID = 2 THEN 3
		WHEN Province.CountryID = 4 THEN 4
		WHEN Province.CountryID = 1000 THEN 2
	END AS LocalizationID,
	Province.Coordinates,
	Province.ProvinceCode,
	Province.RegionID,
	ProvinceCulture.Province AS Name,
	LOWER(ProvinceCulture.SEOProvince) AS SeoName
FROM Province
	INNER JOIN ProvinceCulture ON Province.ID = ProvinceCulture.ProvinceID
	INNER JOIN Region ON Province.RegionID = Region.ID

INSERT INTO DesignbestCore.dbo.City
SELECT City.ID,
	CASE
		WHEN City.CountryID = 3 THEN 1
		WHEN City.CountryID = 2 THEN 3
		WHEN City.CountryID = 4 THEN 4
		WHEN City.CountryID = 1000 THEN 2
	END AS LocalizationID,
	City.ProvinceID,
	City.City AS [Name],
	LOWER(City.SeoCity) AS SeoName
FROM City
	INNER JOIN Province ON Province.ID = City.ProvinceID
	INNER JOIN Region ON Province.RegionID = Region.ID

Custom Properties

L'idea è quella di creare da una semplicissima struttura di tabelle

  • ProductProperty
  • ProductPropertyValue
  • Mapping_Category_ProductProperty
  • Mapping_Product_ProductPropertyValue

l'assegnazione di qualsiasi proprietà (stile, finitura, colori, decori, formati, proprietà di tipologia, designer, campi custom)

Per ognuna di queste custom property è possibile specificare

  • Nome
  • Nome SEO
  • Valore
  • Immagine (opzionale, gestita da Pictures)


Stesso concetto su tabelle separate per gli ShopPoint

  • ShopPointProperty
  • ShopPointPropertyValue
  • Mapping_ShopPoint_ShopPointPropertyValue

assegnazione delle proprietà come gli ShopPointServices

Per quanto riguarda gli ShopPointServices, purtroppo abbiamo ereditato una logica per la quale, se esiste il valore PropertyValue nel mapping, allora il negozio possiede quella Property. Di conseguenza, se il negozio possiede una certa Property e non specifica nessun suo valore, è necessario inserire un PropertyValue vuoto.

Schifezze

Controllare i valori delle custom properties perché il database è pieno di duplicati assegnati a prodotti diversi.
Ad esempio la ProductProperty Caratteristiche con SeoName Caratteristiche - Sedie c'è due volte con ID differenti.
Per rimediare si potrebbero assegnare tutti i prodotti ad un unico di questi ID e cancellare l'altro doppio.
Questa roba fa casino nel count dei prodotti nel telecomando

SELECT [Name],[SeoName], COUNT(SeoName) AS Duplic
FROM ProductPropertyValue
GROUP BY [Name], SeoName
ORDER BY Duplic DESC, [Name] ASC


ProductThumbs

Questa tabella purtroppo è necessaria in quanto i listing prodotti di una tipologia sono troppo lenti (tempo esecuz > 1s).
Una singola riga rappresenta un ProductThumbanil comprensivo di tutti i dati necessari a creare l'anteprima, titolo, ambiente, tipo manu e dati per creare i link.
È tenuta aggiornata in tempo reale dalla backoffice (no task schedulati).
Per aggiornarla manualmente eseguire

EXEC DesignbestCore.dbo.Utility_ProductThumbsMerge

ProductThumbs_Ext

Per aggiornarla manualmente eseguire

DECLARE @TrovaprodottiID int

DECLARE cur3 CURSOR FOR
		SELECT DISTINCT Context FROM DesignbestCore.dbo.Product_Ext

OPEN cur3;
FETCH NEXT FROM cur3 INTO @TrovaprodottiID
WHILE @@FETCH_STATUS = 0 BEGIN
	EXEC DesignbestCore.dbo.Utility_ProductThumbsMerge_Ext @TrovaprodottiID, 1
	EXEC DesignbestCore.dbo.FullText_Products_Update_Ext @TrovaprodottiID

	FETCH NEXT FROM cur3 INTO @TrovaprodottiID
END

CLOSE cur3;
DEALLOCATE cur3;

Shop

ShopNet Casi Particolari

Attenzione che ogni SHOPNET deve avere una mail univoca altrimenti non viene caricato il Vendor su DOKAN (Wordpress vuole che le mail siano univoche per ogni utente).

Tutti i negozi ora appartengono obbligatoriamente ad una ShopNet Queste ShopNet in particolare:

  • ShopNet Trovaprodotti Orfani Tutti i Trovaprodotti che non avevano una ShopNet. Hanno Piva = '000000000000'. Ne viene creata una per ognuno.
  • ShopNet con PIVA nulla - Le vecchie ShopNet senza PIVA. Hanno Piva = '000000000001'
  • ShopNet Anagrafiche - Contiene tutti gli ShopPoint di tipo != 0 che sono anagrafiche. Un padre per molti figli. Ha Piva = '000000000003'.
  • ShopNet Estero - Tutti gli ShopPoint Esteri. Hanno Piva = '000000000004'. Ne viene creata una per ognuno.

ShopFulltextAdditions

Gestione della ricerca full-text dei negozi che aggiunge ambienti, tipologie e produttori alle chiavi di ricerca del singolo negozio.
Sarà necessario reimplementare tutto il passaggio con il task notturno che genera le righe?

Pictures

La gestione delle immagini sarà basata su quella di Nop.

Picture
ID int ID della picture
EntityID int ID dell'entità
EntityKeyGroup nvarchar(100) Nome della Tabella di riferimento
EntityKey nvarchar(100) Formato dell'immagine desiderato
EntityValue nvarchar(255) Nome dell'immagine
DisplayOrder int Ordinamento

Tutte le immagini saranno in un unico folder

product-<EntityID>-<ID>.jpg
bargain-<EntityID>-<ID>.jpg
manufacturer-<EntityID>-<ID>.jpg
shop-<EntityID>-<ID>.jpg

Questa soluzione è meno leggibile dall'umano ma molto più elastica nel caso in cui ci siano diversi cambiamenti di ordine, master/alternative (evita di dover rinominare l'immagine su disco).

Esempio Volendo trovare tutte le immagini relative ad uno shop si potrebbe usare la seguente query:

SELECT *
FROM Picture
WHERE EntityID = 15314 AND KeyGroup = 'Shop'

Avendo come risultato qualcosa di simile.

bla bla bla ....

Convenzioni

Per quanto riguarda i campi EntityGroup e EntityKey ecco cosa proponiamo

  • ShopPoint
    • cover
    • logo
    • gallery
  • Product
    • gallery
    • square
  • Manufacturer
    • logo
    • logo-square
    • cover
    • sponsored (bottone sponsor in HP)
    • gallery
  • Category
    • ambient
  • Trovaprodotti segue le stesse regole nelle tabelle _ext ma negli EntityValue cambia così
    • gallery (prodotti a catalogo creati da cliente)
      • productext-<context>
    • logo
      • manufacturerext-<context>

Entity Deepening - Approfondimenti delle entità

EntityDeepening
ID int ID della picture
LanguageID int Lingua
EntityID int ID dell'entità
EntityKeyGroup nvarchar(100) Nome della Tabella di riferimento
EntityKey nvarchar(100) Formato dell'immagine desiderato
ExternalID int ID esterno
Link nvacrchar(255) Link esterno
Title nvacrchar(255) Link esterno
ShortDescription nvacrchar(255) Descrizione breve
MediaLink nvacrchar(255) Link ad un media
DisplayOrder int Ordinamento

La tabella EntityDeepening rappresenta un calderone generico per poter assegnare ad una qualsiasi nostra entità (Manu, Shop, Product ecc) i seguenti valori:

  • Id esterno
  • Link esterno
  • Titolo articolo
  • Descrizione breve articolo
  • Foto (facoltativa)

che rappresentano approfondimenti su altre piattaforme (es. Magazine).
Verrà utilizzata per Brand Channel (manu) e Progetti (manu e shop).

Di seguito le chiavi EntityGroup ed EntityKey:

  • Manufacturer
    • brandchannel
    • project
  • ShopNet
    • project

Keywords

La tabella Keywords ha come obiettivo quello di estendere i match di una entità durante una fulltext (B&B => beb , Hartè => harte ecc...).
Di seguito le tipologie di entità, EntityGroup

  • Manufacturer
  • Product
  • ShopPoint

Utilizzata per la ricerca fulltext , quei termini che hanno accenti o caratteri strani.

La feature è attiva solo per ITALIA, le parole "extra" vengono aggiunte alle tabelle/viste di ricerca Fulltext.

  • FullText_Products_Update
  • FT_Products_IT
  • FT_Shops_IT

Le selezioni

Utilizziamo il concetto di selezione per definire gruppi di prodotti (oppure negozi ecc) al fine di poterli richiamare in quei moduli che attualmente hanno una logica circostanziale spesso gestiti con xml.
In questo modo possiamo creare N selezioni tramite il backoffice da usare nei casi più svariati.

SEO Regole

Creata una tabella SeoRules culturizzata tramite LanguageId.

La full text ha bisogno di tabelle con unica chiave primaria e i campi testuali sui quali lavorare.
Nel progetto precedente (WM5) utilizzavamo delle viste indicizzate, in questo non è più possibile farlo perché la struttura delle nuove tabelle implica delle self-join che per la fulltext non sono ammesse.
Non ci resta che optare per la creazione di tabelle apposite per la ricerca che dovranno essere mantenute aggiornate con qualche task schedulato.

Prodotti

Le tabelle implicate sono

  • FT_Products_IT
  • FT_Products_EN
  • FT_Products_FR
  • FT_Products_DE
  • FT_Products_IT_Ext
  • FT_Products_EN_Ext
  • FT_Products_FR_Ext
  • FT_Products_DE_Ext

e vengono aggiornate (con MERGE) dalla SP FullText_Products_Update e dalla SP FullText_Products_Update_Ext (in caso di contesto TP)

Per effettuare una ricerca prodotti utilizzare la FullText_Products_Search oppure FullText_Products_Search_Ext (in caso di contesto TP)

Negozi

Qui rimaniamo col vecchio approccio delle viste indicizzate dato che non ci sono self-join al momento.
Script di creazione delle viste:

DROP VIEW IF EXISTS dbo.FT_Shops_IT
GO
DROP VIEW IF EXISTS dbo.FT_Shops_EN
GO
DROP VIEW IF EXISTS dbo.FT_Shops_FR
GO
DROP VIEW IF EXISTS dbo.FT_Shops_DE
GO

-- ITALIA
CREATE VIEW dbo.FT_Shops_IT WITH SCHEMABINDING
AS
	SELECT
		ShopPoint.ID,
		ShopPoint.[Name],
		ShopNet.[Name] AS ShopNetName,
		LocDescription.[LocaleValue] AS [Description],
		ShopPoint.Referente,
		ShopPoint.[Address],
		ShopPoint.Phone,
		ShopPoint.Mail,
        ShopPoint.BackofficeNotes,
		Region.[Name] AS Region,
		Province.[Name] AS Province,
		City.[Name] AS City,
		ShopPoint.Visible
	FROM dbo.ShopPoint
		INNER JOIN dbo.ShopNet ON ShopPoint.ShopNetID = ShopNet.ID
		INNER JOIN dbo.City ON ShopPoint.CityID = City.ID
		INNER JOIN dbo.Province ON City.ProvinceID = Province.ID
		INNER JOIN dbo.Region ON Province.RegionID = Region.ID
		INNER JOIN dbo.LocalizedProperty AS LocDescription ON ShopPoint.ID = LocDescription.EntityId AND LocDescription.LanguageId = 1 AND LocDescription.LocaleKeyGroup = 'ShopPoint' AND LocDescription.LocaleKey = 'Description'
	WHERE ShopPoint.VisibilityTypeID = 0
GO

CREATE UNIQUE CLUSTERED INDEX UQ_ID   
    ON dbo.FT_Shops_IT(ID)
GO

-- WORLD
CREATE VIEW dbo.FT_Shops_EN WITH SCHEMABINDING
AS
	SELECT
		ShopPoint.ID,
		ShopPoint.[Name],
		ShopNet.[Name] AS ShopNetName,
		LocDescription.[LocaleValue] AS [Description],
		ShopPoint.Referente,
		ShopPoint.[Address],
		ShopPoint.Phone,
		ShopPoint.Mail,
        ShopPoint.BackofficeNotes,
		Region.[Name] AS Region,
		Province.[Name] AS Province,
		City.[Name] AS City,
		ShopPoint.Visible
	FROM dbo.ShopPoint
		INNER JOIN dbo.ShopNet ON ShopPoint.ShopNetID = ShopNet.ID
		INNER JOIN dbo.City ON ShopPoint.CityID = City.ID
		INNER JOIN dbo.Province ON City.ProvinceID = Province.ID
		INNER JOIN dbo.Region ON Province.RegionID = Region.ID
		INNER JOIN dbo.LocalizedProperty AS LocDescription ON ShopPoint.ID = LocDescription.EntityId AND LocDescription.LanguageId = 2 AND LocDescription.LocaleKeyGroup = 'ShopPoint' AND LocDescription.LocaleKey = 'Description'
	WHERE ShopPoint.VisibilityTypeID = 0
GO

CREATE UNIQUE CLUSTERED INDEX UQ_ID   
    ON dbo.FT_Shops_EN(ID)
GO

-- FRANCIA
CREATE VIEW dbo.FT_Shops_FR WITH SCHEMABINDING
AS
	SELECT
		ShopPoint.ID,
		ShopPoint.[Name],
		ShopNet.[Name] AS ShopNetName,
		LocDescription.[LocaleValue] AS [Description],
		ShopPoint.Referente,
		ShopPoint.[Address],
		ShopPoint.Phone,
		ShopPoint.Mail,
        ShopPoint.BackofficeNotes,
		Region.[Name] AS Region,
		Province.[Name] AS Province,
		City.[Name] AS City,
		ShopPoint.Visible
	FROM dbo.ShopPoint
		INNER JOIN dbo.ShopNet ON ShopPoint.ShopNetID = ShopNet.ID
		INNER JOIN dbo.City ON ShopPoint.CityID = City.ID
		INNER JOIN dbo.Province ON City.ProvinceID = Province.ID
		INNER JOIN dbo.Region ON Province.RegionID = Region.ID
		INNER JOIN dbo.LocalizedProperty AS LocDescription ON ShopPoint.ID = LocDescription.EntityId AND LocDescription.LanguageId = 3 AND LocDescription.LocaleKeyGroup = 'ShopPoint' AND LocDescription.LocaleKey = 'Description'
	WHERE ShopPoint.VisibilityTypeID = 0
GO

CREATE UNIQUE CLUSTERED INDEX UQ_ID   
    ON dbo.FT_Shops_FR(ID)
GO

-- GERMANIA
CREATE VIEW dbo.FT_Shops_DE WITH SCHEMABINDING
AS
	SELECT
		ShopPoint.ID,
		ShopPoint.[Name],
		ShopNet.[Name] AS ShopNetName,
		LocDescription.[LocaleValue] AS [Description],
		ShopPoint.Referente,
		ShopPoint.[Address],
		ShopPoint.Phone,
		ShopPoint.Mail,
        ShopPoint.BackofficeNotes,
		Region.[Name] AS Region,
		Province.[Name] AS Province,
		City.[Name] AS City,
		ShopPoint.Visible
	FROM dbo.ShopPoint
		INNER JOIN dbo.ShopNet ON ShopPoint.ShopNetID = ShopNet.ID
		INNER JOIN dbo.City ON ShopPoint.CityID = City.ID
		INNER JOIN dbo.Province ON City.ProvinceID = Province.ID
		INNER JOIN dbo.Region ON Province.RegionID = Region.ID
		INNER JOIN dbo.LocalizedProperty AS LocDescription ON ShopPoint.ID = LocDescription.EntityId AND LocDescription.LanguageId = 4 AND LocDescription.LocaleKeyGroup = 'ShopPoint' AND LocDescription.LocaleKey = 'Description'
	WHERE ShopPoint.VisibilityTypeID = 0
GO

CREATE UNIQUE CLUSTERED INDEX UQ_ID   
    ON dbo.FT_Shops_DE(ID)
GO

Le viste implicate sono

  • FT_Shops_IT
  • FT_Shops_EN
  • FT_Shops_FR
  • FT_Shops_DE

Per effettuare una ricerca negozi utilizzare la FullText_Shops_Search

Tabelle senza vincoli

Qui sotto l'elenco delle tabelle sconnesse dai cascading, che devono essere trattate separatamente in caso di cancellazione dati

  • LocalizedProperty
  • LocalizedProperty_Ext
  • Picture
  • Picture_Ext
  • CustomSorting
  • EntityDeepening
  • NopMapping_ShopNets
  • NopMapping_Manufs
  • NopMapping_Designers
  • NopMapping_Categories

Speciali Saloni e Fiere

Siccome ogni singolo prodotto può fare parte di una fiera o un salone, abbiamo implementato un campo json per poter avere elasticità nel mostrare i contenuti.

Ogni Salone o Fiera è inteso come record della tabella Selezioni.

Nella tabella Product_JsonData ogni prodotto (chiave primaria) può contenere un json così strutturato:

[
  {
    "id": 3, // corrisponde a Selezioni.ID
    "name": "Salone del Mobile 2024", 
    "icon": "/img/icons/salone.svg"
  },
  {
    "id": 4, // corrisponde a Selezioni.ID
    "name": "Cersaie 2024", 
    "icon": "/img/icons/cersaie2024.svg"
  }
  // ...
]

Product_JsonData viene leftjoinata ai risultati nei listing fornendo la possibilità di vedere le iconcine corrispondenti alle fiere.

Sul Server

È necessario installare .NET 9.0, la versione Hosting Bundle da https://dotnet.microsoft.com/en-us/download/dotnet/9.0
Il server non chiede riavvio.

Per verificare che il framework sia presente digitare in una console:

dotnet --list-runtimes

per vedere .NET 9

Reverse Proxy

Serve per gestire alcune richieste web dirottandole su un altra macchina (per es. linux).
Si gestisce installando il modulo ARR che si integra con il modulo Rewrite

Ora dall' URL Rewrite del sito in questione

  • Aggiungere una regola vuota inbound
  • Inserire nel Pattern: ^linuxapp($|/.*)
  • Azione Rewrite
  • Url di destinazione http://linuxserver/linuxapp{R:1} dove linuxserver è un alias che punta all'IP interno della macchina Linux con il web server.

Il passaggio avviene sulla porta 80, perciò il web server non deve implementare certificati di nessun tipo.

Wordpress

Per avere wordpress all'indirizzo del reverse proxy è necessario

  • Scaricare ed estrarre lo zip dell'ultimo wordpress in una sottocartella blog (il nome deve coincidere con quello definito nel Rewrite di IIS).
  • Copiare (NON spostare) i file index.php e .htaccess nella root ../blog
  • Aprire index.php e trovare la parte di inclusione di wp-blog-header.php e modificarne il percorso
/** Loads the WordPress Environment and Template */
//require __DIR__ . '/wp-blog-header.php';  // prima
require __DIR__ . '/blog/wp-blog-header.php'; // dopo
  • Creare il virtual host di Apache in ascolto sulla porta 80 ricordando di mettere negli alias linuxserver, nell'esempio sotto è apache-staging.
<VirtualHost *:80>
        ServerAdmin mailto:info@designbest.com
        ServerName www.dbdemo47.com
        ServerAlias apache-staging

        DirectoryIndex index.html index.htm index.php
        DocumentRoot /var/www/designbestblog

        <Directory />
                Options FollowSymLinks
                AllowOverride All
        </Directory>

        <Directory /var/www/designbestblog/>
                Options -Indexes +FollowSymLinks +MultiViews
                AllowOverride All
                Order allow,deny
                Allow from all
                Require all granted
        </Directory>

</VirtualHost>

  • Aprire MySQL e creare il database
  • Installare Wordpress dalla macchina con IIS accedendo a http://apache-staging/blog
  • Entrare in /wp-admin e nelle impostazioni cambiare site url e home inserendo il link pubblico https://www.dbdemo47.com/blog
  • A questo punto worpdress ci sbatte fuori
  • Aprire il file wp-config.php e inserire il valore
$_SERVER['HTTPS'] = 'on';

/* That's all, stop editing! Happy publishing. */

appena prima della scritta "That's all, stop editing!".
Questo forza la creazione dei link di WP in https (perché sistemare site url e home a quanto pare non basta).

  • Tornare in /wp-admin, scegliere una pagina che farà da homepage e valorizzare il suo permalink con home
  • Aprire .htaccess che sta nella sottocartella blog e aggiungere le righe prima di BEGIN WORDPRESS
# Workaround necessario - La homepage fa redirect infinito
# e non ne capiamo il motivo. In questo modo la forziamo.
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /blog
RewriteRule ^/?$ http://apache-staging/blog/home [END]
</IfModule>

Elementor PRO

Il plugin soffre di un bug, quando si cerca di attivare il prodotto una delle sua chiamate redirige senza includere /blog/ nella url e va in errore.

Bisogna attivarlo manualmente

  • Entrare nella sezione "Licenza" dal panello di Elementor
  • Aggiungere alla url &mode=manually
  • Si presenta una pagina nella quale inserire il codice di licenza manualmente

Progetto VS

Program.cs

Nel file Program.cs pregenerato aggiungere i seguenti concetti

Sessione

In un progetto Net.Core la sessione parte disabilitata.
Per iniettare la sessione è necessario attivare un gestore della cache integrato (IDistributedCache) e la sessione stessa con le sue opzioni di configurazione come da esempio

builder.Services.AddDistributedMemoryCache(); //L'implementazione IDistributedCache viene usata come archivio di backup per la sessione
builder.Services.AddSession(options => {
  //options.IdleTimeout = TimeSpan.FromSeconds(10); // default 20 minuti
  //options.Cookie.HttpOnly = true; // default true
  options.Cookie.Name = ".Designbest.Session";  // nome del cookie di sessione
  options.Cookie.IsEssential = true;
});

Dopodiché è necessario attivarla inserendo app.UseSession():
L' ordine del middleware è importante.
Chiamare UseSession dopo e UseRouting prima di MapRazorPages e MapDefaultControllerRoute

// ...
app.UseAuthorization();

app.UseSession(); // attivazione sessione

app.MapRazorPages();

app.Run();

HttpContextAccessor e HttpClient

Abilitare questi servizi

  • HttpContextAccessor permette di accedere all' HttpContext da qualsiasi classe.
  • HttpClient permette di fare request.
builder.Services.AddHttpContextAccessor();  // per avere l'oggetto IHttpContextAccessor nelle classi
builder.Services.AddHttpClient(); // per avere l'oggetto IHttpClientFactory nelle classi

Services/Dipendenze

Implementare tutti i servizi bene separati utilizzando il pattern Interfaccia-Implementazione.
Per abilitarli nel progetto, segnalare interfaccia e implementazione come segue:

builder.Services.AddScoped<IDesignbestContext, DesignbestContext>();
builder.Services.AddScoped<IGeolocalization, MaxMindGeolocalization>();

Ci sono 3 diverse durate del servizio

  • Temporaneo builder.Services.AddTransient(..)
    istanza normale che muore alla fine dello scope della funzione in cui viene chiamata
  • Con Ambito builder.Services.AddScoped(..)
    viene utilizzata la stessa istanza per tutta la durata della richiesta utente
  • Singleton builder.Services.AddSingleton(...)
    viene utilizzata la stessa istanza finche IIS non viene stoppato

Javascript

Tutti i moduli, le funzioni e gli eventi del sito sono gestiti dai nostri script che non dipendono da terze parti (tantomeno da jQuery che per i dispositivi mobile è solo un rallentamento).
Sfruttiamo le ultime funzioni standardizzate in ECMASCRIPT 6, con le definizioni di classi, metodi, membri di classe e chiamate asincrone.

Il cosiddetto thread principale prevede un bundle minificato delle nostre risorse principali.

Google Fonts

L'inclusione dei Google Fonts provoca grandi problemi nelle pagine per i valori di CLS (Content Shift Layout).
Per risolvere abbiamo seguito questa guida: https://csswizardry.com/2020/05/the-fastest-google-fonts/

Componenti riutilizzabili

Di seguito la documentazione dei moduli del progetto che sono riutilizzabili nelle varie pagine: Designbest Core - Componenti riutilizzabili

Compilazione a runtime

Abilitare la compilazione a runtime solo in ambiente di sviluppo.

  • Scaricare il pacchetto nuget Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation
  • Aggiungere il seguente codice al Program.cs
if (builder.Environment.IsDevelopment()) {
    builder.Services.AddRazorPages().AddRazorRuntimeCompilation();
} else {
    builder.Services.AddRazorPages();
}

Problema CopyRefAssembliesToPublishDirectory

Se dopo aver attivato la compilazione a Runtime il progetto dà un errore che parla di

CopyRefAssembliesToPublishDirectory 

il problema potrebbe essere che dalle viste è stata fatta una @inject di un Service del quale non è stata specificata la @using all'interno di /Pages/_ViewImports.cshtml.

Mi è successo con IConfiguration, ho dovuto aggiungere questa riga

@using Microsoft.Extensions.Configuration

Environment - Development, Staging, Production

Per decidere con quale environment la web app deve essere pubblicata è necessario editare manualmente il publish profile aggiungendo la seguente

<Project>
  <PropertyGroup>

    <!-- Altre direttive -->

	<EnvironmentName>Staging</EnvironmentName>
  </PropertyGroup>
</Project>

I valori consentiti sono

  • Production
  • Staging
  • Development - L'unico che permette la visualizzazione degli errori

Passare a .Net Core 9

https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-9.0&tabs=visual-studio

Script type="importmap"

Con .Net Core 9 gli script con type="importmap" non vengono più stampati in pagina in fase rendering.

Sarà per qualche politica di sicurezza,
sostituire questo codice:

<script type="importmap">
	{
		"imports": {
			"@@popperjs/core": "@Html.PopperJs()",
			"bootstrap": "@Html.BootstrapJs()"
		}
	}
</script>

con

@{
	var importMapData = new ImportMapDefinition( new Dictionary<string, string> {
		{ "@popperjs/core",Html.PopperJs() },
		{ "bootstrap",Html.BootstrapJs() }
	}, null, null);
}
<script type="importmap" asp-importmap="@importMapData"></script>

ricordandosi di aggiungere alla in cima alla razor page

@using Microsoft.AspNetCore.Components

Routing -> CoreSeo new

Ci siamo trovati di fronte a diversi problemi da risolvere.

Aggiungere route alla default

Le Razor Pages sono raggiungibili tramite l'espressione regolare specificata dalla direttiva @page
Es. per la pagina Ambient

@page "/prodotti/{ambient}"

Per fare in modo che la stessa pagina sia raggiungibile da più di una route,
basta specificare le nuova route per la pagina in questione da Program.cs

builder.Services.AddRazorPages()
  .AddRazorPagesOptions(options => {
    options.Conventions.AddPageRoute("/Ambient", "/{lang}/prodotti/{ambient}");
  });

In questo modo la pagina viene raggiunta da queste url

  • /prodotti/tavoli
  • /it/prodotti/tavoli

Ma questo non basta perché a quanto pare il net core non applica la stessa cosa alla generazione dei link con IUrlHelper.Page().

Per fare in modo che l'applicativo gestisca anche la generazione dei link delle nuove route è necessario utilizzare AddPageRouteModelConvention() come da esempio:

builder.Services.AddRazorPages()
  .AddRazorPagesOptions(options => {
    options.Conventions.AddPageRouteModelConvention("/Ambient", opts=> {
      var selectorCount = opts.Selectors.Count;
      for (var i = 0; i < selectorCount; i++) {
      	//var selector = opts.Selectors[i];
      	opts.Selectors.Add(new SelectorModel {
      		AttributeRouteModel = new AttributeRouteModel {
      			Order = 0,  // importante
      			SuppressLinkGeneration = false, // importante, di default ne rimane una sola
      			Template = template
      		}
      	});
      }
    });
  });

Fonte: https://learn.microsoft.com/en-us/aspnet/core/razor-pages/razor-pages-conventions?view=aspnetcore-8.0


Custom Route Costraints

Crea un vincolo di route personalizzato.
Per esempio quando i termini consentiti in una route sono molti e cambiano dinamicamente e non basta un'espressione regolare per definirli. Fonte: https://www.learnrazorpages.com/advanced/custom-constraints

Estensioni Utilizzate

Minificazione Risorse

Passando a .net core ci siamo portati dietro le vecchie estensioni per la Minificazione css, scss e js che sono state deprecate (e infatti cominciano a dare i numeri).
In particolare stiamo usando:

  • Web Compiler 2022+ installata come estensione di Visual Studio che leggendo dal file compilerconfig.json gestisce la minificazione degli scss
  • BuildBundlerMinifier installata come pacchetto nuget di progetto, che leggendo il file bundleconfig.json gestisce la minificazione dei file js
  • Bunder & Minifier 2022+ installata come estensione di Visual Studio che leggendo dal file bundleconfig.json gestisce la minificazione e bundle dei file js

Trasformazione del Web.config

Il web.config viene generato automaticamente, ma può avere varianti piazzando un file di trasformazione nella root del progetto

  • web.release.config viene applicato a tutte le build (release)
  • web.Production.config viene applicato solo in ambiente Production
  • web.Staging.config viene applicato solo in ambiente Staging
  • web.Development.config viene applicato solo in ambiente Development

https://learn.microsoft.com/it-it/aspnet/core/host-and-deploy/iis/transform-webconfig?view=aspnetcore-9.0

⚠ Cose Brutte ⚠

Questo capitolo per tenere traccia di tutte le porcherie che non siamo riusciti ad accantonare.

File .resx illeggibili

Dopo un certo aggiornamento di VS tutti i file .resx diventano illeggibili lanciando un errore
"Value cannot be null. Parameter name :Path".

Per poterli consultare

  • Cliccare col tasto destro sul .resx
  • Open With => Managed Resources Editor (Legacy)

https://developercommunity.visualstudio.com/t/Resource-explorer-cannot-open-resx-files/10729146

Passaggio a Microsoft.Data.SqlClient

Fine novembre 2024,
ci hanno deprecato il package System.Data.SqlClient a favore di Microsoft.Data.SqlClient.

Abbiamo rimosso il vecchio pacchetto e sostituito tutti i namespace.

Ma è uscito un nuovo problema dettato dal fatto che i nuovi driver si aspettano che la crittografia sia impostata su ON di default e se non viene specificato qualcosa nella connection string parte un errore che parla di Catena di certificati emessa da un'autorità non attendibile.

Aggiungere al fondo di ogni connection string

;TrustServerCertificate=true

fonte: https://learn.microsoft.com/it-it/troubleshoot/sql/database-engine/connect/certificate-chain-not-trusted?tabs=ole-db-driver-19

Immagini

Le immagini di Designbest, insieme al dominio immagini.designbest.com sono gestite da ImageKit.

Il dominio "clone" per vedere lo stato delle immagini sui nostri server è

  • img.designbest.com.

Sitemaps

Le sitemaps sul dominio delle immagini sono sitemaps di testo.

Sono state uploadate nell'account Google Search Console come da foto:

Query

Per generare queste sitemaps (e per aggiornarle) si usano le seguenti query:

-- Prodotti visibili di manu visibili
SELECT 'https://immagini.designbest.com/pictures/' +
	CAST(Picture.EntityValue as nvarchar(20))+'-'+CAST(Product.ID as nvarchar(10))+'-'+CAST(Picture.ID as nvarchar(10))+'.jpg?tr=h-600',
	Picture.ID
FROM Picture
	INNER JOIN Product ON Picture.EntityID = Product.ID AND Picture.EntityGroup = 'Product' AND Picture.EntityKey = 'gallery'
	INNER JOIN ProductCulture ON Product.ID = ProductCulture.ProductID AND ProductCulture.LanguageId = 1
	INNER JOIN Manufacturer ON Product.ManufacturerID = Manufacturer.ID
	LEFT OUTER JOIN ProductBargain ON Product.ID = ProductBargain.ProductID
WHERE ProductCulture.Visible = 1 AND Manufacturer.Visible = 1 AND ProductBargain.ProductID IS NULL

-- --Manu visibili
SELECT 'https://immagini.designbest.com/pictures/' +
	CAST(Picture.EntityValue as nvarchar(20))+'-'+CAST(Manufacturer.ID as nvarchar(10))+'-'+CAST(Picture.ID as nvarchar(10))+'.jpg',
	Picture.ID
FROM Picture
	INNER JOIN Manufacturer ON Picture.EntityID = Manufacturer.ID AND Picture.EntityGroup = 'Manufacturer' AND Picture.EntityKey IN ('cover','logo', 'sponsored')
WHERE Manufacturer.Visible = 1

-- Shoppoint storechannel visibili
SELECT 'https://immagini.designbest.com/pictures/' +
	CAST(Picture.EntityValue as nvarchar(20))+'-'+CAST(ShopPoint.ID as nvarchar(10))+'-'+CAST(Picture.ID as nvarchar(10))+'.jpg',
	Picture.ID
FROM Picture
	INNER JOIN ShopPoint ON Picture.EntityID = ShopPoint.ID AND Picture.EntityGroup = 'ShopPoint' --AND Picture.EntityKey IN ('cover','logo')
WHERE ShopPoint.Visible = 1 AND ShopPoint.VisibilityTypeID = 0

-- Immagini ambienti
SELECT 'https://immagini.designbest.com/pictures/' +
	CAST(Picture.EntityValue as nvarchar(20))+'-'+CAST(Category.ID as nvarchar(10))+'-'+CAST(Picture.ID as nvarchar(10))+'.jpg',
	Picture.ID
FROM Picture
	INNER JOIN Category ON Picture.EntityID = Category.ID AND Picture.EntityGroup = 'Category'
WHERE Category.CategoryLevel = 1 AND Category.Visible = 1

Creare i file e piazzarli in questi percorsi (il primo va splittato, ma 50.000 risultati per file).

e poi da Google Search Console comunicare l'aggiornamento avvenuto.