Basic cours support #44

Merged
Anri merged 67 commits from cours into main 2024-04-01 18:11:49 +02:00
16 changed files with 150 additions and 66 deletions
Showing only changes of commit 775c5bb7bc - Show all commits

12
Cargo.lock generated
View file

@ -560,9 +560,9 @@ dependencies = [
[[package]] [[package]]
name = "cached" name = "cached"
version = "0.47.0" version = "0.48.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69b0116662497bc24e4b177c90eaf8870e39e2714c3fcfa296327a93f593fc21" checksum = "355face540df58778b96814c48abb3c2ed67c4878a8087ab1819c1fedeec505f"
dependencies = [ dependencies = [
"ahash 0.8.3", "ahash 0.8.3",
"async-trait", "async-trait",
@ -578,9 +578,9 @@ dependencies = [
[[package]] [[package]]
name = "cached_proc_macro" name = "cached_proc_macro"
version = "0.18.1" version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c878c71c2821aa2058722038a59a67583a4240524687c6028571c9b395ded61f" checksum = "9d52f526f7cbc875b296856ca8c964a9f6290556922c303a8a3883e3c676e6a1"
dependencies = [ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
@ -590,9 +590,9 @@ dependencies = [
[[package]] [[package]]
name = "cached_proc_macro_types" name = "cached_proc_macro_types"
version = "0.1.0" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
[[package]] [[package]]
name = "cc" name = "cc"

View file

@ -12,7 +12,7 @@ license = "AGPL-3.0-or-later"
[dependencies] [dependencies]
actix-web = { version = "4.4", default-features = false, features = ["macros", "compress-brotli"] } actix-web = { version = "4.4", default-features = false, features = ["macros", "compress-brotli"] }
actix-files = "0.6" actix-files = "0.6"
cached = { version = "0.47", features = ["async"] } cached = { version = "0.48", features = ["async"] }
ramhorns = "0.14" ramhorns = "0.14"
toml = "0.8" toml = "0.8"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View file

@ -10,6 +10,7 @@
- [Global configuration](#global-configuration) - [Global configuration](#global-configuration)
- [Link shortener for contacts](#link-shortener-for-contacts) - [Link shortener for contacts](#link-shortener-for-contacts)
- [Add content](#add-content) - [Add content](#add-content)
- [Index](#index)
- [Blog](#blog) - [Blog](#blog)
- [Projects](#projects) - [Projects](#projects)
- [Contacts](#contacts) - [Contacts](#contacts)
@ -139,9 +140,27 @@ option: value
Markdown file Markdown file
``` ```
## Index
Markdown file is stored in `/app/data/index.md`
```
---
name: Option<String>
pronouns: Option<String>
avatar: Option<String>
avatar_caption: Option<String>
---
Index content
```
- If no `name`, the `fullname` used in the configuration will be used
- `avatar` is the link of the avatar
## Blog ## Blog
Markdown files are stored in `/app/data/blog/` Markdown files are stored in `/app/data/blog/posts/`
``` ```
--- ---
@ -158,11 +177,15 @@ Post content
- If no `title`, the filename will be used - If no `title`, the filename will be used
- `date` format is `day-month-year` - `date` format is `day-month-year`
- `publish` is default to false. When false, posts are hidden from index - `publish` is default to false. When false, posts are hidden from index
but accessible, see #30 but accessible, see [#30](https://git.mylloon.fr/Anri/mylloon.fr/issues/30)
### About <!-- omit in toc -->
The file is stored at `/app/data/blog/about.md`.
## Projects ## Projects
Markdown files are stored in `/app/data/projects/` Markdown files are stored in `/app/data/projects/apps/`
``` ```
--- ---
@ -177,7 +200,14 @@ Project description
- If no `link` : the div won't be clickable and will be reported as is to the user - If no `link` : the div won't be clickable and will be reported as is to the user
(no corner-arrow) (no corner-arrow)
- Note that only a handful of [`language`s are supported](./static/css/languages.css). - Note that only a handful of [`language`s are supported](./static/css/languages.css)
You can also put apps in an "Archived" category, in this case, store markdown
files in `archive` subdirectory of `apps`.
### About <!-- omit in toc -->
The file is stored at `/app/data/projects/about.md`.
## Contacts ## Contacts
@ -206,6 +236,18 @@ Custom project description
- `description` will be rendered as HTML "title" (text will appear when cursor - `description` will be rendered as HTML "title" (text will appear when cursor
is hover the link) is hover the link)
Also, contacts are categorized, here is the list of the available categories:
- `socials`
- `forges`
- `others`
For example, `socials` contact files are stored in `/app/data/contacts/socials/`.
### About <!-- omit in toc -->
The file is stored at `/app/data/contacts/about.md`.
## Courses ## Courses
Markdown files are stored in `/app/data/cours/` Markdown files are stored in `/app/data/cours/`

View file

@ -47,7 +47,7 @@ async fn main() -> Result<()> {
.service(agreements::security) .service(agreements::security)
.service(agreements::humans) .service(agreements::humans)
.service(agreements::robots) .service(agreements::robots)
.service(agreements::sitemap) .service(agreements::webmanifest)
.service(blog::index) .service(blog::index)
.service(blog::rss) .service(blog::rss)
.service(blog::page) .service(blog::page)

View file

@ -71,8 +71,29 @@ fn build_robotstxt() -> String {
"User-agent: * Allow: /".into() "User-agent: * Allow: /".into()
} }
#[get("/sitemap.xml")] #[get("/app.webmanifest")]
async fn sitemap() -> impl Responder { async fn webmanifest(config: web::Data<Config>) -> impl Responder {
// TODO HttpResponse::Ok()
actix_web::web::Redirect::to("/") .content_type(ContentType("application/manifest+json".parse().unwrap()))
.body(build_webmanifest(config.get_ref().to_owned()))
}
#[derive(Content, Debug)]
struct WebManifestTemplate {
name: String,
description: String,
url: String,
}
#[once(time = 60)]
fn build_webmanifest(config: Config) -> String {
config.tmpl.render(
"app.webmanifest",
WebManifestTemplate {
name: config.fc.clone().app_name.unwrap(),
description: "Easy WebPage generator".to_owned(),
url: get_url(config.fc),
},
Infos::default(),
)
} }

View file

@ -7,7 +7,7 @@ use ::rss::{
extension::atom::{AtomExtension, Link}, extension::atom::{AtomExtension, Link},
Category, Channel, Guid, Image, Item, Category, Channel, Guid, Image, Item,
}; };
use actix_web::{get, web, HttpResponse, Responder}; use actix_web::{get, http::header::ContentType, web, HttpResponse, Responder};
use cached::proc_macro::once; use cached::proc_macro::once;
use chrono::{DateTime, Datelike, Local, NaiveDateTime, Utc}; use chrono::{DateTime, Datelike, Local, NaiveDateTime, Utc};
use chrono_tz::Europe; use chrono_tz::Europe;
@ -27,6 +27,8 @@ use crate::{
}; };
const MIME_TYPE_RSS: &str = "application/rss+xml"; const MIME_TYPE_RSS: &str = "application/rss+xml";
const BLOG_DIR: &str = "blog";
const POST_DIR: &str = "posts";
#[get("/blog")] #[get("/blog")]
async fn index(config: web::Data<Config>) -> impl Responder { async fn index(config: web::Data<Config>) -> impl Responder {
@ -36,13 +38,19 @@ async fn index(config: web::Data<Config>) -> impl Responder {
#[derive(Content, Debug)] #[derive(Content, Debug)]
struct BlogIndexTemplate { struct BlogIndexTemplate {
navbar: NavBar, navbar: NavBar,
about: Option<File>,
posts: Vec<Post>, posts: Vec<Post>,
no_posts: bool, no_posts: bool,
} }
#[once(time = 60)] #[once(time = 60)]
fn build_index(config: Config) -> String { fn build_index(config: Config) -> String {
let mut posts = get_posts(format!("{}/blog", config.locations.data_dir)); let blog_dir = format!("{}/{}", config.locations.data_dir, BLOG_DIR);
let mut posts = get_posts(format!("{}/{}", blog_dir, POST_DIR));
// Get about
let about: Option<File> =
read_file(&format!("{}/about.md", blog_dir), TypeFileMetadata::Generic);
// Sort from newest to oldest // Sort from newest to oldest
posts.sort_by_cached_key(|p| (p.date.year, p.date.month, p.date.day)); posts.sort_by_cached_key(|p| (p.date.year, p.date.month, p.date.day));
@ -55,6 +63,7 @@ fn build_index(config: Config) -> String {
blog: true, blog: true,
..NavBar::default() ..NavBar::default()
}, },
about,
no_posts: posts.is_empty(), no_posts: posts.is_empty(),
posts, posts,
}, },
@ -82,7 +91,7 @@ struct Post {
impl Post { impl Post {
// Fetch the file content // Fetch the file content
fn fetch_content(&mut self, data_dir: &str) { fn fetch_content(&mut self, data_dir: &str) {
let blog_dir = format!("{}/blog", data_dir); let blog_dir = format!("{}/{}/{}", data_dir, BLOG_DIR, POST_DIR);
let ext = ".md"; let ext = ".md";
if let Some(file) = read_file( if let Some(file) = read_file(
@ -217,7 +226,7 @@ fn get_post(
name: String, name: String,
data_dir: String, data_dir: String,
) -> (Infos, String) { ) -> (Infos, String) {
let blog_dir = format!("{}/blog", data_dir); let blog_dir = format!("{}/{}/{}", data_dir, BLOG_DIR, POST_DIR);
let ext = ".md"; let ext = ".md";
*post = read_file( *post = read_file(
@ -272,13 +281,16 @@ fn get_post(
#[get("/blog/rss")] #[get("/blog/rss")]
async fn rss(config: web::Data<Config>) -> impl Responder { async fn rss(config: web::Data<Config>) -> impl Responder {
HttpResponse::Ok() HttpResponse::Ok()
.append_header(("content-type", MIME_TYPE_RSS)) .content_type(ContentType(MIME_TYPE_RSS.parse().unwrap()))
.body(build_rss(config.get_ref().to_owned())) .body(build_rss(config.get_ref().to_owned()))
} }
#[once(time = 10800)] // 3h #[once(time = 10800)] // 3h
fn build_rss(config: Config) -> String { fn build_rss(config: Config) -> String {
let mut posts = get_posts(format!("{}/blog", config.locations.data_dir)); let mut posts = get_posts(format!(
"{}/{}/{}",
config.locations.data_dir, BLOG_DIR, POST_DIR
));
// Sort from newest to oldest // Sort from newest to oldest
posts.sort_by_cached_key(|p| (p.date.year, p.date.month, p.date.day)); posts.sort_by_cached_key(|p| (p.date.year, p.date.month, p.date.day));

View file

@ -13,6 +13,8 @@ use crate::{
template::{Infos, NavBar}, template::{Infos, NavBar},
}; };
const CONTACT_DIR: &str = "contacts";
pub fn pages(cfg: &mut web::ServiceConfig) { pub fn pages(cfg: &mut web::ServiceConfig) {
// Here define the services used // Here define the services used
let routes = |route_path| { let routes = |route_path| {
@ -76,7 +78,7 @@ fn find_links(directory: String) -> Vec<ContactLink> {
#[get("/{service}/{scope}")] #[get("/{service}/{scope}")]
async fn service_redirection(config: web::Data<Config>, req: HttpRequest) -> impl Responder { async fn service_redirection(config: web::Data<Config>, req: HttpRequest) -> impl Responder {
let info = req.match_info(); let info = req.match_info();
let link = find_links(format!("{}/contacts", config.locations.data_dir)) let link = find_links(format!("{}/{}", config.locations.data_dir, CONTACT_DIR))
.iter() .iter()
// Find requested service // Find requested service
.filter(|&x| x.service == *info.query("service")) .filter(|&x| x.service == *info.query("service"))
@ -105,6 +107,7 @@ async fn service_redirection(config: web::Data<Config>, req: HttpRequest) -> imp
#[derive(Content, Debug)] #[derive(Content, Debug)]
struct NetworksTemplate { struct NetworksTemplate {
navbar: NavBar, navbar: NavBar,
about: Option<File>,
socials_exists: bool, socials_exists: bool,
socials: Vec<File>, socials: Vec<File>,
@ -123,9 +126,15 @@ fn remove_paragraphs(list: &mut [File]) {
#[once(time = 60)] #[once(time = 60)]
fn build_page(config: Config) -> String { fn build_page(config: Config) -> String {
let contacts_dir = format!("{}/contacts", config.locations.data_dir); let contacts_dir = format!("{}/{}", config.locations.data_dir, CONTACT_DIR);
let ext = ".md"; let ext = ".md";
// Get about
let about = read_file(
&format!("{}/about.md", contacts_dir),
TypeFileMetadata::Generic,
);
let socials_dir = "socials"; let socials_dir = "socials";
let mut socials = glob(&format!("{contacts_dir}/{socials_dir}/*{ext}")) let mut socials = glob(&format!("{contacts_dir}/{socials_dir}/*{ext}"))
.unwrap() .unwrap()
@ -156,6 +165,8 @@ fn build_page(config: Config) -> String {
contact: true, contact: true,
..NavBar::default() ..NavBar::default()
}, },
about,
socials_exists: !socials.is_empty(), socials_exists: !socials.is_empty(),
socials, socials,

View file

@ -20,6 +20,7 @@ async fn page(config: web::Data<Config>) -> impl Responder {
#[derive(Content, Debug)] #[derive(Content, Debug)]
struct PortfolioTemplate<'a> { struct PortfolioTemplate<'a> {
navbar: NavBar, navbar: NavBar,
about: Option<File>,
location_apps: Option<&'a str>, location_apps: Option<&'a str>,
apps: Option<Vec<File>>, apps: Option<Vec<File>>,
archived_apps: Option<Vec<File>>, archived_apps: Option<Vec<File>>,
@ -30,22 +31,29 @@ struct PortfolioTemplate<'a> {
#[once(time = 60)] #[once(time = 60)]
fn build_page(config: Config) -> String { fn build_page(config: Config) -> String {
let projects_dir = format!("{}/projects", config.locations.data_dir); let projects_dir = format!("{}/projects", config.locations.data_dir);
let apps_dir = format!("{}/apps", projects_dir);
let ext = ".md"; let ext = ".md";
// Get about
let about = read_file(
&format!("{}/about.md", projects_dir),
TypeFileMetadata::Generic,
);
// Get apps // Get apps
let apps = glob(&format!("{projects_dir}/*{ext}")) let apps = glob(&format!("{apps_dir}/*{ext}"))
.unwrap() .unwrap()
.map(|e| read_file(&e.unwrap().to_string_lossy(), TypeFileMetadata::Portfolio).unwrap()) .map(|e| read_file(&e.unwrap().to_string_lossy(), TypeFileMetadata::Portfolio).unwrap())
.collect::<Vec<File>>(); .collect::<Vec<File>>();
let appdata = if apps.is_empty() { let appdata = if apps.is_empty() {
(None, Some(projects_dir.as_str())) (None, Some(apps_dir.as_str()))
} else { } else {
(Some(apps), None) (Some(apps), None)
}; };
// Get archived apps // Get archived apps
let archived_apps = glob(&format!("{projects_dir}/archive/*{ext}")) let archived_apps = glob(&format!("{apps_dir}/archive/*{ext}"))
.unwrap() .unwrap()
.map(|e| read_file(&e.unwrap().to_string_lossy(), TypeFileMetadata::Portfolio).unwrap()) .map(|e| read_file(&e.unwrap().to_string_lossy(), TypeFileMetadata::Portfolio).unwrap())
.collect::<Vec<File>>(); .collect::<Vec<File>>();
@ -63,6 +71,7 @@ fn build_page(config: Config) -> String {
portfolio: true, portfolio: true,
..NavBar::default() ..NavBar::default()
}, },
about,
apps: appdata.0, apps: appdata.0,
location_apps: appdata.1, location_apps: appdata.1,
archived_apps: archived_appdata.0, archived_apps: archived_appdata.0,

View file

@ -1,15 +1,3 @@
<?xml version="1.0" standalone="no"?> <svg xmlns="http://www.w3.org/2000/svg" width="240" height="240" version="1.0" viewBox="0 0 180 180">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" <path d="M38 2c0 4.7 5.6 11.7 8.5 10.7 1.9-.8 1.9-4.7.1-9.2C45.3.4 44.7 0 41.6 0c-3 0-3.6.4-3.6 2zM122 6c-3.4 3.4-1.7 12.7 2.6 14.4 2.7 1 3.9-1 3.6-6.5-.3-8.9-2.5-11.6-6.2-7.9zM78.9 7.1C76.7 11.2 80.1 19 84 19c2.5 0 3.2-2.6 1.9-7.4-1.8-6.8-4.7-8.7-7-4.5zM0 11.7c0 5.5.3 6.4 2.6 8.2 3.3 2.6 4.1 2.6 5.4 0 1.5-2.8-.1-6.6-4.6-11L0 5.6v6.1zM156.3 11.4c-2 4.3-1.1 8.6 1.8 8.6 2.5 0 5.3-6.2 4.5-9.6-.9-3.6-4.3-3.1-6.3 1zM34.3 40.5c-4.7 3.3-3.7 11.6 1.9 15.8 3.6 2.7 7.4 1.6 9.5-2.7 4.1-8.7-4.2-18.2-11.4-13.1zM131.3 41.8c-1.2.9-2.7 3.2-3.4 4.9-2.3 6.9 5.2 15.3 13.8 15.3 8.4 0 11.9-7 7.6-15.3-3.1-6-13.1-8.7-18-4.9zM86.2 80.2c-1.9 1.9-1.4 4.6 1.6 9.1 1.6 2.3 3.2 5 3.5 6 .4.9 1.8 2.2 3.2 2.9 2.2 1 3 .8 5.3-.9 1.5-1.1 2.8-3.2 3-4.7.9-7.3-12-17-16.6-12.4zM142.8 102.1c-2.9 1.6-2.1 6.3 2.7 15.9 8.2 16.4 6.5 18.9-17.3 24.7-19.2 4.7-43.4 2.9-64.6-4.8-10.8-3.9-16-8.3-18.1-15.2-1.6-5.4-2.7-6.7-6.1-6.7-3.2 0-5.4 3.1-5.4 7.6 0 6.3 5 13.8 12.2 18.4 17.8 11.6 54.6 17.8 78.4 13.4 3-.6 3.4-.2 8.8 7.5 6.1 8.6 8.5 10.6 11.6 9.6 4.3-1.4 3.3-7.3-2.8-15.2-1.3-1.8-2.2-3.9-1.9-4.6.3-.8 3.3-2.4 6.8-3.7 11.1-4.2 15.9-11.1 14.6-21.1-.7-5.4-9.1-22.3-12.3-24.8-3-2.3-4.1-2.5-6.6-1z"/>
width="180.000000pt" height="180.000000pt" viewBox="0 0 180.000000 180.000000" preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,180.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none">
<path d="M380 1780 c0 -47 56 -117 85 -107 19 8 19 47 1 92 -13 31 -19 35 -50 35 -30 0 -36 -4 -36 -20z"/>
<path d="M1220 1740 c-34 -34 -17 -127 26 -144 27 -10 39 10 36 65 -3 89 -25 116 -62 79z"/>
<path d="M789 1729 c-22 -41 12 -119 51 -119 25 0 32 26 19 74 -18 68 -47 87 -70 45z"/>
<path d="M0 1683 c0 -55 3 -64 26 -82 33 -26 41 -26 54 0 15 28 -1 66 -46 110 l-34 33 0 -61z"/>
<path d="M1563 1686 c-20 -43 -11 -86 18 -86 25 0 53 62 45 96 -9 36 -43 31 -63 -10z"/>
<path d="M343 1395 c-47 -33 -37 -116 19 -158 36 -27 74 -16 95 27 41 87 -42 182 -114 131z"/>
<path d="M1313 1382 c-12 -9 -27 -32 -34 -49 -23 -69 52 -153 138 -153 84 0 119 70 76 153 -31 60 -131 87 -180 49z"/>
<path d="M862 998 c-19 -19 -14 -46 16 -91 16 -23 32 -50 35 -60 4 -9 18 -22 32 -29 22 -10 30 -8 53 9 15 11 28 32 30 47 9 73 -120 170 -166 124z"/>
<path d="M1428 779 c-29 -16 -21 -63 27 -159 82 -164 65 -189 -173 -247 -192 -47 -434 -29 -646 48 -108 39 -160 83 -181 152 -16 54 -27 67 -61 67 -32 0 -54 -31 -54 -76 0 -63 50 -138 122 -184 178 -116 546 -178 784 -134 30 6 34 2 88 -75 61 -86 85 -106 116 -96 43 14 33 73 -28 152 -13 18 -22 39 -19 46 3 8 33 24 68 37 111 42 159 111 146 211 -7 54 -91 223 -123 248 -30 23 -41 25 -66 10z"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,15 +0,0 @@
{
"name": "Site Anri K.",
"short_name": "Site Anri K.",
"icons": [
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
],
"theme_color": "#2a2424",
"background_color": "#2a2424",
"start_url": "https://www.mylloon.fr/",
"display": "standalone"
}

21
templates/app.webmanifest Normal file
View file

@ -0,0 +1,21 @@
{
"name": "{{#data}}{{name}}{{/data}}",
"start_url": "{{#data}}{{url}}{{/data}}",
"display": "standalone",
"background_color": "#2a2424",
"description": "{{#data}}{{description}}{{/data}}",
"icons": [
{
"src": "/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
],
"theme_color": "#2a2424",
"related_applications": [
{
"platform": "source",
"url": "https://git.mylloon.fr/Anri/mylloon.fr"
}
]
}

View file

@ -16,7 +16,7 @@
{{#data}} {{#data}}
<h1>Blog</h1> <h1>Blog</h1>
<p>Blog perso, je dis peut-être n'importe quoi 🫶</p> {{#about}} {{&content}} {{/about}}
<a id="rss" href="/blog/rss">Lien vers le flux RSS</a> <a id="rss" href="/blog/rss">Lien vers le flux RSS</a>
{{#no_posts}} {{#no_posts}}

View file

@ -8,9 +8,8 @@
<header>{{>navbar.html}}</header> <header>{{>navbar.html}}</header>
<main> <main>
<h1>Contact</h1> <h1>Contact</h1>
<p>Je suis présent relativement partout sur internet 😸</p> {{#data}}{{#about}} {{&content}} {{/about}} {{#socials_exists}}
{{#data}} {{#socials_exists}}
<h2>Réseaux sociaux</h2> <h2>Réseaux sociaux</h2>
<ul> <ul>
{{#socials}} {{>contact/element.html}} {{/socials}} {{#socials}} {{>contact/element.html}} {{/socials}}

View file

@ -3,6 +3,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="author" href="/humans.txt" /> <link rel="author" href="/humans.txt" />
<link rel="manifest" href="/app.webmanifest" />
{{>icons.html}} {{>metadata.html}} {{>icons.html}} {{>metadata.html}}

View file

@ -16,7 +16,6 @@
sizes="16x16" sizes="16x16"
href="/icons/favicon-16x16.png" href="/icons/favicon-16x16.png"
/> />
<link rel="manifest" href="/icons/site.webmanifest" />
<link rel="mask-icon" href="/icons/safari-pinned-tab.svg" color="#5bbad5" /> <link rel="mask-icon" href="/icons/safari-pinned-tab.svg" color="#5bbad5" />
<link rel="shortcut icon" href="/icons/favicon.ico" /> <link rel="shortcut icon" href="/icons/favicon.ico" />
<meta name="msapplication-TileColor" content="#ffffff" /> <meta name="msapplication-TileColor" content="#ffffff" />

View file

@ -10,14 +10,10 @@
<main> <main>
{{#data}} {{#data}}
<h1>Portfolio</h1> <h1>Portfolio</h1>
<p> {{#about}} {{&content}} {{/about}}
Je programme depuis 2018 et j'ai appris une multitude de langages
depuis. Étant passionné de logiciels libres depuis que je m'y intéresse,
je publie tout sur des forges publiques.
</p>
{{#location_apps}}
<!-- Error message --> <!-- Error message -->
{{#location_apps}}
<p>{{location_apps}} {{err_msg}}</p> <p>{{location_apps}} {{err_msg}}</p>
{{/location_apps}} {{^location_apps}} {{/location_apps}} {{^location_apps}}