From an empty folder to a published site, in the network repository (publick-org/publick.org). Written from adding Beverly, and used since for Lawrence, Burlington, and the towns in publick.org #58; each step names where the answer comes from, so a town the engine already reads takes an afternoon, not weeks.
From this repository, with the engine checked out next to it:
PYTHONPATH=../publick-engine python -m pipeline.new_town <Town> <state> <city website URL>
It does steps 1 to 6 as far as public sources answer them, and marks the
rest for a person (the engine’s pipeline/new_town.py):
[digest]: that’s step 8.data/ stays empty: a build with no data, four
weeks of meetings fetched (--full for everything from since), a second
build, the site checks, and pipeline.listings. Then the share image.It writes towns/<town>-<state>/ (the config, data/static/ with the ward
file, the share image) and new-town-<town>-<state>.md, a report of what it
found, where each answer came from, and what’s left. Everything left is a
# To fill in: comment in the config: work through them with the steps
below, deleting each as it’s done. The town’s site checks fail while any is
left. Then publish (step 7).
/AgendaCenter, Malden and
Beverly), a calendar with an Archive Center (/Archive.aspx,
Gloucester), or both. Otherwise look for a CivicClerk portal
(<city>.portal.civicclerk.com: Manchester, Burlington), a DotNetNuke
calendar (Manchester), or a meetings calendar with one documents page
for every board (Wallingford). Anything else (Govstack, Drupal, the
Rhode Island Secretary of State’s portal) needs a new reader in the
engine first: stop here and make that its own piece of work. A reader is written once and works for every town on the
same software.curl and a User-Agent naming Publick. A site that answers with a
browser check (“Just a moment…”) blocks automated reading: ask the
town to allow Publick’s crawler. Never work around the check.[page_meetings]: Methuen, Waltham, Nashua). Not
yet: BoardDocs, Diligent
Community, nested Drive folders; leave the board out and say so in the
pull request.seeclickfix.com/api/v2/places?search=<City>; the place may need its
state, like <city>_ma). Many issues the city
acknowledged means it uses SeeClickFix; a handful nobody answered means
it doesn’t. Cities on other systems (QAlert, Beverly’s MyBeverly) have no
public data: leave 311 out.For Massachusetts, below. For another state, start from a town’s config in
that state (README, “Adding a town”) and the state package’s docstring in the
engine (pipeline/states/<state>/__init__.py), which lists its keys; the
Census and BLS steps are the same everywhere, with the state’s own FIPS code.
[finance]): in any file in the network
repository’s states/ma/dls/, the town’s row (“DOR Code”: “030”,
“Municipality”: “Beverly”).[schools]):
educationtocareer.data.mass.gov/resource/n2xa-p822.json with
$query=SELECT dist_code, dist_name WHERE dist_name like '%<Town>%'
GROUP BY dist_code, dist_name. Usually the DOR code followed by 0000.[housing]): the BLS area code below contains it
(CT25 + the 5-digit place FIPS). census_geo is 16000US25 + the
place FIPS; bps_place is the place FIPS.[labor]): download.bls.gov/pub/time.series/la/la.area
(send a User-Agent with a contact address). The city’s line is
CT25xxxxx000000; the series is LAU + that + 03.shi_name as the state’s PDF writes
the town.In Massachusetts, from MassGIS (below). Elsewhere, the city’s own GIS
(Burlington), or the state’s (NH GRANIT for Manchester, RIGIS for South
Kingstown’s precincts). A town without wards, its seats all elected
townwide, uses its voting precincts for 311 (areas = "precincts") and
shows no ward map; a town with neither (Bangor) has no 311 section yet.
arcgisserver.digital.mass.gov/arcgisserver/rest/services/AGOL/WardsPrecincts2022/MapServer/0/query
with where=TOWN='<TOWN>', outFields=WARD,PRECINCT,WP_DISTRICT,POP_2020,
outSR=4326, f=geojson. Rename the properties to ward, precinct,
district, population_2020, round coordinates to 6 places, and save
it as data/static/<town>-precincts-2022.geojson with name and
source set as in Malden’s.ward; a citywide seat doesn’t. A seat
elected by a group of wards (Beverly’s School Committee districts) gets
wards = [1, 2, 3].term_ends out when no official source gives it.[officials] next_election): the date of the
town’s next regular municipal election, from the city’s election
calendar or its charter. A vacancy filled by a special election is
said in the body’s note until then.[site]
(name, domain <town>-<state>.publick.org, contact_email,
user_agent), [town], [analytics] prefix, [storage] prefix, the
tables above, and [freshness] (drop 311 if the town has none).
What every town shares, or every town in the state, isn’t in a
town’s config: it’s in defaults/network.toml and
defaults/states/<st>.toml (README, “Adding a town”), picked by
[town] state_abbr. Leave it out, or set the town’s own value where
it differs.[site.colors]): the most-used colors in the city
website’s stylesheet; each must reach 4.5:1 on white.[meetings.civicplus], for a CivicPlus city): the
calendar read month by month, with calendars naming the city’s
calendar of public meetings (“City Meetings”), even when the Agenda
Center has the agendas: an Agenda Center lists a meeting only once its
agenda is posted. A meeting in both is shown once.[meetings.aliases]): only names the engine doesn’t
fix itself (it already turns “Health, Board of” round, and matches a
calendar’s “Open Space and Recreation Committee” or “Malden Cultural
Council” to the Agenda Center’s “Open Space & Recreation Committee” or
“Cultural Council”, and leaves a date or a “co-posted with …” out of
a calendar’s “Planning Board Meeting, December 17, 2026”). The site
checks fail a board named for a date that’s left. python -m
pipeline.listings lists the meetings shown as one and why; a calendar
meeting next to an Agenda Center one of the same day, not put
together, needs an alias.[ical_meetings]), a
page of the year’s dates ([schedule_meetings], or schedule_url in
[finalsite_meetings] or [drive_meetings]).data/place.json), and the site says “the
city” or “the town” to match. Set [town] kind only to override it.[meetings.agenda_center.committees]): list
each committee by a phrase in its agenda titles. When titles name two
(“Public Services/Committee of the Whole”), leave the shared one out so
the committee’s own name decides. A board name ending in “Meeting”
makes titles read “… Meeting Meeting”.[terms] in
defaults/) are in every town’s glossary already. Add a
[[glossary]] entry, with its definition, only for a term of the
town’s own (its own offices and ordinances), or one it uses in another
sense than the state’s. A term every town in the state would need goes
in the state’s file instead, with its Spanish.[site]
languages in defaults/network.toml). Nothing else to do: the first
runs draft the town’s own text (tagline, boards, seats) and translate
its summaries, each checked without AI and reviewed by a second AI
model; anything that fails is shown in English. No person checks the
Spanish, and the site says so. Role titles in Spanish use the generic
form (“Vicepresidente”), since the holder changes.PYTHONPATH and PUBLICK_TOWN_DIR
set to the town’s folder, with an empty data/ (it must build), then
after python -m pipeline.fetch_meetings: every board listed under its
usual name, committees under their own boards, no errors.python -m pytest ../publick-engine/site_checks on the build.pipeline/factcheck.py): python -m
pipeline.factcheck with PUBLICK_TOWN_DIR set lists anything not found.towns/<town>-<state>/ (config, empty data/
with the ward file in data/static/, share image). Its run builds and
checks the town.[summaries] since on are
summarized: set it to the first of the month about three months before
launch, so the town’s history doesn’t take the month’s budget (older
meetings keep their records and documents). The month’s budget and the town’s own limit per
run still apply.@publick.org address, the site’s contact_email
among them.[digest] table to its config (the
signup’s settings are in defaults/network.toml), and add
<town>-<state>.publick.org to
DIGEST_TOWNS in wrangler.scheduler.toml. Once that’s merged, run
Actions → Worker: it also makes the town’s Buttondown tag,
<town>-<state>-en.