Architecture • SQL • ASP.NET

Build a Lightweight CMS with SQL + a Simple Import: The Pattern Behind Our Site

We keep content in SQL, author in flat files, and render inside an ASP.NET Web Forms layout using an <asp:Label>. Here’s the blueprint—schema, scripts, and safety tips.

What we’re doing: Using a tiny import pipeline to read an HTML article from tactical.txt and store it in SQL. The page then fetches the HTML and renders it inside the existing layout.

This article shows a pragmatic schema, a parameterized INSERT/UPDATE pattern, an import script (classic VBScript example), and a minimal ASP.NET reader pattern.

1) Minimal Database Schema

We store rendered HTML directly. Admins control content; the layout provides the frame.

-- Articles table
CREATE TABLE dbo.Articles (
    ArticleId      INT IDENTITY(1,1) PRIMARY KEY,
    Slug           NVARCHAR(160)  NOT NULL UNIQUE,     -- e.g., 'excel-templates-sharepoint'
    Title          NVARCHAR(200)  NOT NULL,
    HtmlBody       NVARCHAR(MAX)  NOT NULL,            -- full HTML from tactical.txt
    Summary        NVARCHAR(500)  NULL,                -- optional meta/teaser
    TagsCsv        NVARCHAR(400)  NULL,                -- fast + simple tags
    IsPublished    BIT            NOT NULL DEFAULT(0),
    CreatedUtc     DATETIME2(0)   NOT NULL DEFAULT(SYSUTCDATETIME()),
    UpdatedUtc     DATETIME2(0)   NOT NULL DEFAULT(SYSUTCDATETIME()),
    Author         NVARCHAR(120)  NULL
);
GO

-- Keep simple lookup for menu filtering
CREATE INDEX IX_Articles_Published_Updated ON dbo.Articles (IsPublished, UpdatedUtc DESC);
GO
Why NVARCHAR(MAX)? It comfortably holds your entire article HTML. If you expect uploads or very large content, consider splitting long assets (images/files) to blob storage and reference by URL.

2) Stored Procedure: Idempotent Upsert

We import by slug. If it exists, update; otherwise insert.

CREATE OR ALTER PROCEDURE dbo.usp_Article_Upsert
    @Slug        NVARCHAR(160),
    @Title       NVARCHAR(200),
    @HtmlBody    NVARCHAR(MAX),
    @Summary     NVARCHAR(500) = NULL,
    @TagsCsv     NVARCHAR(400) = NULL,
    @IsPublished BIT = 1,
    @Author      NVARCHAR(120) = NULL
AS
BEGIN
    SET NOCOUNT ON;

    IF EXISTS (SELECT 1 FROM dbo.Articles WHERE Slug = @Slug)
    BEGIN
        UPDATE dbo.Articles
           SET Title      = @Title,
               HtmlBody   = @HtmlBody,
               Summary    = @Summary,
               TagsCsv    = @TagsCsv,
               IsPublished= @IsPublished,
               Author     = @Author,
               UpdatedUtc = SYSUTCDATETIME()
         WHERE Slug = @Slug;
    END
    ELSE
    BEGIN
        INSERT INTO dbo.Articles (Slug, Title, HtmlBody, Summary, TagsCsv, IsPublished, Author)
        VALUES (@Slug, @Title, @HtmlBody, @Summary, @TagsCsv, @IsPublished, @Author);
    END
END
GO

3) Importer: Read tactical.txt and Upsert

Below is a classic VBScript example using ADODB.Command with parameters. It’s tiny, reliable, and fits into scheduled tasks or manual runs.

'=== 0-Import-Create-A-ChatGPTImport.vbs (example) ===
Option Explicit

Dim connStr, slug, title, summary, tags, isPublished, author, bodyPath, htmlBody
connStr = "Provider=SQLOLEDB;Data Source=YOUR-SQL;Initial Catalog=YourDb;Integrated Security=SSPI;"
slug = "excel-templates-sharepoint"
title = "Build a Lightweight CMS with SQL + Import"
summary = "How we import HTML files into SQL and render via ASP.NET."
tags = "cms,sql,aspnet,import"
isPublished = True
author = "Site Admin"
bodyPath = "tactical.txt"   ' your authored HTML article

' Read file
Dim fso, f, s
Set fso = CreateObject("Scripting.FileSystemObject")
Set f = fso.OpenTextFile(bodyPath, 1, False, 0) ' ForReading, ASCII/UTF-8 w/o BOM works fine
s = f.ReadAll
f.Close
htmlBody = s

' Execute proc
Dim cn, cmd
Set cn = CreateObject("ADODB.Connection")
cn.Open connStr

Set cmd = CreateObject("ADODB.Command")
Set cmd.ActiveConnection = cn
cmd.CommandType = 4 ' adCmdStoredProc
cmd.CommandText = "dbo.usp_Article_Upsert"

cmd.Parameters.Append cmd.CreateParameter("@Slug", 202, 1, 160, slug)          ' adVarWChar, adParamInput
cmd.Parameters.Append cmd.CreateParameter("@Title", 202, 1, 200, title)
cmd.Parameters.Append cmd.CreateParameter("@HtmlBody", 203, 1, -1, htmlBody)    ' adLongVarWChar
cmd.Parameters.Append cmd.CreateParameter("@Summary", 202, 1, 500, summary)
cmd.Parameters.Append cmd.CreateParameter("@TagsCsv", 202, 1, 400, tags)
cmd.Parameters.Append cmd.CreateParameter("@IsPublished", 11, 1, , isPublished)  ' adBoolean
cmd.Parameters.Append cmd.CreateParameter("@Author", 202, 1, 120, author)

cmd.Execute

cn.Close
Set cmd = Nothing
Set cn  = Nothing
WScript.Echo "Import complete for slug: " & slug
'=== end ===
Good Parameterized calls prevent SQL injection and survive large HTML bodies. If your tactical.txt is UTF-8 with BOM or contains non-ASCII, consider opening with ADODB.Stream to preserve Unicode.

Optional: PowerShell importer

$cn = New-Object System.Data.SqlClient.SqlConnection "Server=YOUR-SQL;Database=YourDb;Integrated Security=true;"
$cn.Open()
$cmd = $cn.CreateCommand()
$cmd.CommandText = "dbo.usp_Article_Upsert"
$cmd.CommandType = [System.Data.CommandType]::StoredProcedure

$body = Get-Content -Raw -Path "tactical.txt"
$cmd.Parameters.Add("@Slug",[nvarchar],160)      | out-null; $cmd.Parameters["@Slug"].Value="excel-templates-sharepoint"
$cmd.Parameters.Add("@Title",[nvarchar],200)     | out-null; $cmd.Parameters["@Title"].Value="Build a Lightweight CMS with SQL + Import"
$cmd.Parameters.Add("@HtmlBody",[nvarchar],-1)   | out-null; $cmd.Parameters["@HtmlBody"].Value=$body
$cmd.Parameters.Add("@Summary",[nvarchar],500)   | out-null; $cmd.Parameters["@Summary"].Value="How we import HTML files into SQL and render via ASP.NET."
$cmd.Parameters.Add("@TagsCsv",[nvarchar],400)   | out-null; $cmd.Parameters["@TagsCsv"].Value="cms,sql,aspnet,import"
$cmd.Parameters.Add("@IsPublished",[bit])        | out-null; $cmd.Parameters["@IsPublished"].Value=1
$cmd.Parameters.Add("@Author",[nvarchar],120)    | out-null; $cmd.Parameters["@Author"].Value="Site Admin"
$cmd.ExecuteNonQuery() | Out-Null
$cn.Close()

4) Rendering in ASP.NET Web Forms

We fetch by slug and place the HTML into the label that your layout already hosts.

// code-behind (myReader.aspx.cs)
using System;
using System.Data;
using System.Data.SqlClient;

public partial class myReader : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (IsPostBack) return;

        var slug = Request.QueryString["slug"] ?? "excel-templates-sharepoint";
        using (var cn = new SqlConnection("Server=YOUR-SQL;Database=YourDb;Integrated Security=true;"))
        using (var cmd = new SqlCommand(@"SELECT Title, HtmlBody FROM dbo.Articles WHERE Slug=@slug AND IsPublished=1", cn))
        {
            cmd.Parameters.Add("@slug", SqlDbType.NVarChar, 160).Value = slug;
            cn.Open();
            using (var r = cmd.ExecuteReader())
            {
                if (r.Read())
                {
                    Page.Title = r["Title"].ToString();
                    readblog.Text = r["HtmlBody"].ToString(); // this label renders our stored HTML
                }
                else
                {
                    readblog.Text = "<div class='alert alert-warning'>Article not found.</div>";
                }
            }
        }
    }
}
Note Your page already proves HTML renders well inside the <asp:Label ID="readblog">. In other scenarios, a Literal control (<asp:Literal Mode="PassThrough">) is a common alternative.

6) Security & Safety

  • Admins-only authoring. Because we intentionally render stored HTML, keep the import surface restricted to trusted users.
  • Parameterized SQL. As shown above—never string-concatenate SQL.
  • Style scoping. Keep article CSS scoped inside the container (like we do here) to avoid bleeding into the site chrome.
  • Untrusted input? If you ever allow non-admin authors, sanitize/whitelist tags server-side before upsert.
  • Backups. Nightly DB backup + a file copy of authored tactical.txt alongside the import job for easy rollback.

7) Ops & Versioning

Content versions

Easiest path: store historical copies in a sister table.

CREATE TABLE dbo.ArticleHistory (
  HistoryId  BIGINT IDENTITY(1,1) PRIMARY KEY,
  ArticleId  INT         NOT NULL,
  HtmlBody   NVARCHAR(MAX) NOT NULL,
  SnapshotUtc DATETIME2(0) NOT NULL DEFAULT(SYSUTCDATETIME())
);

Deployment

  • Keep each article’s source as tactical.txt in source control.
  • Run the importer per article (or batch) during a deploy step.
  • Health check: load by slug, verify non-empty HTML, and publish.
TL;DR
Author in tactical.txtVBScript/PowerShell upsert via usp_Article_Upsert → Render into <asp:Label ID="readblog"> within <div class="container mt-4">. Clean, fast, and totally under your control.