The source code of my personal website.
- Clone the repository.
- Install the dependencies.
- Create an
.env
file and fill it with your own configuration based on.env.example
placeholders - (Recommended) To enable search feature you'll need to perform a first build of the website, so run
pnpm run build
.
This project supports and uses dev-only pages. Those pages are accessible in your browser in dev mode but they won't be built (so they are not available in preview mode). You can find them thanks to their _dev_
prefix.
Why _dev_
? Because Astro already uses a single underscore to exclude the pages from being built so we need a different prefix to differentiate them and a double underscore could be confusing.
Note
This integration only supports directories or pages with Astro extension (e.g. _dev_tokens.astro
). You won't be able to build dev-only pages from Markdown files.
The following patterns are supported:
- a dev-only page anywhere inside the pages directory
- a dev-only folder containing regular pages anywhere inside the pages directory.
The following pattern is not supported because it does not make sense:
- a dev-only page inside a dev-only folder (eg.
src/pages/_dev_design-system/_dev_non-accessible-page.astro
is not accessible in your browser butsrc/pages/_dev_design-system/accessible-page.astro
) is accessible.
Important
The pages are injected right after reading the configuration file. So if you create a new dev-only page (or rename an existing one), the page will not be recognized. You'll need to reload the Astro dev server. Also, it seems the Astro Dev Toolbar can't be displayed in some dev pages so you do not have access to the Audit app for example. I'm not sure why.
To access the page in your browser, you need to remove the prefix from the slug. For example, the existing src/pages/_dev_design-system
folder can be accessed in your browser with the following url http://localhost:4321/design-system
.
Currently, it is not possible to use Storybook with Astro. So I added an Astro integration to be able to test the components in isolation. This is not a Storybook replacement: you can't play with props, dynamically generate a table of available props, etc.
To create stories for your components, use the following structure:
/src/components
├── button/
│ ├── button.astro
│ └── button.stories.astro
└── link/
├── link.astro
└── link.stories.astro
About this structure:
button.astro
: your Astro componentbutton.stories.astro
: your component's stories
The button.stories.astro
file is treated as a regular Astro page: import the component, use a layout if needed, and use some HTML markup to add explanations regarding the component behavior.
Note
The VS Code extension will infer the file name as Button
, so to import your component you'll need to rename the import (e.g. ButtonComponent
) to avoid conflicts.
The integration supports a base path to inject the stories. So with the current configuration and using the previous structure, you can access your stories in a browser with the following slugs:
/design-system/components/button
/design-system/components/link
If you need to divide your stories in multiple files, you can use a stories
directory in your component directory. For example, both the following structures are supported:
/src/components
├── button/
│ ├── stories/
│ │ ├── primary-button.stories.astro
│ │ └── secondary-button.stories.astro
│ ├── button.astro
│ └── button.stories.astro
└── link/
├── stories/
│ ├── link.stories.astro
│ ├── nav-link.stories.astro
│ └── index.stories.astro
└── link.astro
It is up to you to define links to your sub-stories in index.stories.astro
or button.stories.astro
.
Important
If you create a new story file (ie. .stories.astro
), you'll need to restart the dev server to be able to access it in your browser.
Only .astro
extension is supported for stories. I'd like to use .mdx
but if I'm right, Astro integrations can only inject routes for .astro
, .js
and .ts
files.
This project is i18n-ready and it is available in English and in French right now.
All UI strings are stored as a key/value pair in a JSON file located in src/translations
. Note that for routes, except for the homepage, the translations should match the names used in src/pages
.
Then each templates use some methods to translate those messages in the current locale. It also supports pluralization and route localization.
This project uses Nodemailer to allow sending emails with the contact form (through an API route). Make sure to configure your SMTP options using a .env
file.
The easiest way to get started is:
cp .env.example .env
- Replace the placeholders in
.env
with your own configuration
Note
If you want to test sending emails from localhost
using your own mail server, you might need to add some permissions in your firewall.
The search is powered by Pagefind, a fully static search library. It needs to index the contents in advance to be able to work.
There are some caveats in dev mode:
- You need to run
pnpm build
once before executingpnpm dev
to build the search index and to be able to use the search form. - If you change the Pagefind config (like adding data attributes to filter the contents), the index will not be automatically rebuilt. You need to perform another build and to execute
pnpm dev
again. - The indexed images use the built URLs (the ones processed by Astro) so they can't be displayed in dev environment (so for now, I decided to deactivated them).
You can choose to use a dark theme or light theme while browsing the website. You can also choose to set the theme as auto
. In this case, the website theme will be updated according to your operating system preferences. This is especially useful when you want to change the theme depending on the time of day.
A global feed is available in each language. This projects also supports individual feeds for each collections.
The content directory located at the root of the project is used to store all the website contents. You can provide a custom relative path using an environment variable named CONTENT_PATH
.
The available content types are:
- authors
- blog categories
- blog posts
- blogroll
- bookmarks
- guides
- notes
- pages
- projects
- tags
To check the expected fields in the frontmatter, please consult the files in src/lib/astro/collections/schema
.
Both .md
and .mdx
extensions are supported. However, because of technologies limitations, the .mdx
format is recommended if you want to be able to use extra features like callouts and code blocks.
This project is designed to avoid imports in your content
directory. Elements (even HTML tags) are automatically mapped to custom components when you use the .mdx
extension.
The words count will be automatically calculated from your Markdown files and added as metadata. From there, the reading time for each content can be calculated depending on the current language.
When using MDX format, you can use a directive for your callouts (or admonitions, asides) for example:
:::warning
The contents of the warning.
:::
Both custom titles and attributes are supported:
:::idea[My custom tip]{ariaLabel: "An accessible label for my tip"}
The contents of the tip.
:::
Here are the supported callouts type: "critical", "discovery", "idea", "info", "success" and "warning".
When using MDX format, your code blocks will automatically use the CodeBlock
component. To pass additional props, you can use the following syntax:
```js showLineNumbers filePath=./hello-world.js
console.log("Hello, world!");
```
See src/components/molecules/code-block/code-block.astro
for the supported props.
Caution
<pre><code></code></pre>
syntax is not supported. If fenced code blocks are not enough for you, then you will need to import the component in your file directly.
When using .mdx
format:
- you can use local image paths without the need to import them,
- if you don't specify the images dimensions, they will be inferred for you even with remote images.
Before starting, please follow the instructions in Setup.
/
├── content/
│ ├── authors/
│ │ └── armand-philippot.json
│ ├── blogroll/
│ │ ├── blog1.json
│ │ └── blog2.json
│ ├── bookmarks/
│ │ └── a-bookmark.json
│ ├── en/
│ │ ├── blog/
│ │ │ ├── categories/
│ │ │ │ ├── index.md
│ │ │ │ └── category1.md
│ │ │ ├── posts/
│ │ │ │ ├── index.md
│ │ │ │ └── post1.md
│ │ │ └── index.md
│ │ ├── guides/
│ │ │ ├── index.md
│ │ │ └── guide1.md
│ │ ├── notes/
│ │ │ ├── index.md
│ │ │ └── note1.md
│ │ ├── pages/
│ │ │ ├── 404.md
│ │ │ ├── blogroll.md
│ │ │ ├── bookmarks.md
│ │ │ ├── contact.md
│ │ │ ├── feeds.md
│ │ │ ├── home.md
│ │ │ ├── legal-notice.md
│ │ │ └── search.md
│ │ ├── projects/
│ │ │ ├── index.md
│ │ │ └── project1.md
│ │ └── tags/
│ │ ├── index.md
│ │ └── tag1.md
│ └── fr/
│ └── same as en/
├── public/
├── src/
│ ├── assets/
│ ├── components/
│ ├── lib/
│ │ └── astro/
│ │ └── integrations/
│ ├── pages/
│ │ ├── _dev_design-system/
│ │ │ ├── components/
│ │ │ ├── tokens/
│ │ │ └── index.astro
│ │ ├── api/
│ │ ├── blog/
│ │ │ ├── categories/
│ │ │ │ └── index.astro
│ │ │ ├── articles/
│ │ │ │ └── index.astro
│ │ │ └── index.astro
│ │ ├── blogroll/
│ │ │ └── index.astro
│ │ ├── en/
│ │ │ ├── blog/
│ │ │ │ ├── categories/
│ │ │ │ │ └── index.astro
│ │ │ │ ├── posts/
│ │ │ │ │ └── index.astro
│ │ │ │ └── index.astro
│ │ │ ├── blogroll/
│ │ │ │ └── index.astro
│ │ │ ├── bookmarks/
│ │ │ │ └── index.astro
│ │ │ ├── guides/
│ │ │ │ └── index.astro
│ │ │ ├── notes/
│ │ │ │ └── index.astro
│ │ │ ├── projects/
│ │ │ │ └── index.astro
│ │ │ ├── tags/
│ │ │ │ └── index.astro
│ │ │ ├── [slug].astro
│ │ │ ├── 404.astro
│ │ │ ├── contact.astro
│ │ │ ├── feeds.astro
│ │ │ ├── index.astro
│ │ │ ├── legal-notice.astro
│ │ │ └── search.astro
│ │ ├── etiquettes/
│ │ │ └── index.astro
│ │ ├── guides/
│ │ │ └── index.astro
│ │ ├── notes/
│ │ │ └── index.astro
│ │ ├── projets/
│ │ │ └── index.astro
│ │ ├── signets/
│ │ │ └── index.astro
│ │ ├── [slug].astro
│ │ ├── 404.astro
│ │ ├── contact.astro
│ │ ├── flux.astro
│ │ ├── index.astro
│ │ ├── mentions-legales.astro
│ │ └── recherche.astro
│ ├── services/
│ │ └── mailer/
│ ├── styles/
│ ├── translations/
│ │ └── en.json
│ ├── types/
│ ├── utils/
│ └── content.config.ts
├── package.json
└── config files
In details:
content/
: the website contents (pages, posts...),public/
: any static assets, like fonts, can be placed in this directory,src/assets/
: any assets that must be processed by Astro (like images) can be placed in this directory,src/components/
: the components,src/lib/
: the features based on dependencies (e.g. Astro integration or Shiki transformers),src/pages/
: the special components used to create pages and API routes,src/services/
: the website services (e.g. mailer),src/styles/
: global styles, variables and helpers should be placed in this directory,src/translations/
: the JSON files used to store all UI strings and routes for one language,src/types/
: the Typescript types shared across the application,src/utils/
: all the utilities (constants, helpers, etc.) to build the project.
Important
All the index.md
files in the content
directory are required. These files are used in src/pages
to add metadata and optional content to the index pages of your collections. Pages created in content/pages
can use any filename but some pages are required: 404.md
, blogroll.md
, bookmarks.md
, contact.md
, feeds.md
, home.md
, legal-notice.md
and search.md
. You can also use the .mdx
extension.
You can access the Design system only in dev mode with the following slug: /design-system
.
The components are located in the src/components
directory and should be organized using the Atomic Design Methodology:
/src/components/
├── atoms/
├── molecules/
├── organisms/
└── templates/
When creating a new component you should also create stories for it and use the following structure:
/src/components/atoms/
├── button/
│ ├── button.astro
│ ├── button.stories.astro
│ └── button.test.ts
└── other components
The component stories will be collected when your start the dev server and will be available in the design system (accessible under /design-system
in your browser).
This way you can test them in isolation both visually through stories and with Vitest tests.
This project use CSS variables to define colors, borders, fonts, shadows and spacings. This ensures design harmonization across the components and pages.
When creating new design elements, you should use them. For example:
.some-element {
- padding: 1rem;
- background: #fff;
- border: 1px solid #ccc;
- font-size: 16px;
+ padding: var(--spacing-md);
+ background: var(--color-regular);
+ border: var(--border-size-sm) solid var(--color-border);
+ font-size: var(--font-size-md);
}
You can find all the available tokens in the design system (accessible under /design-system
in your browser).
To add a new language for this website, you need to create a new JSON file in src/translations
using the locale as filename. Here are the required steps:
cp content/en content/es
and keep the filenames of thepages
untranslated,cp src/translations/en.json src/translations/es.json
,nano src/translations/es.json
, translate all the keys in your language then save the file,nano src/translations/index.ts
: in that file, import then reexport your new language,- Copy the pages in
src/pages
in a new directory matching the locale you're adding (e.g.src/pages/es
) and translates the filename so that they match your translations insrc/translations
, - The new language is now available!
If you need to use UI strings or routes in your templates:
- Make sure the key exist in the translations files or add a new key,
- Import the
useI18n()
helper, - Use one the provided methods to display your UI string or route.
DO:
---
import { useI18n } from "src/utils/helpers/i18n";
const { locale, route, translate, translatePlural } = useI18n(Astro.currentLocale);
---
<a href={route("a.route.key")}>
{translate("some.key.available.in.translations")}
</a>
<div>
{translatePlural("some.key.supporting.pluralization", { count: 42 })}
</div>
DON'T:
---
---
<div>Some hardcoded string.</div>
Instead of using directly the getCollection
helper from Astro, you should use the queryCollection
helper declared in this project. This function helps you query a collection with filters, offset and ordering. queryCollection
will also resolves every references!
DO:
---
import { queryCollection } from "src/lib/astro/collections";
const { entries, total } = await queryCollection("blogPosts", {
first: 10,
orderBy: { key: "publishedOn", order: "DESC" },
});
---
// Your template here
DON'T:
---
import { getCollection, getEntries, getEntry } from "astro:content";
const rawBlogPosts = await getCollection("blogPosts");
const blogPosts = await Promise.all(
rawBlogPosts.map(async (blogPost) => {
const category = blogPost.data.category
? await getEntry(blogPost.data.category)
: null;
const tags = blogPost.data.tags
? await getEntries(blogPost.data.tags)
: null;
return {
...blogPost,
data: {
...blogPost.data,
category,
tags,
},
};
}),
);
const orderedBlogPosts = blogPosts.sort(/* some method to sort posts */);
const firstTenBlogPosts = orderedBlogPosts.slice(0, 10);
---
// Your template here
As you can see, queryCollection
helps reduce a lot of boilerplate code and it can also be used to resolve multiple collections! If you only need to resolve one entry, you can use queryEntry
instead.
The versioning of this repository is managed with changesets. This helps automate updating package versions, and changelogs.
So if you decide to submit a pull request keep that in mind:
- you have to decide if your changes are worth to be added in the changelog (patch, minor or major update) or not.
- if you think those changes are important enough, then you need to include a changeset in your PR with some details about what changed.
Then, when I think it's time, I'll published a new version of the website based on those changesets.
All commands are run from the root of the project, from a terminal.
Command | Action |
---|---|
pnpm i |
Installs dependencies |
pnpm dev |
Starts local dev server at localhost:4321 |
pnpm build |
Build your production site to ./dist/ |
pnpm preview |
Preview your build locally, before deploying |
pnpm astro ... |
Run Astro CLI commands like add |
pnpm astro -- --help |
Get help using the Astro CLI |
Command | Action |
---|---|
pnpm lint |
Run all linters |
pnpm lint:formatting |
Run Prettier to check files |
pnpm lint:scripts |
Lint astro and scripts files using ESlint |
pnpm lint:spelling |
Lint spelling errors in all files |
pnpm lint:styles |
Lint astro and styles files using Stylelint |
pnpm lint:types |
Run type-checking with Astro |
pnpm fix |
Run all fixers |
pnpm lint:formatting |
Run Prettier to format files |
pnpm fix:scripts |
Fix astro and scripts files using ESlint |
pnpm fix:styles |
Fix astro and styles files using Stylelint |
Command | Action |
---|---|
pnpm test:e2e |
Run end-to-end tests using Cypress |
pnpm test:e2e:ui |
Run end-to-end tests using the Cypress UI |
pnpm test:unit |
Run unit tests using Vitest |
pnpm test:unit:coverage |
Run unit tests using Vitest and coverage |
pnpm test:unit:watch |
Run unit tests using Vitest and watch |
Command | Action |
---|---|
pnpm ci:version |
Bump version and generate a changelog |
pnpm ci:release |
Create a new release |
- Thanks to @MoustaphaDev for the inspiration for my Dev-only pages feature with astro-dev-only-routes.
- Thanks @Princesseuh, @delucis and @HiDeoo for the inspiration to handle RSS feeds.
- Thanks @Princesseuh for the inspiration for my custom glob loader.
The source code is licensed under the MIT license.