diff --git a/_articles/faq/chromium-142-local-network-access-issue.md b/_articles/faq/chromium-142-local-network-access-issue.md index e3f99a82..55750f61 100644 --- a/_articles/faq/chromium-142-local-network-access-issue.md +++ b/_articles/faq/chromium-142-local-network-access-issue.md @@ -3,37 +3,60 @@ layout: default-layout noTitleIndex: true needAutoGenerateSidebar: true needGenerateH3Content: true -title: Error message - Permission was denied for this request to access the unknown address space -keywords: Dynamic Web TWAIN, Error Troubleshooting, CORS, unknown address space, Chromium, 142 -breadcrumbText: Error message - Permission was denied for this request to access the unknown address space -description: CORS unknown address space +title: Error message - Local network access permission denied +keywords: Dynamic Web TWAIN, Error Troubleshooting, CORS, unknown address space, Chromium, 142, Firefox, 153, Local Network Access +breadcrumbText: Error message - Local network access permission denied +description: Local network access permission denied in Chromium and Firefox date: 2025-11-04 17:21:42 +0000 -last_modified: 2026-02-12 10:20:00 -0800 +last_modified: 2026-09-01 08:31:31 +0000 --- # Error Troubleshooting > [!IMPORTANT] -> This is an evolving browser behavior. Details in this article may change as Chromium updates Local Network Access. +> This is an evolving browser security behavior. Details in this article may change as Chromium and Firefox continue updating their Local Network Access implementations. -## Error message - CORS errors caused by local network access permissions in Chromium 142 and later +## Error message - Local network access permission denied ### Overview -Local Network Access (LNA) is a browser security model that has been enforced in Chromium-based browsers since version 142 (released October 28, 2025), including Chrome, Edge, Brave, and Opera. It blocks web apps from reaching local or loopback targets unless the user explicitly grants permission, which can affect Dynamic Web TWAIN Service behavior. +Local Network Access (LNA) is a browser security model that restricts requests from public network origins to local or loopback destinations - including `localhost` / `127.0.0.1` - unless the user explicitly grants permission. Dynamic Web TWAIN relies on a locally installed service that listens on a loopback address, so if this permission is not granted, DWT cannot communicate with that service. -These restrictions limit requests from **public network locations** to **local or loopback locations** unless the required site permission is granted. +When a page first requests local access, the browser shows an LNA permission prompt. The symptoms and resolution steps below apply when a user dismisses this prompt or clicks **Block**. -Starting in **Chrome 145**, the site-setting label changed from one local network access permission to two: +Both Chromium-based browsers (Chrome, Edge, Brave, Opera) and Firefox now enforce LNA, but on different rollout timelines and with different permission names - choose your browser below. + +### Root Cause + +A page hosted on a public origin (for example, `https://your-domain.com`) is attempting to reach a loopback address (`127.0.0.1`), which is now treated as a protected local-network request. Under this model, such requests are blocked by default unless the required permission has been granted. + +> [!WARNING] +> This is driven by browser security policy decisions. Dynamic Web TWAIN cannot bypass these restrictions programmatically. +> Browser behavior will continue evolving, and temporary workarounds may be removed in future versions. You should plan your deployment and UX flow around current browser permission requirements. + +> [!NOTE] +> Browser permission popup UI changes frequently. Starting with Dynamic Web TWAIN **v19.3.1**, static popup screenshots were removed from this FAQ - for the latest browser-specific screenshots, see: +> [https://dynamsoft.github.io/Dynamic-Web-TWAIN/local-network-access.html](https://dynamsoft.github.io/Dynamic-Web-TWAIN/local-network-access.html) + +
+ +- [Chromium 142+](#chromium142plus) +- [Firefox 153+](#firefox153plus) + +
+ +Local Network Access has been enforced in Chromium-based browsers since version 142 (released October 28, 2025), including Chrome, Edge, Brave, and Opera. + +> [!NOTE] +> For Chromium's background and design rationale, see [Chrome's developer blog](https://developer.chrome.com/blog/local-network-access) and the [Chrome Platform Status entry](https://chromestatus.com/feature/5152728072060928). + +Starting in **Chrome 145**, the site-setting label changed from one local network access permission to two: - `loopback-network` shown as **Apps on device** - `local-network` shown as **Local Network** Dynamic Web TWAIN Service communicates with `localhost` / `127.0.0.1`, so **Apps on device** (`loopback-network`) is the key permission for most deployments. -When your page first requests local access, Chromium will show an LNA permission prompt. -This FAQ and the symptoms below apply when users dismiss this prompt or click **Block**. - ![LNA prompt](/assets/imgs/local-network-access/LNA-prompt.png) ### Symptoms @@ -56,7 +79,7 @@ from origin 'https://your-domain.com' has been blocked by CORS policy: Permission was denied for this request to access the `unknown` address space. ``` -This happens because a page on a public origin (for example, `https://your-domain.com`) is trying to access a loopback address (`127.0.0.1`), which Chromium now treats as a protected local-network request. +This is the Local Network Access restriction described in Root Cause above. --- @@ -73,25 +96,10 @@ Observed behavior depends on Chromium version and Dynamic Web TWAIN (DWT) versio > [!NOTE] > (*) Blocking WebSocket requests is on Chromium's roadmap, and may be enforced in a future release. -> Other browsers are also introducing local network permission controls. - -### Root Cause - -Chromium 142 introduced [Local Network Access (LNA)](https://chromestatus.com/feature/5152728072060928), which restricts requests from public network locations to local/loopback network locations unless permission is granted. - -> [!NOTE] -> For background and design rationale, see Chrome's developer blog: [New permission prompt for Local Network Access](https://developer.chrome.com/blog/local-network-access). - -Under this model, requests from a public origin (a publicly hosted site) to local or loopback targets (including `localhost` and `127.0.0.1`) can be blocked by default. - -Dynamic Web TWAIN relies on a locally installed service that listens on a loopback address. If browser permission is not granted, communication with that local service fails. +> Firefox has already begun enforcing Local Network Access, including blocking WebSocket connections starting in Firefox 154 - see the Firefox tab above. ### Resolution -> [!WARNING] -> This is driven by browser security policy decisions. Dynamic Web TWAIN cannot bypass these restrictions programmatically. -> Browser behavior will continue evolving, and temporary workarounds may be removed in future versions. You should plan your deployment and UX flow around current browser permission requirements. - ***1. To manually correct this in Chrome*** - Navigate to your Dynamic Web TWAIN page. @@ -103,11 +111,6 @@ Dynamic Web TWAIN relies on a locally installed service that listens on a loopba ![local-network.png](/assets/imgs/local-network-access/local-network.png) -> [!NOTE] -> Chrome updates permission popup UI frequently. Starting with Dynamic Web TWAIN **v19.3.1**, static screenshots were removed from built-in popups. -> For the latest browser-specific screenshots, see: -> [https://dynamsoft.github.io/Dynamic-Web-TWAIN/local-network-access.html](https://dynamsoft.github.io/Dynamic-Web-TWAIN/local-network-access.html) - ***2. (For Admins) Apply this setting across an enterprise*** Enterprise administrators can deploy Chrome and/or Edge policies to set local-network permission to `Allow` for your website. @@ -116,15 +119,117 @@ Please refer to: - [Chrome Enterprise Policy List and Management Documentation](https://chromeenterprise.google/policies/#LocalNetworkAccessAllowedForUrls) - [Microsoft Edge Browser Policy Documentation](https://learn.microsoft.com/en-us/deployedge/microsoft-edge-browser-policies/localnetworkaccessallowedforurls) -***3. Developer Notes*** +### Product Improvements Related to Local Network Access + +Starting from v19.3, Dynamic Web TWAIN now includes UX enhancements to better surface local-service connectivity and permission issues. + +These changes do not alter or bypass Chromium's security model. They make permission-related failures easier to identify and guide users to the correct browser settings. + +The key improvements include: + +- **Guide the user to grant local network access** + If the service is installed (detected via WebSocket) but access to the local service is failing, and the detected permission state is "prompt", DWT prompts the user to grant access. + + *Dialog 1 - Permission Granting Guidance* + + ![permission granting dialog](/assets/imgs/local-network-access/permission-granting-dialog.png) + +- **Explicit detection of blocked local network access** + If the permission can be detected and is "denied", a clear dialog explains the cause and directs users to a guide, which tells how to enable the permission in site settings. + + ![prompt blocked](/assets/imgs/local-network-access/prompt-blocked.png) + + *Dialog 2 - Site Settings Guidance* + +- **Clearer messaging during service installation** + The service installation dialog explains that connection failure may be caused either by missing service installation or denied local-network permission. + + ![dialog installation](/assets/imgs/local-network-access/service-installation-dialog.png) + + *Dialog 3 - Service Installation* + +These improvements are available starting with Dynamic Web TWAIN v19.3. + +For older versions, a supplemental JavaScript file can be provided on request by contacting [Dynamsoft Support](mailto:support@dynamsoft.com). + +> [!NOTE] +> This supplemental JavaScript file improves user guidance only and does not change browser permission requirements. + +
+ +
+ +Firefox is rolling out its own Local Network Access protections, with a similar goal to Chromium's but on a different timeline and with different permission names. + +> [!NOTE] +> For background and configuration details, see Mozilla's documentation: [Control personal device and local network permissions in Firefox](https://support.mozilla.org/en-US/kb/control-personal-device-local-network-permissions-firefox). -**a) If running inside an `iframe`** +Rollout timeline: + +| Firefox Version | Behavior | +|------------------|------------------------------------------------------------------------| +| 149 | LNA enforced only for users with Enhanced Tracking Protection set to **Strict** | +| 151 | Gradual rollout begins for all users | +| **153** | Enforced **by default** for all desktop users | +| **154** | Enforcement extended to **WebSocket** connections | + +Dynamic Web TWAIN Service communicates with the browser over WebSocket, so **Firefox 154+** is the version that directly affects DWT, in the same way Chromium's LNA does. + +Firefox groups this permission differently than Chromium: + +- **Device apps and services** - access to your own computer / localhost and apps running on it. This is the permission relevant to DWT Service. +- **Local network devices** - access to other hardware on the same LAN (printers, routers, IoT devices, etc.). Not required for DWT Service. + +### Symptoms + +Because Firefox blocks the WebSocket connection itself (rather than a subset of requests, as Chromium currently does), DWT cannot distinguish "service not installed" from "service installed but permission blocked." As a result, Firefox users who dismiss or block the LNA prompt will consistently see: + +**Browser repeatedly prompts to download the service**, even though it is already installed - regardless of DWT version. + +![Firefox LNA prompt](/assets/imgs/local-network-access/firefox-LNA-prompt.png) + +> [!NOTE] +> As observed on Firefox 153+, DWT currently shows this generic service-installation dialog on Firefox rather than the enhanced guidance dialogs described under "Product Improvements Related to Local Network Access" in the Chromium tab - even though Firefox does support querying the underlying permission (see Developer Notes below). Until DWT's guided dialogs are extended to Firefox, use the manual steps below to resolve this. + +### Resolution + +***1. To manually correct this in Firefox*** + +- Navigate to your Dynamic Web TWAIN page. +- When Firefox shows the permission prompt near the address bar ("**\ wants to access other apps and services on this device**"), click **Allow**. +- To fix it after dismissing/blocking the prompt, or to manage it in advance: open Firefox **Settings → Privacy & Security → Permissions → Device apps and services → Settings...**, find your site, and set it to **Allow**. + +***2. (For Admins) Apply this setting across an enterprise*** + +Firefox Enterprise / ESR supports the `LocalNetworkAccess` policy via `policies.json`, which works differently from Chrome/Edge's `LocalNetworkAccessAllowedForUrls` - it uses a `SkipDomains` exception list rather than an allow-list: + +```json +{ + "policies": { + "LocalNetworkAccess": { + "SkipDomains": ["your-domain.com", "*.your-domain.com"] + } + } +} +``` + +Please refer to: [Firefox LocalNetworkAccess policy documentation](https://firefox-admin-docs.mozilla.org/reference/policies/localnetworkaccess/) + +
+ +
+ +## Developer Notes + +These notes apply to Dynamic Web TWAIN deployments on both Chromium-based browsers and Firefox, since both implement Local Network Access against the same underlying permission model. + +### If running inside an `iframe` > [!IMPORTANT] > If Dynamic Web TWAIN runs inside a cross-origin iframe, `loopback-network` permissions must be explicitly allowed in the iframe `allow` attribute. > If the iframe is same-origin, no additional iframe permission configuration is required. -For Chrome 145+, use `loopback-network` (and `local-network` only if needed). For older versions, include `local-network-access`. +For Chrome 145+, use `loopback-network` (and `local-network` only if needed). For older Chromium versions, include `local-network-access`. ```html @@ -137,9 +242,12 @@ For Chrome 145+, use `loopback-network` (and `local-network` only if needed). Fo ``` -**b) (Optional enhancement) Permission check for improved UX** +> [!NOTE] +> Firefox implements the same underlying Local Network Access specification as Chromium and reuses the `loopback-network` permission name, so the same `allow` attribute pattern above is expected to carry over. This has not been explicitly confirmed against a cross-origin iframe in Firefox - test before relying on it in production, and contact [Dynamsoft Support](https://www.dynamsoft.com/contact/) if you run into issues. + +### Permission check for improved UX -You can optionally query LNA permissions at runtime. This is not required, but it can help you guide users before initialization fails. +You can optionally query LNA permissions at runtime. This is not required, but it can help you guide users before initialization fails. The permission name `loopback-network` is shared by Chrome 145+ and Firefox 153+; the code below also falls back to the older Chromium 142-144 name. ```javascript // Helper: query the first supported permission name from a list. @@ -156,7 +264,7 @@ async function queryFirstSupportedPermission(names) { } (async () => { - // Chrome 145+: loopback-network; Chrome 142-144: local-network-access. + // Chrome 145+ and Firefox 153+: loopback-network; Chrome 142-144: local-network-access. const loopbackPerm = await queryFirstSupportedPermission([ "loopback-network", "local-network-access" @@ -172,18 +280,18 @@ async function queryFirstSupportedPermission(names) { if (loopbackPerm.state === "denied") { const currentSite = encodeURIComponent(window.location.origin); - const settingsUrl = `chrome://settings/content/siteDetails?site=${currentSite}`; + const chromeSettingsUrl = `chrome://settings/content/siteDetails?site=${currentSite}`; console.log( "Local network permission is denied.\n" + - `Open: ${settingsUrl}\n` + - "Then allow Local Network for this site." + `Chrome/Edge: open ${chromeSettingsUrl} and allow Local Network / Apps on device.\n` + + "Firefox: open Settings -> Privacy & Security -> Permissions -> Device apps and services, and allow this site." ); return; } if (loopbackPerm.state === "prompt") { alert( - "To connect with the local scanning service, Chrome may ask for Local Network permission. " + + "To connect with the local scanning service, your browser may ask for Local Network permission. " + "Please click Allow when prompted." ); } @@ -193,51 +301,6 @@ async function queryFirstSupportedPermission(names) { })(); ``` -If permission is not granted, direct users to: - -> Chrome -> Settings -> Privacy and security -> Site settings -> Local Network / Apps on device - -### Product Improvements Related to Local Network Access - -Starting from v19.3, Dynamic Web TWAIN now includes UX enhancements to better surface local-service connectivity and permission issues. - -These changes do not alter or bypass Chromium's security model. They make permission-related failures easier to identify and guide users to the correct browser settings. - -The key improvements include: - -- **Guide the user to grant local network access** - If the service is installed (detected with WebSocket) and the access to the local service is not through or the detected permission is "prompt", prompt the user to grant access. - - *Dialog 1 - Permission Granting Guidance* - - ![permission granting dialog](/assets/imgs/local-network-access/permission-granting-dialog.png) - -- **Explicit detection of blocked local network access** - If the permission can be detected and is "denied", a clear dialog explains the cause and directs users to a guide, which tells how to enable the permission in site settings. - - ![prompt blocked](/assets/imgs/local-network-access/prompt-blocked.png) - - *Dialog 2 - Site Settings Guidance* - -- **Clearer messaging during service installation** - The service installation dialog explains that connection failure may be caused either by missing service installation or denied local-network permission. - - ![dialog installation](/assets/imgs/local-network-access/service-installation-dialog.png) - - *Dialog 3 - Service Installation* - -- **Latest popup screenshots hosted externally (v19.3.1+)** - Because Chromium updates native permission popups frequently, static popup screenshots were removed from this FAQ in v19.3.1. - Use this page for the latest screenshots: - [https://dynamsoft.github.io/Dynamic-Web-TWAIN/local-network-access.html](https://dynamsoft.github.io/Dynamic-Web-TWAIN/local-network-access.html) - -These improvements are available starting with Dynamic Web TWAIN v19.3. - -For older versions, a supplemental JavaScript file can be provided on request by contacting [Dynamsoft Support](mailto:support@dynamsoft.com). - -> [!NOTE] -> This supplemental JavaScript file improves user guidance only and does not change browser permission requirements. - ## Other Causes of Failure to Connect to the Service There are other causes of service connection failure. See [another FAQ](/_articles/faq/service-prompting-to-install-repeatedly.md). diff --git a/_articles/faq/index.md b/_articles/faq/index.md index 7f029e1b..db029c58 100644 --- a/_articles/faq/index.md +++ b/_articles/faq/index.md @@ -169,7 +169,7 @@ description: Dynamic Web TWAIN SDK Documentation FAQ 22. [The scanner's UI or the system's file dialog does not open when scanning](/_articles/faq/service-is-blocked.md) 23. [How do I know which SDK version I am using?](/_articles/faq/find-SDK-version.md) 24. [Where can I download the Dynamic Web TWAIN Service installers only?](/_articles/faq/download-service-only.md) -25. [Error message - CORS Errors caused by local network access permissions when using Chromium 142 and later](/_articles/faq/chromium-142-local-network-access-issue.md) +25. [Error message - Local network access permission denied (Chromium and Firefox)](/_articles/faq/chromium-142-local-network-access-issue.md) 26. [macOS Rosetta Discontinuation and Dynamic Web TWAIN Service](/_articles/faq/mac-rosetta-discontinuation.md) ## Error Troubleshooting @@ -206,7 +206,7 @@ description: Dynamic Web TWAIN SDK Documentation FAQ 30. [How to debug on Dynamic Web TWAIN online demo?](/_articles/faq/debug-on-online-demo.md) 31. [Dynamic Web TWAIN prompts the .deb installer for Windows](/_articles/faq/incorrect-installer-for-windowsARM64.md) 32. [Why am I unable to load the TIFF file into Dynamic Web TWAIN?](/_articles/faq/unable-to-load-4-bit-tiff.md) -33. [Error message - CORS Errors caused by local network access permissions when using Chromium 142 and later](/_articles/faq/chromium-142-local-network-access-issue.md) +33. [Error message - Local network access permission denied (Chromium and Firefox)](/_articles/faq/chromium-142-local-network-access-issue.md) 34. [Error message - File is Damaged and Can’t Be Opened in macOS](/_articles/faq/file-damaged-on-macos.md) 35. [File Dialog Closes Immediately on Linux](/_articles/faq/file-dialog-on-linux.md) diff --git a/_config.yml b/_config.yml index f495c324..5a9882e8 100644 --- a/_config.yml +++ b/_config.yml @@ -41,6 +41,17 @@ dnt_icon: /web-twain/docs/assets/img-icon/icon-dnt.svg baseurl: /web-twain/docs/ +# Disable copy markdown plugin in template: +copy_markdown: + enabled: false + +# Publish plain-Markdown copies of pages for llms.txt (see _plugins/copy_markdown.rb) +copy_markdown_advanced: + enabled: true + collections: + - articles + pages: false + future: true useVersionTreeV2: true diff --git a/_plugins/copy_markdown.rb b/_plugins/copy_markdown.rb index 982c522c..82788806 100644 --- a/_plugins/copy_markdown.rb +++ b/_plugins/copy_markdown.rb @@ -1,14 +1,208 @@ -Jekyll::Hooks.register :site, :post_write do |site| - site.collections.each do |label, collection| - next unless label == 'articles' - collection.docs.each do |doc| - source_path = File.join(site.source, doc.relative_path) - next unless File.exist?(source_path) +# _plugins/copy_markdown.rb +# +# Publishes a plain-Markdown copy of each content page so llms.txt (and similar +# llms*.txt files) can link to Markdown versions of every page, e.g. +# https://www.dynamsoft.com/web-twain/docs/general-usage/index.md +# +# A verbatim copy of the source file is not enough: its links are either +# repo-relative (/_articles/foo.md), relative (./foo.md), or unrendered Liquid +# ({{site.general-usage}}...). None of those resolve on the published site. +# This plugin snapshots each page's Markdown AFTER Jekyll has resolved links: +# - jekyll-relative-links (a :lowest generator) already rewrote +# /_articles/foo.md and ./foo.md into the canonical site URL +# (/web-twain/docs/foo.html) inside doc.content; +# - in :pre_render we run the content through Liquid once more, so {{site.*}} +# links are expanded too; +# - we then rewrite canonical .../foo.html links into their full .md URLs so +# the published Markdown stays self-contained. +# The original YAML front matter is preserved as-is. Links inside inline HTML +# () and image/asset links are intentionally left untouched. +# +# The snapshot is taken in :pre_render (where the resolved Markdown still +# exists) and written to disk only in :site :post_write, after Jekyll has +# cleaned the destination folder; extra files written earlier would be pruned +# as orphans. +# +# Which pages/collections are published, and the URL base, come from +# _config.yml: +# +# copy_markdown_advanced: +# enabled: true +# collections: ["articles"] # array of collection labels, or true = all +# pages: false # also publish top-level Markdown pages +# exclude: ["README.md"] # optional: path names or directory prefixes to skip +# full_url: "" # optional; defaults to site.docFullPath, then url + baseurl +# +# The plugin is inert unless the copy_markdown_advanced block is present and enabled. +require 'fileutils' + +module CopyMarkdownForLlm + KEY = 'copy_markdown_advanced' + MD_EXTENSIONS = %w[.md .markdown].freeze + + # "collection-doc:path" or "page:path" => rendered Markdown (front matter + body) + BUFFER = {} + + module_function + + def plugin_config(site) + cfg = site.config[KEY] + cfg.is_a?(Hash) ? cfg : {} + end + + def enabled?(site) + cfg = plugin_config(site) + return false if cfg.empty? + + cfg.fetch('enabled', true) + end + + def markdown?(item) + MD_EXTENSIONS.include?(File.extname(item.relative_path).downcase) + end + + def excluded?(site, relative_path) + patterns = plugin_config(site)['exclude'] + return false unless patterns + + Array(patterns).any? do |pat| + pat = pat.to_s.sub(%r{/+\z}, '') + relative_path == pat || relative_path.start_with?("#{pat}/") || File.basename(relative_path) == pat + end + end + + def collection_enabled?(site, label) + value = plugin_config(site)['collections'] + return false if value.nil? || value == false + + value == true || Array(value).map(&:to_s).include?(label.to_s) + end - dest_path = doc.destination(site.dest).sub(/\.html$/, '.md') - dest_dir = File.dirname(dest_path) - FileUtils.mkdir_p(dest_dir) - FileUtils.cp(source_path, dest_path) + # Is this document/page a Markdown page we should publish a .md copy of? + def process?(site, item) + relative_path = item.relative_path.tr('\\', '/') + return false unless markdown?(item) + return false if excluded?(site, relative_path) + + if item.respond_to?(:collection) && item.collection + collection_enabled?(site, item.collection.label) + else + !!plugin_config(site)['pages'] end end + + # Absolute base used for the rewritten links (read from config, nothing + # hard-coded): copy_markdown.full_url > docFullPath > url + baseurl. + def link_bases(site) + full = plugin_config(site)['full_url'].to_s + full = site.config['docFullPath'].to_s if full.empty? + full = "#{site.config['url']}#{site.config['baseurl']}" if full.empty? + full = full.sub(%r{/+\z}, '') + root = site.config['baseurl'].to_s.sub(%r{/+\z}, '') + [full, root] + end + + # Rewrite an internal docs URL (…/foo.html in either its root-absolute or + # full-URL form) to the .md form. Anything else is returned unchanged; a + # "#fragment" is preserved. + def to_full_md(dest, full_base, root_base) + rel = + if !root_base.empty? && dest.start_with?("#{root_base}/") + dest.sub(%r{\A#{Regexp.escape(root_base)}}, '') + elsif !full_base.empty? && dest.start_with?("#{full_base}/") + dest.sub(%r{\A#{Regexp.escape(full_base)}}, '') + end + return dest unless rel + + path, fragment = rel.split('#', 2) + return dest unless path =~ /\.html\z/i + + output = if full_base.empty? + "#{root_base}#{path.sub(/\.html\z/i, '')}.md" + else + "#{full_base}#{path.sub(/\.html\z/i, '')}.md" + end + fragment ? "#{output}##{fragment}" : output + end + + # @return [String, nil] final Markdown (original front matter + body), or nil. + def snapshot(site, item, payload) + source_path = File.join(site.source, item.relative_path) + return nil unless File.exist?(source_path) + + full_base, root_base = link_bases(site) + + # Keep the original YAML front matter byte-for-byte. + raw = File.binread(source_path).force_encoding('UTF-8') + front_matter = raw[/\A---\s*\n.*?\n---\s*\n+/m] || '' + + # doc.content already has its .md/relative links resolved by + # jekyll-relative-links; expand the remaining {{ }} / {% %} via Liquid. + content = item.content.to_s + if item.respond_to?(:render_with_liquid?) && item.render_with_liquid? + registers = { :site => site, :page => payload.is_a?(Hash) ? payload['page'] : nil } + content = site.liquid_renderer.file(item.path).parse(content).render!(payload, :registers => registers) + end + + # Canonical site links -> full .md links, for Markdown links only. Links in + # inline HTML () are intentionally left untouched. + content = content.gsub(%r{(\]\()([^)\s]+)}) do + head = Regexp.last_match(1) + dest = Regexp.last_match(2) + "#{head}#{to_full_md(dest, full_base, root_base)}" + end + + front_matter + content + end + + # Filesystem path of the .md copy inside site.dest (mirrors the .html page URL). + def dest_path(site, item) + return nil unless item.url.to_s =~ /\.html\z/i + + File.join(site.dest, item.url.sub(/\.html\z/i, '').sub(%r{\A/+}, '') + '.md') + end + + def buffer_key(item) + kind = item.respond_to?(:collection) && item.collection ? 'collection' : 'page' + "#{kind}:#{item.relative_path.tr('\\', '/')}" + end +end + +# Take the snapshot while the fully-resolved Markdown still exists (before the +# document/page is converted to HTML). +def copy_markdown_pre_render(item, payload) + return unless item.respond_to?(:site) && CopyMarkdownForLlm.enabled?(item.site) + return unless CopyMarkdownForLlm.process?(item.site, item) + + content = CopyMarkdownForLlm.snapshot(item.site, item, payload) + CopyMarkdownForLlm::BUFFER[CopyMarkdownForLlm.buffer_key(item)] = content if content +end + +Jekyll::Hooks.register :documents, :pre_render do |doc, payload| + copy_markdown_pre_render(doc, payload) +end + +Jekyll::Hooks.register :pages, :pre_render do |page, payload| + copy_markdown_pre_render(page, payload) +end + +# Write the .md copies after Jekyll has cleaned the destination folder. +Jekyll::Hooks.register :site, :post_write do |site| + next unless CopyMarkdownForLlm.enabled?(site) + + items = site.pages.dup + site.collections.each_value { |collection| items.concat(collection.docs) } + + items.each do |item| + next unless CopyMarkdownForLlm.process?(site, item) + + content = CopyMarkdownForLlm::BUFFER.delete(CopyMarkdownForLlm.buffer_key(item)) + next unless content + + path = CopyMarkdownForLlm.dest_path(site, item) + next unless path + + FileUtils.mkdir_p(File.dirname(path)) + File.binwrite(path, content) + end end diff --git a/assets/imgs/local-network-access/firefox-LNA-prompt.png b/assets/imgs/local-network-access/firefox-LNA-prompt.png new file mode 100644 index 00000000..9ab9dafc Binary files /dev/null and b/assets/imgs/local-network-access/firefox-LNA-prompt.png differ