how to extract meta tags using google sheets

How to Extract Meta Tags from Multiple URLs Using Google Sheets (Free & Automated)

On this page

    Stop copying and pasting meta tags one URL at a time. If you’re an SEO professional, digital marketer, or agency managing multiple websites, you’ve probably spent hours manually checking title tags and meta descriptions across dozens (or hundreds) of pages. There’s a better way.

    In this guide, I’ll show you how to build a free, automated meta tag extractor that works directly inside Google Sheets using Google Apps Script. No external tools, no subscriptions, no complicated setup.

    Why You Need This

    As SEO professionals, we regularly need to audit meta tags for:

    • Client audits – Quickly assess current meta tag optimization
    • Competitor analysis – See how competitors are optimizing their pages
    • Migration projects – Document existing meta tags before website changes
    • Content optimization – Identify pages with missing or duplicate meta descriptions
    • Reporting – Show clients exactly what needs improvement

    Manual extraction is tedious. Paid tools like Screaming Frog are great, but overkill for quick checks. This Google Sheets solution gives you instant access to meta tag data without leaving your spreadsheet.

    What You’ll Build

    By the end of this tutorial, you’ll have a custom Google Sheets tool that:

    ✅ Extracts title tags and meta descriptions from any URL
    ✅ Processes hundreds of URLs in batch
    ✅ Color-codes results (green = success, red = error)
    ✅ Works directly in your spreadsheet with a custom menu
    ✅ Handles errors gracefully and shows HTTP status codes
    ✅ Decodes HTML entities properly

    Prerequisites

    • A Google account (free)
    • A Google Sheet with URLs you want to analyze
    • 10 minutes to set this up

    That’s it. No coding experience required.

    Step-by-Step Setup

    Step 1: Prepare Your Google Sheet

    1. Open your Google Sheet or create a new one
    2. Add your URLs in Column A (starting from A2 if you have a header in A1)
    3. Your sheet should look something like this:
    google sheet preparation

    Step 2: Open the Script Editor

    1. In your Google Sheet, click ExtensionsApps Script
    2. You’ll see a blank script editor
    3. Delete any default code that appears
    4. Keep this window open – we’ll paste our code here

    Step 3: Add the Meta Tag Extractor Script

    Copy and paste this complete script into the Apps Script editor:

    /**
     * Meta Tag Extractor for Google Sheets
     * Fetches title tags and meta descriptions from URLs
     * Author: Ankit Chauhan
     */
    
    function onOpen() {
      // Create custom menu
      const ui = SpreadsheetApp.getUi();
      ui.createMenu('🔍 Meta Tag Extractor')
        .addItem('Extract Meta Tags', 'extractMetaTags')
        .addItem('Extract Selected Rows', 'extractSelectedRows')
        .addSeparator()
        .addItem('Clear Results', 'clearResults')
        .addToUi();
    }
    
    /**
     * Main function to extract meta tags from all URLs
     */
    function extractMetaTags() {
      const sheet = SpreadsheetApp.getActiveSheet();
      const ui = SpreadsheetApp.getUi();
      
      // Ask user which column contains URLs
      const response = ui.prompt(
        'Extract Meta Tags',
        'Which column contains your URLs? (e.g., A, B, C)',
        ui.ButtonSet.OK_CANCEL
      );
      
      if (response.getSelectedButton() != ui.Button.OK) {
        return;
      }
      
      const urlColumn = response.getResponseText().toUpperCase();
      const urlColumnIndex = urlColumn.charCodeAt(0) - 64; // A=1, B=2, etc.
      
      // Ask where to start (skip header row?)
      const startRowResponse = ui.prompt(
        'Start Row',
        'Which row to start from? (e.g., 2 to skip header)',
        ui.ButtonSet.OK_CANCEL
      );
      
      if (startRowResponse.getSelectedButton() != ui.Button.OK) {
        return;
      }
      
      const startRow = parseInt(startRowResponse.getResponseText()) || 2;
      
      // Get all URLs
      const lastRow = sheet.getLastRow();
      if (lastRow < startRow) {
        ui.alert('No data found in the specified range.');
        return;
      }
      
      const urls = sheet.getRange(startRow, urlColumnIndex, lastRow - startRow + 1, 1).getValues();
      
      // Set up result columns (next to URL column)
      const titleColumn = urlColumnIndex + 1;
      const descColumn = urlColumnIndex + 2;
      const statusColumn = urlColumnIndex + 3;
      
      // Add headers if starting at row 1
      if (startRow === 1) {
        sheet.getRange(1, titleColumn).setValue('Title Tag');
        sheet.getRange(1, descColumn).setValue('Meta Description');
        sheet.getRange(1, statusColumn).setValue('Status');
      }
      
      // Process each URL
      let processed = 0;
      let errors = 0;
      
      urls.forEach((row, index) => {
        const url = row[0];
        const currentRow = startRow + index;
        
        if (!url || url.toString().trim() === '') {
          return; // Skip empty rows
        }
        
        try {
          // Show progress
          SpreadsheetApp.flush();
          
          // Fetch and extract
          const result = fetchMetaTags(url.toString().trim());
          
          // Write results
          sheet.getRange(currentRow, titleColumn).setValue(result.title);
          sheet.getRange(currentRow, descColumn).setValue(result.description);
          sheet.getRange(currentRow, statusColumn).setValue(result.status);
          
          // Color code status
          const statusCell = sheet.getRange(currentRow, statusColumn);
          if (result.status === 'Success') {
            statusCell.setBackground('#d9ead3'); // Light green
          } else {
            statusCell.setBackground('#f4cccc'); // Light red
            errors++;
          }
          
          processed++;
          
          // Be polite to servers - add small delay
          Utilities.sleep(500);
          
        } catch (error) {
          sheet.getRange(currentRow, titleColumn).setValue('Error');
          sheet.getRange(currentRow, descColumn).setValue(error.toString());
          sheet.getRange(currentRow, statusColumn).setValue('Failed');
          sheet.getRange(currentRow, statusColumn).setBackground('#f4cccc');
          errors++;
        }
      });
      
      ui.alert(
        'Complete!',
        `Processed: ${processed} URLs\nErrors: ${errors}`,
        ui.ButtonSet.OK
      );
    }
    
    /**
     * Extract meta tags from selected rows only
     */
    function extractSelectedRows() {
      const sheet = SpreadsheetApp.getActiveSheet();
      const ui = SpreadsheetApp.getUi();
      const selection = sheet.getActiveRange();
      
      const startRow = selection.getRow();
      const numRows = selection.getNumRows();
      const urlColumnIndex = selection.getColumn();
      
      const titleColumn = urlColumnIndex + 1;
      const descColumn = urlColumnIndex + 2;
      const statusColumn = urlColumnIndex + 3;
      
      for (let i = 0; i < numRows; i++) {
        const currentRow = startRow + i;
        const url = sheet.getRange(currentRow, urlColumnIndex).getValue();
        
        if (!url || url.toString().trim() === '') {
          continue;
        }
        
        try {
          const result = fetchMetaTags(url.toString().trim());
          
          sheet.getRange(currentRow, titleColumn).setValue(result.title);
          sheet.getRange(currentRow, descColumn).setValue(result.description);
          sheet.getRange(currentRow, statusColumn).setValue(result.status);
          
          const statusCell = sheet.getRange(currentRow, statusColumn);
          statusCell.setBackground(result.status === 'Success' ? '#d9ead3' : '#f4cccc');
          
          Utilities.sleep(500);
          
        } catch (error) {
          sheet.getRange(currentRow, titleColumn).setValue('Error');
          sheet.getRange(currentRow, descColumn).setValue(error.toString());
          sheet.getRange(currentRow, statusColumn).setValue('Failed');
        }
      }
      
      ui.alert('Selected rows processed!');
    }
    
    /**
     * Core function to fetch and parse meta tags
     */
    function fetchMetaTags(url) {
      try {
        // Validate URL
        if (!url.match(/^https?:\/\//i)) {
          url = 'https://' + url;
        }
        
        // Fetch the page
        const response = UrlFetchApp.fetch(url, {
          'muteHttpExceptions': true,
          'followRedirects': true,
          'validateHttpsCertificates': false,
          'headers': {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
          }
        });
        
        const statusCode = response.getResponseCode();
        
        if (statusCode !== 200) {
          return {
            title: 'N/A',
            description: 'N/A',
            status: `HTTP ${statusCode}`
          };
        }
        
        const html = response.getContentText();
        
        // Extract title tag
        let title = 'No title found';
        const titleMatch = html.match(/<title[^>]*>(.*?)<\/title>/i);
        if (titleMatch && titleMatch[1]) {
          title = titleMatch[1].trim();
          // Decode HTML entities
          title = decodeHtmlEntities(title);
        }
        
        // Extract meta description
        let description = 'No description found';
        const metaDescMatch = html.match(/<meta\s+name=["']description["']\s+content=["'](.*?)["']/i);
        if (metaDescMatch && metaDescMatch[1]) {
          description = metaDescMatch[1].trim();
          description = decodeHtmlEntities(description);
        } else {
          // Try alternative format
          const altMetaMatch = html.match(/<meta\s+content=["'](.*?)["']\s+name=["']description["']/i);
          if (altMetaMatch && altMetaMatch[1]) {
            description = altMetaMatch[1].trim();
            description = decodeHtmlEntities(description);
          }
        }
        
        return {
          title: title,
          description: description,
          status: 'Success'
        };
        
      } catch (error) {
        return {
          title: 'Error',
          description: error.toString(),
          status: 'Failed'
        };
      }
    }
    
    /**
     * Decode HTML entities
     */
    function decodeHtmlEntities(text) {
      return text
        .replace(/&amp;/g, '&')
        .replace(/&lt;/g, '<')
        .replace(/&gt;/g, '>')
        .replace(/&quot;/g, '"')
        .replace(/&#39;/g, "'")
        .replace(/&nbsp;/g, ' ')
        .replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(dec));
    }
    
    /**
     * Clear all results
     */
    function clearResults() {
      const sheet = SpreadsheetApp.getActiveSheet();
      const ui = SpreadsheetApp.getUi();
      
      const response = ui.alert(
        'Clear Results',
        'This will clear all Title, Description, and Status columns. Continue?',
        ui.ButtonSet.YES_NO
      );
      
      if (response === ui.Button.YES) {
        const lastRow = sheet.getLastRow();
        const lastCol = sheet.getLastColumn();
        
        // Clear formatting and content from result columns
        for (let col = 2; col <= lastCol; col++) {
          sheet.getRange(2, col, lastRow - 1, 1).clearContent();
          sheet.getRange(2, col, lastRow - 1, 1).setBackground(null);
        }
        
        ui.alert('Results cleared!');
      }
    }
    

    Step 4: Save and Name Your Project

    1. Click the 💾 Save icon (or press Ctrl+S / Cmd+S)
    2. Give it a name: “Meta Tag Extractor”
    3. Close the Apps Script editor

    Step 5: Refresh and Authorize

    1. Go back to your Google Sheet and refresh the page (F5 or Cmd+R)
    2. You should now see a new menu item: 🔍 Meta Tag Extractor
    3. Click 🔍 Meta Tag ExtractorExtract Meta Tags
    4. Google will ask you to authorize the script:
      • Click Review Permissions
      • Select your Google account
      • Click AdvancedGo to Meta Tag Extractor (unsafe)
      • Click Allow

    Don’t worry – Google flags all custom scripts as “unsafe” by default. This is your own code running in your own spreadsheet.

    How to Use Your Meta Tag Extractor

    Method 1: Extract All URLs

    1. Click 🔍 Meta Tag ExtractorExtract Meta Tags
    2. Enter the column letter containing your URLs (e.g., “A”)
    3. Enter the row to start from (e.g., “2” to skip headers)
    4. Click OK and watch the magic happen!

    The script will add three columns next to your URLs:

    • Title Tag – The page’s <title> content
    • Meta Description – The meta description content
    • Status – Success (green) or error code (red)

    Method 2: Extract Selected URLs Only

    Need to check just a few URLs or retry failed ones?

    1. Select the URL cells you want to extract
    2. Click 🔍 Meta Tag ExtractorExtract Selected Rows
    3. Results appear instantly in adjacent columns

    Method 3: Clear Results

    Want to start over?

    1. Click 🔍 Meta Tag ExtractorClear Results
    2. Confirm, and all extracted data is removed

    Understanding the Results

    Success (Green)

    When a URL is successfully fetched, you’ll see:

    • Title Tag: The actual <title> content from the page
    • Meta Description: The meta description content
    • Status: “Success” with green background

    Errors (Red)

    If something goes wrong, you’ll see:

    • HTTP 404: Page not found
    • HTTP 403: Access forbidden
    • HTTP 500: Server error
    • Failed: Connection timeout or other error

    Real-World Use Cases

    1. SEO Audit Workflow

    1. Export URLs from sitemap or crawl tool
    2. Paste into Google Sheet
    3. Run Meta Tag Extractor
    4. Add columns for:
       - Title length (=LEN(B2))
       - Description length (=LEN(C2))
       - Optimization notes
    5. Share with client or team
    

    2. Competitor Analysis

    1. List competitor page URLs
    2. Extract their meta tags
    3. Compare to your own pages
    4. Identify opportunities and gaps
    

    3. Content Migration

    1. Document old site meta tags
    2. Create new site structure
    3. Map old tags to new pages
    4. Verify after migration
    

    Advanced Customizations

    Want to extend this script? Here are some ideas:

    Add Character Count

    Add this after the title/description columns:

    // After extracting tags, add length formulas
    const titleLengthCol = titleColumn + 3;
    const descLengthCol = titleColumn + 4;
    
    sheet.getRange(currentRow, titleLengthCol).setFormula(`=LEN(${sheet.getRange(currentRow, titleColumn).getA1Notation()})`);
    sheet.getRange(currentRow, descLengthCol).setFormula(`=LEN(${sheet.getRange(currentRow, descColumn).getA1Notation()})`);
    

    Extract Open Graph Tags

    Add this to the fetchMetaTags function:

    // Extract OG Title
    let ogTitle = 'No OG title';
    const ogTitleMatch = html.match(/<meta\s+property=["']og:title["']\s+content=["'](.*?)["']/i);
    if (ogTitleMatch && ogTitleMatch[1]) {
      ogTitle = ogTitleMatch[1].trim();
    }
    
    // Extract OG Description
    let ogDescription = 'No OG description';
    const ogDescMatch = html.match(/<meta\s+property=["']og:description["']\s+content=["'](.*?)["']/i);
    if (ogDescMatch && ogDescMatch[1]) {
      ogDescription = ogDescMatch[1].trim();
    }
    
    return {
      title: title,
      description: description,
      ogTitle: ogTitle,
      ogDescription: ogDescription,
      status: 'Success'
    };
    

    Add H1 Tag Extraction

    // Extract H1
    let h1 = 'No H1 found';
    const h1Match = html.match(/<h1[^>]*>(.*?)<\/h1>/i);
    if (h1Match && h1Match[1]) {
      h1 = h1Match[1].trim().replace(/<[^>]*>/g, ''); // Strip HTML tags
      h1 = decodeHtmlEntities(h1);
    }
    

    Troubleshooting Common Issues

    “Script execution exceeded maximum time”

    This happens with very large URL lists. Solutions:

    • Process URLs in smaller batches (50-100 at a time)
    • Use the “Extract Selected Rows” feature
    • Increase the sleep delay: Utilities.sleep(1000);

    “Access Denied” or “403 Forbidden”

    Some websites block automated requests. Try:

    • Adding more realistic User-Agent headers
    • Using a different IP (run from different location)
    • Manually checking if the site requires login

    Missing Meta Descriptions

    If the script shows “No description found”:

    • The page genuinely has no meta description
    • The meta tag uses a different format
    • Content is JavaScript-rendered (this script only reads static HTML)

    Character Encoding Issues

    If you see strange characters:

    • The page uses a non-UTF-8 encoding
    • Add more entity decoders to the decodeHtmlEntities function
    • Or manually fix in the sheet after extraction

    Performance Tips

    1. Batch processing: Process 50-100 URLs at a time
    2. Use filters: Only extract what you need with “Selected Rows”
    3. Save versions: Duplicate your sheet before major extractions
    4. Rate limiting: The built-in 500ms delay is polite. Don’t remove it.
    5. Cache results: Keep successful extractions, only retry errors

    Comparison to Other Tools

    Feature This Script Screaming Frog SEMrush Ahrefs
    Cost Free $259/year $139.95/mo $129/mo
    URLs Unlimited* 500 (free) Varies Varies
    Setup 10 minutes Download + install Signup Signup
    Location Google Sheets Desktop app Web Web
    Customizable Yes (code) Limited No No
    Sharing Easy (Sheets) Export required Dashboard Dashboard

    *Subject to Google Apps Script quotas (typically 20,000 UrlFetch calls per day)

    Conclusion

    You now have a powerful, free meta tag extraction tool that lives right in your Google Sheets. No subscriptions, no downloads, no complexity.

    This script is perfect for:

    • Quick SEO audits
    • Client reports
    • Competitive analysis
    • Pre-migration documentation
    • Training junior SEO staff

    The best part? You can customize it endlessly. Extract H1s, canonical tags, schema markup, open graph tags – whatever you need.

    Next Steps

    1. Save this script in your Google Sheet library
    2. Create a template with pre-formatted columns and formulas
    3. Share with your team so everyone can use it
    4. Extend the functionality based on your specific needs

    Want to take it further? Here are some advanced projects:

    • Schedule automated runs using Google Apps Script triggers
    • Email results automatically to stakeholders
    • Integrate with APIs to push data to other tools
    • Build a full SEO auditor that checks more on-page elements

    Have questions or improvements? Contact me. I’d love to see how you customize this for your workflow!

    Found this useful? Share it with your SEO and digital marketing colleagues. Let’s make everyone’s life a little easier.

    Post Category:

    Share this article:

    Ankit Chauhan is an SEO Consultant and Researcher. Having more than 7 years of extensive experience in SEO, Ankit loves to share his SEO expertise with the community through his blog. Ankit Chauhan is a big-time SEO nerd with an obsession for search engines and how they work. Ankit loves to read Google patents about search engines and conduct SEO experiments in his free time.

    Related Posts

    conversions drop

    How I diagnosed 87% drop in form conversions for a B2B industrial client

    seo meta tags

    How to Generate SEO Title Tags and Meta Descriptions at Scale Using Google Sheets and AI

    optimizing vps for wordpress performance without paying for managed hosting

    VPS Optimization for Maximizing WordPress Performance Without Managed Hosting

    Let’s Make Something Rank

    Whether you’re a brand in India, an agency overseas, or a founder who’s tired of being buried under your competitors - I’m ready when you are.

    Let’s build something search engines (and your users) can’t ignore.

    Drop a message. I’ll respond like a human, not a sales funnel.