Skip to content

✨ feat(font): add assetsPrefix support for CDN deployments#30

Open
jcdogo wants to merge 1 commit into
rishi-raj-jain:masterfrom
dogomedia:features/assets-prefix
Open

✨ feat(font): add assetsPrefix support for CDN deployments#30
jcdogo wants to merge 1 commit into
rishi-raj-jain:masterfrom
dogomedia:features/assets-prefix

Conversation

@jcdogo

@jcdogo jcdogo commented Apr 2, 2025

Copy link
Copy Markdown

Description:

This pull request introduces a new optional assetsPrefix configuration property to astro-font. This feature allows users to specify a custom URL prefix for generated font assets (@font-face src URLs and <link rel="preload"> hrefs), enabling seamless integration with CDNs or projects where assets are served from a non-root path, as configured via Astro's build.assetsPrefix.

Problem:

Currently, astro-font generates root-relative URLs for font assets (e.g., /fonts/font.woff2). This works well for standard setups but breaks when using Astro's build.assetsPrefix feature to serve assets from a different domain (CDN) or a subpath (e.g., https://mycdn.com/assets/ or /my-base-path/). In such cases, the browser attempts to load fonts from the main site's root instead of the designated asset prefix, leading to 404 errors.

While astro-font correctly uses local font paths for its internal analysis (calculating fallback metrics for CLS optimization), the final output URLs do not respect the assetsPrefix.

Solution:

This PR implements the following changes:

  1. Added assetsPrefix?: string: A new optional property is added to the main Config interface within utils.ts.
  2. Passed assetsPrefix Prop: The <AstroFont> component now accepts an optional assetsPrefix prop. This single prop value is then merged into each individual font configuration object internally.
  3. Introduced generatePublicUrl Utility: A new function generatePublicUrl(localPath, basePath, assetsPrefix) is created in utils.ts. This function handles the logic for generating the final public URL:
    • If assetsPrefix is provided, it prepends the prefix to the font path relative to the project's public directory (determined by basePath).
    • If assetsPrefix is not provided, it defaults to the original behavior, generating a root-relative path (/path/to/font.woff2).
    • It correctly handles existing absolute URLs (e.g., https://...) passed in src.path, returning them directly.
  4. Updated URL Generation: The createPreloads and createBaseCSS functions now utilize generatePublicUrl to ensure the correct prefix is applied when generating <link> tags and src: url(...) values in @font-face rules.
  5. Maintained Local Analysis: The core logic for reading font files (getFontBuffer, getFallbackFont) continues to use the local filesystem path provided in src.path, ensuring the CLS optimization via fallback metric calculation remains unaffected and functions correctly even when assetsPrefix is used. This also ensures compatibility with the fetch: true option (where the font is downloaded locally first, and the assetsPrefix is then applied to the local generated path).

How to Use:

  1. Determine your assets prefix (e.g., from Astro's build.assetsPrefix or your CDN URL).
  2. Pass this prefix as a prop to the <AstroFont> component:
---
import { AstroFont } from "astro-font";
import { join } from "path";
// Example: Get prefix from environment or config helper
const assetsPrefix = import.meta.env.ASSETS_PREFIX || "https://mycdn.com/assets";

const interLocalPath = join(process.cwd(), "public", "fonts", "Inter-Regular.woff2");
---

<AstroFont
  assetsPrefix={assetsPrefix}
  config={[
    {
      name: "Inter",
      src: [
        {
          // Local path for analysis
          path: interLocalPath,
          style: "normal",
          weight: "400",
        }
      ],
      display: "swap",
      fallback: "sans-serif",
      cssVariable: "font-inter"
    }
    // ... other fonts
  ]}
/>

The resulting preload links and CSS URLs will now be correctly prefixed, e.g.:

  • href="https://mycdn.com/assets/fonts/Inter-Regular.woff2"
  • src: url(https://mycdn.com/assets/fonts/Inter-Regular.woff2)

Summary by CodeRabbit

  • New Features
    • Enhanced font configuration to support an optional custom prefix for asset URLs.
    • Improved asset URL generation for more reliable handling of both absolute and relative paths.

@coderabbitai

coderabbitai Bot commented Apr 2, 2025

Copy link
Copy Markdown

Walkthrough

The pull request updates the font configuration in the Astro component and its utility functions. In AstroFont.astro, a new Props interface is introduced to include an optional assetsPrefix property. The component now destructures both originalConfig and assetsPrefix, creating a modified configuration array (configWithPrefix) that is passed to generateFonts. In the utility module, the Config interface is enhanced with an optional assetsPrefix and a new function, generatePublicUrl, is implemented to streamline asset URL generation. Existing functions for preloading and CSS generation now use this new URL generator.

Changes

File(s) Change Summary
packages/astro-font/AstroFont.astro Updated type definitions by adding a new Props interface with properties config: Config[] and assetsPrefix?: string. Replaced the original destructuring of props with one that extracts originalConfig and assetsPrefix, and introduced configWithPrefix to include the assets prefix in configurations. Called generateFonts with the updated configuration.
packages/astro-font/utils.ts Enhanced the Config interface by adding an optional assetsPrefix property. Added a new function, generatePublicUrl, which constructs public URLs by handling absolute URLs, computing relative paths, and concatenating an assets prefix if provided. Modified createPreloads and createBaseCSS to use the new URL generation logic.

Sequence Diagram(s)

sequenceDiagram
    participant AF as AstroFont Component
    participant AP as Astro Props
    participant CM as Config Modifier
    participant GF as generateFonts

    AF->>AP: Retrieve originalConfig and assetsPrefix
    AP-->>AF: Return props values
    AF->>CM: Map originalConfig to configWithPrefix (adds assetsPrefix)
    CM-->>AF: Return updated configurations
    AF->>GF: Call generateFonts(configWithPrefix)
    GF-->>AF: Process font generation
Loading
sequenceDiagram
    participant Caller as Preload/CSS Generator
    participant GPU as generatePublicUrl

    Caller->>GPU: Call with localPath, basePath, and [assetsPrefix]
    GPU->>GPU: Check if localPath is absolute?
    alt localPath is absolute
        GPU-->>Caller: Return absolute localPath
    else localPath is relative
        GPU->>GPU: Compute relative path and adjust assetsPrefix (ensure trailing slash, etc.)
        GPU-->>Caller: Return constructed public URL
    end
Loading

Poem

I'm a hopping rabbit, nibbling on new code,
Sprinkling assets with a prefix in my abode.
Fonts twist and twirl, with URLs newfound,
In the code garden where logic is unbound.
With joyful leaps, I celebrate this smooth download!

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/astro-font/utils.ts (1)

1-428: Consider adding a unit test for the new URL generation logic.

The implementation looks solid, but it would be beneficial to add tests specifically for the generatePublicUrl function to verify it handles different edge cases correctly:

  • URLs with and without the prefix
  • Various combinations of slashes
  • Absolute URLs
🧰 Tools
🪛 Biome (1.9.4)

[error] 147-147: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e567d89 and 7556ad7.

📒 Files selected for processing (2)
  • packages/astro-font/AstroFont.astro (1 hunks)
  • packages/astro-font/utils.ts (3 hunks)
🔇 Additional comments (8)
packages/astro-font/AstroFont.astro (3)

5-8: Nice addition of the assetsPrefix property!

The new Props interface clearly defines the component's contract, making the optional assetsPrefix parameter explicit for users.


10-16: Good approach for maintaining backward compatibility.

The implementation properly handles the new assetsPrefix property by:

  1. Destructuring the props with appropriate renaming
  2. Creating a new configuration that includes the prefix in each font config

This ensures the prefix is properly propagated to all utility functions.


18-18: LGTM! Proper usage of the enhanced config.

The generateFonts function is now receiving the enhanced configuration with the assets prefix data.

packages/astro-font/utils.ts (5)

60-60: LGTM! Interface updated correctly.

The Config interface has been properly extended with the optional assetsPrefix property.


75-77: Good refactoring of the getBasePath function.

The function is now more concise and readable.


79-95: Well-implemented URL generation function.

The new generatePublicUrl function:

  1. Correctly handles absolute URLs
  2. Properly normalizes both the prefix and the relative path
  3. Falls back to the original behavior when no prefix is provided
  4. Is well-commented with clear explanations

This is the core of the feature and has been implemented thoughtfully.


351-361: Good implementation of the preload URL generation.

The function now correctly:

  1. Uses the new URL generation logic
  2. Extracts the base path calculation for clarity
  3. Passes the optional assets prefix

This ensures preload links will work correctly with CDN deployments.


363-375: CSS generation properly updated to use the new URL format.

The createBaseCSS function now correctly generates URLs with the assets prefix applied, ensuring that @font-face rules will reference fonts at the correct locations when using CDNs.

@scottmessinger

Copy link
Copy Markdown

@rishi-raj-jain This would be a really helpful addition to this library!

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for CDN deployments by introducing an optional assetsPrefix configuration property to astro-font. This enables users to specify custom URL prefixes for font assets, allowing fonts to be served from CDNs or non-root paths as configured through Astro's build.assetsPrefix.

Changes:

  • Added assetsPrefix optional property to the Config interface
  • Introduced generatePublicUrl utility function to handle public URL generation with optional prefix support
  • Updated createPreloads and createBaseCSS to use the new URL generation logic

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

File Description
packages/astro-font/utils.ts Added assetsPrefix to Config interface, implemented generatePublicUrl function for URL generation, and updated createPreloads and createBaseCSS to use the new URL generation logic
packages/astro-font/AstroFont.astro Added assetsPrefix prop to component interface, implemented logic to propagate the prefix to all font configurations

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 45 to 61
interface Config {
name: string
src: Source[]
fetch?: boolean
verbose?: boolean
selector?: string
preload?: boolean
cacheDir?: string
basePath?: string
fallbackName?: string
googleFontsURL?: string
cssVariable?: string | boolean
fallback: 'serif' | 'sans-serif' | 'monospace'
// https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display
display: 'auto' | 'block' | 'swap' | 'fallback' | 'optional' | (string & {})
assetsPrefix?: string
}

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Config interface needs to be exported to allow the import statement in AstroFont.astro (line 2) to work properly. The file currently imports Config from ./dist/utils, but since this interface is not exported, TypeScript compilation will fail or the import will not resolve correctly. Change interface Config to export interface Config to fix this issue.

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +90
if (assetsPrefix) {
// Ensure prefix ends with a slash and relative path doesn't start with one
const normalizedPrefix = assetsPrefix.endsWith('/') ? assetsPrefix : assetsPrefix + '/';
const normalizedRelative = pathRelativeToPublic.startsWith('/') ? pathRelativeToPublic.substring(1) : pathRelativeToPublic;
return normalizedPrefix + normalizedRelative;

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generatePublicUrl function should validate the assetsPrefix parameter to prevent potential security issues. If a user provides a malicious prefix containing special characters or path traversal sequences (e.g., javascript:, data:, or ../), this could lead to XSS vulnerabilities or unintended behavior. Consider adding validation to ensure the assetsPrefix starts with http://, https://, or /, and doesn't contain dangerous protocols or path traversal patterns.

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +15
// Add the assetsPrefix passed to the component to each font config
assetsPrefix: assetsPrefix,

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When assetsPrefix is undefined, the spread operator on line 15 will add assetsPrefix: undefined to each config object. While this works functionally, it's more explicit and cleaner to conditionally add the property only when it's defined. Consider using: ...(assetsPrefix ? { assetsPrefix } : {}) instead of assetsPrefix: assetsPrefix to avoid adding undefined properties to the config objects.

Suggested change
// Add the assetsPrefix passed to the component to each font config
assetsPrefix: assetsPrefix,
// Add the assetsPrefix passed to the component to each font config, only when defined
...(assetsPrefix ? { assetsPrefix } : {}),

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +8
interface Props {
config: Config[];
assetsPrefix?: string;
}

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Props interface is redefined locally in this file instead of extending the existing Props interface from utils.ts. This creates a discrepancy where the utils.ts exports Props with only a config property, but this file defines a different Props interface with both config and assetsPrefix. This could cause confusion and type inconsistencies. Consider either updating the exported Props interface in utils.ts to include the optional assetsPrefix property, or using a different name for this local interface (e.g., AstroFontProps) to avoid shadowing the exported type.

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +93

if (assetsPrefix) {
// Ensure prefix ends with a slash and relative path doesn't start with one
const normalizedPrefix = assetsPrefix.endsWith('/') ? assetsPrefix : assetsPrefix + '/';
const normalizedRelative = pathRelativeToPublic.startsWith('/') ? pathRelativeToPublic.substring(1) : pathRelativeToPublic;
return normalizedPrefix + normalizedRelative;
} else {
// Original behavior: return root-relative path, ensuring it starts with '/'
return '/' + (pathRelativeToPublic.startsWith('/') ? pathRelativeToPublic.substring(1) : pathRelativeToPublic);

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path normalization logic could produce incorrect results on Windows when pathRelativeToPublic contains backslashes. The relative() function from pathe should handle cross-platform paths, but the string manipulation with startsWith('/') and substring(1) assumes forward slashes. Consider normalizing the path separators before the slash checks, or use pathe's normalize() function to ensure consistent forward slashes across platforms. For example: const normalizedRelative = pathRelativeToPublic.replace(/\\/g, '/').replace(/^\//, '');

Suggested change
if (assetsPrefix) {
// Ensure prefix ends with a slash and relative path doesn't start with one
const normalizedPrefix = assetsPrefix.endsWith('/') ? assetsPrefix : assetsPrefix + '/';
const normalizedRelative = pathRelativeToPublic.startsWith('/') ? pathRelativeToPublic.substring(1) : pathRelativeToPublic;
return normalizedPrefix + normalizedRelative;
} else {
// Original behavior: return root-relative path, ensuring it starts with '/'
return '/' + (pathRelativeToPublic.startsWith('/') ? pathRelativeToPublic.substring(1) : pathRelativeToPublic);
// Normalize path separators and remove any leading slash for consistent URL construction
const normalizedRelative = pathRelativeToPublic.replace(/\\/g, '/').replace(/^\//, '');
if (assetsPrefix) {
// Ensure prefix ends with a slash and relative path doesn't start with one
const normalizedPrefix = assetsPrefix.endsWith('/') ? assetsPrefix : assetsPrefix + '/';
return normalizedPrefix + normalizedRelative;
} else {
// Original behavior: return root-relative path, ensuring it starts with '/'
return '/' + normalizedRelative;

Copilot uses AI. Check for mistakes.
Comment thread packages/astro-font/utils.ts
Comment on lines +80 to +82
if (localPath.startsWith('https:') || localPath.startsWith('http:')) {
return localPath;
}

Copilot AI Feb 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The absolute URL detection only checks for https: and http: protocols, but doesn't handle protocol-relative URLs (starting with //) or other schemes like file://. While these cases may be rare in practice, consider using a more comprehensive check such as testing if the path contains :// or starts with // to handle all absolute URL formats consistently. For example: if (localPath.includes('://') || localPath.startsWith('//')) would be more robust.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants