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.
5) Simple Search & Metadata
Start lightweight; you can add full-text later.
-- naive search by title/tags
CREATE OR ALTER PROCEDURE dbo.usp_Article_Search
@q NVARCHAR(100)
AS
BEGIN
SET NOCOUNT ON;
SELECT TOP 25 ArticleId, Slug, Title, Summary, UpdatedUtc
FROM dbo.Articles
WHERE IsPublished = 1
AND (
Title LIKE '%' + @q + '%'
OR TagsCsv LIKE '%' + @q + '%'
OR Summary LIKE '%' + @q + '%'
)
ORDER BY UpdatedUtc DESC;
END
GO
Upgrade path: Add a persisted SearchVector column, or enable SQL Server Full-Text on Title, Summary, and a cleaned version of HtmlBody (HTML-stripped).
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.txt → VBScript/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.