Skip to main content

next_core/next_app/
mod.rs

1pub mod app_client_references_chunks;
2pub mod app_client_shared_chunks;
3pub mod app_entry;
4pub mod app_page_entry;
5pub mod app_route_entry;
6pub mod metadata;
7
8use std::{
9    cmp::Ordering,
10    fmt::{Display, Formatter, Write},
11    ops::Deref,
12};
13
14use anyhow::{Result, bail};
15use bincode::{Decode, Encode};
16use turbo_rcstr::RcStr;
17use turbo_tasks::trace::TraceRawVcs;
18
19pub use crate::next_app::{
20    app_client_references_chunks::{
21        ClientReferencesChunks, get_app_client_references_chunks,
22        get_client_references_chunks_for_hmr,
23    },
24    app_client_shared_chunks::get_app_client_shared_chunk_group,
25    app_entry::AppEntry,
26    app_page_entry::get_app_page_entry,
27    app_route_entry::get_app_route_entry,
28};
29
30/// See [AppPage].
31#[turbo_tasks::task_input]
32#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, TraceRawVcs, Encode, Decode)]
33pub enum PageSegment {
34    /// e.g. `/dashboard`
35    Static(RcStr),
36    /// e.g. `/[id]`
37    Dynamic(RcStr),
38    /// e.g. `/[...slug]`
39    CatchAll(RcStr),
40    /// e.g. `/[[...slug]]`
41    OptionalCatchAll(RcStr),
42    /// e.g. `/(shop)`
43    Group(RcStr),
44    /// e.g. `/@auth`
45    Parallel(RcStr),
46    /// The final page type appended. (e.g. `/dashboard/page`,
47    /// `/api/hello/route`)
48    PageType(PageType),
49}
50
51impl PageSegment {
52    pub fn parse(segment: &str) -> Result<Self> {
53        if segment.is_empty() {
54            bail!("empty segments are not allowed");
55        }
56
57        if segment.contains('/') {
58            bail!("slashes are not allowed in segments");
59        }
60
61        if let Some(s) = segment.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
62            return Ok(PageSegment::Group(s.into()));
63        }
64
65        if let Some(s) = segment.strip_prefix('@') {
66            return Ok(PageSegment::Parallel(s.into()));
67        }
68
69        if let Some(s) = segment
70            .strip_prefix("[[...")
71            .and_then(|s| s.strip_suffix("]]"))
72        {
73            return Ok(PageSegment::OptionalCatchAll(s.into()));
74        }
75
76        if let Some(s) = segment
77            .strip_prefix("[...")
78            .and_then(|s| s.strip_suffix(']'))
79        {
80            return Ok(PageSegment::CatchAll(s.into()));
81        }
82
83        if let Some(s) = segment.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
84            return Ok(PageSegment::Dynamic(s.into()));
85        }
86
87        Ok(PageSegment::Static(segment.into()))
88    }
89}
90
91impl Display for PageSegment {
92    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
93        match self {
94            PageSegment::Static(s) => f.write_str(s),
95            PageSegment::Dynamic(s) => {
96                f.write_char('[')?;
97                f.write_str(s)?;
98                f.write_char(']')
99            }
100            PageSegment::CatchAll(s) => {
101                f.write_str("[...")?;
102                f.write_str(s)?;
103                f.write_char(']')
104            }
105            PageSegment::OptionalCatchAll(s) => {
106                f.write_str("[[...")?;
107                f.write_str(s)?;
108                f.write_str("]]")
109            }
110            PageSegment::Group(s) => {
111                f.write_char('(')?;
112                f.write_str(s)?;
113                f.write_char(')')
114            }
115            PageSegment::Parallel(s) => {
116                f.write_char('@')?;
117                f.write_str(s)
118            }
119            PageSegment::PageType(s) => Display::fmt(s, f),
120        }
121    }
122}
123
124#[turbo_tasks::task_input]
125#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, TraceRawVcs, Encode, Decode)]
126pub enum PageType {
127    Page,
128    Route,
129}
130
131impl Display for PageType {
132    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
133        f.write_str(match self {
134            PageType::Page => "page",
135            PageType::Route => "route",
136        })
137    }
138}
139
140/// Describes the pathname including all internal modifiers such as
141/// intercepting routes, parallel routes and route/page suffixes that are not
142/// part of the pathname.
143#[turbo_tasks::task_input]
144#[derive(Clone, Debug, Hash, PartialEq, Eq, Default, TraceRawVcs, Encode, Decode)]
145pub struct AppPage(pub Vec<PageSegment>);
146
147impl AppPage {
148    pub fn new() -> Self {
149        Self::default()
150    }
151
152    pub fn push(&mut self, segment: PageSegment) -> Result<()> {
153        let has_catchall = self.0.iter().any(|segment| {
154            matches!(
155                segment,
156                PageSegment::CatchAll(..) | PageSegment::OptionalCatchAll(..)
157            )
158        });
159
160        if has_catchall
161            && matches!(
162                segment,
163                PageSegment::Static(..)
164                    | PageSegment::Dynamic(..)
165                    | PageSegment::CatchAll(..)
166                    | PageSegment::OptionalCatchAll(..)
167            )
168        {
169            bail!(
170                "Invalid segment {:?}, catch all segment must be the last segment modifying the \
171                 path (segments: {:?})",
172                segment,
173                self.0
174            )
175        }
176
177        if self.is_complete() {
178            bail!(
179                "Invalid segment {:?}, this page path already has the final PageType appended \
180                 (segments: {:?})",
181                segment,
182                self.0
183            )
184        }
185
186        self.0.push(segment);
187        Ok(())
188    }
189
190    pub fn push_str(&mut self, segment: &str) -> Result<()> {
191        if segment.is_empty() {
192            return Ok(());
193        }
194
195        self.push(PageSegment::parse(segment)?)
196    }
197
198    pub fn clone_push(&self, segment: PageSegment) -> Result<Self> {
199        let mut cloned = self.clone();
200        cloned.push(segment)?;
201        Ok(cloned)
202    }
203
204    pub fn clone_push_str(&self, segment: &str) -> Result<Self> {
205        let mut cloned = self.clone();
206        cloned.push_str(segment)?;
207        Ok(cloned)
208    }
209
210    pub fn parse(page: &str) -> Result<Self> {
211        let mut app_page = Self::new();
212
213        for segment in page.split('/') {
214            app_page.push_str(segment)?;
215        }
216
217        if let Some(last) = app_page.0.last_mut()
218            && let PageSegment::Static(last_name) = &*last
219        {
220            // Next.js internals sometimes omit extensions when creating synthetic page entries
221            if last_name == "page" || last_name.starts_with("page.") {
222                *last = PageSegment::PageType(PageType::Page);
223            } else if last_name == "route" || last_name.starts_with("route.") {
224                *last = PageSegment::PageType(PageType::Route);
225            }
226            // can also be metadata (and be neither Page nor Route)
227        }
228
229        Ok(app_page)
230    }
231
232    pub fn is_root(&self) -> bool {
233        self.0.is_empty()
234    }
235
236    pub fn is_complete(&self) -> bool {
237        matches!(self.0.last(), Some(PageSegment::PageType(..)))
238    }
239
240    /// The `PageType` is the last segment for completed pages. We need to find
241    /// the last segment that is not a `PageType`, `Group`, or `Parallel`
242    /// segment, because these do not inform the routing structure.
243    pub fn get_last_routing_segment(&self) -> Option<&PageSegment> {
244        self.0.iter().rev().find(|segment| {
245            !matches!(
246                segment,
247                PageSegment::PageType(_) | PageSegment::Group(_) | PageSegment::Parallel(_)
248            )
249        })
250    }
251
252    pub fn is_catchall(&self) -> bool {
253        matches!(
254            self.get_last_routing_segment(),
255            Some(PageSegment::CatchAll(_) | PageSegment::OptionalCatchAll(_))
256        )
257    }
258
259    pub fn is_intercepting(&self) -> bool {
260        let segment = if self.is_complete() {
261            // The `PageType` is the last segment for completed pages.
262            self.0.iter().nth_back(1)
263        } else {
264            self.0.last()
265        };
266
267        matches!(
268            segment,
269            Some(PageSegment::Static(segment))
270                if segment.starts_with("(.)")
271                    || segment.starts_with("(..)")
272                    || segment.starts_with("(...)")
273        )
274    }
275
276    /// Returns true if there is only one segment and it is a group.
277    pub fn is_first_layer_group_route(&self) -> bool {
278        self.0.len() == 1 && matches!(self.0.last(), Some(PageSegment::Group(_)))
279    }
280
281    pub fn complete(&self, page_type: PageType) -> Result<Self> {
282        self.clone_push(PageSegment::PageType(page_type))
283    }
284}
285
286impl Display for AppPage {
287    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
288        if self.0.is_empty() {
289            return f.write_char('/');
290        }
291
292        for segment in &self.0 {
293            f.write_char('/')?;
294            Display::fmt(segment, f)?;
295        }
296
297        Ok(())
298    }
299}
300
301impl Deref for AppPage {
302    type Target = [PageSegment];
303
304    fn deref(&self) -> &Self::Target {
305        &self.0
306    }
307}
308
309impl Ord for AppPage {
310    fn cmp(&self, other: &Self) -> Ordering {
311        // next.js does some weird stuff when looking up routes, so we have to emit the
312        // correct path (shortest segments, but alphabetically the last).
313        // https://github.com/vercel/next.js/blob/194311d8c96144d68e65cd9abb26924d25978da7/packages/next/src/server/base-server.ts#L3003
314        self.len().cmp(&other.len()).then(other.0.cmp(&self.0))
315    }
316}
317
318impl PartialOrd for AppPage {
319    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
320        Some(self.cmp(other))
321    }
322}
323
324/// Path segments for a router path (not including parallel routes and groups).
325///
326/// Also see [AppPath].
327#[turbo_tasks::task_input]
328#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, TraceRawVcs, Encode, Decode)]
329pub enum PathSegment {
330    /// e.g. `/dashboard`
331    Static(RcStr),
332    /// e.g. `/[id]`
333    Dynamic(RcStr),
334    /// e.g. `/[...slug]`
335    CatchAll(RcStr),
336    /// e.g. `/[[...slug]]`
337    OptionalCatchAll(RcStr),
338}
339
340impl Display for PathSegment {
341    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
342        match self {
343            PathSegment::Static(s) => f.write_str(s),
344            PathSegment::Dynamic(s) => {
345                f.write_char('[')?;
346                f.write_str(s)?;
347                f.write_char(']')
348            }
349            PathSegment::CatchAll(s) => {
350                f.write_str("[...")?;
351                f.write_str(s)?;
352                f.write_char(']')
353            }
354            PathSegment::OptionalCatchAll(s) => {
355                f.write_str("[[...")?;
356                f.write_str(s)?;
357                f.write_str("]]")
358            }
359        }
360    }
361}
362
363/// The pathname (including dynamic placeholders) for the next.js router to
364/// resolve.
365///
366/// Does not include internal modifiers as it's the equivalent of the http
367/// request path.
368#[turbo_tasks::task_input]
369#[derive(Clone, Debug, Hash, PartialEq, Eq, Default, TraceRawVcs, Encode, Decode)]
370pub struct AppPath(pub Vec<PathSegment>);
371
372impl AppPath {
373    pub fn is_dynamic(&self) -> bool {
374        self.iter().any(|segment| {
375            matches!(
376                (segment,),
377                (PathSegment::Dynamic(_)
378                    | PathSegment::CatchAll(_)
379                    | PathSegment::OptionalCatchAll(_),)
380            )
381        })
382    }
383
384    pub fn is_root(&self) -> bool {
385        self.0.is_empty()
386    }
387
388    pub fn is_catchall(&self) -> bool {
389        // can only be the last segment.
390        matches!(
391            self.last(),
392            Some(PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_))
393        )
394    }
395
396    pub fn contains(&self, other: &AppPath) -> bool {
397        // TODO: handle OptionalCatchAll properly.
398        for (i, segment) in other.0.iter().enumerate() {
399            let Some(self_segment) = self.0.get(i) else {
400                // other is longer than self
401                return false;
402            };
403
404            if self_segment == segment {
405                continue;
406            }
407
408            if matches!(
409                segment,
410                PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_)
411            ) {
412                return true;
413            }
414
415            return false;
416        }
417
418        true
419    }
420
421    /// Returns true if any segment in the path is an interception route.
422    /// Unlike `AppPage::is_intercepting()`, this also identifies descendants
423    /// below the interception marker.
424    pub fn contains_interception(&self) -> bool {
425        self.iter().any(|segment| {
426            matches!(
427                segment,
428                PathSegment::Static(s) if s.starts_with("(.)") || s.starts_with("(..)") || s.starts_with("(...)")
429            )
430        })
431    }
432
433    /// Returns the ordinary route that an interception route substitutes for.
434    /// This is the route that must handle a direct request or hard refresh.
435    pub fn intercepted_path(&self) -> Option<AppPath> {
436        let (interception_index, segment) =
437            self.iter().enumerate().find_map(|(index, segment)| {
438                let PathSegment::Static(segment) = segment else {
439                    return None;
440                };
441
442                let (marker, target) =
443                    ["(..)(..)", "(...)", "(..)", "(.)"]
444                        .into_iter()
445                        .find_map(|marker| {
446                            segment.strip_prefix(marker).map(|target| (marker, target))
447                        })?;
448                Some((index, (marker, target)))
449            })?;
450
451        let (marker, target) = segment;
452        if target.is_empty() {
453            return None;
454        }
455
456        let mut canonical_segments = match marker {
457            "(...)" => Vec::new(),
458            _ => self.0[..interception_index].to_vec(),
459        };
460        let levels_to_pop = match marker {
461            "(..)(..)" => 2,
462            "(..)" => 1,
463            _ => 0,
464        };
465        for _ in 0..levels_to_pop {
466            canonical_segments.pop()?;
467        }
468        let target = if let Some(target) = target
469            .strip_prefix("[[...")
470            .and_then(|target| target.strip_suffix("]]"))
471        {
472            PathSegment::OptionalCatchAll(target.into())
473        } else if let Some(target) = target
474            .strip_prefix("[...")
475            .and_then(|target| target.strip_suffix(']'))
476        {
477            PathSegment::CatchAll(target.into())
478        } else if let Some(target) = target
479            .strip_prefix('[')
480            .and_then(|target| target.strip_suffix(']'))
481        {
482            PathSegment::Dynamic(target.into())
483        } else {
484            PathSegment::Static(target.into())
485        };
486        canonical_segments.push(target);
487        canonical_segments.extend_from_slice(&self.0[interception_index + 1..]);
488
489        Some(AppPath(canonical_segments))
490    }
491
492    /// Returns whether the supplied ordinary route patterns cover every URL matched by this
493    /// route pattern.
494    pub fn is_route_pattern_covered_by<'a>(
495        &self,
496        ordinary_routes: impl IntoIterator<Item = &'a AppPath>,
497    ) -> bool {
498        let route = RoutePattern::new(self);
499        let ordinary_routes = ordinary_routes
500            .into_iter()
501            .map(RoutePattern::new)
502            .collect::<Vec<_>>();
503
504        if !route.unbounded {
505            return ordinary_routes
506                .iter()
507                .any(|ordinary| pattern_covers_at_length(ordinary, &route, route.min_length));
508        }
509
510        let Some(unbounded_coverage_start) = ordinary_routes
511            .iter()
512            .filter(|ordinary| ordinary.unbounded && prefix_covers(ordinary, &route))
513            .map(|ordinary| ordinary.min_length.max(route.min_length))
514            .min()
515        else {
516            return false;
517        };
518
519        // An optional or required catchall can have its shorter paths covered by fixed routes
520        // before another catchall takes over the remaining suffix.
521        (route.min_length..unbounded_coverage_start).all(|length| {
522            ordinary_routes
523                .iter()
524                .any(|ordinary| pattern_covers_at_length(ordinary, &route, length))
525        })
526    }
527}
528
529struct RoutePattern<'a> {
530    prefix: &'a [PathSegment],
531    min_length: usize,
532    unbounded: bool,
533}
534
535impl<'a> RoutePattern<'a> {
536    fn new(path: &'a AppPath) -> Self {
537        match path.last() {
538            Some(PathSegment::CatchAll(_)) => Self {
539                prefix: &path[..path.len() - 1],
540                min_length: path.len(),
541                unbounded: true,
542            },
543            Some(PathSegment::OptionalCatchAll(_)) => Self {
544                prefix: &path[..path.len() - 1],
545                min_length: path.len() - 1,
546                unbounded: true,
547            },
548            _ => Self {
549                prefix: path,
550                min_length: path.len(),
551                unbounded: false,
552            },
553        }
554    }
555}
556
557fn accepts_length(pattern: &RoutePattern<'_>, length: usize) -> bool {
558    if pattern.unbounded {
559        length >= pattern.min_length
560    } else {
561        length == pattern.min_length
562    }
563}
564
565fn prefix_covers(canonical: &RoutePattern<'_>, intercepted: &RoutePattern<'_>) -> bool {
566    canonical
567        .prefix
568        .iter()
569        .enumerate()
570        .all(|(index, canonical_segment)| match canonical_segment {
571            PathSegment::Dynamic(_) => true,
572            PathSegment::Static(canonical_segment) => matches!(
573                intercepted.prefix.get(index),
574                Some(PathSegment::Static(intercepted_segment))
575                    if canonical_segment == intercepted_segment
576            ),
577            PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_) => {
578                unreachable!("catchall segments are excluded from the route prefix")
579            }
580        })
581}
582
583fn pattern_covers_at_length(
584    canonical: &RoutePattern<'_>,
585    intercepted: &RoutePattern<'_>,
586    length: usize,
587) -> bool {
588    accepts_length(canonical, length)
589        && accepts_length(intercepted, length)
590        && prefix_covers(canonical, intercepted)
591}
592
593impl Deref for AppPath {
594    type Target = [PathSegment];
595
596    fn deref(&self) -> &Self::Target {
597        &self.0
598    }
599}
600
601impl Display for AppPath {
602    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
603        if self.0.is_empty() {
604            return f.write_char('/');
605        }
606
607        for segment in &self.0 {
608            f.write_char('/')?;
609            Display::fmt(segment, f)?;
610        }
611
612        Ok(())
613    }
614}
615
616impl Ord for AppPath {
617    fn cmp(&self, other: &Self) -> Ordering {
618        // next.js does some weird stuff when looking up routes, so we have to emit the
619        // correct path (shortest segments, but alphabetically the last).
620        // https://github.com/vercel/next.js/blob/194311d8c96144d68e65cd9abb26924d25978da7/packages/next/src/server/base-server.ts#L3003
621        self.len().cmp(&other.len()).then(other.0.cmp(&self.0))
622    }
623}
624
625impl PartialOrd for AppPath {
626    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
627        Some(self.cmp(other))
628    }
629}
630
631impl From<AppPage> for AppPath {
632    fn from(value: AppPage) -> Self {
633        AppPath(
634            value
635                .0
636                .into_iter()
637                .filter_map(|segment| match segment {
638                    PageSegment::Static(s) => Some(PathSegment::Static(s)),
639                    PageSegment::Dynamic(s) => Some(PathSegment::Dynamic(s)),
640                    PageSegment::CatchAll(s) => Some(PathSegment::CatchAll(s)),
641                    PageSegment::OptionalCatchAll(s) => Some(PathSegment::OptionalCatchAll(s)),
642                    _ => None,
643                })
644                .collect(),
645        )
646    }
647}
648
649#[cfg(test)]
650mod test {
651    use crate::next_app::{AppPage, AppPath, PageSegment, PageType};
652
653    #[test]
654    fn resolves_intercepted_app_paths() {
655        for (interception, canonical) in [
656            ("/(.)photo/[id]", "/photo/[id]"),
657            ("/feed/(.)photo/[id]", "/feed/photo/[id]"),
658            ("/feed/(..)photo/[id]", "/photo/[id]"),
659            ("/feed/nested/(..)(..)photo/[id]", "/photo/[id]"),
660            ("/feed/(...)photo/[id]", "/photo/[id]"),
661            ("/(.)[username]/[id]", "/[username]/[id]"),
662            ("/(.)[...slug]", "/[...slug]"),
663            ("/(.)[[...slug]]", "/[[...slug]]"),
664        ] {
665            let interception =
666                AppPath::from(AppPage::parse(interception.trim_start_matches('/')).unwrap());
667            let canonical =
668                AppPath::from(AppPage::parse(canonical.trim_start_matches('/')).unwrap());
669
670            assert_eq!(interception.intercepted_path(), Some(canonical));
671        }
672    }
673
674    #[test]
675    fn checks_route_pattern_coverage() {
676        for (route, ordinary_routes, expected) in [
677            ("/photo/[id]", &["/photo/[slug]"][..], true),
678            ("/showcase/[...parts]", &["/[...slug]"][..], true),
679            (
680                "/items/[...parts]",
681                &["/items/[id]", "/items/[id]/[...rest]"][..],
682                true,
683            ),
684            (
685                "/items/[[...parts]]",
686                &["/items", "/items/[...rest]"][..],
687                true,
688            ),
689            ("/photo/[id]", &[][..], false),
690            ("/items/[...parts]", &["/items/[id]"][..], false),
691            ("/items/[[...parts]]", &["/items/[...rest]"][..], false),
692            ("/items/[id]", &["/items/one", "/items/two"][..], false),
693        ] {
694            let route = AppPath::from(AppPage::parse(route.trim_start_matches('/')).unwrap());
695            let ordinary_routes = ordinary_routes
696                .iter()
697                .map(|route| AppPath::from(AppPage::parse(route.trim_start_matches('/')).unwrap()))
698                .collect::<Vec<_>>();
699
700            assert_eq!(
701                route.is_route_pattern_covered_by(ordinary_routes.iter()),
702                expected,
703                "coverage for {route} from {ordinary_routes:?}"
704            );
705        }
706    }
707
708    #[test]
709    fn test_normalize_metadata_route() {
710        assert_eq!(
711            AppPage::parse("(group)/foo/@par/bar/page.tsx").unwrap(),
712            AppPage(vec![
713                PageSegment::Group("group".into()),
714                PageSegment::Static("foo".into()),
715                PageSegment::Parallel("par".into()),
716                PageSegment::Static("bar".into()),
717                PageSegment::PageType(PageType::Page),
718            ])
719        );
720        assert_eq!(
721            AppPage::parse("(group)/foo/@par/bar/page").unwrap(),
722            AppPage(vec![
723                PageSegment::Group("group".into()),
724                PageSegment::Static("foo".into()),
725                PageSegment::Parallel("par".into()),
726                PageSegment::Static("bar".into()),
727                PageSegment::PageType(PageType::Page),
728            ])
729        );
730
731        assert_eq!(
732            AppPage::parse("(group)/foo/@par/bar/route.tsx").unwrap(),
733            AppPage(vec![
734                PageSegment::Group("group".into()),
735                PageSegment::Static("foo".into()),
736                PageSegment::Parallel("par".into()),
737                PageSegment::Static("bar".into()),
738                PageSegment::PageType(PageType::Route),
739            ])
740        );
741        assert_eq!(
742            AppPage::parse("(group)/foo/@par/bar/route").unwrap(),
743            AppPage(vec![
744                PageSegment::Group("group".into()),
745                PageSegment::Static("foo".into()),
746                PageSegment::Parallel("par".into()),
747                PageSegment::Static("bar".into()),
748                PageSegment::PageType(PageType::Route),
749            ])
750        );
751
752        assert_eq!(
753            AppPage::parse("foo/sitemap").unwrap(),
754            AppPage(vec![
755                PageSegment::Static("foo".into()),
756                PageSegment::Static("sitemap".into()),
757            ])
758        );
759
760        assert_eq!(
761            AppPage::parse("foo/robots.txt").unwrap(),
762            AppPage(vec![
763                PageSegment::Static("foo".into()),
764                PageSegment::Static("robots.txt".into()),
765            ])
766        );
767    }
768}