Building Your Own Free URL Shortener for your Custom Domain

In today's digital landscape, URL shorteners have become essential tools for sharing links efficiently. However, most free URL shortening services come with significant limitations:
- Custom Domain Restrictions: Many services charge premium fees for using your own domain
- Privacy Concerns: Closed-source solutions may track user data in ways you can't control
- Limited Control: You can't modify the code or customize the behavior
- Usage Limits: Free tiers often restrict the number of URLs, domains, or monthly hits
This article will show you how to build your own URL shortener using GitLab Pages - completely free, with full control over your code and data, and no usage limits.
Why GitLab Pages?¶
GitLab Pages offers several advantages for hosting a URL shortener:
- Free hosting for static sites
- Custom domain support at no extra cost
- Automatic HTTPS with Let's Encrypt
- CI/CD integration for automated deployment
- Unlimited URLs and redirects
- No traffic limits
How It Works¶
Our URL shortener uses a simple but effective approach:
- Store redirect mappings in a YAML configuration file
- Generate static HTML pages with meta refresh redirects
- Deploy these pages to GitLab Pages
- Access your short URLs through your custom domain
The entire solution requires just a few files and minimal Python code.
Step 1: Set Up Your GitLab Repository¶
First, create a new GitLab repository for your URL shortener:
- Log in to GitLab and click "New project"
- Choose "Create blank project"
- Name it something like "url-shortener"
- Make it public or private (both work with GitLab Pages)
Step 2: Create the Core Files¶
You'll need these essential files:
1. main.py - The Generator Script¶
This script reads your redirect mappings and generates the HTML files:
import logging
import os
import shutil
import yaml
from typing import Dict, Any
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s",
)
logger = logging.getLogger(__name__)
def load_redirects(config_file: str = "redirects.yaml") -> Dict[str, str]:
    """Load redirects from YAML configuration file."""
    logger.info("Loading redirects from %s", config_file)
    try:
        with open(config_file, "r", encoding="utf-8") as fh:
            redirects = yaml.safe_load(fh) or {}
        logger.info("Loaded %d redirect entries", len(redirects))
        return redirects
    except FileNotFoundError:
        logger.warning("%s not found; nothing to generate", config_file)
        return {}
    except yaml.YAMLError as exc:
        logger.exception("Failed to parse %s: %s", config_file, exc)
        return {}
def create_redirect_html(url: str) -> str:
    """Generate HTML content for a redirect page."""
    return f'<meta http-equiv="refresh" content="0; url={url}">'
def setup_output_directory(output_dir: str = "public") -> None:
    """Setup and clean the output directory."""
    logger.info("Rebuilding %s directory", output_dir)
    shutil.rmtree(output_dir, ignore_errors=True)
    os.makedirs(output_dir, exist_ok=True)
def generate_redirect_files(
    redirects: Dict[str, str], output_dir: str = "public"
) -> None:
    """Generate redirect HTML files for all redirect mappings."""
    for path, url in redirects.items():
        target_dir = os.path.join(output_dir, path)
        os.makedirs(target_dir, exist_ok=True)
        target_file = os.path.join(target_dir, "index.html")
        logger.info("Creating redirect for '%s' -> %s", path, url)
        with open(target_file, "w", encoding="utf-8") as f:
            f.write(create_redirect_html(url))
def main() -> None:
    """Main function to generate redirect pages."""
    redirects = load_redirects()
    setup_output_directory()
    generate_redirect_files(redirects)
    logger.info("Redirect generation complete")
if __name__ == "__main__":
    main()
Code Explanation for Beginners:
- 
Imports and Setup (Lines 1-11): - We import necessary Python modules: loggingfor tracking what's happening,osandshutilfor file operations, andyamlfor reading configuration files.
- 
typing.Dictis used for type hints, making the code more readable and maintainable.
- We set up logging to show timestamps and line numbers, which helps with debugging.
 
- We import necessary Python modules: 
- 
Loading Redirects (Lines 14-28): - The load_redirectsfunction reads our YAML configuration file.
- It uses a try/exceptblock to handle potential errors gracefully:- If the file doesn't exist, it logs a warning and returns an empty dictionary.
- If the YAML is invalid, it logs the error and returns an empty dictionary.
 
- The or {}part ensures we always have a dictionary, even if the YAML file is empty.
 
- The 
- 
Creating HTML (Lines 31-33): - The create_redirect_htmlfunction generates a simple HTML meta tag.
- This tag tells browsers to automatically redirect to the specified URL.
- We use an f-string (the f'...'syntax) to insert the URL into the HTML.
 
- The 
- 
Setting Up Output Directory (Lines 36-40): - The setup_output_directoryfunction prepares where we'll save our files.
- It removes any existing directory (with shutil.rmtree) and creates a fresh one.
- The exist_ok=Trueparameter prevents errors if the directory already exists.
 
- The 
- 
Generating Redirect Files (Lines 43-55): - For each path-URL pair in our redirects dictionary:- We create a directory for the path (creating nested directories if needed).
- We create an index.htmlfile in that directory.
- We write the redirect HTML to the file.
 
 
- For each path-URL pair in our redirects dictionary:
- 
Main Function (Lines 58-63): - This ties everything together in a logical sequence:- Load the redirects from the YAML file
- Set up the output directory
- Generate all the redirect files
 
 
- This ties everything together in a logical sequence:
- 
Script Entry Point (Lines 66-67): - The if __name__ == "__main__":check ensures the code only runs when executed directly.
- This is a Python convention that prevents code from running when imported as a module.
 
- The 
2. redirects.yaml - Your URL Mappings¶
This file contains all your short URL mappings:
# Use the pattern short-url-slug: redirect-link for every entry.
blog: https://yourblog.com
gitlab: https://gitlab.com/yourusername
twitter: https://twitter.com/yourusername
latest-project: https://gitlab.com/yourusername/awesome-project
YAML Explanation for Beginners:
YAML is a human-readable data format that's simpler than JSON or XML. In this file:
- 
Comments start with #and are ignored when the file is processed.
- 
Key-value pairs are written as key: value- in our case, the key is the short URL path, and the value is the target URL.
- No quotes needed for most strings, but you can use them for values with special characters.
- Indentation matters in YAML (though our simple example doesn't use nested structures).
Each line defines one redirect. When someone visits yourdomain.com/blog, they'll be redirected to https://yourblog.com.
3. pages/404.html - Custom 404 Page¶
Create a user-friendly 404 page that redirects back after a few seconds:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>404 - Page Not Found</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            text-align: center;
            margin-top: 100px;
            line-height: 1.6;
        }
        h1 {
            color: #333;
        }
        p {
            color: #666;
        }
    </style>
    <script>
        window.addEventListener('DOMContentLoaded', () => {
            setTimeout(() => {
                if (window.history.length > 1) {
                    window.history.back();
                } else {
                    window.location.href = '/';
                }
            }, 5000);
        });
    </script>
</head>
<body>
    <h1>Page Not Found</h1>
    <p>Redirecting back in 5 seconds...</p>
    <p><a href="javascript:history.back()">Click here</a> if you are not redirected automatically.</p>
</body>
</html>
HTML Explanation for Beginners:
This is a custom 404 (page not found) error page with automatic redirection:
- 
Document Structure: - 
<!DOCTYPE html>declares this as an HTML5 document.
- 
<html>,<head>, and<body>are the standard HTML structure elements.
 
- 
- 
Meta Information: - 
<meta charset="UTF-8">specifies character encoding.
- 
<meta name="viewport"...>helps with mobile responsiveness.
- 
<title>sets the browser tab title.
 
- 
- 
CSS Styling: - The <style>section contains CSS that centers the content and styles the text.
- Simple styling keeps the page clean and readable.
 
- The 
- 
JavaScript Functionality: - The <script>section contains code that automatically redirects after 5 seconds.
- It checks if there's a previous page in history:- If yes, it goes back to that page.
- If not, it redirects to the homepage.
 
- The DOMContentLoadedevent ensures the script runs after the page loads.
 
- The 
- 
Page Content: - A heading explains that the page wasn't found.
- Text informs the user about the automatic redirection.
- A clickable link allows immediate redirection for impatient users.
 
4. .gitlab-ci.yml - CI/CD Configuration¶
This file automates the build and deployment process:
workflow:
  rules:
    - if: '$CI_COMMIT_BRANCH == "release"'
    - when: never
stages:
  - build
  - deploy
build:
  image: python:3.13
  stage: build
  script:
    - python -V
    - pip install -r requirements.txt
    - mkdir public
    - uv run main.py
    - mv pages/* public/
    - ls -la public
  artifacts:
    paths:
      - public
    expire_in: 1 week
pages:
  stage: deploy
  dependencies:
    - build
  script:
    - echo "Publishing GitLab Pages from public/"
  artifacts:
    paths:
      - public
GitLab CI/CD Explanation for Beginners:
This YAML file configures GitLab's Continuous Integration/Continuous Deployment (CI/CD) pipeline:
- 
Workflow Rules: - The workflowsection defines when the pipeline should run.
- It only runs when changes are pushed to the "release" branch.
- The when: neverrule prevents the pipeline from running in other cases.
 
- The 
- 
Stages: - The pipeline has two sequential stages: buildanddeploy.
- Each stage must complete successfully before the next one starts.
 
- The pipeline has two sequential stages: 
- 
Build Job: - Uses Python 3.13 as the base Docker image.
- Runs several commands in sequence:- Shows the Python version (python -V)
- Installs dependencies from requirements.txt
- Creates a public directory
- Runs our main.py script using the uv package runner
- Moves files from pages/ to public/
- Lists the contents of public/ for verification
 
- Shows the Python version (
- Saves the public/ directory as an "artifact" (preserved output)
- Artifacts expire after one week to save storage space
 
- 
Pages Job: - Depends on the build job (won't run if build fails)
- Simply publishes the contents of public/ to GitLab Pages
- Preserves the public/ directory as a permanent artifact
 
GitLab automatically recognizes the "pages" job name and deploys its artifacts to your GitLab Pages site.
5. requirements.txt - Dependencies¶
A simple file listing the required Python packages:
pyyaml>=6.0.3
Requirements.txt Explanation for Beginners:
This simple file tells Python's package manager what external libraries our project needs:
- 
Package Specification: - 
pyyamlis the package name (for YAML file processing)
- 
>=6.0.3means we need version 6.0.3 or newer
 
- 
- 
Usage: - When someone runs pip install -r requirements.txt, all listed packages will be installed
- The -rflag tells pip to read requirements from the file
 
- When someone runs 
- 
Benefits: - Ensures everyone working on the project uses compatible package versions
- Makes it easy to set up the project on a new computer
- Simplifies deployment in CI/CD environments
 
For our URL shortener, we only need the PyYAML package to read our configuration file.
Step 3: Configure GitLab Pages and Custom Domain¶
After pushing your code to GitLab, the CI/CD pipeline will automatically build and deploy your URL shortener. Let's configure GitLab Pages for public access and set up your custom domain.
Configuring Project Visibility¶
First, ensure your GitLab Pages site is accessible to everyone:
- Go to your project's Settings > General
- Scroll down to the Visibility, project features, permissions section
- Expand this section and find Pages under Project Features
- Make sure Pages access control is set to Everyone or Public
- Click Save changes
This ensures your URL shortener is accessible to all users, even if your repository is private.
Setting Up Your Custom Domain¶
To use your own domain with your URL shortener:
- Go to your project's Settings > Pages
- You should see your site is already live at https://yourusername.gitlab.io/your-project-name
- Click New Domain to add your custom domain
- Enter your domain (e.g., short.yourdomain.comor justyourdomain.com)
- Optionally check Automatic certificate management using Let's Encrypt for free HTTPS
- Click Create New Domain
DNS Configuration¶
After adding your domain, you need to configure DNS records at your domain registrar:
For a Subdomain (e.g., short.yourdomain.com):¶
- Add a CNAME record:- 
Name/Host: short(or whatever subdomain you chose)
- 
Value/Target: yourusername.gitlab.io(your GitLab Pages domain)
- TTL: 3600 (or as recommended by your registrar)
 
- 
Name/Host: 
For an Apex/Root Domain (e.g., yourdomain.com):¶
Since apex domains can't use CNAME records, you have two options:
Option 1: Using A records (pointing directly to GitLab's IP addresses):
- 
Add A records pointing to GitLab's Pages servers: Name: @ (or leave blank) Value: 35.185.44.232 TTL: 3600Add another A record with the same name and TTL but with this IP: Value: 35.186.234.248
Option 2: Using DNS provider's CNAME-like feature: Many DNS providers offer services like ALIAS, ANAME, or CNAME flattening:
- Create an ALIAS/ANAME record:- Name: @ (or leave blank)
- 
Value: yourusername.gitlab.io
 
Verifying Domain Ownership¶
GitLab requires you to verify ownership of your domain:
- 
In the GitLab Pages domain settings, you'll see a Verification status section 
- 
GitLab will provide you with a verification code (e.g., gitlab-pages-verification-code=00112233445566778899aabbccddeeff)
- 
Add a TXT record to your DNS settings: For a subdomain (e.g., short.yourdomain.com):- 
Name/Host: _gitlab-pages-verification-code.short
- 
Value/Content: gitlab-pages-verification-code=00112233445566778899aabbccddeeff
 For an apex domain (e.g., yourdomain.com):- 
Name/Host: _gitlab-pages-verification-code
- 
Value/Content: gitlab-pages-verification-code=00112233445566778899aabbccddeeff
 
- 
Name/Host: 
- 
Wait for DNS propagation (can take up to 24-48 hours) 
- 
Go back to your GitLab project's Pages settings and click Verify domain 
Once verified, your custom domain will be active, and GitLab will automatically issue an SSL certificate if you selected that option. You can now access your URL shortener through your custom domain!
Step 4: Using Your URL Shortener¶
Once deployed, your URL shortener works like this:
- 
To create a new short URL, add an entry to redirects.yaml:awesome: https://example.com/my/very/long/url/that/needs/shortening 
- 
Commit and push the change to GitLab 
- 
The CI/CD pipeline automatically rebuilds and deploys your site 
- 
Your new short URL is now available at https://short.yourdomain.com/awesome
Advanced Customization Options¶
Custom Landing Page¶
Create an index.html file in the pages directory to serve as your landing page:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My URL Shortener</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            line-height: 1.6;
        }
        h1 {
            color: #333;
        }
    </style>
</head>
<body>
    <h1>My Personal URL Shortener</h1>
    <p>This is a custom URL shortening service.</p>
    <p>Available short links:</p>
    <ul>
        <li><a href="/blog">/blog</a> - My blog</li>
        <li><a href="/gitlab">/gitlab</a> - My gitlab profile</li>
        <!-- Add more links as needed -->
    </ul>
</body>
</html>
Landing Page HTML Explanation for Beginners:
This HTML file creates a simple homepage for your URL shortener:
- 
Document Structure: - Standard HTML5 structure with <!DOCTYPE>,<html>,<head>, and<body>tags
- 
lang="en"attribute specifies English as the document language
 
- Standard HTML5 structure with 
- 
Meta Information: - Character encoding and viewport settings for proper display on all devices
- Page title that appears in the browser tab
 
- 
CSS Styling: - Embedded CSS in the <style>tag keeps everything in one file
- Sets a clean, readable font (Arial or similar sans-serif)
- Limits content width to 800px and centers it on the page
- Adds spacing and line height for better readability
- Colors the heading for visual hierarchy
 
- Embedded CSS in the 
- 
Content Structure: - Main heading identifies the site
- Brief description explains the purpose
- List of available short links with their destinations
- Comment showing where to add more links
 
- 
Navigation: - Each link in the list points to one of your short URLs
- When clicked, these trigger the redirects you've configured
- The description after each link helps users understand where they'll go
 
This landing page serves as both documentation and a navigation hub for your URL shortener.
Analytics Integration¶
Since you control the code, you can add analytics tracking to your redirects:
def create_redirect_html(url: str) -> str:
    """Generate HTML content for a redirect page with analytics."""
    return f'''
    <!DOCTYPE html>
    <html>
    <head>
        <meta http-equiv="refresh" content="1; url={url}">
        <!-- Google Analytics or other tracking code -->
        <script async src="https://www.googletagmanager.com/gtag/js?id=YOUR-ID"></script>
        <script>
            window.dataLayer = window.dataLayer || [];
            function gtag(){{dataLayer.push(arguments);}}
            gtag('js', new Date());
            gtag('config', 'YOUR-ID');
            gtag('event', 'redirect', {{
                'event_category': 'outbound',
                'event_label': '{url}',
                'transport_type': 'beacon'
            }});
        </script>
    </head>
    <body>
        <p>Redirecting to <a href="{url}">{url}</a>...</p>
    </body>
    </html>
    '''
Analytics Integration Explanation for Beginners:
This enhanced version of our redirect function adds Google Analytics tracking:
- 
Function Structure: - Same function name but with more complex HTML output
- Uses Python's triple-quote string (''') for multi-line text
- The fprefix creates an f-string, allowing variable insertion with{url}
 
- 
HTML Changes: - Adds a full HTML document structure instead of just a meta tag
- Increases the redirect delay to 1 second to allow tracking to execute
- Includes a visible message with a clickable link
 
- 
Google Analytics Integration: - Loads the Google Analytics JavaScript library
- Sets up the basic tracking configuration
- Creates a custom event for the redirect:- Category: "outbound"
- Label: The destination URL
- Transport: "beacon" (ensures the tracking data is sent even during page unload)
 
 
- 
Double Curly Braces: - Notice the {{and}}in the JavaScript - these are escaped curly braces
- In Python f-strings, you need to double the braces to output actual braces in the result
 
- Notice the 
- 
Implementation Steps: - Replace YOUR-IDwith your actual Google Analytics ID
- Use this function instead of the simpler version in main.py
- Now you'll be able to track which redirects are used most frequently
 
- Replace 
Delayed Redirects¶
You can add a delay to show a message before redirecting:
def create_redirect_html(url: str, delay_seconds: int = 3) -> str:
    """Generate HTML content for a redirect page with delay."""
    return f'''
    <!DOCTYPE html>
    <html>
    <head>
        <meta http-equiv="refresh" content="{delay_seconds}; url={url}">
        <style>
            body {{ font-family: Arial, sans-serif; text-align: center; margin-top: 100px; }}
        </style>
    </head>
    <body>
        <h1>Redirecting in {delay_seconds} seconds...</h1>
        <p>You are being redirected to: <a href="{url}">{url}</a></p>
    </body>
    </html>
    '''
Delayed Redirect Explanation for Beginners:
This function creates a redirect page with a customizable delay:
- 
Function Parameters: - Takes the destination URL as before
- Adds a new delay_secondsparameter with a default value of 3
- The default value means this parameter is optional
 
- 
Meta Refresh Modification: - Changes the contentattribute to include the delay:"{delay_seconds}; url={url}"
- The browser will wait that many seconds before redirecting
 
- Changes the 
- 
Improved User Experience: - Adds basic styling with CSS to make the page look better
- Notice the double curly braces in the CSS ({{ }}) to escape them in the f-string
- Includes a heading that shows the countdown time
- Provides a clickable link so users can proceed immediately if they don't want to wait
 
- 
Use Cases: - Showing important messages before redirecting
- Giving users time to read information
- Displaying advertisements or notices
- Allowing tracking scripts to fully execute
 
- 
Implementation: - You can call this with just a URL: create_redirect_html("https://example.com")
- Or specify a custom delay: create_redirect_html("https://example.com", 5)
 
- You can call this with just a URL: 
This approach makes your redirects more user-friendly while still maintaining their primary function.
Benefits Over Commercial Solutions¶
This DIY approach offers several advantages:
- Complete Privacy: No third-party tracking your users
- Unlimited Usage: No caps on URLs or traffic
- Full Control: Modify the code to suit your needs
- Custom Domain for Free: Use any domain you own
- Transparent Operation: Open-source code you can inspect
- No Vendor Lock-in: Export your redirects anytime
- Zero Cost: Free hosting on GitLab Pages
Reference Implementation¶
For a complete, production-ready implementation of this concept, check out the Redirector repository by Krishnakanth Allika. This repository includes several enhancements beyond the basic implementation described in this article:
Advanced Features¶
- 
Comprehensive Logging - Detailed logging with timestamps and line numbers
- Error handling with appropriate log levels
- Exception tracking for troubleshooting
 
- 
Modern Python Packaging - Uses pyproject.tomlfor project configuration
- Compatible with Python 3.13+
- Leverages uvpackage manager for faster, more reliable dependency management
 
- Uses 
- 
Robust Testing Framework - Extensive unit tests with pytest
- Test coverage reporting
- Integration tests for full workflow validation
- Dedicated test runner script (run_tests.py)
 
- 
CI/CD Integration - Automated build and deployment pipeline
- Artifact management
- GitLab Pages integration
 
- 
Error Handling - Graceful handling of missing configuration files
- YAML parsing error management
- Empty configuration handling
 
- 
Custom 404 Page - User-friendly error page
- Automatic redirection after timeout
- Clean, responsive design
 
The repository demonstrates best practices for Python development, including type hints, docstrings, and modular code organization. It's an excellent reference for building production-quality static site generators and URL shorteners.
Conclusion¶
Building your own URL shortener with GitLab Pages gives you a powerful, customizable solution without the limitations of commercial services. With just a few simple files and minimal code, you can create a professional URL shortening service that's completely under your control.
The best part? It's entirely free, even with your custom domain, and there are no artificial limits on usage. Whether you're sharing links for personal projects, managing redirects for your business, or just want more control over your online presence, this solution provides everything you need.
Start building your own URL shortener today and take control of your short links!
Last updated 2025-10-26 16:17:54.582279 IST
[^top]
Comments