leptos_next_metadata_macros/
lib.rs1use proc_macro::TokenStream;
4use quote::quote;
5use syn::{parse::Parse, parse::ParseStream, parse_macro_input, Expr, Ident, Result, Token};
6
7#[proc_macro]
32pub fn metadata(input: TokenStream) -> TokenStream {
33 let input = parse_macro_input!(input as MetadataInput);
34
35 match generate_metadata_code(input) {
36 Ok(tokens) => tokens.into(),
37 Err(e) => e.to_compile_error().into(),
38 }
39}
40
41#[proc_macro]
68pub fn generate_metadata(input: TokenStream) -> TokenStream {
69 let parsed = parse_macro_input!(input as GenerateMetadataInputParser);
70 let input = GenerateMetadataInput {
71 closure: parsed.closure,
72 };
73
74 generate_dynamic_metadata_code(input).into()
75}
76
77struct GenerateMetadataInput {
79 closure: Expr,
81}
82
83struct GenerateMetadataInputParser {
85 closure: Expr,
86}
87
88impl Parse for GenerateMetadataInputParser {
89 fn parse(input: ParseStream) -> Result<Self> {
90 let closure: Expr = input.parse()?;
92
93 Ok(GenerateMetadataInputParser { closure })
94 }
95}
96
97fn generate_dynamic_metadata_code(input: GenerateMetadataInput) -> proc_macro2::TokenStream {
99 let closure = &input.closure;
100
101 quote! {
102 {
103 use leptos::*;
104 use leptos::prelude::*;
105 use leptos_meta::*;
106 use leptos_next_metadata::metadata::Metadata;
107
108 let (metadata, set_metadata) = signal(Metadata::default());
110
111 leptos::task::spawn_local(async move {
113 let result = (#closure)().await;
114 set_metadata.set(result);
115 });
116
117 move || {
119 let meta = metadata.get();
120
121 let title_view = meta.title.as_ref().map(|title| {
123 match title {
124 leptos_next_metadata::metadata::Title::Static(s) => {
125 view! { <Title text=s.to_string()/> }
126 },
127 leptos_next_metadata::metadata::Title::Template { template: _, default } => {
128 view! { <Title text=default.to_string()/> }
129 }
130 }
131 });
132
133 let description_view = meta.description.as_ref().map(|desc| {
134 view! { <Meta name="description" content=desc.to_string()/> }
135 });
136
137 let keywords_view = meta.keywords.as_ref().map(|keywords| {
138 match keywords {
139 leptos_next_metadata::metadata::Keywords::Single(k) => {
140 view! { <Meta name="keywords" content=k.to_string()/> }
141 },
142 leptos_next_metadata::metadata::Keywords::Multiple(ks) => {
143 let keywords_str = ks.join(", ");
144 view! { <Meta name="keywords" content=keywords_str/> }
145 }
146 }
147 });
148
149 let canonical_view = meta.canonical.as_ref().map(|url| {
150 view! { <Link rel="canonical" href=url.to_string()/> }
151 });
152
153 let og_view = meta.open_graph.as_ref().map(|og| {
155 view! {
156 <>
157 {og.title.as_ref().map(|title| {
158 view! { <Meta property="og:title" content=title.to_string()/> }
159 })}
160
161 {og.description.as_ref().map(|desc| {
162 view! { <Meta property="og:description" content=desc.to_string()/> }
163 })}
164
165 {og.r#type.as_ref().map(|og_type| {
166 view! { <Meta property="og:type" content=og_type.to_string()/> }
167 })}
168
169 {og.url.as_ref().map(|url| {
170 view! { <Meta property="og:url" content=url.to_string()/> }
171 })}
172
173 {og.site_name.as_ref().map(|name| {
174 view! { <Meta property="og:site_name" content=name.to_string()/> }
175 })}
176
177 </>
179 }
180 });
181
182 let twitter_view = meta.twitter.as_ref().map(|twitter| {
184 view! {
185 <>
186 {twitter.card.as_ref().map(|card| {
187 let card_str = match card {
188 leptos_next_metadata::metadata::TwitterCard::Summary => "summary",
189 leptos_next_metadata::metadata::TwitterCard::SummaryLargeImage => "summary_large_image",
190 leptos_next_metadata::metadata::TwitterCard::App => "app",
191 leptos_next_metadata::metadata::TwitterCard::Player => "player",
192 };
193 view! { <Meta name="twitter:card" content=card_str.to_string()/> }
194 })}
195
196 {twitter.title.as_ref().map(|title| {
197 view! { <Meta name="twitter:title" content=title.to_string()/> }
198 })}
199
200 {twitter.description.as_ref().map(|desc| {
201 view! { <Meta name="twitter:description" content=desc.to_string()/> }
202 })}
203 </>
204 }
205 });
206
207 view! {
211 <>
212 {title_view.map(|view| view)}
213 {description_view.map(|view| view)}
214 {keywords_view.map(|view| view)}
215 {canonical_view.map(|view| view)}
216 {og_view.map(|view| view)}
217 {twitter_view.map(|view| view)}
218 </>
219 }
220 }
221 }
222 }
223}
224
225struct MetadataInput {
227 fields: Vec<MetadataField>,
228}
229
230struct MetadataField {
231 name: Ident,
232 value: MetadataValue,
233}
234
235#[allow(clippy::large_enum_variant)]
236enum MetadataValue {
237 Simple(Expr),
239 Nested(Vec<MetadataField>),
241 Array(Vec<MetadataValue>),
243}
244
245impl Parse for MetadataInput {
246 fn parse(input: ParseStream) -> Result<Self> {
247 let mut fields = Vec::new();
248
249 while !input.is_empty() {
250 let name: Ident = input.parse()?;
251 input.parse::<Token![:]>()?;
252 let value = MetadataValue::parse(input)?;
253
254 fields.push(MetadataField { name, value });
255
256 if input.peek(Token![,]) {
258 input.parse::<Token![,]>()?;
259 }
260 }
261
262 Ok(MetadataInput { fields })
263 }
264}
265
266impl Parse for MetadataValue {
267 fn parse(input: ParseStream) -> Result<Self> {
268 if input.peek(syn::token::Brace) {
269 let content;
271 syn::braced!(content in input);
272
273 let mut fields = Vec::new();
274 while !content.is_empty() {
275 let name: Ident = content.parse()?;
276 content.parse::<Token![:]>()?;
277 let value = MetadataValue::parse(&content)?;
278
279 fields.push(MetadataField { name, value });
280
281 if content.peek(Token![,]) {
282 content.parse::<Token![,]>()?;
283 }
284 }
285
286 Ok(MetadataValue::Nested(fields))
287 } else if input.peek(syn::token::Bracket) {
288 let content;
290 syn::bracketed!(content in input);
291
292 let mut values = Vec::new();
293 while !content.is_empty() {
294 let value = MetadataValue::parse(&content)?;
295 values.push(value);
296
297 if content.peek(Token![,]) {
298 content.parse::<Token![,]>()?;
299 }
300 }
301
302 Ok(MetadataValue::Array(values))
303 } else {
304 let expr: Expr = input.parse()?;
306 Ok(MetadataValue::Simple(expr))
307 }
308 }
309}
310
311fn generate_metadata_code(input: MetadataInput) -> Result<proc_macro2::TokenStream> {
312 let mut meta_tags = Vec::new();
313
314 for field in input.fields {
315 let field_name = field.name.to_string();
316 let tags = generate_field_meta_tags(&field_name, &field.value)?;
317 meta_tags.extend(tags);
318 }
319
320 Ok(quote! {
321 {
322 use leptos_meta::*;
323
324 view! {
325 <>
326 #(#meta_tags)*
327 </>
328 }
329 }
330 })
331}
332
333fn generate_field_meta_tags(
334 field_name: &str,
335 value: &MetadataValue,
336) -> Result<Vec<proc_macro2::TokenStream>> {
337 let mut tags = Vec::new();
338
339 match value {
340 MetadataValue::Simple(expr) => {
341 let tag = match field_name {
342 "title" => quote! {
343 <Title text=#expr/>
344 },
345 "description" => quote! {
346 <Meta name="description" content=#expr/>
347 },
348 "keywords" => quote! {
349 <Meta name="keywords" content=#expr/>
350 },
351 "author" => quote! {
352 <Meta name="author" content=#expr/>
353 },
354 "robots" => quote! {
355 <Meta name="robots" content=#expr/>
356 },
357 "canonical" => quote! {
358 <Link rel="canonical" href=#expr/>
359 },
360 "viewport" => quote! {
361 <Meta name="viewport" content=#expr/>
362 },
363 "themeColor" => quote! {
364 <Meta name="theme-color" content=#expr/>
365 },
366 "colorScheme" => quote! {
367 <Meta name="color-scheme" content=#expr/>
368 },
369 "referrer" => quote! {
370 <Meta name="referrer" content=#expr/>
371 },
372 "formatDetection" => quote! {
373 <Meta name="format-detection" content=#expr/>
374 },
375 _ => quote! {
376 <Meta name=#field_name content=#expr/>
377 },
378 };
379 tags.push(tag);
380 }
381 MetadataValue::Nested(fields) => {
382 match field_name {
383 "openGraph" | "open_graph" => {
384 for field in fields {
385 let field_name = field.name.to_string();
386 let nested_tags = generate_og_meta_tags(&field_name, &field.value)?;
387 tags.extend(nested_tags);
388 }
389 }
390 "twitter" => {
391 for field in fields {
392 let field_name = field.name.to_string();
393 let nested_tags = generate_twitter_meta_tags(&field_name, &field.value)?;
394 tags.extend(nested_tags);
395 }
396 }
397 _ => {
398 for field in fields {
400 let field_name = field.name.to_string();
401 if let MetadataValue::Simple(value) = &field.value {
402 tags.push(quote! {
403 <Meta name=format!("{}:{}", #field_name, #field_name) content=#value/>
404 });
405 }
406 }
407 }
408 }
409 }
410 MetadataValue::Array(values) => {
411 let array_tags = generate_array_meta_tags(field_name, values)?;
412 tags.extend(array_tags);
413 }
414 }
415
416 Ok(tags)
417}
418
419fn generate_og_meta_tags(
420 field_name: &str,
421 value: &MetadataValue,
422) -> Result<Vec<proc_macro2::TokenStream>> {
423 let mut tags = Vec::new();
424
425 match value {
426 MetadataValue::Simple(expr) => {
427 let tag = match field_name {
428 "title" => quote! {
429 <Meta property="og:title" content=#expr/>
430 },
431 "description" => quote! {
432 <Meta property="og:description" content=#expr/>
433 },
434 "type" => quote! {
435 <Meta property="og:type" content=#expr/>
436 },
437 "url" => quote! {
438 <Meta property="og:url" content=#expr/>
439 },
440 "siteName" | "site_name" => quote! {
441 <Meta property="og:site_name" content=#expr/>
442 },
443 "locale" => quote! {
444 <Meta property="og:locale" content=#expr/>
445 },
446 _ => quote! {
447 <Meta property=format!("og:{}", #field_name) content=#expr/>
448 },
449 };
450 tags.push(tag);
451 }
452 MetadataValue::Array(array_values) => {
453 match field_name {
454 "images" => {
455 for image_value in array_values {
456 if let MetadataValue::Simple(url) = image_value {
457 tags.push(quote! {
458 <Meta property="og:image" content=#url/>
459 });
460 }
461 }
462 }
463 "videos" => {
464 for video_value in array_values {
465 if let MetadataValue::Simple(url) = video_value {
466 tags.push(quote! {
467 <Meta property="og:video" content=#url/>
468 });
469 }
470 }
471 }
472 _ => {
473 }
475 }
476 }
477 _ => {
478 }
480 }
481
482 Ok(tags)
483}
484
485fn generate_twitter_meta_tags(
486 field_name: &str,
487 value: &MetadataValue,
488) -> Result<Vec<proc_macro2::TokenStream>> {
489 let mut tags = Vec::new();
490
491 match value {
492 MetadataValue::Simple(expr) => {
493 let tag = match field_name {
494 "card" => quote! {
495 <Meta name="twitter:card" content=#expr/>
496 },
497 "site" => quote! {
498 <Meta name="twitter:site" content=#expr/>
499 },
500 "creator" => quote! {
501 <Meta name="twitter:creator" content=#expr/>
502 },
503 "title" => quote! {
504 <Meta name="twitter:title" content=#expr/>
505 },
506 "description" => quote! {
507 <Meta name="twitter:description" content=#expr/>
508 },
509 "image" => quote! {
510 <Meta name="twitter:image" content=#expr/>
511 },
512 _ => quote! {
513 <Meta name=format!("twitter:{}", #field_name) content=#expr/>
514 },
515 };
516 tags.push(tag);
517 }
518 MetadataValue::Array(array_values) => {
519 match field_name {
520 "images" => {
521 for image_value in array_values {
522 if let MetadataValue::Simple(url) = image_value {
523 tags.push(quote! {
524 <Meta name="twitter:image" content=#url/>
525 });
526 }
527 }
528 }
529 _ => {
530 }
532 }
533 }
534 _ => {
535 }
537 }
538
539 Ok(tags)
540}
541
542fn generate_array_meta_tags(
543 field_name: &str,
544 values: &[MetadataValue],
545) -> Result<Vec<proc_macro2::TokenStream>> {
546 let mut tags = Vec::new();
547
548 match field_name {
549 "keywords" => {
550 let mut keyword_exprs = Vec::new();
552 for value in values {
553 if let MetadataValue::Simple(expr) = value {
554 keyword_exprs.push(expr);
555 }
556 }
557
558 if !keyword_exprs.is_empty() {
559 tags.push(quote! {
560 <Meta name="keywords" content={[#(#keyword_exprs),*].join(", ")}/>
561 });
562 }
563 }
564 "images" => {
565 for value in values {
567 if let MetadataValue::Simple(url) = value {
568 tags.push(quote! {
569 <Meta name="image" content=#url/>
570 });
571 }
572 }
573 }
574 "authors" => {
575 for value in values {
577 if let MetadataValue::Simple(author) = value {
578 tags.push(quote! {
579 <Meta name="author" content=#author/>
580 });
581 }
582 }
583 }
584 _ => {
585 for value in values {
587 if let MetadataValue::Simple(expr) = value {
588 tags.push(quote! {
589 <Meta name=#field_name content=#expr/>
590 });
591 }
592 }
593 }
594 }
595
596 Ok(tags)
597}