Skip to content

Commit 56127ad

Browse files
committedSep 11, 2024··
feat(homepage): build homepage
1 parent 971d92c commit 56127ad

File tree

14 files changed

+539
-137
lines changed

14 files changed

+539
-137
lines changed
 

‎crates/rari-doc/src/build.rs

+25-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ use rari_types::globals::build_out_root;
77
use rayon::iter::{IntoParallelIterator, ParallelIterator};
88
use tracing::{error, span, Level};
99

10-
use crate::cached_readers::{blog_files, curriculum_files, generic_pages_files};
10+
use crate::cached_readers::{
11+
blog_files, contributor_spotlight_files, curriculum_files, generic_pages_files,
12+
};
1113
use crate::error::DocError;
1214
use crate::pages::build::{
1315
build_blog_post, build_contributor_spotlight, build_curriculum, build_doc, build_generic_page,
@@ -43,7 +45,7 @@ pub fn build_single_page(page: &Page) {
4345
serde_json::to_writer(buffed, &built_page).unwrap();
4446

4547
if let Some(in_path) = page.full_path().parent() {
46-
copy_additional_files(in_path, &out_path).unwrap();
48+
copy_additional_files(in_path, &out_path, page.full_path()).unwrap();
4749
}
4850
}
4951
Err(e) => {
@@ -94,3 +96,24 @@ pub fn build_generic_pages() -> Result<Vec<Cow<'static, str>>, DocError> {
9496
})
9597
.collect())
9698
}
99+
100+
pub fn build_contributor_spotlight_pages() -> Result<Vec<Cow<'static, str>>, DocError> {
101+
Ok(contributor_spotlight_files()
102+
.values()
103+
.map(|page| {
104+
build_single_page(page);
105+
Cow::Owned(page.url().to_string())
106+
})
107+
.collect())
108+
}
109+
110+
pub fn build_spas() -> Result<Vec<Cow<'static, str>>, DocError> {
111+
Ok(SPA::all()
112+
.iter()
113+
.filter_map(|(slug, locale)| SPA::from_slug(slug, *locale))
114+
.map(|page| {
115+
build_single_page(&page);
116+
Cow::Owned(page.url().to_string())
117+
})
118+
.collect())
119+
}

‎crates/rari-doc/src/cached_readers.rs

+46-6
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ use std::path::{Path, PathBuf};
44
use std::sync::{Arc, LazyLock, OnceLock, RwLock};
55

66
use rari_types::globals::{
7-
blog_root, cache_content, content_root, content_translated_root, curriculum_root,
8-
generic_pages_root,
7+
blog_root, cache_content, content_root, content_translated_root, contributor_spotlight_root,
8+
curriculum_root, generic_pages_root,
99
};
1010
use rari_types::locale::Locale;
1111
use rari_utils::io::read_to_string;
@@ -15,6 +15,7 @@ use crate::error::DocError;
1515
use crate::html::sidebar::{MetaSidebar, Sidebar};
1616
use crate::pages::page::{Page, PageLike};
1717
use crate::pages::types::blog::{Author, AuthorFrontmatter, BlogPost, BlogPostBuildMeta};
18+
use crate::pages::types::contributors::ContributorSpotlight;
1819
use crate::pages::types::curriculum::{CurriculumIndexEntry, CurriculumPage};
1920
use crate::pages::types::doc::Doc;
2021
use crate::pages::types::generic::GenericPage;
@@ -31,7 +32,8 @@ type SidebarFilesCache = Arc<RwLock<HashMap<(String, Locale), Arc<MetaSidebar>>>
3132
pub static CACHED_SIDEBAR_FILES: LazyLock<SidebarFilesCache> =
3233
LazyLock::new(|| Arc::new(RwLock::new(HashMap::new())));
3334
pub static CACHED_CURRICULUM: OnceLock<CurriculumFiles> = OnceLock::new();
34-
pub static GENERIC_PAGES_FILES: OnceLock<GenericPagesFiles> = OnceLock::new();
35+
pub static GENERIC_PAGES_FILES: OnceLock<UrlToPageMap> = OnceLock::new();
36+
pub static CONTRIBUTOR_SPOTLIGHT_FILES: OnceLock<UrlToPageMap> = OnceLock::new();
3537

3638
#[derive(Debug, Default, Clone)]
3739
pub struct BlogFiles {
@@ -177,6 +179,30 @@ pub fn gather_curriculum() -> Result<CurriculumFiles, DocError> {
177179
}
178180
}
179181

182+
pub fn gather_contributre_spotlight() -> Result<HashMap<String, Page>, DocError> {
183+
if let Some(root) = contributor_spotlight_root() {
184+
Ok(read_docs_parallel::<ContributorSpotlight>(&[root], None)?
185+
.into_iter()
186+
.filter_map(|page| {
187+
if let Page::ContributorSpotlight(cs) = page {
188+
Some(cs)
189+
} else {
190+
None
191+
}
192+
})
193+
.flat_map(|cs| {
194+
Locale::all()
195+
.iter()
196+
.map(|locale| Page::ContributorSpotlight(Arc::new(cs.as_locale(*locale))))
197+
.collect::<Vec<_>>()
198+
})
199+
.map(|page| (page.url().to_ascii_lowercase(), page))
200+
.collect())
201+
} else {
202+
Err(DocError::NoGenericPagesRoot)
203+
}
204+
}
205+
180206
pub fn curriculum_files() -> Cow<'static, CurriculumFiles> {
181207
if cache_content() {
182208
Cow::Borrowed(CACHED_CURRICULUM.get_or_init(|| {
@@ -303,10 +329,10 @@ pub fn read_and_cache_doc_pages() -> Result<Vec<Page>, DocError> {
303329
Ok(docs)
304330
}
305331

306-
pub type GenericPagesFiles = HashMap<String, Page>;
332+
pub type UrlToPageMap = HashMap<String, Page>;
307333

308-
pub fn generic_pages_files() -> Cow<'static, GenericPagesFiles> {
309-
fn gather() -> GenericPagesFiles {
334+
pub fn generic_pages_files() -> Cow<'static, UrlToPageMap> {
335+
fn gather() -> UrlToPageMap {
310336
gather_generic_pages().unwrap_or_else(|e| {
311337
error!("{e}");
312338
Default::default()
@@ -318,3 +344,17 @@ pub fn generic_pages_files() -> Cow<'static, GenericPagesFiles> {
318344
Cow::Owned(gather())
319345
}
320346
}
347+
348+
pub fn contributor_spotlight_files() -> Cow<'static, UrlToPageMap> {
349+
fn gather() -> UrlToPageMap {
350+
gather_contributre_spotlight().unwrap_or_else(|e| {
351+
error!("{e}");
352+
Default::default()
353+
})
354+
}
355+
if cache_content() {
356+
Cow::Borrowed(CONTRIBUTOR_SPOTLIGHT_FILES.get_or_init(gather))
357+
} else {
358+
Cow::Owned(gather())
359+
}
360+
}

‎crates/rari-doc/src/html/rewriter.rs

+23-14
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use rari_l10n::l10n_json_data;
77
use rari_md::node_card::NoteCard;
88
use rari_types::fm_types::PageType;
99
use rari_types::locale::Locale;
10+
use tracing::warn;
1011
use url::Url;
1112

1213
use crate::error::DocError;
@@ -85,20 +86,28 @@ pub fn post_process_html<T: PageLike>(
8586
el.set_attribute("src", url.path())?;
8687
let file = page.full_path().parent().unwrap().join(&src);
8788
let (width, height) = if src.ends_with(".svg") {
88-
let meta = svg_metadata::Metadata::parse_file(&file)?;
89-
(
90-
meta.width
91-
.map(|width| width.width)
92-
.or(meta.view_box.map(|vb| vb.width))
93-
.map(|width| format!("{:.0}", width)),
94-
meta.height
95-
.map(|height| height.height)
96-
.or(meta.view_box.map(|vb| vb.height))
97-
.map(|height| format!("{:.0}", height)),
98-
)
99-
} else {
100-
let dim = imagesize::size(&file)?;
89+
match svg_metadata::Metadata::parse_file(&file) {
90+
Ok(meta) => (
91+
meta.width
92+
.map(|width| width.width)
93+
.or(meta.view_box.map(|vb| vb.width))
94+
.map(|width| format!("{:.0}", width)),
95+
meta.height
96+
.map(|height| height.height)
97+
.or(meta.view_box.map(|vb| vb.height))
98+
.map(|height| format!("{:.0}", height)),
99+
),
100+
Err(e) => {
101+
warn!("Error parsing {}: {e}", file.display());
102+
(None, None)
103+
}
104+
}
105+
} else if let Ok(dim) = imagesize::size(&file)
106+
.inspect_err(|e| warn!("Error opening {}: {e}", file.display()))
107+
{
101108
(Some(dim.width.to_string()), Some(dim.height.to_string()))
109+
} else {
110+
(None, None)
102111
};
103112
if let Some(width) = width {
104113
el.set_attribute("width", &width)?;
@@ -143,7 +152,7 @@ pub fn post_process_html<T: PageLike>(
143152
el.set_attribute("aria-current", "page")?;
144153
}
145154
if !Page::exists(resolved_href_no_hash) && !Page::ignore(href) {
146-
tracing::info!("{resolved_href_no_hash} {href}");
155+
tracing::debug!("{resolved_href_no_hash} {href}");
147156
let class = el.get_attribute("class").unwrap_or_default();
148157
el.set_attribute(
149158
"class",

‎crates/rari-doc/src/pages/build.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ pub fn build_content<T: PageLike>(page: &T) -> Result<PageContent, DocError> {
157157
let encoded_html = render_md_to_html(&ks_rendered_doc, page.locale())?;
158158
let html = decode_ref(&encoded_html, &templs)?;
159159
let post_processed_html = post_process_html(&html, page, false)?;
160+
160161
let mut fragment = Html::parse_fragment(&post_processed_html);
161162
if page.page_type() == PageType::Curriculum {
162163
bubble_up_curriculum_page(&mut fragment)?;
@@ -322,7 +323,8 @@ pub fn build_blog_post(post: &BlogPost) -> Result<BuiltDocy, DocError> {
322323
}
323324

324325
pub fn build_generic_page(page: &GenericPage) -> Result<BuiltDocy, DocError> {
325-
let PageContent { body, toc, .. } = build_content(page)?;
326+
let built = build_content(page);
327+
let PageContent { body, toc, .. } = built?;
326328
Ok(BuiltDocy::GenericPage(Box::new(JsonGenericPage {
327329
hy_data: JsonGenericHyData {
328330
sections: body,
@@ -399,11 +401,11 @@ pub fn build_contributor_spotlight(cs: &ContributorSpotlight) -> Result<BuiltDoc
399401
)))
400402
}
401403

402-
pub fn copy_additional_files(from: &Path, to: &Path) -> Result<(), DocError> {
404+
pub fn copy_additional_files(from: &Path, to: &Path, ignore: &Path) -> Result<(), DocError> {
403405
for from in fs::read_dir(from)?
404406
.filter_map(Result::ok)
405407
.map(|f| f.path())
406-
.filter(|p| p.is_file())
408+
.filter(|p| p.is_file() && p != ignore)
407409
{
408410
if let Some(filename) = from.file_name() {
409411
let to = to.to_path_buf().join(filename);

‎crates/rari-doc/src/pages/json.rs

+68-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
use std::path::PathBuf;
22

3-
use chrono::NaiveDateTime;
3+
use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
44
use rari_data::baseline::SupportStatusWithByKey;
55
use rari_types::locale::{Locale, Native};
66
use serde::{Deserialize, Serialize};
77

88
use super::types::contributors::Usernames;
99
use super::types::curriculum::{CurriculumIndexEntry, CurriculumSidebarEntry, Template, Topic};
1010
use crate::pages::types::blog::BlogMeta;
11-
use crate::pages::types::spa::BlogIndex;
1211
use crate::specs::Specification;
1312
use crate::utils::modified_dt;
1413

@@ -117,6 +116,11 @@ pub struct JsonDoc {
117116
pub browser_compat: Vec<String>,
118117
}
119118

119+
#[derive(Debug, Clone, Serialize)]
120+
pub struct BlogIndex {
121+
pub posts: Vec<BlogMeta>,
122+
}
123+
120124
#[derive(Debug, Clone, Serialize)]
121125
#[serde(untagged)]
122126
pub enum HyData {
@@ -246,6 +250,7 @@ pub enum BuiltDocy {
246250
ContributorSpotlight(Box<JsonContributorSpotlight>),
247251
BasicSPA(Box<JsonBasicSPA>),
248252
GenericPage(Box<JsonGenericPage>),
253+
HomePageSPA(Box<JsonHomePageSPA>),
249254
}
250255

251256
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
@@ -285,12 +290,72 @@ pub struct UrlNTitle {
285290
pub struct JsonBasicSPA {
286291
pub slug: &'static str,
287292
pub page_title: &'static str,
288-
pub page_description: &'static str,
293+
pub page_description: Option<&'static str>,
289294
pub only_follow: bool,
290295
pub no_indexing: bool,
291296
pub url: String,
292297
}
293298

299+
#[derive(Debug, Clone, Serialize)]
300+
#[serde(rename_all = "camelCase")]
301+
pub struct HomePageFeaturedArticle {
302+
pub mdn_url: String,
303+
pub summay: String,
304+
pub title: String,
305+
pub tag: Option<Parent>,
306+
}
307+
308+
#[derive(Debug, Clone, Serialize)]
309+
#[serde(rename_all = "camelCase")]
310+
pub struct HomePageFeaturedContributor {
311+
pub contributor_name: String,
312+
pub url: String,
313+
pub quote: String,
314+
}
315+
316+
#[derive(Debug, Clone, Serialize)]
317+
pub struct NameUrl {
318+
pub name: String,
319+
pub url: String,
320+
}
321+
322+
#[derive(Debug, Clone, Serialize)]
323+
pub struct HomePageLatestNewsItem {
324+
pub url: String,
325+
pub title: String,
326+
pub author: Option<String>,
327+
pub source: NameUrl,
328+
pub published_at: NaiveDate,
329+
}
330+
331+
#[derive(Debug, Clone, Serialize)]
332+
pub struct HomePageRecentContribution {
333+
pub number: i64,
334+
pub title: String,
335+
pub updated_at: DateTime<Utc>,
336+
pub url: String,
337+
pub repo: NameUrl,
338+
}
339+
340+
#[derive(Debug, Clone, Serialize)]
341+
pub struct ItemContainer<T>
342+
where
343+
T: Clone + Serialize,
344+
{
345+
pub items: Vec<T>,
346+
}
347+
#[derive(Debug, Clone, Serialize)]
348+
#[serde(rename_all = "camelCase")]
349+
pub struct JsonHomePageSPA {
350+
pub slug: &'static str,
351+
pub page_title: &'static str,
352+
pub page_description: Option<&'static str>,
353+
pub featured_articles: Vec<HomePageFeaturedArticle>,
354+
pub featured_contributor: Option<HomePageFeaturedContributor>,
355+
pub latest_news: ItemContainer<HomePageLatestNewsItem>,
356+
pub recent_contributions: ItemContainer<HomePageRecentContribution>,
357+
}
358+
294359
#[derive(Debug, Clone, Serialize)]
295360
#[serde(rename_all = "camelCase")]
296361
pub struct JsonGenericHyData {

‎crates/rari-doc/src/pages/types/contributors.rs

+18-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ use std::path::{Path, PathBuf};
22
use std::sync::Arc;
33

44
use concat_in_place::strcat;
5-
use constcat::concat;
65
use rari_md::m2h;
76
use rari_types::error::EnvError;
87
use rari_types::fm_types::{FeatureStatus, PageType};
@@ -135,6 +134,23 @@ pub struct ContributorSpotlight {
135134
content_start: usize,
136135
}
137136

137+
impl ContributorSpotlight {
138+
pub fn as_locale(&self, locale: Locale) -> Self {
139+
let Self {
140+
mut meta,
141+
raw,
142+
content_start,
143+
} = self.clone();
144+
meta.locale = locale;
145+
meta.url = strcat!("/" locale.as_url_str() "/community/" meta.slug.as_str());
146+
Self {
147+
meta,
148+
raw,
149+
content_start,
150+
}
151+
}
152+
}
153+
138154
impl PageReader for ContributorSpotlight {
139155
fn read(path: impl Into<PathBuf>, locale: Option<Locale>) -> Result<Page, DocError> {
140156
read_contributor_spotlight(path, locale.unwrap_or_default())
@@ -206,7 +222,7 @@ impl PageLike for ContributorSpotlight {
206222
}
207223

208224
fn base_slug(&self) -> &str {
209-
concat!("/", Locale::EnUs.as_url_str(), "/")
225+
"/en-US/"
210226
}
211227

212228
fn trailing_slash(&self) -> bool {

‎crates/rari-doc/src/pages/types/generic.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,14 @@ impl GenericPage {
9292
}
9393

9494
pub fn as_locale(&self, locale: Locale) -> Self {
95-
let GenericPage {
95+
let Self {
9696
mut meta,
9797
raw,
9898
content_start,
9999
} = self.clone();
100100
meta.locale = locale;
101101
meta.url = strcat!("/" locale.as_url_str() "/" meta.slug.as_str());
102-
GenericPage {
102+
Self {
103103
meta,
104104
raw,
105105
content_start,

‎crates/rari-doc/src/pages/types/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ pub mod curriculum;
44
pub mod doc;
55
pub mod generic;
66
pub mod spa;
7+
pub mod spa_homepage;

‎crates/rari-doc/src/pages/types/spa.rs

+140-77
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,33 @@ use concat_in_place::strcat;
66
use constcat::concat;
77
use phf::{phf_map, Map};
88
use rari_types::fm_types::{FeatureStatus, PageType};
9+
use rari_types::globals::content_translated_root;
910
use rari_types::locale::Locale;
1011
use rari_types::RariEnv;
11-
use serde::Serialize;
1212

13+
use super::spa_homepage::{
14+
featured_articles, featured_contributor, lastet_news, recent_contributions,
15+
};
1316
use crate::cached_readers::blog_files;
1417
use crate::error::DocError;
15-
use crate::pages::json::{BuiltDocy, HyData, JsonBasicSPA, JsonBlogPost, JsonBlogPostDoc};
18+
use crate::pages::json::{
19+
BlogIndex, BuiltDocy, HyData, ItemContainer, JsonBasicSPA, JsonBlogPost, JsonBlogPostDoc,
20+
JsonHomePageSPA,
21+
};
1622
use crate::pages::page::{Page, PageLike, PageReader};
1723
use crate::pages::title::page_title;
1824
use crate::pages::types::blog::BlogMeta;
1925

20-
#[derive(Debug, Clone, Serialize)]
21-
pub struct BlogIndex {
22-
pub posts: Vec<BlogMeta>,
26+
#[derive(Debug, Clone, Copy)]
27+
pub struct BasicSPA {
28+
pub only_follow: bool,
29+
pub no_indexing: bool,
2330
}
2431

25-
#[derive(Debug, Clone)]
32+
#[derive(Debug, Copy, Clone)]
2633
pub enum SPAData {
27-
BlogIndex(BlogIndex),
34+
BlogIndex,
35+
HomePage,
2836
BasicSPA(BasicSPA),
2937
}
3038

@@ -35,8 +43,9 @@ pub struct SPA {
3543
pub url: String,
3644
pub locale: Locale,
3745
pub page_type: PageType,
38-
pub typ: SPAData,
46+
pub data: SPAData,
3947
pub base_slug: Cow<'static, str>,
48+
pub page_description: Option<&'static str>,
4049
}
4150
impl SPA {
4251
pub fn from_url(url: &str) -> Option<Page> {
@@ -45,50 +54,46 @@ impl SPA {
4554
_ => None,
4655
}
4756
}
57+
4858
pub fn from_slug(slug: &str, locale: Locale) -> Option<Page> {
49-
if let Some(basic_spa) = BASIC_SPAS.get(slug) {
50-
return Some(Page::SPA(Arc::new(SPA {
51-
page_title: basic_spa.page_title,
52-
slug: basic_spa.slug,
53-
url: strcat!("/" locale.as_url_str() basic_spa.slug),
59+
BASIC_SPAS.get(slug).and_then(|build_spa| {
60+
if build_spa.en_us_only && locale != Locale::EnUs { None } else {
61+
Some(Page::SPA(Arc::new(SPA {
62+
page_title: build_spa.page_title,
63+
slug: build_spa.slug,
64+
url: strcat!("/" locale.as_url_str() "/" build_spa.slug if build_spa.trailing_slash { "/" } else { "" }),
5465
locale,
5566
page_type: PageType::SPA,
56-
typ: SPAData::BasicSPA(*basic_spa),
67+
data: build_spa.data,
5768
base_slug: Cow::Owned(strcat!("/" locale.as_url_str() "/")),
58-
})));
59-
}
60-
match (slug, locale) {
61-
("blog" | "blog/", Locale::EnUs) => Some(Page::SPA(Arc::new(SPA {
62-
page_title: "MDN Blog",
63-
slug: "blog",
64-
url: "/en-US/blog/".to_string(),
65-
locale: Locale::EnUs,
66-
page_type: PageType::SPA,
67-
typ: SPAData::BlogIndex(BlogIndex {
68-
posts: blog_files()
69-
.sorted_meta
70-
.iter()
71-
.rev()
72-
.map(BlogMeta::from)
73-
.map(|mut m| {
74-
m.links = Default::default();
75-
m
76-
})
77-
.collect(),
78-
}),
79-
base_slug: Cow::Borrowed(concat!("/", Locale::EnUs.as_url_str(), "/")),
80-
}))),
81-
_ => None,
82-
}
69+
page_description: build_spa.page_description,
70+
})))}
71+
})
8372
}
8473

8574
pub fn is_spa(slug: &str, locale: Locale) -> bool {
86-
BASIC_SPAS.contains_key(slug) || matches!((slug, locale), ("blog" | "blog/", Locale::EnUs))
75+
BASIC_SPAS
76+
.get(slug)
77+
.map(|build_spa| locale == Default::default() || !build_spa.en_us_only)
78+
.unwrap_or_default()
79+
}
80+
81+
pub fn all() -> Vec<(&'static &'static str, Locale)> {
82+
BASIC_SPAS
83+
.entries()
84+
.flat_map(|(slug, build_spa)| {
85+
if build_spa.en_us_only || content_translated_root().is_none() {
86+
vec![(slug, Locale::EnUs)]
87+
} else {
88+
Locale::all().iter().map(|locale| (slug, *locale)).collect()
89+
}
90+
})
91+
.collect()
8792
}
8893

8994
pub fn as_built_doc(&self) -> Result<BuiltDocy, DocError> {
90-
match &self.typ {
91-
SPAData::BlogIndex(b) => Ok(BuiltDocy::BlogPost(Box::new(JsonBlogPost {
95+
match &self.data {
96+
SPAData::BlogIndex => Ok(BuiltDocy::BlogPost(Box::new(JsonBlogPost {
9297
doc: JsonBlogPostDoc {
9398
title: self.title().to_string(),
9499
mdn_url: self.url().to_owned(),
@@ -100,18 +105,55 @@ impl SPA {
100105
url: self.url().to_owned(),
101106
locale: self.locale(),
102107
blog_meta: None,
103-
hy_data: Some(HyData::BlogIndex(b.clone())),
108+
hy_data: Some(HyData::BlogIndex(BlogIndex {
109+
posts: blog_files()
110+
.sorted_meta
111+
.iter()
112+
.rev()
113+
.map(BlogMeta::from)
114+
.map(|mut m| {
115+
m.links = Default::default();
116+
m
117+
})
118+
.collect(),
119+
})),
104120
page_title: self.title().to_owned(),
105121
..Default::default()
106122
}))),
107123
SPAData::BasicSPA(basic_spa) => Ok(BuiltDocy::BasicSPA(Box::new(JsonBasicSPA {
108124
slug: self.slug,
109125
page_title: self.page_title,
110-
page_description: basic_spa.page_description,
126+
page_description: self.page_description,
111127
only_follow: basic_spa.only_follow,
112128
no_indexing: basic_spa.no_indexing,
113129
url: strcat!(self.base_slug.as_ref() self.slug),
114130
}))),
131+
SPAData::HomePage => Ok(BuiltDocy::HomePageSPA(Box::new(JsonHomePageSPA {
132+
slug: self.slug,
133+
page_title: self.page_title,
134+
page_description: self.page_description,
135+
featured_articles: featured_articles(
136+
&[
137+
"/en-US/blog/mdn-scrimba-partnership/",
138+
"/en-US/blog/learn-javascript-console-methods/",
139+
"/en-US/blog/introduction-to-web-sustainability/",
140+
"/en-US/docs/Web/API/CSS_Custom_Highlight_API",
141+
],
142+
self.locale,
143+
)?,
144+
featured_contributor: featured_contributor(self.locale)?,
145+
latest_news: ItemContainer {
146+
items: lastet_news(&[
147+
"/en-US/blog/mdn-scrimba-partnership/",
148+
"/en-US/blog/mdn-http-observatory-launch/",
149+
"/en-US/blog/mdn-curriculum-launch/",
150+
"/en-US/blog/baseline-evolution-on-mdn/",
151+
])?,
152+
},
153+
recent_contributions: ItemContainer {
154+
items: recent_contributions()?,
155+
},
156+
}))),
115157
}
116158
}
117159
}
@@ -184,115 +226,136 @@ impl PageLike for SPA {
184226
}
185227
}
186228

187-
#[derive(Debug, Clone, Copy, Default)]
188-
pub struct BasicSPA {
229+
#[derive(Debug, Clone, Copy)]
230+
pub struct BuildSPA {
189231
pub slug: &'static str,
190232
pub page_title: &'static str,
191-
pub page_description: &'static str,
192-
pub only_follow: bool,
193-
pub no_indexing: bool,
233+
pub page_description: Option<&'static str>,
234+
pub trailing_slash: bool,
235+
pub en_us_only: bool,
236+
pub data: SPAData,
194237
}
195238

196-
const DEFAULT_BASIC_SPA: BasicSPA = BasicSPA {
239+
const DEFAULT_BASIC_SPA: BuildSPA = BuildSPA {
197240
slug: "",
198241
page_title: "",
199-
page_description: "",
200-
only_follow: false,
201-
no_indexing: false,
242+
page_description: None,
243+
trailing_slash: false,
244+
en_us_only: false,
245+
data: SPAData::BasicSPA(BasicSPA {
246+
only_follow: false,
247+
no_indexing: false,
248+
}),
202249
};
203250

204251
const MDN_PLUS_TITLE: &str = "MDN Plus";
205252
const OBSERVATORY_TITLE_FULL: &str = "HTTP Observatory | MDN";
206253

207-
const OBSERVATORY_DESCRIPTION: &str =
208-
"Test your site’s HTTP headers, including CSP and HSTS, to find security problems and get actionable recommendations to make your website more secure. Test other websites to see how you compare.";
254+
const OBSERVATORY_DESCRIPTION: Option<&str> =
255+
Some("Test your site’s HTTP headers, including CSP and HSTS, to find security problems and get actionable recommendations to make your website more secure. Test other websites to see how you compare.");
209256

210-
static BASIC_SPAS: Map<&'static str, BasicSPA> = phf_map!(
211-
"play" => BasicSPA {
257+
static BASIC_SPAS: Map<&'static str, BuildSPA> = phf_map!(
258+
"" => BuildSPA {
259+
slug: "",
260+
page_title: "MDN Web Docs",
261+
page_description: None,
262+
trailing_slash: true,
263+
data: SPAData::HomePage,
264+
..DEFAULT_BASIC_SPA
265+
},
266+
"blog" => BuildSPA {
267+
slug: "blog",
268+
page_title: "MDN Blog",
269+
page_description: None,
270+
trailing_slash: true,
271+
en_us_only: true,
272+
data: SPAData::BlogIndex
273+
},
274+
"play" => BuildSPA {
212275
slug: "play",
213276
page_title: "Playground | MDN",
214277
..DEFAULT_BASIC_SPA
215278
},
216-
"observatory" => BasicSPA {
279+
"observatory" => BuildSPA {
217280
slug: "observatory",
218281
page_title: concat!("HTTP Header Security Test - ", OBSERVATORY_TITLE_FULL),
219282
page_description: OBSERVATORY_DESCRIPTION,
220283
..DEFAULT_BASIC_SPA
221284
},
222-
"observatory/analyze" => BasicSPA {
285+
"observatory/analyze" => BuildSPA {
223286
slug: "observatory/analyze",
224287
page_title: concat!("Scan results - ", OBSERVATORY_TITLE_FULL),
225288
page_description: OBSERVATORY_DESCRIPTION,
226-
no_indexing: true,
289+
data: SPAData::BasicSPA(BasicSPA { no_indexing: true, only_follow: false }),
227290
..DEFAULT_BASIC_SPA
228291
},
229-
"observatory/docs/tests_and_scoring" => BasicSPA {
292+
"observatory/docs/tests_and_scoring" => BuildSPA {
230293
slug: "observatory/docs/tests_and_scoring",
231294
page_title: concat!("Tests & Scoring - ", OBSERVATORY_TITLE_FULL),
232295
page_description: OBSERVATORY_DESCRIPTION,
233296
..DEFAULT_BASIC_SPA
234297
},
235-
"observatory/docs/faq" => BasicSPA {
298+
"observatory/docs/faq" => BuildSPA {
236299
slug: "observatory/docs/faq",
237300
page_title: concat!("FAQ - ", OBSERVATORY_TITLE_FULL),
238301
page_description: OBSERVATORY_DESCRIPTION,
239302
..DEFAULT_BASIC_SPA
240303
},
241-
"search" => BasicSPA {
304+
"search" => BuildSPA {
242305
slug: "search",
243306
page_title: "Search",
244-
only_follow: true,
307+
data: SPAData::BasicSPA(BasicSPA { only_follow: true, no_indexing: false }),
245308
..DEFAULT_BASIC_SPA
246309
},
247-
"plus" => BasicSPA {
310+
"plus" => BuildSPA {
248311
slug: "plus",
249312
page_title: MDN_PLUS_TITLE,
250313
..DEFAULT_BASIC_SPA
251314
},
252-
"plus/ai-help" => BasicSPA {
315+
"plus/ai-help" => BuildSPA {
253316
slug: "plus/ai-help",
254317
page_title: concat!("AI Help | ", MDN_PLUS_TITLE),
255318
..DEFAULT_BASIC_SPA
256319
},
257-
"plus/collections" => BasicSPA {
320+
"plus/collections" => BuildSPA {
258321
slug: "plus/collections",
259322
page_title: concat!("Collections | ", MDN_PLUS_TITLE),
260-
no_indexing: true,
323+
data: SPAData::BasicSPA(BasicSPA { no_indexing: true, only_follow: false }),
261324
..DEFAULT_BASIC_SPA
262325
},
263-
"plus/collections/frequently_viewed" => BasicSPA {
326+
"plus/collections/frequently_viewed" => BuildSPA {
264327
slug: "plus/collections/frequently_viewed",
265328
page_title: concat!("Frequently viewed articles | ", MDN_PLUS_TITLE),
266-
no_indexing: true,
329+
data: SPAData::BasicSPA(BasicSPA { no_indexing: true, only_follow: false }),
267330
..DEFAULT_BASIC_SPA
268331
},
269-
"plus/updates" => BasicSPA {
332+
"plus/updates" => BuildSPA {
270333
slug: "plus/updates",
271334
page_title: concat!("Updates | ", MDN_PLUS_TITLE),
272335
..DEFAULT_BASIC_SPA
273336
},
274-
"plus/settings" => BasicSPA {
337+
"plus/settings" => BuildSPA {
275338
slug: "plus/settings",
276339
page_title: concat!("Settings | ", MDN_PLUS_TITLE),
277-
no_indexing: true,
340+
data: SPAData::BasicSPA(BasicSPA { no_indexing: true, only_follow: false }),
278341
..DEFAULT_BASIC_SPA
279342
},
280-
"about" => BasicSPA {
343+
"about" => BuildSPA {
281344
slug: "about",
282345
page_title: "About MDN",
283346
..DEFAULT_BASIC_SPA
284347
},
285-
"community" => BasicSPA {
348+
"community" => BuildSPA {
286349
slug: "community",
287350
page_title: "Contribute to MDN",
288351
..DEFAULT_BASIC_SPA
289352
},
290-
"advertising" => BasicSPA {
353+
"advertising" => BuildSPA {
291354
slug: "advertising",
292355
page_title: "Advertise with us",
293356
..DEFAULT_BASIC_SPA
294357
},
295-
"newsletter" => BasicSPA {
358+
"newsletter" => BuildSPA {
296359
slug: "newsletter",
297360
page_title: "Stay Informed with MDN",
298361
..DEFAULT_BASIC_SPA
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
use std::path::Path;
2+
use std::process::Command;
3+
use std::sync::LazyLock;
4+
5+
use chrono::{DateTime, Utc};
6+
use concat_in_place::strcat;
7+
use rari_types::globals::{content_root, content_translated_root};
8+
use rari_types::locale::Locale;
9+
use regex::Regex;
10+
11+
use crate::cached_readers::contributor_spotlight_files;
12+
use crate::error::DocError;
13+
use crate::helpers::summary_hack::get_hacky_summary_md;
14+
use crate::pages::json::{
15+
HomePageFeaturedArticle, HomePageFeaturedContributor, HomePageLatestNewsItem,
16+
HomePageRecentContribution, NameUrl, Parent,
17+
};
18+
use crate::pages::page::{url_path_to_page_with_other_locale_and_fallback, Page, PageLike};
19+
use crate::pages::parents::parents;
20+
21+
pub fn lastet_news(urls: &[&str]) -> Result<Vec<HomePageLatestNewsItem>, DocError> {
22+
urls.iter()
23+
.filter_map(|url| match Page::page_from_url_path(url) {
24+
Ok(Page::BlogPost(post)) => Some(Ok(HomePageLatestNewsItem {
25+
url: post.url().to_string(),
26+
title: post.title().to_string(),
27+
author: Some(post.meta.author.clone()),
28+
source: NameUrl {
29+
name: "developer.mozilla.org".to_string(),
30+
url: strcat!("/" Locale::default().as_url_str() "/blog/"),
31+
},
32+
published_at: post.meta.date,
33+
})),
34+
Err(e) => Some(Err(e)),
35+
x => {
36+
tracing::debug!("{x:?}");
37+
None
38+
}
39+
})
40+
.collect()
41+
}
42+
43+
pub fn featured_articles(
44+
urls: &[&str],
45+
locale: Locale,
46+
) -> Result<Vec<HomePageFeaturedArticle>, DocError> {
47+
urls.iter()
48+
.filter_map(|url| {
49+
match url_path_to_page_with_other_locale_and_fallback(url, Some(locale)) {
50+
Ok(Page::BlogPost(post)) => Some(Ok(HomePageFeaturedArticle {
51+
mdn_url: post.url().to_string(),
52+
summay: post.meta.description.clone(),
53+
title: post.title().to_string(),
54+
tag: Some(Parent {
55+
uri: strcat!("/" Locale::default().as_url_str() "/blog/"),
56+
title: "Blog".to_string(),
57+
}),
58+
})),
59+
Ok(ref page @ Page::Doc(ref doc)) => Some(Ok(HomePageFeaturedArticle {
60+
mdn_url: doc.url().to_string(),
61+
summay: get_hacky_summary_md(page).unwrap_or_default(),
62+
title: doc.title().to_string(),
63+
tag: parents(page).get(1).cloned(),
64+
})),
65+
Err(e) => Some(Err(e)),
66+
x => {
67+
tracing::debug!("{x:?}");
68+
None
69+
}
70+
}
71+
})
72+
.collect()
73+
}
74+
75+
pub fn recent_contributions() -> Result<Vec<HomePageRecentContribution>, DocError> {
76+
let mut content = recent_contributions_from_git(content_root(), "mdn/content")?;
77+
if let Some(translated_root) = content_translated_root() {
78+
content.extend(recent_contributions_from_git(
79+
translated_root,
80+
"mdn/translated_content",
81+
)?);
82+
};
83+
content.sort_by(|a, b| a.updated_at.cmp(&b.updated_at));
84+
Ok(content)
85+
}
86+
87+
static GIT_LOG_LINE: LazyLock<Regex> =
88+
LazyLock::new(|| Regex::new(r#"^(?<date>[^ ]+) (?<msg>.*[^\)])( \(#(?<pr>\d+)\))?$"#).unwrap());
89+
90+
fn recent_contributions_from_git(
91+
path: &Path,
92+
repo: &str,
93+
) -> Result<Vec<HomePageRecentContribution>, DocError> {
94+
let output = Command::new("git")
95+
.args(["rev-parse", "--show-toplevel"])
96+
.current_dir(path)
97+
.output()
98+
.expect("failed to execute git rev-parse");
99+
100+
let repo_root_raw = String::from_utf8_lossy(&output.stdout);
101+
let repo_root = repo_root_raw.trim();
102+
103+
let output = Command::new("git")
104+
.args([
105+
"log",
106+
"--no-merges",
107+
"--pretty=format:%aI %s",
108+
"-n 10",
109+
"-z",
110+
])
111+
.current_dir(repo_root)
112+
.output()
113+
.expect("failed to execute process");
114+
115+
let output_str = String::from_utf8_lossy(&output.stdout);
116+
Ok(output_str
117+
.split(['\0'])
118+
.filter_map(|line| {
119+
GIT_LOG_LINE.captures(line.trim()).and_then(|cap| {
120+
match (cap.name("date"), cap.name("msg"), cap.name("pr")) {
121+
(Some(date), Some(msg), Some(pr)) => Some(HomePageRecentContribution {
122+
number: pr.as_str().parse::<i64>().unwrap_or_default(),
123+
title: msg.as_str().to_string(),
124+
updated_at: date.as_str().parse::<DateTime<Utc>>().unwrap_or_default(),
125+
url: strcat!("https://github.com/" repo "/pull/" pr.as_str()),
126+
repo: NameUrl {
127+
name: repo.to_string(),
128+
url: strcat!("https://github.com/" repo),
129+
},
130+
}),
131+
_ => None,
132+
}
133+
})
134+
})
135+
.take(5)
136+
.collect())
137+
}
138+
139+
pub fn featured_contributor(
140+
locale: Locale,
141+
) -> Result<Option<HomePageFeaturedContributor>, DocError> {
142+
Ok(contributor_spotlight_files()
143+
.values()
144+
.find_map(|cs| {
145+
if let Page::ContributorSpotlight(cs) = cs {
146+
if cs.meta.is_featured && cs.locale() == locale {
147+
Some(cs)
148+
} else {
149+
None
150+
}
151+
} else {
152+
None
153+
}
154+
})
155+
.map(|cs| HomePageFeaturedContributor {
156+
contributor_name: cs.meta.contributor_name.clone(),
157+
url: cs.url().to_string(),
158+
quote: cs.meta.quote.clone(),
159+
}))
160+
}

‎crates/rari-doc/src/resolve.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ pub fn url_path_to_path_buf(
3737
let tail: Vec<_> = split.collect();
3838
let (typ, slug) = match tail.as_slice() {
3939
["docs", tail] => (PageCategory::Doc, *tail),
40-
["blog"] if locale == Default::default() => (PageCategory::SPA, Default::default()),
40+
["blog"] | ["blog", ""] if locale == Default::default() => (PageCategory::SPA, "blog"),
4141
["blog", tail] if locale == Default::default() => (PageCategory::BlogPost, *tail),
4242
["curriculum", tail] if locale == Default::default() => (PageCategory::Curriculum, *tail),
4343
["community", tail] if locale == Default::default() && tail.starts_with("spotlight") => {
@@ -47,7 +47,6 @@ pub fn url_path_to_path_buf(
4747
_ => {
4848
let (_, slug) = strip_locale_from_url(url_path);
4949
let slug = slug.strip_prefix('/').unwrap_or(slug);
50-
println!("{slug}");
5150
if SPA::is_spa(slug, locale) {
5251
(PageCategory::SPA, slug)
5352
} else if GenericPage::is_generic(slug, locale) {

‎crates/rari-types/src/locale.rs

+18-12
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize};
55
use serde_variant::to_variant_name;
66
use thiserror::Error;
77

8+
use crate::globals::content_translated_root;
9+
810
#[derive(PartialEq, Debug, Clone, Copy, Deserialize, Serialize, Default, PartialOrd, Eq, Ord)]
911
pub enum Native {
1012
#[default]
@@ -108,18 +110,22 @@ impl Locale {
108110
}
109111
}
110112

111-
pub const fn all() -> &'static [Self] {
112-
&[
113-
Self::EnUs,
114-
Self::Es,
115-
Self::Fr,
116-
Self::Ja,
117-
Self::Ko,
118-
Self::PtBr,
119-
Self::Ru,
120-
Self::ZhCn,
121-
Self::ZhTw,
122-
]
113+
pub fn all() -> &'static [Self] {
114+
if content_translated_root().is_some() {
115+
&[
116+
Self::EnUs,
117+
Self::Es,
118+
Self::Fr,
119+
Self::Ja,
120+
Self::Ko,
121+
Self::PtBr,
122+
Self::Ru,
123+
Self::ZhCn,
124+
Self::ZhTw,
125+
]
126+
} else {
127+
&[Self::EnUs]
128+
}
123129
}
124130
}
125131

‎src/main.rs

+31-14
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ use std::sync::{Arc, RwLock};
77
use std::thread::spawn;
88

99
use clap::{Args, Parser, Subcommand};
10-
use rari_doc::build::{build_blog_pages, build_curriculum_pages, build_docs, build_generic_pages};
10+
use rari_doc::build::{
11+
build_blog_pages, build_contributor_spotlight_pages, build_curriculum_pages, build_docs,
12+
build_generic_pages, build_spas,
13+
};
1114
use rari_doc::cached_readers::{read_and_cache_doc_pages, CACHED_DOC_PAGE_FILES};
1215
use rari_doc::pages::types::doc::Doc;
1316
use rari_doc::reader::read_docs_parallel;
@@ -65,6 +68,8 @@ struct BuildArgs {
6568
#[arg(long)]
6669
skip_content: bool,
6770
#[arg(long)]
71+
skip_contributors: bool,
72+
#[arg(long)]
6873
skip_blog: bool,
6974
#[arg(long)]
7075
skip_curriculum: bool,
@@ -154,24 +159,28 @@ fn main() -> Result<(), anyhow::Error> {
154159
.set(Arc::new(RwLock::new(HashMap::new())))
155160
.unwrap();
156161
}
162+
let mut urls = Vec::new();
163+
let mut docs = Vec::new();
157164
println!("Building everything 🛠️");
158-
let start = std::time::Instant::now();
159-
let docs = if !args.files.is_empty() {
160-
read_docs_parallel::<Doc>(&args.files, None)?
161-
} else if !args.cache_content {
162-
let files: &[_] = if let Some(translated_root) = content_translated_root() {
163-
&[content_root(), translated_root]
165+
if !args.skip_content {
166+
let start = std::time::Instant::now();
167+
docs = if !args.files.is_empty() {
168+
read_docs_parallel::<Doc>(&args.files, None)?
169+
} else if !args.cache_content {
170+
let files: &[_] = if let Some(translated_root) = content_translated_root() {
171+
&[content_root(), translated_root]
172+
} else {
173+
&[content_root()]
174+
};
175+
read_docs_parallel::<Doc>(files, None)?
164176
} else {
165-
&[content_root()]
177+
read_and_cache_doc_pages()?
166178
};
167-
read_docs_parallel::<Doc>(files, None)?
168-
} else {
169-
read_and_cache_doc_pages()?
170-
};
171-
println!("Took: {: >10.3?} for {}", start.elapsed(), docs.len());
172-
let mut urls = Vec::new();
179+
println!("Took: {: >10.3?} for {}", start.elapsed(), docs.len());
180+
}
173181
if !args.skip_spas {
174182
let start = std::time::Instant::now();
183+
urls.extend(build_spas()?);
175184
urls.extend(build_generic_pages()?);
176185
println!("Took: {: >10.3?} to build spas", start.elapsed());
177186
}
@@ -190,6 +199,14 @@ fn main() -> Result<(), anyhow::Error> {
190199
urls.extend(build_blog_pages()?);
191200
println!("Took: {: >10.3?} to build blog", start.elapsed());
192201
}
202+
if !args.skip_contributors && args.files.is_empty() {
203+
let start = std::time::Instant::now();
204+
urls.extend(build_contributor_spotlight_pages()?);
205+
println!(
206+
"Took: {: >10.3?} to build contributor spotlight",
207+
start.elapsed()
208+
);
209+
}
193210
if !args.skip_sitemap && args.files.is_empty() && !urls.is_empty() {
194211
let start = std::time::Instant::now();
195212
let out_path = build_out_root()?;

‎src/serve.rs

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use tiny_http::{Response, Server};
1010
use tracing::{error, span, Level};
1111

1212
fn get_json(url: &str) -> Result<BuiltDocy, Error> {
13+
let url = url.strip_suffix("index.json").unwrap_or(url);
1314
let page = Page::page_from_url_path(url)?;
1415

1516
let slug = &page.slug();

0 commit comments

Comments
 (0)
Please sign in to comment.