DEV Community

Cover image for Self-hosted Telegra.ph in 1 prompt and 3 minutes
Sergei Vorniches
Sergei Vorniches

Posted on

Self-hosted Telegra.ph in 1 prompt and 3 minutes

For the past two years, I’ve barely written any code myself. Perhaps only about 10% of the code in my personal and commercial projects is hand-written – everything else is generated by AI. Over that time, I’ve developed a specific approach to building projects and assembled a set of tools that I now use for this purpose. That’s what I want to share with you in this post.

Since 2022, a lot has changed. First, the original GitHub Copilot appeared in open beta, followed by early models from OpenAI – messy, losing context, and unable to deliver coherent results over longer spans. But in recent years, neural nets have evolved beyond advanced autocomplete; they’re now fully capable tools for solving tasks. The key is to frame those tasks correctly. And here, as I’ve noticed, even experienced programmers sometimes struggle with and question the very idea of using AI to write code. That’s what prompted me to write this post.

This post will be especially useful for beginners – those who, like me once, are more interested in “making things” than in writing code line by line. I’ll show you how to create a self-hosted solution similar to Telegra.ph, but with a Markdown editor, using my own tools.

The approach I describe below works with various models: ChatGPT, Claude, DeepSeek, even the free version of GitHub Copilot in its current form (although it’s the most inconvenient to work with). I recently shared my experience with Copilot on dev.to – honestly, it’s more torture than actual work.

I recommend using Cursor, which you can try in its Pro version for two weeks right now. It currently provides the most pleasant and seamless experience for programming with AI that I’ve encountered so far. I’ll be describing my work with it from here on.

System Requirements

To work on this project you’ll need:

  • Python version 3.10 or higher
  • Pip for installing dependencies
  • Git to manage the repository
  • Docker Desktop (free plan)
  • Cursor, or access to any AI chat tool
  • The snap2txt utility, installable via pip install snap2txt
  • Prototype (project base)

Working on the Project

I want this section to be as clear as possible for beginners, so I’ll explain some things in detail.

1. Create the Project from the Prototype Repository

First, I copy the git clone command with the address of the base project repository (Prototype) and create a new project named tapnote in my working folder.

Prototype is a quick-start kit for Django projects running in Docker containers. I prefer Docker over venv or virtualenv because Docker provides complete OS-level isolation and guarantees an identical environment on every machine. This is convenient for fast deployments on platforms like Railway.app or DigitalOcean. The Prototype includes a Dockerfile and docker-compose with the basic settings, a helper for working with the OpenAI API, and a shell script to launch everything with a single command.

To run setup.sh, I change into the new directory with cd tapnote and execute ./setup.sh. In a few seconds, everything is ready.

2. Verify the Container is Running

A new container with the project’s name will appear in Docker Desktop. If it’s running successfully, you can open the 'web' tab and check the logs for errors. Also, navigating to localhost:9009 in your browser should display Django’s default start page.

3. Open the Project in Cursor and Perform Initial Setup

By "initial setup," I mean both initializing Git and creating the project’s working context. For this, I run git init and make the first commit. In this project, there will be just two commits – before and after the prompt. To create the context, I use the snap2txt tool with the --il parameter.

snap2txt is a Python utility that condenses the entire project into a single text file. I built it about a year and a half ago and use it almost daily. It’s necessary to create a context file for the project, which is then fed to the AI along with the task description. This way, the AI immediately has access to the full context of the project – the directory structure and all files. The --il and --wl parameters refer to using an ignore list or white list, respectively—these are separate files at the root of the utility that specify which files to include or exclude in the context. The --il flag works similarly to .gitignore, while --wl does the opposite.

Now that the context file is created, I open the Composer panel inside the editor and double-click on project_contents.txt to add the project context to Composer.

4. Detail the Task, Accept the Files, and Run the Necessary Commands

Here I’ve taken a small shortcut by inserting a pre-prepared prompt into the Composer chat. My goal was to demonstrate creating a project in one prompt, so I refined it while preparing this article. For simpler projects, a plain description without prior preparation will suffice. For a project like this, you might need 2–3 iterations to fix details you didn’t consider at first. However, with good planning, you can create a complex project in one go.

Working in Cursor versus an external chat requires different query configurations. Cursor interacts directly with the project files, so it needs fewer explanations regarding file structure and response formatting. The external chat, however, only generates instructions to create files in the right directories and their contents. Also, each model has its quirks—for example, reasoning models like o1 often handle large tasks better but tend to "smear" code and comments across the response, reducing readability. You can handle this behavior with additional instructions, but I won’t go into those details here.

The ideal prompt for this project didn’t materialize immediately; the structure of the description played a major role. I broke it into three large groups with detailed descriptions of the required changes to the base project:

**FUNCTIONAL**

- A general description of the project’s functionality and the names of the apps (for Django).
- A description of the homepage’s functionality.
- A description of the publishing workflow.
- A description of the details of Markdown rendering.
- A description of the mechanism for editing published posts.
- A description of additional pages, in this case the 404 page.

---

**STYLING**

- A general description of the desired appearance:
  - Details on colors, spacing, and button styles.
  - Specific CSS requirements (for example, no outlines).
- A description of how Markdown elements should look after rendering: header sizes, code appearance, etc.
- The desired font, which for simplicity is linked directly.
- A note on the necessity of using Tailwind, which is also linked directly in the base template for convenience.

---

**NOTES**

- Additional instructions for editing files.
- A reiteration of details that are important not to overlook (as an extra precaution, optional).
- Specific requests for rendering certain elements (for example, adding `target="_blank"` to links).
Enter fullscreen mode Exit fullscreen mode

And here's the building prompt itself:

**FUNCTIONAL**

- Create a simple, minimalistic, Telegraph-like blog app named **'TapNote'**.
- The homepage will function as a simplistic text editor:
  - Contains only a **textarea** with the placeholder text: *'write in markdown'*.
  - Includes a **'Publish'** button located on the right side of the textarea.
- **Publishing workflow**:
  - When the author fills the textarea and presses the 'Publish' button, the note becomes available via a unique link: `domain/hashcode`.
  - The `hashcode` is a randomly generated string (long enough to handle billions of notes) and must be unique for each note; visually resembles an API key.
  - When a user opens the link, they see the **rendered version** of the Markdown note saved in the database.
- Markdown rendering details:
  - Render Markdown elements into **proper HTML** with visually distinguishable styling:
    - Headers (`#`, `##`, etc.) should be styled with different font sizes and weights.
    - Code snippets should appear in a distinct, styled block; code inside the snippet must be properly rendered, all indents are saved correctly.
    - Ensure all Markdown elements are properly rendered with clear and intuitive styling.
  - **Links and image rendering**:
    - Links in Markdown (e.g., `[text](https://example.com)`) should be rendered as clickable HTML `<a>` elements with target="_blank" – Important!. Confirm during implementation that this functionality is applied consistently to all links in the rendered Markdown.
    - Image links in Markdown (e.g., `![alt text](https://example.com/image.png)`) should display the referenced image.
    - Ensure that both links and images are sanitized during rendering to prevent XSS vulnerabilities.
- **Editing functionality**:
  - When a note is published, generate a unique **edit token** and store it in a browser cookie.
  - Allow the original author to see an **Edit** button when accessing their post if the cookie token matches the database token.
  - Clicking the **Edit** button opens the Markdown editor, pre-filled with the note's existing content.
  - Include a backup edit link (`domain/hashcode/edit?token=unique_token`) for users who clear cookies or switch devices.
  - Ensure that tokens are securely handled to prevent unauthorized access.
  - **404 error handling**:
  - Create a custom 404 page with a minimalistic template displaying only "404" as the title of the page.
  - Explicitly override Django's default 404 behavior by using a custom error view in `urls.py` that renders this template.
  - Ensure the custom 404 page is applied globally across the app.

---

**STYLING**

- The overall design should be **light and minimalistic**:
  - Use only **black and white**— strictly avoid outlines, no grey color, **no borders in text area** , except for buttons and code snippet.
  - Rendered elements should have visible padding
  - Rendered code snippet should have a light gray background
  - Choose the font **Space Mono** for the entire project.
  - Buttons should:
    - Be rounded.
    - Have a light border, no background color.
- Markdown rendering specifics:
  - Titles and headers must have distinct font sizes and weights.
  - Code blocks should appear styled as readable snippets.
  - Ensure all Markdown content looks clean and consistent when rendered.
- Frontend framework: **TailwindCSS**:
  - Integrate Tailwind by linking it via a <script> tag in the header template.

---

**NOTES**

- **Docker restrictions**:
  - Do not modify Docker-related files or scripts.
  - Work only within the **Django codebase**.
- Create all necessary Django apps to support the required functionality.
- Provide all updated and newly created files.
- Follow standard Django project layout
- New apps should be created at the project root level
- Include **commands** needed for maintaining the app (e.g., migrations) in a format suitable for execution inside the Docker container (not on the host machine). Commands must be provided in this chat, not in a separate file.
- **MARKDOWN RENDERING REQUIREMENTS**
  - All links must open in new tabs using target="_blank"
  - Add rel="noopener noreferrer" to all links for security
  - Process both regular links and image tags appropriately
  - Use string replacement after markdown rendering to ensure consistent link behavior
  - Avoid using complex markdown extensions that might cause compatibility issues
– **404 PAGE REQUIREMENTS**
  - Must work in production (DEBUG=False in settings.py) environment
  - Should take over the entire viewport with fixed positioning
  - Must override Django's default 404 page
Enter fullscreen mode Exit fullscreen mode

After sending the task to the Composer chat, wait a few seconds while it creates and populates the necessary files. Then, save them by clicking "Accept" in the file list window at the bottom of the chat, and run the necessary commands.

I want to emphasize the commands. For some reason, Composer periodically gets these wrong. Sometimes it provides commands ready to run in the container’s terminal – in this case, simply click "Run" in the commands window, and they’ll execute. Other times, there are comments in the commands window, and if you try to run them via the button, they won’t execute because the comments get fed into the terminal. In that case, copy the commands one by one and execute them manually. Occasionally, the commands appear without docker-compose and are meant for execution in a virtual environment. In that case, open the container in Docker Desktop, copy and run these commands one by one in the 'Exec' tab.

5. Refresh the Browser Page and Create a Test Note

After executing the commands—and assuming all migrations have been successfully created—go back to the web interface. You can refresh the default Django page if it’s still open, or reopen the site at localhost:9009. You should now see a <textarea> for Markdown and a "Publish" button. Fill in a test note and publish it. All the elements described in the prompt should render as expected, with code and images displaying correctly. In this project, images aren’t stored on disk; they’re pulled from the specified source – in this case, from the demo page of a WordPress template.

Test the editing functionality and wrap up the work on the project.

6. Final Commit

Now that everything is set, you can make the second, final commit. From project initialization to a finished prototype took about three minutes in real time. You can view the project state described in this post here.


During the preparation of this post the Markdown editor underwent some changes and now supports extended markup, even rendering YouTube videos within a post. This project is maintained in a separate repository with detailed instructions for usage and self-deployment.

The project I’ve demonstrated here is just a simple example, showing how quickly you can go from idea to a working solution if you make the most of modern tools. If you’ve long wanted to build your own project but kept postponing it due to the need to get into implementation details – perhaps now is the perfect time to give it a try.

When I think back to just over two years ago, it was unimaginable to consider such an approach to development – back then, copilot-AI could only generate boilerplate functions. It’s breathtaking to see how far we’ve come; and after all, we're only at the beginning of this journey. So when Zuckerberg says that in 2025, mid-level code in Facebook will be prepared by AI assistants, I completely believe it. Because it’s almost here.

Top comments (0)