Skip to content

build_page

Class info

Classes

Name Children Inherits
HTMLBuilder
mkdocs_mknodes.commands.build_page
Handles the HTML generation phase of building MkDocs sites.
    MarkdownBuilder
    mkdocs_mknodes.commands.build_page
    Handles the initial phase of building Websites.
      MkNodesConfig
      mkdocs_mknodes.plugin.mknodesconfig

        🛈 DocStrings

        The Mkdocs Plugin.

        HTMLBuilder

        Handles the HTML generation phase of building MkDocs sites.

        Source code in mkdocs_mknodes/commands/build_page.py
        181
        182
        183
        184
        185
        186
        187
        188
        189
        190
        191
        192
        193
        194
        195
        196
        197
        198
        199
        200
        201
        202
        203
        204
        205
        206
        207
        208
        209
        210
        211
        212
        213
        214
        215
        216
        217
        218
        219
        220
        221
        222
        223
        224
        225
        226
        227
        228
        229
        230
        231
        232
        233
        234
        235
        236
        237
        238
        239
        240
        241
        242
        243
        244
        245
        246
        247
        248
        249
        250
        251
        252
        253
        254
        255
        256
        257
        258
        259
        260
        261
        262
        263
        264
        265
        266
        267
        268
        269
        270
        271
        272
        273
        274
        275
        276
        277
        278
        279
        280
        281
        282
        283
        284
        285
        286
        287
        288
        289
        290
        291
        292
        293
        294
        295
        296
        297
        298
        299
        300
        301
        302
        303
        304
        305
        306
        307
        308
        309
        310
        311
        312
        313
        314
        315
        316
        317
        318
        319
        320
        321
        322
        323
        324
        325
        326
        327
        328
        329
        330
        331
        332
        333
        334
        335
        336
        337
        338
        339
        340
        341
        342
        343
        344
        345
        346
        347
        348
        349
        350
        351
        352
        353
        354
        355
        356
        357
        358
        359
        360
        361
        362
        363
        364
        365
        366
        367
        368
        369
        370
        371
        372
        373
        374
        375
        376
        377
        378
        379
        380
        381
        382
        383
        384
        385
        386
        387
        388
        389
        390
        391
        392
        393
        394
        395
        396
        397
        398
        399
        400
        401
        402
        403
        404
        405
        406
        407
        408
        409
        410
        411
        412
        413
        414
        415
        416
        417
        418
        419
        420
        421
        422
        423
        424
        425
        426
        427
        428
        429
        430
        431
        432
        class HTMLBuilder:
            """Handles the HTML generation phase of building MkDocs sites."""
        
            def __init__(self, config: MkNodesConfig):
                """Initialize the HTML builder.
        
                Args:
                    config: MkDocs configuration
                """
                self.config = config
        
            def build_html(
                self,
                nav: Navigation,
                files: Files,
                live_server_url: str | None = None,
                dirty: bool = False,
            ) -> None:
                """Build HTML files from processed markdown.
        
                Args:
                    nav: Navigation structure
                    files: Collection of files
                    live_server_url: An optional URL of the live server to use
                    dirty: Whether this is a dirty build
                """
                env = self.config.theme.get_env()
                with logfire.span("plugins callback: on_env", env=env, config=self.config):
                    env = self.config.plugins.on_env(env, config=self.config, files=files)
                inclusion = (
                    InclusionLevel.is_in_serve if live_server_url else InclusionLevel.is_included
                )
                with logfire.span("copy_static_files"):
                    files.copy_static_files(dirty=dirty, inclusion=inclusion)
                self._build_templates(env, files, nav)
                self._build_pages(files, nav, env, dirty, inclusion)
        
                with logfire.span("plugins callback: on_post_build", config=self.config):
                    self.config.plugins.on_post_build(config=self.config)
        
            @logfire.instrument("Build templates")
            def _build_templates(
                self, env: jinja2.Environment, files: Files, nav: Navigation
            ) -> None:
                """Build all templates.
        
                Args:
                    env: Jinja environment
                    files: Collection of files
                    nav: Navigation structure
                """
                for template in self.config.theme.static_templates:
                    self._build_theme_template(template, env, files, nav)
                for template in self.config.extra_templates:
                    self._build_extra_template(template, files, nav)
        
            @logfire.instrument("Build pages")
            def _build_pages(
                self,
                files: Files,
                nav: Navigation,
                env: jinja2.Environment,
                dirty: bool,
                inclusion: Callable[[InclusionLevel], bool],
            ) -> None:
                """Build all pages.
        
                Args:
                    files: Collection of files
                    nav: Navigation structure
                    env: Jinja environment
                    dirty: Whether this is a dirty build
                    inclusion: Inclusion level for pages
                """
                logger.debug("Building markdown pages.")
                doc_files = files.documentation_pages(inclusion=inclusion)
                log_level = self.config.validation.links.anchors
                for file in doc_files:
                    assert file.page
                    excl = file.inclusion.is_excluded()
                    self._build_page(file.page, doc_files, nav, env, dirty, excl)
                    with logfire.span("validate_anchor_links"):
                        file.page.validate_anchor_links(files=files, log_level=log_level)
        
            @logfire.instrument("Build page {page.file.url}")
            def _build_page(
                self,
                page: Page,
                doc_files: Sequence[File],
                nav: Navigation,
                env: jinja2.Environment,
                dirty: bool = False,
                excluded: bool = False,
            ) -> None:
                """Build a single page.
        
                Args:
                    page: Page to build
                    doc_files: Collection of documentation files
                    nav: Navigation structure
                    env: Jinja environment
                    dirty: Whether this is a dirty build
                    excluded: Whether the page is excluded
                """
                self.config._current_page = page
                try:
                    if dirty and not page.file.is_modified():
                        return
        
                    logger.debug("Building page %s", page.file.src_uri)
                    page.active = True
        
                    ctx = templatecontext.get_context(nav, doc_files, self.config, page)
                    template = env.get_template(page.meta.get("template", "main.html"))
                    ctx = self.config.plugins.on_page_context(
                        ctx,  # type: ignore
                        page=page,
                        config=self.config,  # type: ignore
                        nav=nav,
                    )
        
                    if excluded:
                        page.content = DRAFT_CONTENT + (page.content or "")
        
                    output = template.render(ctx)
                    output = self.config.plugins.on_post_page(
                        output, page=page, config=self.config
                    )
        
                    if output.strip():
                        text = output.encode("utf-8", errors="xmlcharrefreplace")
                        pathhelpers.write_file(text, page.file.abs_dest_path)
                    else:
                        logger.info(
                            "Page skipped: '%s'. Generated empty output.", page.file.src_uri
                        )
        
                except Exception as e:
                    message = f"Error building page '{page.file.src_uri}':"
                    if not isinstance(e, exceptions.BuildError):
                        message += f" {e}"
                    logger.exception(message)
                    raise
                finally:
                    page.active = False
                    self.config._current_page = None
        
            def _build_template(
                self,
                name: str,
                template: jinja2.Template,
                files: Files,
                nav: Navigation,
            ) -> str:
                """Build a template and return its rendered output.
        
                Args:
                    name: Template name
                    template: Template object
                    files: Collection of files
                    nav: Navigation structure
        
                Returns:
                    Rendered template as string
                """
                template = self.config.plugins.on_pre_template(
                    template, template_name=name, config=self.config
                )
        
                if utils.is_error_template(name):
                    base_url = urlsplit(self.config.site_url or "/").path
                else:
                    base_url = htmlfilters.relative_url_mkdocs(".", name)
        
                context = templatecontext.get_context(nav, files, self.config, base_url=base_url)
                ctx = self.config.plugins.on_template_context(
                    context,  # type: ignore
                    template_name=name,
                    config=self.config,  # type: ignore
                )
                output = template.render(ctx)
                return self.config.plugins.on_post_template(
                    output, template_name=name, config=self.config
                )
        
            def _build_theme_template(
                self,
                template_name: str,
                env: jinja2.Environment,
                files: Files,
                nav: Navigation,
            ) -> None:
                """Build a theme template.
        
                Args:
                    template_name: Name of the template
                    env: Jinja environment
                    files: Collection of files
                    nav: Navigation structure
                """
                logger.debug("Building theme template: %s", template_name)
        
                try:
                    template = env.get_template(template_name)
                except TemplateNotFound:
                    logger.warning("Template skipped: %r not found in theme dirs.", template_name)
                    return
        
                output = self._build_template(template_name, template, files, nav)
        
                if output.strip():
                    output_path = upath.UPath(self.config.site_dir) / template_name
                    pathhelpers.write_file(output.encode(), output_path)
                    if template_name == "sitemap.xml":
                        docs = files.documentation_pages()
                        pages = [f.page for f in docs if f.page is not None]
                        ts = utils.get_build_timestamp(pages=pages)
                        utils.write_gzip(f"{output_path}.gz", output, timestamp=ts)
                else:
                    logger.info("Template skipped: %r generated empty output.", template_name)
        
            def _build_extra_template(
                self,
                template_name: str,
                files: Files,
                nav: Navigation,
            ) -> None:
                """Build a user template not part of the theme.
        
                Args:
                    template_name: Name of the template
                    files: Collection of files
                    nav: Navigation structure
                """
                logger.debug("Building extra template: %s", template_name)
        
                file = files.get_file_from_path(template_name)
                if file is None:
                    logger.warning("Template skipped: %r not found in docs_dir.", template_name)
                    return
        
                try:
                    template = jinja2.Template(file.content_string)
                except Exception:
                    logger.exception("Error reading template %r", template_name)
                    return
        
                output = self._build_template(template_name, template, files, nav)
                if output.strip():
                    pathhelpers.write_file(output.encode(), file.abs_dest_path)
                else:
                    logger.info("Template skipped: %r generated empty output.", template_name)
        

        __init__

        __init__(config: MkNodesConfig)
        

        Initialize the HTML builder.

        Parameters:

        Name Type Description Default
        config MkNodesConfig

        MkDocs configuration

        required
        Source code in mkdocs_mknodes/commands/build_page.py
        184
        185
        186
        187
        188
        189
        190
        def __init__(self, config: MkNodesConfig):
            """Initialize the HTML builder.
        
            Args:
                config: MkDocs configuration
            """
            self.config = config
        

        build_html

        build_html(
            nav: Navigation, files: Files, live_server_url: str | None = None, dirty: bool = False
        ) -> None
        

        Build HTML files from processed markdown.

        Parameters:

        Name Type Description Default
        nav Navigation

        Navigation structure

        required
        files Files

        Collection of files

        required
        live_server_url str | None

        An optional URL of the live server to use

        None
        dirty bool

        Whether this is a dirty build

        False
        Source code in mkdocs_mknodes/commands/build_page.py
        192
        193
        194
        195
        196
        197
        198
        199
        200
        201
        202
        203
        204
        205
        206
        207
        208
        209
        210
        211
        212
        213
        214
        215
        216
        217
        218
        219
        def build_html(
            self,
            nav: Navigation,
            files: Files,
            live_server_url: str | None = None,
            dirty: bool = False,
        ) -> None:
            """Build HTML files from processed markdown.
        
            Args:
                nav: Navigation structure
                files: Collection of files
                live_server_url: An optional URL of the live server to use
                dirty: Whether this is a dirty build
            """
            env = self.config.theme.get_env()
            with logfire.span("plugins callback: on_env", env=env, config=self.config):
                env = self.config.plugins.on_env(env, config=self.config, files=files)
            inclusion = (
                InclusionLevel.is_in_serve if live_server_url else InclusionLevel.is_included
            )
            with logfire.span("copy_static_files"):
                files.copy_static_files(dirty=dirty, inclusion=inclusion)
            self._build_templates(env, files, nav)
            self._build_pages(files, nav, env, dirty, inclusion)
        
            with logfire.span("plugins callback: on_post_build", config=self.config):
                self.config.plugins.on_post_build(config=self.config)
        

        MarkdownBuilder

        Handles the initial phase of building Websites.

        File collection and markdown processing.

        Source code in mkdocs_mknodes/commands/build_page.py
         41
         42
         43
         44
         45
         46
         47
         48
         49
         50
         51
         52
         53
         54
         55
         56
         57
         58
         59
         60
         61
         62
         63
         64
         65
         66
         67
         68
         69
         70
         71
         72
         73
         74
         75
         76
         77
         78
         79
         80
         81
         82
         83
         84
         85
         86
         87
         88
         89
         90
         91
         92
         93
         94
         95
         96
         97
         98
         99
        100
        101
        102
        103
        104
        105
        106
        107
        108
        109
        110
        111
        112
        113
        114
        115
        116
        117
        118
        119
        120
        121
        122
        123
        124
        125
        126
        127
        128
        129
        130
        131
        132
        133
        134
        135
        136
        137
        138
        139
        140
        141
        142
        143
        144
        145
        146
        147
        148
        149
        150
        151
        152
        153
        154
        155
        156
        157
        158
        159
        160
        161
        162
        163
        164
        165
        166
        167
        168
        169
        170
        171
        172
        173
        174
        175
        176
        177
        178
        class MarkdownBuilder:
            """Handles the initial phase of building Websites.
        
            File collection and markdown processing.
            """
        
            def __init__(self, config: MkNodesConfig | None = None):
                """Initialize the markdown builder.
        
                Args:
                    config: Optional MkDocs configuration
                """
                self.config = config or MkNodesConfig()
        
            def build_from_config(
                self,
                config_path: str | os.PathLike[str],
                **kwargs: Any,
            ) -> tuple[Navigation, Files]:
                """Build markdown content from config file.
        
                Args:
                    config_path: Path to the MkDocs config file
                    kwargs: Additional config overrides
        
                Returns:
                    Navigation structure
                """
                cfg_builder = configbuilder.ConfigBuilder()
                cfg_builder.add_config_file(config_path)
                self.config = cfg_builder.build_mkdocs_config(**kwargs)
        
                with logfire.span("plugins callback: on_startup", config=self.config):
                    self.config.plugins.on_startup(command="build", dirty=False)
        
                nav, files = self.process_markdown()
                return nav, files
        
            @utils.handle_exceptions
            @utils.count_warnings
            def process_markdown(self, dirty: bool = False) -> tuple[Navigation, Files]:
                """Process markdown files and build navigation structure.
        
                Args:
                    dirty: Do a dirty build
        
                Returns:
                    Navigation structure
                """
                if self.config is None:
                    msg = "Configuration must be set before processing markdown"
                    raise ValueError(msg)
        
                with logfire.span("plugins callback: on_config", config=self.config):
                    self.config = self.config.plugins.on_config(self.config)
                with logfire.span("plugins callback: on_pre_build", config=self.config):
                    self.config.plugins.on_pre_build(config=self.config)
        
                if not dirty:
                    logger.info("Cleaning site directory")
                    pathhelpers.clean_directory(self.config.site_dir)
        
                files = utils.get_files(self.config)
                env = self.config.theme.get_env()
                files.add_files_from_theme(env, self.config)
        
                with logfire.span("plugins callback: on_files", files=files, config=self.config):
                    files = self.config.plugins.on_files(files, config=self.config)
        
                utils.set_exclusions(files._files, self.config)
                nav = get_navigation(files, self.config)
        
                with logfire.span("plugins callback: on_nav", config=self.config, nav=nav):
                    nav = self.config.plugins.on_nav(nav, config=self.config, files=files)
        
                self._process_pages(files)
                return nav, files
        
            @logfire.instrument("Populate pages")
            def _process_pages(self, files: Files) -> None:
                """Process all pages, reading their content and applying plugins.
        
                Args:
                    files: Collection of files to process
                """
                for file in files.documentation_pages():
                    logger.debug("Reading: %s", file.src_uri)
                    if file.page is None and file.inclusion.is_not_in_nav():
                        Page(None, file, self.config)
                    assert file.page is not None
                    self._populate_page(file.page, files)
        
            @logfire.instrument("populate page for {page.file.src_uri}")
            def _populate_page(self, page: Page, files: Files) -> None:
                """Read page content from docs_dir and render Markdown.
        
                Args:
                    page: Page to populate
                    files: Collection of files
                """
                self.config._current_page = page
                try:
                    with logfire.span(
                        "plugins callback: on_pre_page", page=page, config=self.config
                    ):
                        page = self.config.plugins.on_pre_page(
                            page, config=self.config, files=files
                        )
        
                    with logfire.span("read_source", page=page):
                        page.read_source(self.config)
                    assert page.markdown is not None
        
                    with logfire.span(
                        "plugins callback: on_page_markdown", page=page, config=self.config
                    ):
                        page.markdown = self.config.plugins.on_page_markdown(
                            page.markdown, page=page, config=self.config, files=files
                        )
        
                    with logfire.span("render", page=page, config=self.config):
                        page.render(self.config, files)
                    assert page.content is not None
        
                    with logfire.span(
                        "plugins callback: on_page_content", page=page, config=self.config
                    ):
                        page.content = self.config.plugins.on_page_content(
                            page.content, page=page, config=self.config, files=files
                        )
                except Exception as e:
                    message = f"Error reading page '{page.file.src_uri}':"
                    if not isinstance(e, exceptions.BuildError):
                        message += f" {e}"
                    logger.exception(message)
                    raise
                finally:
                    self.config._current_page = None
        

        __init__

        __init__(config: MkNodesConfig | None = None)
        

        Initialize the markdown builder.

        Parameters:

        Name Type Description Default
        config MkNodesConfig | None

        Optional MkDocs configuration

        None
        Source code in mkdocs_mknodes/commands/build_page.py
        47
        48
        49
        50
        51
        52
        53
        def __init__(self, config: MkNodesConfig | None = None):
            """Initialize the markdown builder.
        
            Args:
                config: Optional MkDocs configuration
            """
            self.config = config or MkNodesConfig()
        

        build_from_config

        build_from_config(
            config_path: str | PathLike[str], **kwargs: Any
        ) -> tuple[Navigation, Files]
        

        Build markdown content from config file.

        Parameters:

        Name Type Description Default
        config_path str | PathLike[str]

        Path to the MkDocs config file

        required
        kwargs Any

        Additional config overrides

        {}

        Returns:

        Type Description
        tuple[Navigation, Files]

        Navigation structure

        Source code in mkdocs_mknodes/commands/build_page.py
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
        75
        76
        77
        def build_from_config(
            self,
            config_path: str | os.PathLike[str],
            **kwargs: Any,
        ) -> tuple[Navigation, Files]:
            """Build markdown content from config file.
        
            Args:
                config_path: Path to the MkDocs config file
                kwargs: Additional config overrides
        
            Returns:
                Navigation structure
            """
            cfg_builder = configbuilder.ConfigBuilder()
            cfg_builder.add_config_file(config_path)
            self.config = cfg_builder.build_mkdocs_config(**kwargs)
        
            with logfire.span("plugins callback: on_startup", config=self.config):
                self.config.plugins.on_startup(command="build", dirty=False)
        
            nav, files = self.process_markdown()
            return nav, files
        

        process_markdown

        process_markdown(dirty: bool = False) -> tuple[Navigation, Files]
        

        Process markdown files and build navigation structure.

        Parameters:

        Name Type Description Default
        dirty bool

        Do a dirty build

        False

        Returns:

        Type Description
        tuple[Navigation, Files]

        Navigation structure

        Source code in mkdocs_mknodes/commands/build_page.py
         79
         80
         81
         82
         83
         84
         85
         86
         87
         88
         89
         90
         91
         92
         93
         94
         95
         96
         97
         98
         99
        100
        101
        102
        103
        104
        105
        106
        107
        108
        109
        110
        111
        112
        113
        114
        115
        116
        117
        @utils.handle_exceptions
        @utils.count_warnings
        def process_markdown(self, dirty: bool = False) -> tuple[Navigation, Files]:
            """Process markdown files and build navigation structure.
        
            Args:
                dirty: Do a dirty build
        
            Returns:
                Navigation structure
            """
            if self.config is None:
                msg = "Configuration must be set before processing markdown"
                raise ValueError(msg)
        
            with logfire.span("plugins callback: on_config", config=self.config):
                self.config = self.config.plugins.on_config(self.config)
            with logfire.span("plugins callback: on_pre_build", config=self.config):
                self.config.plugins.on_pre_build(config=self.config)
        
            if not dirty:
                logger.info("Cleaning site directory")
                pathhelpers.clean_directory(self.config.site_dir)
        
            files = utils.get_files(self.config)
            env = self.config.theme.get_env()
            files.add_files_from_theme(env, self.config)
        
            with logfire.span("plugins callback: on_files", files=files, config=self.config):
                files = self.config.plugins.on_files(files, config=self.config)
        
            utils.set_exclusions(files._files, self.config)
            nav = get_navigation(files, self.config)
        
            with logfire.span("plugins callback: on_nav", config=self.config, nav=nav):
                nav = self.config.plugins.on_nav(nav, config=self.config, files=files)
        
            self._process_pages(files)
            return nav, files
        

        build

        build(
            config_path: str | PathLike[str],
            repo_path: str,
            build_fn: str | None,
            *,
            site_dir: str | None = None,
            clone_depth: int = 100,
            **kwargs: Any
        ) -> None
        

        Build a MkNodes-based website.

        Parameters:

        Name Type Description Default
        config_path str | PathLike[str]

        Path to the MkDocs config file

        required
        repo_path str

        Repository path/URL to build docs for

        required
        build_fn str | None

        Fully qualified name of build function to use

        required
        site_dir str | None

        Output directory for built site

        None
        clone_depth int

        Number of commits to fetch for Git repos

        100
        kwargs Any

        Additional config overrides passed to MkDocs

        {}
        Source code in mkdocs_mknodes/commands/build_page.py
        436
        437
        438
        439
        440
        441
        442
        443
        444
        445
        446
        447
        448
        449
        450
        451
        452
        453
        454
        455
        456
        457
        458
        def build(
            config_path: str | os.PathLike[str],
            repo_path: str,
            build_fn: str | None,
            *,
            site_dir: str | None = None,
            clone_depth: int = 100,
            **kwargs: Any,
        ) -> None:
            """Build a MkNodes-based website.
        
            Args:
                config_path: Path to the MkDocs config file
                repo_path: Repository path/URL to build docs for
                build_fn: Fully qualified name of build function to use
                site_dir: Output directory for built site
                clone_depth: Number of commits to fetch for Git repos
                kwargs: Additional config overrides passed to MkDocs
            """
            md_builder = MarkdownBuilder()
            nav, files = md_builder.build_from_config(config_path, site_dir=site_dir, **kwargs)
            html_builder = HTMLBuilder(md_builder.config)
            html_builder.build_html(nav, files)