Dans cet article, je vais décrire ma manière d’utiliser Astro Build pour créer le site web
de Clever Tech Ware (CTW).
Recherche d’un thème
Ma première étape a été de rechercher un thème qui me plaisait. J’ai donc parcouru les thèmes proposés par Astro Build.
J’ai constaté que tous les thèmes ne sont pas gratuits.
Cela étant dit, le prix des thèmes n’est pas exorbitant.
Pour mon simple besoin de démarrer un site vitrine, j’ai décidé de me concentrer sur les thèmes gratuits que je peux personnaliser si la licence me le permet.
2 thèmes ont retenu mon attention :
Ce qui est intéressant, c’est que les 2 thèmes sont sous licence MIT me permettant de personaliser le thème à ma guise.
Après avoir testé les 2 thèmes, j’ai décidé de partir sur le thème Astrolus en reprenant une idée que j’adorais sur Hello Astro : le lien vers la page du blog.
Création du site
Objectif
L’objectif que je me suis fixé pour le site est :
- créer un site simple, rapide et dont je peux gérer le contenu facilement à l’aide d’un système de version telle que GIT,
- déployer facilement le site à partir d’un stockage peu coûteux (au vue du peu de trafic attendu au début et du peu de contenu dans le site au démarrage),
- automatiser la mise à jour et le déploiement du site.
Installation d’Astro Build avec le thème Astrolus
Ici rien de plus simple, j’ai forké le dépôt.
Ensuite, je l’ai récupéré en local et installé les dépendances :
$> git clone git@github.com:clevertechware/astrolus.git website
$> cd website
$> npm install
$> npm start
Modification du thème
Ma première étape a été de modifier la couleur primaire du thème.
Astrolus utilise le framework css Tailwind pour gérer le style. Avec tailwind, un design système se configure
via le fichier tailwind.config.cjs
situé à la racine des sources du projet.
On édite donc ce fichier et on remplace la couleur primaire #9333EA
par la nôtre #0DCBFD
:
Petit upgrade de la stack
Quand j’ai forké le dépôt, j’ai constaté que le projet utilisait une version ancienne d’Astro (v1). Ayant vu qu’une
nouvelle version existait, j’ai mis à jour la version des dépendances (astro et tailwind) dans le
fichier package.json
{
"dependencies": {
"astro": "^2.10.1"
},
"devDependencies": {
"@astrojs/tailwind": "^4.0.0"
}
}
💡 Des outils existent pour mettre à jour automatiquement les dépendances d’un projet. Je vous ferai prochainement un article sur le sujet. 🙂
Note : ne pas oublier d’appliquer ces changements via une réinstallation des dépendances (aka : npm install
)
Modification du bouton “Getting Started” pour pointer vers la page du blog
Pour rappel, Astro fonctionne sur un principe de composant réutilisable. Chaque composant est un fichier .astro
qui
contient du code HTML, CSS et JavaScript.
Assez logiquement, le menu du haut se trouve dans le header composant. Il suffira donc de modifier le lien du bouton pour le faire pointer vers la page d’accueil du blog qui recensera l’ensemble des articles de blogs.
On modifie donc le code HTML suivant dans le fichier src/components/AppHeader.astro
:
<div class="mt-12 lg:mt-0">
<a href="/register"
class="relative flex h-9 w-full items-center justify-center px-4 before:absolute before:inset-0 before:rounded-full before:bg-primary before:transition before:duration-300 hover:before:scale-105 active:duration-75 active:before:scale-95 sm:w-max">
<span class="relative text-sm font-semibold text-white">Get Started</span>
</a>
</div>
Par le code HTML suivant :
<div class="mt-12 lg:mt-0">
<a href="/blog"
class="relative flex h-9 w-full items-center justify-center px-4 before:absolute before:inset-0 before:rounded-full before:bg-primary before:transition before:duration-300 hover:before:scale-105 active:duration-75 active:before:scale-95 sm:w-max">
<span class="relative text-sm font-semibold text-white">Blog</span>
</a>
</div>
Afin d’obtenir ceci :
En revanche, si on clique, on obtient une erreur 404, car la page /blog
n’existe pas encore :
Tout ceci est normal, mais ne devrait pas se produire.
Pour s’en assurer, nous allons rajouter des tests automatisés.
Stratégie de test du site
Les tests sont une partie importante de mon travail.
Voyons donc comment on peut incorporer des tests automatisés dans notre projet Astro 😎
Test e2e
Dans le contexte de ce site, je vais utiliser les tests e2e pour vérifier que le site fonctionne correctement.
Pour ce faire, j’ai choisi d’utiliser le framework Playwright.
Il s’intègre très bien avec Astro et permet de faire des tests e2e très facilement. Pour l’installer, il suffit de lancer la commande suivante :
$> npm init playwright@latest
Getting started with writing end-to-end tests with Playwright:
Initializing project in '.'
✔ Where to put your end-to-end tests? · e2e
✔ Add a GitHub Actions workflow? (y/N) · false
✔ Install Playwright browsers (can be done manually via 'npx playwright install')? (Y/n) · true
Installing Playwright Test (npm install --save-dev @playwright/test)…
added 3 packages, and audited 465 packages in 745ms
181 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Downloading browsers (npx playwright install)…
Writing playwright.config.ts.
Writing e2e/example.spec.ts.
Writing tests-examples/demo-todo-app.spec.ts.
Writing package.json.
✔ Success! Created a Playwright Test project at /private/tmp/website
Inside that directory, you can run several commands:
npx playwright test
Runs the end-to-end tests.
npx playwright test --ui
Starts the interactive UI mode.
npx playwright test --project=chromium
Runs the tests only on Desktop Chrome.
npx playwright test example
Runs the tests in a specific file.
npx playwright test --debug
Runs the tests in debug mode.
npx playwright codegen
Auto generate tests with Codegen.
We suggest that you begin by typing:
npx playwright test
And check out the following files:
- ./e2e/example.spec.ts - Example end-to-end test
- ./tests-examples/demo-todo-app.spec.ts - Demo Todo App end-to-end tests
- ./playwright.config.ts - Playwright Test configuration
Visit https://playwright.dev/docs/intro for more information. ✨
Happy hacking! 🎭
Plutôt que de créer un répertoire tests
à la racine du projet, je préfère utiliser un répertoire e2e
afin
de bien accentuer que nous aurons des tests e2e.
Je crée le fichier e2e/index.spec.ts
suivant :
import { expect, test } from '@playwright/test';
test.describe('Welcome page', () => {
test.beforeEach(async ({page}) => {
await page.goto('/');
});
test('should have right title', async ({page}) => {
await expect(page).toHaveTitle('Tailus astro theme');
});
test('should have a navigation bar with link to services section', async ({page}) => {
const servicesLink = page.locator('[href*="/#features"]');
const servicesLinkLabelSpan = servicesLink.locator('span');
await expect(servicesLinkLabelSpan).toHaveText(`Features`);
});
test('should redirect to blog page on blog link click ', async ({page}) => {
const blogLink = page.getByRole('link', {name: 'Blog'});
await blogLink.click();
expect(page.url()).toContain('/blog');
await expect(page).toHaveTitle('Tailus astro theme');
});
});
Quand on lance les tests, aucuns ne passent 🥹:
$> npx playwright test
Running 15 tests using 4 workers
1) [chromium] › index.spec.ts:12:7 › Welcome page › should have a navigation bar with link to services section
Error: page.goto: Protocol error (Page.navigate): Cannot navigate to invalid URL
=========================== logs ===========================
navigating to "/", waiting until "load"
============================================================
3 | test.describe('Welcome page', () => {
4 | test.beforeEach(async ({page}) => {
> 5 | await page.goto('/');
| ^
6 | });
7 |
8 | test('should have right title', async ({page}) => {
at /private/tmp/website/e2e/index.spec.ts:5:16
2) [chromium] › index.spec.ts:8:7 › Welcome page › should have right title ─────────────────
Le problème vient du fait que le server hébergeant notre site n’est pas lancé en local.
De plus, par défaut, l’url de base des tests n’est pas configurée. Cette url de base est intéressante, car elle évite
d’être dupliquée sur l’ensemble des tests.
On va donc modifier la configuration de playwright pour lui indiquer l’url du site à tester via le fichier
playwright.config.ts
ainsi qu’activer les tests uniquement sur le navigateur Chromium (suffisant pour notre cas
d’usage).
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
// ...
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {...devices['Desktop Chrome']},
},
/*
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
*/
],
// ...
/* Run your local dev server before starting the tests */
webServer: {
command: 'npm run start',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
}
});
Désormais l’ensemble des tests passent à l’exception du dernier qui échoue car la page /blog
n’existe pas encore 😊
$> npx playwright test
Running 5 tests using 4 workers
1) [chromium] › index.spec.ts:20:7 › Welcome page › should redirect to blog page on blog link click
Error: Timed out 5000ms waiting for expect(received).toHaveTitle(expected)
Expected string: "Tailus astro theme"
Received string: "404: Not Found"
Call log:
- expect.toHaveTitle with timeout 5000ms
- waiting for locator(':root')
- locator resolved to <html lang="en">…</html>
- unexpected value "404: Not Found"
- locator resolved to <html lang="en">…</html>
- unexpected value "404: Not Found"
- locator resolved to <html lang="en">…</html>
- unexpected value "404: Not Found"
- locator resolved to <html lang="en">…</html>
- unexpected value "404: Not Found"
- locator resolved to <html lang="en">…</html>
- unexpected value "404: Not Found"
- locator resolved to <html lang="en">…</html>
- unexpected value "404: Not Found"
- locator resolved to <html lang="en">…</html>
- unexpected value "404: Not Found"
- locator resolved to <html lang="en">…</html>
- unexpected value "404: Not Found"
- locator resolved to <html lang="en">…</html>
- unexpected value "404: Not Found"
24 |
25 | expect(page.url()).toContain('/blog');
> 26 | await expect(page).toHaveTitle('Tailus astro theme');
| ^
27 | });
28 | });
29 |
at /private/tmp/website/e2e/index.spec.ts:26:24
1 failed
[chromium] › index.spec.ts:20:7 › Welcome page › should redirect to blog page on blog link click
4 passed (6.3s)
Il ne nous reste plus qu’à créer cette page pour que le test passe 😎.
Création de la partie blog du site
Pour la partie blog du site, on va s’appuyer sur une nouvelle fonctionnalité d’Astro qui s’appelle les content collections.
Le concept de content collections est intéressant. Il permet de séparer le contenu de leur projection (présentation). On va pouvoir créer un ensemble de fichiers markdown qui contiendront le contenu de nos articles de blog. Astro se chargera de les transformer en page HTML en utilisant un template.
La définition de nos collections se fait via un fichier src/content/config.ts
.
// 1. Import utilities from `astro:content`
import { defineCollection, reference, z } from 'astro:content';
// 2. Define your collection(s)
const authorCollection = defineCollection({
type: 'data',
schema: z.object({
name: z.string(),
email: z.string().email()
})
});
const blogCollection = defineCollection({
type: 'content',
schema: z.object({
draft: z.boolean(),
title: z.string(),
description: z.string().optional(),
// Reference a single author from the `authors` collection by `id`
author: reference('authors'),
})
});
// 3. Export a single `collections` object to register your collection(s)
// This key should match your collection directory name in "src/content"
export const collections = {
'blogs': blogCollection,
'authors': authorCollection
};
Une collection peut être vue comme une collection mongodb au sein de laquelle on va pouvoir stocker des documents structurés.
La collection blogCollection
définira la structure de la donnée hébergée par la collection blogs
.
À titre personnel, je distingue 2 type de collections :
- les collections de données (
type: data
), - les collections de contenu (
type: content
).
On définira leur structure via l’entrée schema
.
À noter que la définition de la structure de la collection se fait via l’outil de validation de données Zod. Il permet de définir la structure des données et de valider que les données respectent bien cette dernière.
Créons un fichier src/content/authors/foo.yaml
:
name: Foo
email: foo@example.com
Une fois la collection définie, on peut créer un fichier markdown (ex: first.md
) dans le répertoire src/content/blogs
avec le contenu suivant :
---
draft: false
title: Mon 1er article de blog
author: foo
---
## Markdown avec Astro
Et voilà, le contenu de mon 1er article de blog écrit sous la forme de markdown avec Astro Build.
Ceci alimente notre première collection blogs.
Afficher la liste des articles de blog
Maintenant que nous avons créé notre premier article de blog, nous allons construire la page qui affichera la liste des blogs
disponibles en nous appuyant sur la collection blogs
.
Dans un premier temps, on va créer un composant pour afficher un cadre représentant un article de blog avec
un titre et un lien vers l’article de blog. Pour cela, on crée le fichier suivant src/components/BlogCard.astro
:
---
const {title, slug, description} = Astro.props;
---
<div class="blog-card group mx-auto p-6 rounded-3xl bg-white border border-gray-100 bg-opacity-50 shadow-2xl">
<div class="mt-6 grid grid-rows-3">
<h3 class="text-2xl font-semibold text-gray-800 dark:text-white">{title}</h3>
<a class="inline-block" href={`/posts/${slug}`}>
{description && <p class="text-gray-500">{description}</p>}
<span class="text-info">Lire la suite</span>
</a>
</div>
</div>
<style>
.blog-card {
height: 570px;
}
</style>
Ensuite, on construit notre page affichant la liste de blogs en créant le fichier src/pages/blog.astro
avec le contenu suivant :
---
import { getCollection } from 'astro:content';
import BlogCard from '../components/BlogCard.astro';
import Layout from '../layouts/Layout.astro';
// Les blogs en mode draft ne sont pas affichés.
const blogEntries = await getCollection('blogs', ({data}) => !data.draft);
---
<Layout title="Tailus astro theme">
<main class="space-y-40 mb-40 relative pt-36">
<ul class="grid grid-cols-4 list-none">
{blogEntries.map(blogPostEntry => (
<li class="mx-2 my-2 sm:mx-3">
<BlogCard title={blogPostEntry.data.title} slug={blogPostEntry.slug}/>
</li>
))}
</ul>
</main>
</Layout>
Désormais si on relance nos tests e2e, on constate que le test passe 💪🚀.
$> npx playwright test
Running 5 tests using 4 workers
5 passed (1.5s)
To open last HTML report run:
npx playwright show-report
Si on va sur la page /blog
, on constate que notre article de blog est bien affiché :
On va rajouter un test e2e pour vérifier le comportement de notre page blog en créant le fichier
e2e/blog.spec.ts
:
import { expect, test } from '@playwright/test';
test.describe('Blog page', () => {
test.beforeEach(async ({page}) => {
await page.goto('/blog');
});
test('should have right title', async ({page}) => {
await expect(page).toHaveTitle('Tailus astro theme');
});
test('should have a blog link button', async ({page}) => {
const blogLink = page.getByRole('link', {name: 'Blog'});
const blogLinkLocator = blogLink.locator('span');
await expect(blogLinkLocator).toHaveText(`Blog`);
});
test('should display some post blog', async ({page}) => {
const blogCardLink = page.locator('.blog-card');
const blogCards = await blogCardLink.count();
expect(blogCards).toBeGreaterThanOrEqual(1);
});
test('should display post blog', async ({page}) => {
const blogCardLink = page.locator('.blog-card');
const postBlog = blogCardLink.first();
const postTitleLocator = await postBlog.getByRole('heading').textContent();
await blogCardLink.first().getByRole('link').click();
await expect(page.getByRole('heading', {level: 1})).toHaveText(postTitleLocator!!);
});
});
On lance nos tests et on s’attend donc à ce que le test should display post blog
échoue car la page /posts
n’existe
pas encore.
$> npx playwright test
Running 10 tests using 4 workers
1) [chromium] › blog.spec.ts:37:7 › Blog page › should display post blog ─────────────────────────
Error: Timed out 5000ms waiting for expect(received).toHaveText(expected)
Expected string: "Mon 1er article de blog"
Received string: "404: Not found"
Call log:
- expect.toHaveText with timeout 5000ms
- waiting for getByRole('heading', { level: 1 })
- locator resolved to <h1>…</h1>
- unexpected value "404: Not found"
- locator resolved to <h1>…</h1>
- unexpected value "404: Not found"
- locator resolved to <h1>…</h1>
- unexpected value "404: Not found"
- locator resolved to <h1>…</h1>
- unexpected value "404: Not found"
- locator resolved to <h1>…</h1>
- unexpected value "404: Not found"
- locator resolved to <h1>…</h1>
- unexpected value "404: Not found"
- locator resolved to <h1>…</h1>
- unexpected value "404: Not found"
- locator resolved to <h1>…</h1>
- unexpected value "404: Not found"
- locator resolved to <h1>…</h1>
- unexpected value "404: Not found"
42 | await blogCardLink.first().getByRole('link').click();
43 |
> 44 | await expect(page.getByRole('heading', {level: 1})).toHaveText(postTitleLocator!!);
| ^
45 | });
46 |
47 | });
at /private/tmp/website/e2e/blog.spec.ts:44:57
1 failed
[chromium] › blog.spec.ts:37:7 › Blog page › should display post blog ──────────────────────────
9 passed (6.2s)
Serving HTML report at http://localhost:9323. Press Ctrl+C to quit.
Afficher un article de blog
Maintenant que nous avons la liste des articles de blog, nous allons créer la page qui affichera un article en nous appuyant sur le principe des routes dynamiques d’Astro.
Pour ce faire, créer le fichier src/pages/posts/[...slug].astro
qui permettra de faire correspondre la requête
entrante au paramètre slug
associé à notre article de blog situé dans la collection blog
:
---
import { getCollection, getEntry } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
// 1. Generate a new path for every collection entry
export async function getStaticPaths() {
const blogEntries = await getCollection('blogs');
return blogEntries.map(entry => ({
params: {slug: entry.slug}, props: {entry},
}));
}
// 2. For your template, you can get the entry directly from the prop
const {entry} = Astro.props;
const author = await getEntry(entry.data.author);
const {Content} = await entry.render();
---
<Layout title="Tailus astro theme">
<main class="space-y-40 mb-40 relative pt-36">
<article class="mx-auto w-8/12">
<h1 class="text-gray-900 font-bold mb-10 text-center">{entry.data.title}</h1>
<section class="my-5 dark:text-gray-300 text-justify post-content">
<Content/>
</section>
<section class="post-info font-thin italic text-right">
<span class="">{author.data.name}</span>
</section>
</article>
</main>
</Layout>
<style>
.post-info {
margin-bottom: 2rem;
margin-top: 2rem;
}
</style>
On relance nos tests:
$ npx playwright test
Running 10 tests using 4 workers
10 passed (1.5s)
To open last HTML report run:
npx playwright show-report
Tous les tests passent ✅ Désormais, vous pouvez accéder à votre article de blog 😊
Bilan
Au travers de cet article, vous avez vu comment je me suis appuyé sur Astro Build pour créer le site vitrine de Clever Tech Ware.
L’une des conclusions dans mon aventure avec Astro est que ce fut l’outil adapté à mon propre besoin :
Avoir un outil simple, rapide qui me permette de gérer le contenu de mon site via GIT avec une partie dynamique pour la partie blog.
J’aurais pu prévoir une base de donnée, un serveur d’API, un serveur d’authentification, etc. Mais pour mon besoin, cela aurait été de l’ over engineering. Astro Build m’a permis de me concentrer sur l’essentiel : le contenu de mon site.
En plus, cela m’a permis d’héberger ce site à moindre coût 🙂 mais stay tuned, je vous en parlerai dans un prochain article 😉.