simonwillisonblog Documentation

repository·main·Indexed 19 days ago

https://github.com/simonw/simonwillisonblog

Codebase for Simon Willison's personal weblog (simonwillison.net). Includes a custom full-text search engine, an S3 Manager admin tool powered by s3-web-manager-django, and various Django management commands for importing data from JSON, XML, Quora, iNaturalist, and other sources.

Tokens
5.9K
Snippets
31
Records
36
Agent score
64%

What's inside simonwillisonblog

  1. How the built-in search engine works

    main

    The blog features a built-in search engine implemented via the search function in blog/search.py. It provides full-text search and tag-based filtering.

    Key characteristics:

    • Indexing: The search index is automatically built and updated whenever new content is added.
    • Matching: Keywords are matched against the full text of blog entries and blogmarks.
    • Ranking: Results are ranked by relevance and support further filtering by tags.
    • Integration: The search interface is integrated directly into the blog's UI.
    # Implementation reference
    # See blog/search.py for the search function implementation
  2. How Guide Sections work in the Guide system

    main

    Guide Sections are purely organizational groupings of chapters within a guide. They are used to structure the Table of Contents (TOC) and sidebar display but do not have their own pages, URLs, or content (like descriptions or draft statuses).

    Ordering Logic:

    • Top-level ordering: Standalone chapters and sections share a single ordering namespace at the guide level. A standalone chapter with order=0 appears before a section with order=1.
    • Nested ordering: Chapters assigned to a section use their order field to determine their position within that specific section.
    • Navigation: While sections organize the UI, they are invisible to the 'Previous/Next' navigation logic, which follows a flat, linear walk of all chapters.
    NAME OF GUIDE
    - Getting started          (standalone chapter, order=0)
    - Basics                   (section, order=1)
      - Glossary               (chapter in section, order=0)
      - Installation           (chapter in section, order=1)
    - Intermediate             (section, order=2)
      - Chapter A              (chapter in section, order=0)
  3. Use the S3 Manager admin tool

    main

    The blog includes an admin tool located at /tools/s3/ for managing files in an S3 bucket. This tool is powered by s3-web-manager-django and allows for browsing and uploading files.

    Configuration Note: Object URLs generated for copied or viewed files use the S3_WEB_MANAGER_PUBLIC_URL_BASE setting. In this project, it is configured to https://static.simonwillison.net/, meaning URLs are constructed using the public static host followed by the object path in the bucket.

  4. Configure GuideSection in Django Admin

    main

    To manage sections via the admin interface, implement GuideSectionAdmin with the following configuration:

    • list_display: title, guide, order
    • list_filter: guide
    • prepopulated_fields: slug (derived from title)

    Additionally, update ChapterAdmin to include section in its fields, list_display, and list_filter.

  5. Identify content type from HTML structure

    main

    When parsing Wayback Machine HTML, the content type is determined by inspecting specific elements within a div with class entry entryPage:

    • Entry: Identified by the presence of an <h2> tag. Contains title and body.
    • Quotation: Identified by the presence of a <blockquote> tag. Contains quotation, source, and source_url.
    • Blogmark: Identified if neither of the above are present. Contains link_url, link_title, via_url, via_title, and commentary.
  6. Scrape Ask MetaFilter comments using Python

    main

    This notebook demonstrates how to scrape comment data from MetaFilter's 'Ask' section. The process involves two main steps: first, identifying the URLs of individual comments by parsing the activity pages, and second, visiting each comment URL to extract structured data like the title, date, time, HTML content, and tags.

    import requests
    from BeautifulSoup import BeautifulSoup as Soup
    
    # 1. Find comment URLs from an activity page
    url = 'http://www.metafilter.com/activity/18146/comments/ask/'
    page = Soup(requests.get(url).content)
    divs = page.findAll('div', {'class': 'copy'})
    
    urls = []
    for div in divs:
        for blockquote in div.findAll('blockquote'):
            urls.append(blockquote.findAll('a')[-1]['href'])
    
    # 2. Extract structured data from each URL
    def get_comment(url):
        # Implementation details for parsing individual comment pages
        ...
    
    fetched = [get_comment(u) for u in urls]
  7. Recover content from the Wayback Machine

    main

    This guide demonstrates how to extract and reconstruct blog content (Entries, Blogmarks, or Quotations) from a Wayback Machine archive (tar.gz format) and convert it into a structured JSON format compatible with a Django-based blog system.

    Workflow Overview

    1. Extract Paths: Open the .tar.gz archive and identify relevant HTML file paths using regex.
    2. Identify Missing Content: Compare the paths found in the archive against existing records in the Django database (Entry, Blogmark, or Quotation models) to find what is missing.
    3. Parse HTML: Use BeautifulSoup to scrape the HTML content from the archive, identifying the content type (entry, quotation, or blogmark) based on specific DOM structures.
    4. Export to JSON: Save the reconstructed items into a JSON file for import.
    # Example of the core logic: parsing an item from soup
    import json
    from BeautifulSoup import BeautifulSoup as Soup
    
    # Assuming 'missing' contains paths to files in the tar archive
    items = []
    for path in missing:
        html = tar.extractfile('simonwillison.net%sindex.html' % path).read()
        soup = Soup(html)
        item = soup_to_item(soup)
        item['slug'] = [b for b in path.split('/') if b][-1]
        items.append(item)
    
    # Export to JSON
    with open('/tmp/missing-content.json', 'w') as f:
        json.dump(items, f, indent=2)
  8. Setup Django environment for content recovery

    main

    To compare archive content against the existing database, you must initialize the Django environment within your script by pointing to the project directory and setting the DJANGO_SETTINGS_MODULE.

    MYPROJECT = '/path/to/simonwillisonblog/'
    import os, sys
    sys.path.insert(0, MYPROJECT)
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
    import django
    django.setup()
    
    from blog.models import Entry, Blogmark, Quotation
  9. Flatten the TOC for linear navigation

    main

    When computing 'Previous' and 'Next' chapter links, you must ignore the section groupings. Use the flatten_toc function to convert the nested TOC structure into a simple, linear list of Chapter objects.

    def flatten_toc(toc):
        flat = []
        for item in toc:
            if item["type"] == "chapter":
                flat.append(item["chapter"])
            else:
                flat.extend(item["chapters"])
        return flat
  10. Build a nested Table of Contents (TOC)

    main

    Use the build_guide_toc function to generate a data structure that interleaves standalone chapters and sections for use in templates.

    Return Format: A list of dictionaries, sorted by order. Each dictionary contains:

    • For chapters: {"type": "chapter", "order": int, "chapter": Chapter}
    • For sections: {"type": "section", "order": int, "section": GuideSection, "chapters": [Chapter, ...]}

    Empty sections (sections with no chapters) are automatically excluded from the result.

    def build_guide_toc(guide, include_drafts=False):
        """
        Returns a list of items, each either:
          {"type": "chapter", "order": int, "chapter": Chapter}
          {"type": "section", "order": int, "section": GuideSection, "chapters": [Chapter, ...]}
        Sorted by order. Empty sections are excluded.
        """
        # ... implementation ...
  11. Extract structured data with get_comment()

    main

    The get_comment(url) function takes a specific comment URL (including the fragment/hash) and returns a dictionary containing the following fields:

    • title: The post title (extracted from h1.posttitle).
    • date: The date of the comment.
    • time: The time of the comment.
    • html: The raw HTML content of the comment.
    • url: The original URL.
    • comment_id: The hash extracted from the URL.
    • tags: A list of strings representing the tags associated with the post.
    def get_comment(url):
        url, hash = url.split('#')
        soup = Soup(requests.get(url).content.decode('utf8'))
        div = soup.find('div', {'id': 'c' + hash})
        span = div.find('span', {'class': 'smallcopy'})
        time = span.findAll('a', {'target': '_self'})[-1].text
        date = span.renderContents().split(' on ')[-1].strip().split(' [')[0]
        span.extract()
        html = div.renderContents().decode('utf8')
        if html.endswith('<br />'):
            html = html[:-(len('<br />'))]
        return {
            'title': soup.find('h1', {'class': 'posttitle'}).renderContents().split('<br')[0],
            'date': date,
            'time': time,
            'html': html,
            'url': url,
            'comment_id': hash,
            'tags': [a.text for a in soup.findAll('a', {'rel': 'tag'})],
        }