Kung Fu School

I am developing a website for a Kung Fu school. The current website is outdated and does not reflect current best practices in web design. My work involves designing and developing a complete redesign of the website. Additionally, I would like to integrate other services such as content management, registration, and learning tracking.

STACKAstro, TypeScript, Tailwind CSS
Kung Fu School project

The problem

The global quality and user experience of the old website are not satisfactory. Visitors can’t find information easily and can’t view the website easily on small screens.

The old Lighthouse scores were: 82% 82% 68% 69%

The solution

Mockup & Design System

I built a mini design system in Figma with Tailwind font sizes and colors, and with components and variants for icons, cards, menu, buttons.

Screenshot of the design system in Figma


The website is now responsive thanks to media queries from Tailwind. I designed it with a mobile-first strategy.

Screenshot of menu

I designed a mobile menu with some custom animations inside the Tailwind config:

export default {
  theme: {
    extend: {
      animation: {
        openmenu: 'openmenu 0.5s ease-in both',
        closemenu: 'closemenu 0.5s ease-in both',
      keyframes: {
        openmenu: {
          '0%': { top: '-400px' },
          '100%': { top: '0px' },
        closemenu: {
          '0%': { top: '0px' },
          '100%': { top: '-400px' },

Performance Report

The new scores are 99% 100% 100% 100% (mobile) and 100% 100% 100% 100% (desktop)

Pagespeed report

I took care of assets loading such as fonts and images. To have a good first content paint metric, the first image in the viewport has the loading:“eager” attribute.

To display as soon as possible the most original font for titles “Houji,” I used a combination of preloading and display: swap.

    <style is:global>
      @font-face {
        font-family: 'Houji';
          url('/kungfuschool/fonts/houji.woff2') format('woff2'),
          url('/kungfuschool/fonts/houji.woff2') format('woff');
        font-weight: normal;
        font-style: normal;
        font-display: swap;

An example of the component, with custom social images for each page and Open Graph specifications:

<meta http-equiv="Cache-control" content="public" />
<meta http-equiv="Expires" content="259200" />
<meta name="revisit-after" content="5 days" />
<meta name="robots" content="index, follow" />
<meta name="keywords" content="Kung Fu, School, ..."/>
<meta property="article:published_time" content={publishedDateString} />

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content={canonicalWebsiteURL} />
<meta name="twitter:creator" content="@jeromeabeldev" />

<meta property="og:site_name" content="Kung Fu School" />
<meta property="og:type" content="website" />
<meta property="og:url" content={canonicalWebsiteURL} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={socialImageURL} />
<meta property="og:image:alt" content={title} />
<meta property="og:image:width" content={image.width.toString()} />
<meta property="og:image:height" content={image.height.toString()} />


This project’s aim was also to practice more modeling, user stories, Git actions, conventional & small Git commits.

The user story template in GitHub issues:

Kanban User Stories

The uses cases of the first release:

Kanban User Stories

Display Images From A Folder In Astro

Well, displaying images in Astro is sometimes counter-intuitive. We have to manage between static building and specific import behaviors from Vite.

The hard part is to overcome the fact that import can’t build variable file paths. I found a solution to display images automatically from the folder of a blog post, instead of writing an array with the filenames.

// File: src/pages/blog/[slug].astro
// Filter out images from the news folder
const images = Object.keys(
		{ eager: true }))
		.filter((src) => !src.includes(coverName) && src.includes(entry.slug));
  images.length > 0 ? (
    <div class="grid grid-cols-2 gap-8">
      { images.map((path) => ( <CustomImage imagePath={path} width={700} /> )) }
  ) : null
// File: src/components/CustomImage.astro

import type { ImageMetadata } from 'astro';
import { Image } from 'astro:assets';

interface Props {
    imagePath: string;
    alt?: string;
    width?: number;

const { imagePath, alt = '', width = 1440 } = Astro.props;
const images = import.meta.glob<{ default: ImageMetadata }>(
if (!images[imagePath])
    throw new Error(`"${imagePath}" does not exist in glob: "/src/content/*/*/"`);

<Image src={images[imagePath]()} {alt} {width} />

What I Learned


I wrote some notes about Astro on LinkedIn: